From 873d8a788719ed2979d77b966867a296a9f66cf3 Mon Sep 17 00:00:00 2001 From: Stuart Meeks Date: Thu, 20 Aug 2026 16:36:39 +0000 Subject: [PATCH] chore: adopt the standard docs, editor config and SDK pin PR B of the adoption sequence in ADOPTING.md. Touches no CI and needs no ruleset work. - SECURITY.md (4.11), CONTRIBUTING.md and .github/PULL_REQUEST_TEMPLATE.md (5.4), CLAUDE.md (5.7). - Canonical .gitignore and .editorconfig (5.2), both byte-identical to templates/. The .gitignore additions are load-bearing here: `**/[Cc]:/` catches the stray directory this repo's Windows-style PackageOutputPath creates on Linux, which PR C removes at the source, and `artifacts/` matches where 1.8 puts pack output. - global.json gains the SDK pin (10.0.100, rollForward latestFeature) next to the existing Microsoft.Testing.Platform runner setting (1.9). An unpinned SDK gives a contributor different analyzer results from CI, and TreatWarningsAsErrors turns that into a build that fails for them and passes for everyone else. SECURITY.md and CLAUDE.md are written for this repo rather than copied. The scope section is specific: this library opens no file, socket or OS store, so the only real boundary is terminal control sequences reaching the console through the two caller-controlled paths (AppName via Figgle, and a FromProvider tagline). CLAUDE.md's "easy to get wrong" list names the five invariants that are silent when broken -- the single-Markup-call design, the markup escaping, the duplicated line-ending normalisation, Gradient trusting SplashColors to have validated, and spaces skipping the colour escape. Two accuracy fixes while here, both required by 5.3: - README now states its target frameworks and its dependency floors, and says why the floors are not per-TFM (1.5, not 1.4 -- every dependency here is pre-1.0 and runtime-independent). - SplashTagline.RandomBuiltIn's XML doc said "~200 quotes". The pool holds 313, counted from Quotes.cs. Corrected to "~300", matching what the README already claimed. Documentation only; the pool is unchanged. Verified locally: Release build at zero warnings, 80 tests passing on both net8.0 and net10.0. Co-Authored-By: Claude Opus 5 (1M context) --- .editorconfig | 6 + .github/PULL_REQUEST_TEMPLATE.md | 21 +++ .gitignore | 10 ++ CHANGELOG.md | 13 ++ CLAUDE.md | 120 ++++++++++++++++++ CONTRIBUTING.md | 40 ++++++ README.md | 22 ++++ SECURITY.md | 56 ++++++++ global.json | 4 + .../SplashTagline.cs | 2 +- 10 files changed, 293 insertions(+), 1 deletion(-) create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 CLAUDE.md create mode 100644 CONTRIBUTING.md create mode 100644 SECURITY.md diff --git a/.editorconfig b/.editorconfig index 41f2e91..bd0de72 100644 --- a/.editorconfig +++ b/.editorconfig @@ -59,6 +59,12 @@ dotnet_naming_rule.private_fields_should_be_camel_case.style = camel_case_with_u dotnet_naming_symbols.private_fields.applicable_kinds = field dotnet_naming_symbols.private_fields.applicable_accessibilities = private +# A const IS a field, so without this the rule demands `_nonceSize` for +# `private const int NonceSize` — PascalCase constants are correct .NET style and +# the codebase uses them throughout. Restricting the rule to instance fields keeps +# it aimed at what it was written for. Found when EnforceCodeStyleInBuild surfaced +# 76 IDE1006 violations, every one of them a constant. +dotnet_naming_symbols.private_fields.required_modifiers = dotnet_naming_style.camel_case_with_underscore.capitalization = camel_case dotnet_naming_style.camel_case_with_underscore.required_prefix = _ diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..923388a --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,21 @@ +## What changed + + + +## Why + + + +## Checklist + +- [ ] Build is clean — no new warnings (`TreatWarningsAsErrors` is on) +- [ ] Tests pass on **every** shipped target framework +- [ ] Public API changes carry XML docs +- [ ] `CHANGELOG.md` updated under `[Unreleased]` +- [ ] Dependency floors unchanged, or the consumer impact is described below + +## Consumer impact + + diff --git a/.gitignore b/.gitignore index 47a94ef..8fdfdb9 100644 --- a/.gitignore +++ b/.gitignore @@ -225,6 +225,16 @@ PublishScripts/ *.nupkg # NuGet Symbol Packages *.snupkg +# Stray "C:/" / "c:/" directory created on non-Windows when the csproj's +# Windows-style PackageOutputPath is interpreted as a relative path. +**/[Cc]:/ + +# Per-user Claude Code state — never commit +.claude/settings.local.json +.claude/projects/ + +# Pack output (matches CI's --output flag) +artifacts/ # The packages folder can be ignored because of Package Restore **/[Pp]ackages/* # except build/, which is used as an MSBuild target. diff --git a/CHANGELOG.md b/CHANGELOG.md index e7491dc..8b28f2d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- **Adopted the standards baseline docs, editor config and SDK pin.** + `SECURITY.md`, `CONTRIBUTING.md`, `CLAUDE.md` and a pull request template added; + `.gitignore` and `.editorconfig` replaced with the canonical copies; `global.json` + now pins the SDK band (`10.0.100`, `rollForward: latestFeature`) alongside the + existing Microsoft.Testing.Platform runner setting. An unpinned SDK gives a + contributor different analyzer results from CI, and `TreatWarningsAsErrors` turns + that into a build that fails for them and passes for everyone else. +- **README now states its target frameworks and dependency floors** (§5.3), and + explains why the floors are not per-target-framework. +- **`SplashTagline.RandomBuiltIn`'s XML doc corrected** from "~200 quotes" to + "~300": the built-in pool holds 313 entries. Documentation only; the pool itself + is unchanged. + - **Test suite migrated to xUnit.net v3 (`xunit.v3` 4.0.0)** from `xunit` 2.9.3. Contributor-facing only — no library code, public API, or shipped package contents changed. v3 test projects are self-executing console apps and run on diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..c37bd63 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,120 @@ +# CLAUDE.md — NextIteration.SpectreConsole.Splash + +## This package + +A Figgle-and-Spectre.Console splash screen for .NET CLIs. A consumer calls +`SplashScreen.Show("my-cli")` for the defaults, or passes a `SplashOptions` to choose the +Figgle font, a `SplashColors` gradient palette of `#RRGGBB` stops, and a `SplashTagline` +strategy (`None`, `RandomBuiltIn` from a ~300-entry built-in pool, or `FromProvider` with a +callback). There is no DI registration and no command branch — it is one static entry point +with no state. Nothing here consumes another package in the estate, and nothing in the estate +consumes it. + +## Things that are easy to get wrong here + +- **The single `AnsiConsole.Markup` call is the whole point of the library.** `Renderer` + assembles logo and tagline into one markup string and `SplashScreen.Show` emits it with one + call. The precursor wrote one `AnsiConsole.Write(char)` per character — ~250 calls for a + typical Roman logo, each flushing its own ANSI colour escape — and measured ~60 ms against + under 10 ms for the batched path. Any change that writes incrementally, or that renders + through a Spectre widget instead of a markup string, gives that back. The figure is quoted + in the README and in the `SplashScreen` XML docs, so a rewrite has to re-measure both. +- **Markup escaping is load-bearing, not defensive tidiness.** Figgle glyphs contain `[` and + `]`, and Spectre parses those as markup control characters. This is not hypothetical in the + default font: in Roman, the glyphs for `w`, `K`, `2`, `3`, `5` and `6` all contain a + bracket, so an app called `wget` or `k8s` hits it immediately. `RenderLogo` doubles them + inline per character; `RenderTagline` escapes through `EscapeMarkup` *after* padding, + because padding can only add spaces but reordering the two would be easy and wrong. + Dropping either escape does not throw — it silently corrupts the logo or swallows part of + the tagline as a bogus colour tag. +- **Line-ending normalisation is implemented twice, and the two must agree.** + `SplashScreen.MaxLineLength` scans for `\n` and discounts a preceding `\r` to compute the + width the gradient is generated at; `Renderer.Render` independently splits on `\n` and + `TrimEnd('\r')`s to compute the width the logo is rendered at. Both handle `\r` because the + code treats Figgle's line endings as platform-dependent (on Linux with Figgle 0.6.6 the + output is `\n`-only, so the Windows leg of the matrix is what actually exercises the other + branch). If they ever disagree the gradient is + sampled to the wrong width and the sweep no longer reaches the end of the logo — a visual + bug with no exception and no failing test unless one is written for the pair together. +- **`Gradient` deliberately trusts its input; `SplashColors` is the only validator.** + `HexToRgb` and `CharToHex` do no checking at all — `CharToHex` maps anything unrecognised + to `0` with an "unreachable" comment — because `SplashColors`' constructor has already + rejected anything that is not `#RRGGBB`. That invariant is the contract between the two + types. Any new path that reaches `Gradient.Generate` without going through a constructed + `SplashColors` silently renders wrong colours instead of throwing. +- **Spaces skip the colour escape entirely.** `RenderLogo` appends a bare `' '` rather than + wrapping it, which saves ~14 markup characters per space and is a large fraction of a + Figgle logo. It is safe only because a space has no visible foreground; a change that gives + the splash a background colour makes it wrong. +- **The font renders on first `Show`, not at class load.** `FiggleFonts.Roman` is touched + lazily via the `SplashOptions` default. Hoisting it into a static initialiser moves that + cost onto every consumer that loads the assembly without showing a splash. + +## Repository baseline + +This repo conforms to +[NextIteration.Standards](https://github.com/StuartMeeks/NextIteration.Standards). +Build properties, test stack, CI shape, and branch protection are defined there, not +here. Before changing any of those, read `STANDARD.md`; if this repo needs to deviate, +that is an `EXCEPTIONS.md` entry in the standards repo, not a local difference. + +## Non-negotiables + +- **The build must be clean.** `TreatWarningsAsErrors` is on and analyzers run at + `latest`. A warning is a build failure. +- **Tests must pass on every shipped target framework** (`net8.0` and `net10.0`). A change + that only passes on one is not finished. Shipping a target you do not test is a defect, + not a scoping decision. +- **Dependency floors are deliberate.** A `PackageReference` version in a library is a + *minimum* NuGet forces on every consumer, so raising a floor is a consumer-visible change + even when nothing in the code needs it. Never raise one to silence a warning. This repo + has no *per-TFM* floors, and that is not an oversight: Figgle, Figgle.Fonts and + Spectre.Console are all pre-1.0 and version independently of the .NET runtime, so + `STANDARD.md` §1.5 gives them a single common floor at the version actually built and + tested against. §1.4's per-TFM rule applies only to runtime-aligned Microsoft platform + packages, of which this repo references none. +- **Public API changes need XML docs.** `GenerateDocumentationFile` is on and the public + surface is fully documented. +- **Update `CHANGELOG.md`** under `[Unreleased]`, saying what changed and why. + +## Dependabot + +Minor and patch updates auto-merge behind CI. Major updates stay open for a human — that +is deliberate, not a backlog to clear. `dependabot.yml` here has **no `ignore` block**, and +adding one would be wrong: §4.10 scopes that list to packages carrying per-TFM floors, and +this repo has none, so every major bump it files is genuinely reviewable. `audit-drift.sh` +checks the `ignore` set against the repo's actual per-TFM floors, so an inherited entry +fails the audit rather than passing quietly. + +## After opening a pull request + +Watch CI to completion, report the real check results, then **offer to merge** in the same +message. Do not stop silently and wait to be asked. + +- If branch protection blocks the merge, say so and offer `gh pr merge --admin`. These + repos require a code-owner review only the maintainer can give, which is why `--admin` is + the tool — but that mechanic is not the reason the offer is wanted. The reason is simply + that the maintainer has grown comfortable delegating this to an agent, so treat the + latest instruction as authoritative over this file. +- **Merge only on an explicit yes.** The offer is pre-approved; the action is not. +- Never offer while checks are failing or still running. Report that state instead. +- Report the checks that actually ran. A skipped check is not a passing check, and branch + protection treats them differently from how they read in a summary. + +## CI + +The single required status check is `ci` — an aggregating gate over `build` and `test`. +Renaming those jobs is safe; the ruleset never names them. Do not make them required +checks directly. + +**`ci.yml` is not yours to edit freely.** `STANDARD.md` §3.0.1 requires its non-comment +content to match `templates/.github/workflows/ci.yml` in the standards repo exactly, apart +from the tag glob and steps carrying an `EXCEPTIONS.md` entry. This repo has no such entry, +so the only permitted difference is `tags: [ 'v*' ]` and the header comment. +`audit-drift.sh` checks this. Change the template first, then every repo — never this file +alone. + +The `test` matrix runs Linux, Windows and macOS. Nothing here touches the filesystem or an +OS store, so no platform has its own backend — but Figgle's line endings and Spectre's +console capability detection both differ by platform, and §3.1.1 removes platforms by +exception only. Do not drop a leg to save CI minutes. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..7d02bb1 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,40 @@ +# Contributing + +Issues and pull requests are welcome. + +## Before you open a PR + +- **The build must be clean.** `TreatWarningsAsErrors` is on and analyzers run at + `latest`. A warning is a build failure, not a suggestion. +- **Tests run on every target framework** the project ships. `dotnet test` covers + `net8.0` and `net10.0`; a change that only passes on one is not finished. +- **Public API changes need XML docs.** `GenerateDocumentationFile` is on and the + public surface is fully documented — keep it that way. +- **Update `CHANGELOG.md`.** Keep a Changelog format, under `[Unreleased]`. Say what + changed and why; "bump dependency" without a reason is not useful six months later. + +## Dependency changes + +Dependency floors are deliberate. A `PackageReference` version in a library is a +*minimum* NuGet forces on every consumer, so raising a floor is a +consumer-visible change even when nothing in the code needs it. Read +`STANDARD.md` sections 1.4 and 1.5 in `NextIteration.Standards` before changing one. + +This package has no *per-target-framework* floors, which is a consequence of +those two clauses rather than an omission. Figgle, Figgle.Fonts and +Spectre.Console are all pre-1.0 and version independently of the .NET runtime, so +section 1.5 gives them a single common floor at the version actually built and +tested against — per-TFM floors would be meaningless where breaking changes land +between minors. Section 1.4's per-TFM rule covers runtime-aligned Microsoft +platform packages, and this package references none. + +Minor and patch bumps arrive automatically via Dependabot and merge behind CI. +Major bumps stay open for a human — that is deliberate, not a backlog. + +## Repository conventions + +These repositories share a baseline defined in +[NextIteration.Standards](https://github.com/StuartMeeks/NextIteration.Standards): +build properties, test stack, CI shape, and branch protection. If a change would +deviate from it, raise that there first — a per-repo exception is a documented +entry, not a quiet difference. diff --git a/README.md b/README.md index 8b6c65c..0cde7c2 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,9 @@ A reusable Figgle-and-Spectre.Console splash screen for .NET CLIs. - Performance-first: the entire splash is assembled as one markup string and emitted via a single `AnsiConsole.Markup` call. +Targets `net8.0` and `net10.0`. Both targets carry an identical public surface and are +covered by the same test suite on Linux, Windows, and macOS. + ## Install ```bash @@ -76,6 +79,25 @@ Additional wins: - Space characters skip the colour-escape wrapper entirely (saves ~14 markup chars per space). +## Requirements + +- **.NET 8.0** or **.NET 10.0** (the package multi-targets `net8.0;net10.0`) +- **Spectre.Console** 0.57.2+ +- **Figgle** and **Figgle.Fonts** 0.6.6+ + +These floors are the versions the package is built and tested against. All three are +pre-1.0 and version independently of the .NET runtime, so each takes a single common +floor rather than one per target framework — a per-TFM floor would be meaningless where +breaking changes land between minors. + +Everything else is transitive. + +## Contributing + +Issues and PRs welcome — see [CONTRIBUTING.md](./CONTRIBUTING.md). This repository +follows the baseline in +[NextIteration.Standards](https://github.com/StuartMeeks/NextIteration.Standards). + ## License MIT — see [LICENSE](./LICENSE). diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..e283bac --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,56 @@ +# Security policy + +## Reporting a vulnerability + +Report privately through GitHub's **Report a vulnerability** button under this +repository's Security tab, which opens a private advisory visible only to the +maintainers. Please do not open a public issue for a suspected vulnerability. + +Include the affected package and version, what an attacker can achieve, and a +reproduction if you have one. + +You can expect an acknowledgement within 7 days, an assessment within 14, and +credit in the advisory and changelog unless you ask otherwise. + +## Supported versions + +Only the latest released minor of each package receives security fixes. These are +pre-1.0 libraries and there are no long-term support branches. + +## Scope + +This library reads no input it did not receive from its caller, opens no file, +network socket or OS store, and stores nothing. It renders a string to the +terminal. That makes the attack surface narrow, and worth stating precisely +rather than waving at. + +The one real boundary is **terminal control sequences**. The rendered splash is a +Spectre.Console markup string emitted with a single `AnsiConsole.Markup` call, so +anything the caller supplies that reaches the output has to survive markup +escaping. Two paths carry caller-controlled text: + +- **`AppName`**, which is rendered through a Figgle font first. Figgle glyphs + legitimately contain `[` and `]`, so the renderer doubles both — they become + markup literals rather than colour tags. +- **`SplashTagline.FromProvider`**, whose return value is escaped the same way + after word-wrapping and padding. + +A caller-supplied string that escaped that handling and reached the terminal as a +live escape sequence would be in scope: on some terminals control sequences can +reposition the cursor, alter the window title, or in the worst historical cases +be replayed as input. If you can make either path emit an unescaped `[` that +Spectre interprets, or get a raw ESC through, that is a report worth filing. + +Two things are explicitly **not** claimed: + +- **The tagline provider is trusted code, not untrusted input.** `FromProvider` + takes a `Func` the consumer wrote. A consumer that pipes unvalidated + remote data into it owns that decision; the library escapes markup but does not + sanitise arbitrary control characters. +- **Nothing here is a security control.** There is no secret, no credential, no + persistence, and no privilege boundary being enforced. A splash screen cannot + protect anything, so no report should assume it was trying to. + +Reports demonstrating a break *within* those stated boundaries are in scope and +welcome. Reports that only restate a documented limitation are not +vulnerabilities. diff --git a/global.json b/global.json index 3140116..1d364c6 100644 --- a/global.json +++ b/global.json @@ -1,4 +1,8 @@ { + "sdk": { + "version": "10.0.100", + "rollForward": "latestFeature" + }, "test": { "runner": "Microsoft.Testing.Platform" } diff --git a/src/NextIteration.SpectreConsole.Splash/SplashTagline.cs b/src/NextIteration.SpectreConsole.Splash/SplashTagline.cs index 94a621d..a6c8490 100644 --- a/src/NextIteration.SpectreConsole.Splash/SplashTagline.cs +++ b/src/NextIteration.SpectreConsole.Splash/SplashTagline.cs @@ -6,7 +6,7 @@ namespace NextIteration.SpectreConsole.Splash /// Tagline strategy for the splash screen. /// /// — render no tagline at all. - /// — pick a random quote from the library's built-in list (~200 quotes). + /// — pick a random quote from the library's built-in list (~300 quotes). /// — supply your own provider callback. /// ///