diff --git a/OPERATIONS.md b/OPERATIONS.md index e5bc83c..29e621b 100644 --- a/OPERATIONS.md +++ b/OPERATIONS.md @@ -26,7 +26,7 @@ docker run --rm --pull=always -v "$PWD":/check --workdir /check mstruebing/edito Three gaps in that list are CI's rather than this runbook's, reproduced here so a local run matches CI rather than quietly exceeding it. The `jq` glob covers `repo-config/*.json` and does not reach `repo-config/operational/develop.json`, so a malformed operational payload passes. And `sentence-split` is implemented and tested but named by no invocation, so nothing runs it. The third is `home-path`, which is in `DEFAULT_RULES` and so runs on every bare local run, yet is named by neither CI list, so the pattern-detectable half of the representative-data rule gates nothing in CI. It is clean tree-wide today, which is why the gap is a hole rather than a backlog. -Run the `editorconfig-checker` line before pushing any new file. This repository defaults to CRLF, most tooling writes LF, and a new file therefore fails that check on its first CI run rather than locally. +Run the `editorconfig-checker` line before pushing a new file, and before pushing an existing file that a script rewrote rather than an editor. This repository defaults to CRLF and most tooling writes LF, so a new file fails that check on its first CI run rather than locally. A scripted rewrite is the same hazard on a file that was already correct, since reading and rewriting a whole file in text mode converts every line ending in it, which no prose or Markdown gate reports. The first prose invocation gates. The second reports the backlog that is corrected as each file is next edited, or cleared in a deliberate batch, and it exits non-zero locally whenever findings exist. It is warn-only in CI because the workflow step sets `continue-on-error: true`, not because the command is lenient, so a non-zero exit locally is the expected result rather than a problem. diff --git a/TODO.md b/TODO.md index 561d426..60100fd 100644 --- a/TODO.md +++ b/TODO.md @@ -31,7 +31,7 @@ One pull request clearing the prose findings the hub's own docs and spec still c - **Clear the remaining [#519][issue-519] prose backlog, outside the snippets.** The whole-tree figure moves as readily with a fix to the gate as with a fix to the prose, so it is re-measured rather than quoted. - **Blocked by** - Nothing. - **Issue** - [#519][issue-519], whose headline numbers are stale and whose four planned changes are two-thirds landed. - - **Checked** - `develop` at `c64e3e0` on 2026-08-07, where `python3 scripts/prose_lint.py --summary` reported 373 violations across 26 files and `catalog/snippets` reported 0. The 131 across 15 the comment batch leaves is a figure on the branch carrying that batch rather than on this anchor, and it becomes the anchor's own number when that branch merges. The snippets sweep took the tree from 557 across 45, of which 184 across 19 were snippets. + - **Checked** - `develop` at `c9c92dd` on 2026-08-07, where `python3 scripts/prose_lint.py --summary` reported 131 violations across 15 files, the comment batch having merged and made that figure the anchor's own rather than a branch's. The hub-only Markdown batch clears 90 of those across 9 files, leaving 41 across the 6 carried ones. The snippets sweep took the tree from 557 across 45, of which 184 across 19 were snippets, and the comment batch took it from 373 across 26. - **Open** - Whether the gate becomes a carried file rather than a hub-only one, which "Reducing the Carried Surface Further" asks from the other direction. - **Settled** - `comment-wrap` and `comment-case` are in `DEFAULT_RULES` and `reports/` is exempt as a generated tree, which is why the figures differ from the 668 and 119 the issue records. - **Settled** - `sentence-split` is defined but excluded from `DEFAULT_RULES`, so a sweep never reports it and a wrapped sentence in Markdown prose is not a finding. @@ -40,6 +40,10 @@ One pull request clearing the prose findings the hub's own docs and spec still c - **Settled** - An ellipsis read as a sentence terminator, so `RUN_ON` reported one schematic comment line as two sentences and the split it asked for would have broken the fragment the line exists to show. The guard is that a dot preceded by a dot never terminates, fixed and tested before any prose moved, and the verdict diff in both directions was that one finding and nothing else. - **Settled** - A comment opening on a lowercase identifier was the bulk of what `comment-case` reported, and the rule intends those restructured rather than exempted, which is what the batch did rather than widening any exemption. - **Settled** - The remainder is prose in Markdown, and six of the fifteen files are carried, being [`GOVERNANCE.md`][governance] at 14, [`WORKFLOW.md`][workflow] at 14, [`CODESTYLE.md`][codestyle] at 6, [`.github/copilot-instructions.md`][copilot-instructions] at 4, `repo-config/README.md` at 2 and [`HISTORY.md`][history] at 1. So the next batch splits again at that line, since the carried half rewrites byte-locked sections and owes a re-vendor while the hub-only half owes nothing. + - **Settled** - The hub-only Markdown batch cleared 90 findings across 9 files, 79 dash and 11 semicolon, the largest being [`docs/devcontainer.md`][devcontainer-doc] at 22, [`docs/ssh-signing.md`][ssh-signing] at 17 and [`spec/type-model.md`][type-model] at 17. Every dash finding in the tree was hub-only, so what the carried half now holds is 41 semicolons and no dash at all. + - **Settled** - The exemption pass raised two candidates and the corpus rejected both, so the checker is unchanged by this batch. `LABEL_DASH` exempts `- **Label** - text` and reaches neither a code-span label nor a paragraph-leading one, yet the tree already spells a code-span definition list with a colon in five files against four dash-spelled lines in one, and already spells a paragraph label as `**Label.** Sentence` against nine dash-spelled lines confined to two troubleshooting sections. Both were a file that had not adopted the tree's own convention rather than a construct the rule fails to describe, which is the same verdict the comment batch reached on a lowercase-opening comment. + - **Settled** - A bulleted list terminating its items with semicolons appeared in exactly one place tree-wide, and its own last item already ended in a period, so it was punctuation to make consistent rather than the list construction the semicolon exemption protects. A line-based checker cannot see a list spread across bullets in any case, since the unit it judges is the sentence within a line. + - **Settled** - Rewriting a file with a script rather than an editor converts every line ending in it, which took two files from CRLF to LF with every prose and Markdown gate still green and only `editorconfig-checker` reporting it. [`OPERATIONS.md`][operations] scoped that instruction to a new file, and this batch widens it to a scripted rewrite of an existing one. - **Settled** - `home-path` is in `DEFAULT_RULES` and is named by neither CI list, so the pattern-detectable half of the representative-data rule runs on every bare local run and gates nothing in CI. It is clean tree-wide, so this is a hole rather than a backlog, and it is recorded in [`OPERATIONS.md`][operations] beside the two gaps already named there. ### Giving the Fleet's Own Pins Something to Resolve Against @@ -469,6 +473,7 @@ Each was checked against the tree and has nothing left to do anywhere. Closing i [audit-doc]: ./AUDIT.md [codestyle]: ./CODESTYLE.md [copilot-instructions]: ./.github/copilot-instructions.md +[devcontainer-doc]: ./docs/devcontainer.md [divergences]: ./spec/divergences.json [divergences-report]: ./reports/divergences.md [files]: ./spec/files.json @@ -489,6 +494,7 @@ Each was checked against the tree and has nothing left to do anywhere. Closing i [secrets-schema]: ./spec/secrets.schema.json [section-model]: ./spec/section-model.md [snippets]: ./catalog/snippets/ +[ssh-signing]: ./docs/ssh-signing.md [standup]: ./STANDUP.md [type-model]: ./spec/type-model.md [validate]: ./spec/validate.py diff --git a/catalog/README.md b/catalog/README.md index 8a5b79c..933bc7a 100644 --- a/catalog/README.md +++ b/catalog/README.md @@ -1,9 +1,9 @@ # Catalog Reusable reference snippets: concrete config artifacts a repo can copy or compare against. These are -**reference material, not run here** - this repo ships no build, so the workflow snippets below are not active. Each is the canonical shape the audit (`AUDIT.md`) checks a downstream implementation against. +**reference material, not run here**. This repo ships no build, so the workflow snippets below are not active. Each is the canonical shape the audit (`AUDIT.md`) checks a downstream implementation against. -- `snippets/workflows/` - the reusable build/publish workflow tasks that a code-shipping repo runs (this docs repo keeps only the source-only orchestrator set in `.github/workflows/`). See `snippets/workflows/README.md` for the mapping from each file to the `WORKFLOW.md` guarantees it implements. -- `snippets/configs/` - config exemplars: `vscode-tasks.json` (.NET clean-compile task group) and `vscode-tasks-python.json` (the Python equivalent - `ruff`/type-check/`pytest`, all `type: process` so no `&&` chaining breaks Windows PowerShell 5.1), `dependabot.yml` (multi-ecosystem dual-target reference), `docker-hub-readme.md` (the size-limited Docker Hub overview, distinct from the project `README.md`). -- `snippets/devcontainer/` - `.devcontainer` definitions for the .NET and Python toolchains. -- `snippets/vscode/` - the composable `.code-workspace` fragments: `base.jsonc` (standard set) plus `dotnet.jsonc`, `python.jsonc`, `docker.jsonc` per-type additions. See `snippets/vscode/README.md`. +- `snippets/workflows/`: the reusable build/publish workflow tasks that a code-shipping repo runs (this docs repo keeps only the source-only orchestrator set in `.github/workflows/`). See `snippets/workflows/README.md` for the mapping from each file to the `WORKFLOW.md` guarantees it implements. +- `snippets/configs/`: the config exemplars `vscode-tasks.json` (.NET clean-compile task group) and `vscode-tasks-python.json` (the Python equivalent, running `ruff`/type-check/`pytest`, all `type: process` so no `&&` chaining breaks Windows PowerShell 5.1), plus `dependabot.yml` (multi-ecosystem dual-target reference), `docker-hub-readme.md` (the size-limited Docker Hub overview, distinct from the project `README.md`). +- `snippets/devcontainer/`: `.devcontainer` definitions for the .NET and Python toolchains. +- `snippets/vscode/`: the composable `.code-workspace` fragments: `base.jsonc` (standard set) plus `dotnet.jsonc`, `python.jsonc`, `docker.jsonc` per-type additions. See `snippets/vscode/README.md`. diff --git a/docs/devcontainer.md b/docs/devcontainer.md index 6ee03e8..3821a62 100644 --- a/docs/devcontainer.md +++ b/docs/devcontainer.md @@ -1,6 +1,6 @@ # Devcontainer Setup -This repo ships no application toolchain. It keeps two per-language [Dev Container][containers-link] definitions under `catalog/snippets/devcontainer/` as reference for fleet code repos; each carries one toolchain, extension surface, and `postCreateCommand`. The mechanics below - SSH commit signing, bind mounts, and `gh` auth - apply to any repo that uses them. +This repo ships no application toolchain. It keeps two per-language [Dev Container][containers-link] definitions under `catalog/snippets/devcontainer/` as a reference for fleet code repos, each carrying one toolchain, extension surface, and `postCreateCommand`. The mechanics below (SSH commit signing, bind mounts, and `gh` auth) apply to any repo that uses them. | Devcontainer | Image | Toolchain | | ------------ | ----- | --------- | @@ -9,7 +9,7 @@ This repo ships no application toolchain. It keeps two per-language [Dev Contain In a repo that carries one of these definitions, install the [Dev Containers extension][marketplace-link] and pick **Reopen in Container**. -Prerequisite: complete [host setup][host-setup] first - without git config, an SSH key, and the allowed-signers file on the host, the devcontainer cannot sign commits. +Prerequisite: complete [host setup][host-setup] first. Without git config, an SSH key, and the allowed-signers file on the host, the devcontainer cannot sign commits. ## What's Inside (Both Containers) @@ -21,15 +21,15 @@ Prerequisite: complete [host setup][host-setup] first - without git config, an S The .NET container additionally ships the `csharpier`/`dotnet-outdated` local tools (restored by `catalog/snippets/devcontainer/dotnet/post-create.sh`). The Python container additionally ships `uv` (installed by `catalog/snippets/devcontainer/python/post-create.sh` from a version-pinned URL) and pre-syncs the Python package venv where one is present. -Each devcontainer's extension list and the matching workspace's `recommendations` are kept identical - when you add an extension to one, add it to the other. +Each devcontainer's extension list and the matching workspace's `recommendations` are kept identical, so when you add an extension to one, add it to the other. ## Bind Mounts (Both Containers) -The host SSH key, allowed-signers file, and `gh` config directory are mounted into the container so commits sign correctly and `gh` is pre-authenticated **when the host stores its `gh` token in a file** (`~/.config/gh/hosts.yml`). Hosts that store the token in macOS Keychain or Linux libsecret will need an in-container `gh auth login` instead - see [`gh` credential store][gh-credential-store] below for the full picture. +The host SSH key, allowed-signers file, and `gh` config directory are mounted into the container so commits sign correctly and `gh` is pre-authenticated **when the host stores its `gh` token in a file** (`~/.config/gh/hosts.yml`). Hosts that store the token in macOS Keychain or Linux libsecret will need an in-container `gh auth login` instead. See [`gh` credential store][gh-credential-store] below for the full picture. | Host path | Container path | Mode | Purpose | | --------- | -------------- | ---- | ------- | -| `~/.ssh/id_ed25519.pub` | `/home/vscode/.ssh/id_ed25519.pub` | read-only | Public half of the SSH key. The private key never enters the container - SSH agent forwarding handles signing. | +| `~/.ssh/id_ed25519.pub` | `/home/vscode/.ssh/id_ed25519.pub` | read-only | Public half of the SSH key. The private key never enters the container, since SSH agent forwarding handles signing. | | `~/.config/git/allowed_signers` | `/home/vscode/.config/git/allowed_signers` | read-only | Maps your email to your public key so `git verify-commit` and `git log --show-signature` work inside the container. | | `~/.config/gh` | `/home/vscode/.config/gh` | read-write | `gh` CLI auth state shared with the host. See [`gh` credential store][gh-credential-store] below. | @@ -43,8 +43,8 @@ Both `devcontainer.json` files run two scripts at well-defined points: - **`onCreateCommand`** - `sudo install -d -m 700 -o vscode -g vscode /home/vscode/.ssh`. On macOS hosts the bind-mount surfaces `/home/vscode/.ssh` as root-owned, which would block writes from inside the container (e.g. `gh` updating `known_hosts`). This chown fixes it. Idempotent on Linux and WSL2. - **`postCreateCommand`** - language-specific: - - .NET: `catalog/snippets/devcontainer/dotnet/post-create.sh` - runs `dotnet tool restore` (csharpier, dotnet-outdated). - - Python: `catalog/snippets/devcontainer/python/post-create.sh` - installs the pinned `uv` and pre-syncs the Python package if present. + - .NET: `catalog/snippets/devcontainer/dotnet/post-create.sh`, which runs `dotnet tool restore` (csharpier, dotnet-outdated). + - Python: `catalog/snippets/devcontainer/python/post-create.sh`, which installs the pinned `uv` and pre-syncs the Python package if present. Re-runs of either are idempotent. No git hooks are installed by default. @@ -60,18 +60,18 @@ To force them to run again after editing a script: VS Code -> Command Palette -> | WSL2 | file (no native credential store) | | macOS | macOS Keychain | -The bind-mount of `~/.config/gh` covers the **file** case. If your host stores the token in Keychain or libsecret, the bind-mount carries the rest of `gh` config but **not the token** - the container will report "no authentication" until you either: +The bind-mount of `~/.config/gh` covers the **file** case. If your host stores the token in Keychain or libsecret, the bind-mount carries the rest of `gh` config but **not the token**, so the container will report "no authentication" until you either: 1. Re-run `gh auth login` inside the container (writes a file token to the mounted directory), or 2. Skip in-container `gh` and run those commands on the host instead. -The file-token path is slightly less secure than Keychain/libsecret because it's plaintext on disk inside `~/.config/gh/hosts.yml`. For most contributors that's an acceptable trade-off; if it isn't, use option 2. +The file-token path is slightly less secure than Keychain/libsecret because it's plaintext on disk inside `~/.config/gh/hosts.yml`. For most contributors that's an acceptable trade-off, and if it isn't, use option 2. ## Verify the Devcontainer After **Reopen in Container** finishes, run the language-appropriate checks. -**Both containers** - verify SSH signing and `gh`: +**Both containers.** Verify SSH signing and `gh`: ```shell gh auth status # logged in as you @@ -96,19 +96,19 @@ which dotnet # nothing - dotnet intentiona cd && uv sync && uv run pytest # tests pass ``` -If `git -c gpg.format=ssh commit -S` errors with `signing failed: no allowed signers`, the bind-mount of `allowed_signers` is missing or the file on the host is empty - re-run the snippet in [host setup][host-setup]. +If `git -c gpg.format=ssh commit -S` errors with `signing failed: no allowed signers`, the bind-mount of `allowed_signers` is missing or the file on the host is empty, so re-run the snippet in [host setup][host-setup]. ## Troubleshooting -**Permission denied writing to `~/.ssh/known_hosts` in the container** - The `onCreateCommand` should have chowned `~/.ssh` to `vscode`. Rebuild the container; if it persists, open a shell and run the same `sudo install -d -m 700 -o vscode -g vscode ~/.ssh` manually. +**Permission denied writing to `~/.ssh/known_hosts` in the container.** The `onCreateCommand` should have chowned `~/.ssh` to `vscode`. Rebuild the container, and if it persists, open a shell and run the same `sudo install -d -m 700 -o vscode -g vscode ~/.ssh` manually. -**`git commit` fails with "no SSH agent socket"** - VS Code Dev Containers forwards `SSH_AUTH_SOCK` automatically, but only if the host has `ssh-agent` running with at least one key. Run `ssh-add -l` on the host first; if it says "could not open a connection to your authentication agent", start the agent (see [host setup][host-setup]). +**`git commit` fails with "no SSH agent socket".** VS Code Dev Containers forwards `SSH_AUTH_SOCK` automatically, but only if the host has `ssh-agent` running with at least one key. Run `ssh-add -l` on the host first, and if it says "could not open a connection to your authentication agent", start the agent (see [host setup][host-setup]). -**uv not on `PATH` after rebuild** (Python container) - The post-create installer adds `~/.local/bin` to `PATH` via the user shell init scripts, which take effect on next shell. Either re-open the integrated terminal or `source ~/.bashrc`. +**uv not on `PATH` after rebuild** (Python container). The post-create installer adds `~/.local/bin` to `PATH` via the user shell init scripts, which take effect on next shell. Either re-open the integrated terminal or `source ~/.bashrc`. -**Container builds but extensions don't auto-install** - Make sure VS Code is using the Dev Containers extension (not "Remote - SSH" or "Remote - Tunnels"). The extension auto-install is keyed on `customizations.vscode.extensions` and only Dev Containers honors that. +**Container builds but extensions don't auto-install.** Make sure VS Code is using the Dev Containers extension (not "Remote - SSH" or "Remote - Tunnels"). The extension auto-install is keyed on `customizations.vscode.extensions` and only Dev Containers honors that. -**Wrong-language work in the wrong container** - The `.NET` container has no `uv` and no Python extensions; the Python container has no `dotnet` SDK and no C# extensions. This is intentional - use the matching container rather than installing the missing toolchain ad hoc. +**Wrong-language work in the wrong container.** The `.NET` container has no `uv` and no Python extensions, and the Python container has no `dotnet` SDK and no C# extensions. This is intentional, so use the matching container rather than installing the missing toolchain ad hoc. diff --git a/docs/repo-config-carry.md b/docs/repo-config-carry.md index 1493415..472661d 100644 --- a/docs/repo-config-carry.md +++ b/docs/repo-config-carry.md @@ -1,6 +1,6 @@ # repo-config: Carry, Apply, and Regenerate (Hub-Only) -The **process** for carrying the `repo-config/` baseline to a fleet repo, applying it, and regenerating the canonical payloads. This doc is **hub-only** - it is not carried downstream (it describes what the hub does *to* a repo, not a fact about any one repo). The carried [`repo-config/README.md`][repo-config-readme] states only the current facts about a repo's own config. This carry/apply/regen procedure lives here so it never ships into a downstream copy. +The **process** for carrying the `repo-config/` baseline to a fleet repo, applying it, and regenerating the canonical payloads. This doc is **hub-only** and is not carried downstream (it describes what the hub does *to* a repo, not a fact about any one repo). The carried [`repo-config/README.md`][repo-config-readme] states only the current facts about a repo's own config. This carry/apply/regen procedure lives here so it never ships into a downstream copy. ## Downstream Carry @@ -8,12 +8,12 @@ Every fleet repo carries the `repo-config/` directory. The hub keeps the canonic - **The payloads carry and the script does not.** A `release` repo carries `develop.json`, an `operational` repo carries `operational/develop.json` instead, and `main.json` and `settings.json` are shared by both models. `configure.sh` stays in the hub and is run from a hub checkout against the repo named on the command line, per [GOVERNANCE.md "Hub-Hosted Tooling"][governance-hub-hosted-tooling], because it holds nothing per-repo and a copy of it is only current until the next fix. A repo still holding a copy has it deleted as it is next visited. Note the split this leaves: an apply or check run from the hub reads the hub's payloads rather than the repo's own, which is the single source the model is for, and the repo's carried payloads remain what its own `AUDIT.md` diffs the live rulesets against. - **Carried files name no fleet repo as an illustrative example.** A carried file adds no template-repo reference and names no sibling fleet repo as an example (any fleet repo may be private, so such a link 404s in a public carrier, and it couples the repos). A contextually relevant link a reader of *this* repo's content needs is fine. See [GOVERNANCE.md "Documentation Style Conventions"][governance-documentation-style]. To point at a current good example, name it in the onboarding/conformance issue or the hub-only [`reports/conformance-matrix.md`][conformance-matrix]. -- **Adapted self-audit carry.** A downstream repo carries **locally adapted** `AUDIT.md` and `spec/secrets.json`, scoped to self-auditing its own rulesets, settings, and secrets against the committed `repo-config/` baseline - the standard shape, so the carried tooling is self-contained. The hub's fleet-wide audit remains authoritative. The adapted `AUDIT.md` is a settings diff, a normalized ruleset diff against the carried payloads (an operational carry swaps in `operational/develop.json`), and a names-only secrets check, all targeting the current repo - adapt this shape, don't invent. A current well-formed example is named in the onboarding/conformance issue. -- **Adapted `spec/secrets.json` shape.** The repo-scoped adaptation carries `baseline` (the App pair, which every fleet repo needs for the merge-bot) plus a `mechanisms` entry for each publish mechanism the repo actually uses, and the `targetMechanisms` routing entries for those mechanisms. **A source-only repo whose publish targets all map to a null mechanism (nothing to route) carries just `baseline` (plus a `note`)** - it omits `targetMechanisms` and `mechanisms` entirely, because a lone `targetMechanisms` map with no `mechanisms` reads as a schema bug (the audit enumerates `baseline` + `mechanisms`, never `targetMechanisms`, so an all-null routing map is dead weight). A `release` repo that uses a real mechanism (e.g. `nuget-oidc`, `docker-hub`, `codecov`) carries that `mechanisms` entry **and** its `targetMechanisms`/`typeMechanisms` routing, which the audit then picks up. +- **Adapted self-audit carry.** A downstream repo carries **locally adapted** `AUDIT.md` and `spec/secrets.json`, scoped to self-auditing its own rulesets, settings, and secrets against the committed `repo-config/` baseline, the standard shape, so the carried tooling is self-contained. The hub's fleet-wide audit remains authoritative. The adapted `AUDIT.md` is a settings diff, a normalized ruleset diff against the carried payloads (an operational carry swaps in `operational/develop.json`), and a names-only secrets check, all targeting the current repo. Adapt this shape, don't invent. A current well-formed example is named in the onboarding/conformance issue. +- **Adapted `spec/secrets.json` shape.** The repo-scoped adaptation carries `baseline` (the App pair, which every fleet repo needs for the merge-bot) plus a `mechanisms` entry for each publish mechanism the repo actually uses, and the `targetMechanisms` routing entries for those mechanisms. **A source-only repo whose publish targets all map to a null mechanism (nothing to route) carries just `baseline` (plus a `note`)**, omitting `targetMechanisms` and `mechanisms` entirely, because a lone `targetMechanisms` map with no `mechanisms` reads as a schema bug (the audit enumerates `baseline` + `mechanisms`, never `targetMechanisms`, so an all-null routing map is dead weight). A `release` repo that uses a real mechanism (e.g. `nuget-oidc`, `docker-hub`, `codecov`) carries that `mechanisms` entry **and** its `targetMechanisms`/`typeMechanisms` routing, which the audit then picks up. ## Applying the Config -**Configure by importing the JSON payloads, never by hand-building the rules** (hand reconstruction has gone wrong on past setups). The result must be **exactly two rulesets named `develop` and `main`** - the names are load-bearing (`AGENTS.md` and the workflows reference them). Only the `develop` *content* varies by model. +**Configure by importing the JSON payloads, never by hand-building the rules** (hand reconstruction has gone wrong on past setups). The result must be **exactly two rulesets named `develop` and `main`**, and the names are load-bearing (`AGENTS.md` and the workflows reference them). Only the `develop` *content* varies by model. First remove all legacy classic branch-protection rules and any stray rulesets, then run `configure.sh apply` from a hub checkout, naming the target repo and its model (the script applies `settings.json` and the Dependabot security features alongside the rulesets). Name the model rather than leaving it to the lookup. The script reads the registry beside it and resolves a repo it does not find through `defaults.workflowModel` to `release`, so an unregistered operational repo silently takes the release `develop` ruleset. The carried-payload inference the script also carries is for a run with no registry to consult, which a run from here never is: @@ -49,7 +49,7 @@ done ## Brownfield Migration (Maintainer Only) -`Require signed commits` rejects any pre-existing unsigned commit, so the first `develop -> main` release on a repo with unsigned history is blocked. Re-signing that history is a non-fast-forward that the `Block force pushes` rule rejects, **and the admin bypass does not cover `git push --force`**. Completing it requires temporarily disabling the ruleset and a maintainer force-push. This is a one-time, maintainer-performed migration that deliberately uses the force-push [GOVERNANCE.md "Git and Commit Rules"][governance-git-and-commit-rules] forbids agents from running - **an agent must never execute it - surface it to the maintainer**. Greenfield repos where signing is live before the first commit never hit this. When the rewrite touches commits committed under a bot or web-flow identity (`dependabot[bot]`, `github-actions[bot]`), set each commit's committer to the signing identity before re-signing so the committer GitHub verifies matches your key, rather than leaving your key over another identity's commit (see the history-rewrite rule in [GOVERNANCE.md "Git and Commit Rules"][governance-git-and-commit-rules]). +`Require signed commits` rejects any pre-existing unsigned commit, so the first `develop -> main` release on a repo with unsigned history is blocked. Re-signing that history is a non-fast-forward that the `Block force pushes` rule rejects, **and the admin bypass does not cover `git push --force`**. Completing it requires temporarily disabling the ruleset and a maintainer force-push. This is a one-time, maintainer-performed migration that deliberately uses the force-push [GOVERNANCE.md "Git and Commit Rules"][governance-git-and-commit-rules] forbids agents from running. **An agent must never execute it, and must surface it to the maintainer instead.** Greenfield repos where signing is live before the first commit never hit this. When the rewrite touches commits committed under a bot or web-flow identity (`dependabot[bot]`, `github-actions[bot]`), set each commit's committer to the signing identity before re-signing so the committer GitHub verifies matches your key, rather than leaving your key over another identity's commit (see the history-rewrite rule in [GOVERNANCE.md "Git and Commit Rules"][governance-git-and-commit-rules]). diff --git a/docs/ssh-signing.md b/docs/ssh-signing.md index b10dfd4..72e7d19 100644 --- a/docs/ssh-signing.md +++ b/docs/ssh-signing.md @@ -1,18 +1,18 @@ # SSH Commit Signing -This repo enforces signed commits on `main` and `develop` via branch protection. Use SSH signing - one Ed25519 key serves both authentication (push) and signing. +This repo enforces signed commits on `main` and `develop` via branch protection. Use SSH signing, since one Ed25519 key serves both authentication (push) and signing. If you haven't generated a key and configured git yet, follow [host setup][host-setup] first. ## Why SSH Signing - **One key for everything**. Same `id_ed25519` you use for `git push` also signs commits. No GPG keyring, no expirations to chase. -- **GitHub native**. GitHub treats authentication and signing keys independently but accepts the same public key for both - register it twice on the SSH and GPG keys page. +- **GitHub native**. GitHub treats authentication and signing keys independently but accepts the same public key for both, so register it twice on the SSH and GPG keys page. - **Survives rotation cleanly**. When you rotate the key, update the `allowed_signers` file and old signatures still verify against the historical entry. ## Configuration -Per-user (host) git config - set once: +Per-user (host) git config, set once: ```shell git config --global gpg.format ssh @@ -22,7 +22,7 @@ git config --global tag.gpgsign true git config --global gpg.ssh.allowedSignersFile ~/.config/git/allowed_signers ``` -The `allowed_signers` file is what `git verify-commit` consults - without it, signatures sign fine but verify as "unknown signer". Format: +The `allowed_signers` file is what `git verify-commit` consults. Without it, signatures sign fine but verify as "unknown signer". Format: ```text you@example.com namespaces="git" ssh-ed25519 AAAA... your_public_key_contents_here @@ -36,7 +36,7 @@ echo "$(git config user.email) namespaces=\"git\" $(cat ~/.ssh/id_ed25519.pub)" >> ~/.config/git/allowed_signers ``` -If you collaborate with others, append their entries to the same file - each line maps an email to a public key. +If you collaborate with others, append their entries to the same file, where each line maps an email to a public key. ## Per-OS Setup Notes @@ -49,7 +49,7 @@ systemctl --user enable --now ssh-agent.socket ssh-add ~/.ssh/id_ed25519 ``` -The agent socket lives at `$XDG_RUNTIME_DIR/ssh-agent.socket`. Make sure your shell exports `SSH_AUTH_SOCK` to point at it - most distros do this in `/etc/X11/Xsession.d` or systemd user environment. +The agent socket lives at `$XDG_RUNTIME_DIR/ssh-agent.socket`. Make sure your shell exports `SSH_AUTH_SOCK` to point at it. Most distros do this in `/etc/X11/Xsession.d` or systemd user environment. For shells without systemd integration, fall back to ad-hoc agent in `~/.bashrc` or `~/.zshrc`: @@ -60,7 +60,7 @@ if [ -z "$SSH_AUTH_SOCK" ] || ! ssh-add -l >/dev/null 2>&1; then fi ``` -WSL2 specifically: WSL inherits no agent from Windows. Run `ssh-agent` inside WSL; do not try to forward an agent from the Windows side. +WSL2 specifically: WSL inherits no agent from Windows. Run `ssh-agent` inside WSL, and do not try to forward an agent from the Windows side. ### macOS @@ -82,24 +82,24 @@ Host github.com AddKeysToAgent yes ``` -The Keychain prompt for the passphrase appears on first use after each reboot; subsequent sessions are silent. +The Keychain prompt for the passphrase appears on first use after each reboot, and subsequent sessions are silent. ### Windows (without WSL) Native Windows is **not supported** for the devcontainer setup in this repo. Use WSL2 instead. The reason: VS Code Dev Containers needs a Linux-like file system for the bind-mounts to behave consistently, and Docker Desktop's WSL2 backend is the supported path. -If you must work on Windows directly without a devcontainer, OpenSSH for Windows can sign with `gpg.format=ssh` - but the bind-mounted devcontainer setup expects Linux/WSL2 paths. +If you must work on Windows directly without a devcontainer, OpenSSH for Windows can sign with `gpg.format=ssh`, but the bind-mounted devcontainer setup expects Linux/WSL2 paths. ## Verify Signing -The `-S` flag and `-c gpg.format=ssh` override are explicit so the verification works even before `commit.gpgsign` and `gpg.format` are set globally - useful when verifying a fresh setup mid-configuration. +The `-S` flag and `-c gpg.format=ssh` override are explicit so the verification works even before `commit.gpgsign` and `gpg.format` are set globally, which is useful when verifying a fresh setup mid-configuration. ```shell git -c gpg.format=ssh commit -S --allow-empty -m "verify-signing" git log --show-signature -1 ``` -Expected output includes `Good "git" signature for `. If you see `error: gpg.ssh.allowedSignersFile needs to be configured` or `No signature`, walk back through the host setup - most often `allowed_signers` is missing the entry, or the `user.signingkey` and `gpg.ssh.allowedSignersFile` configs aren't set yet. +Expected output includes `Good "git" signature for `. If you see `error: gpg.ssh.allowedSignersFile needs to be configured` or `No signature`, walk back through the host setup. Most often `allowed_signers` is missing the entry, or the `user.signingkey` and `gpg.ssh.allowedSignersFile` configs aren't set yet. ## Inside the Devcontainer @@ -109,17 +109,17 @@ The container picks up: - The `~/.ssh/id_ed25519.pub` and `~/.config/git/allowed_signers` files via bind-mount declared in `devcontainer.json`. - The forwarded SSH agent socket from `SSH_AUTH_SOCK`, so signing happens with the host's loaded private key without the private key ever entering the container. -If the container's `~/.ssh` directory exists with the wrong owner (root, surfaced by macOS bind-mount semantics), `gh auth login` writes to `~/.ssh/known_hosts` may fail. The `onCreateCommand` in `devcontainer.json` chowns the directory to `vscode` to fix this - see [devcontainer setup][devcontainer] for the rationale. +If the container's `~/.ssh` directory exists with the wrong owner (root, surfaced by macOS bind-mount semantics), `gh auth login` writes to `~/.ssh/known_hosts` may fail. The `onCreateCommand` in `devcontainer.json` chowns the directory to `vscode` to fix this. See [devcontainer setup][devcontainer] for the rationale. ## Troubleshooting -**`gpg.ssh.allowedSignersFile needs to be configured`** - Set `git config --global gpg.ssh.allowedSignersFile ~/.config/git/allowed_signers` and ensure the file exists. +**`gpg.ssh.allowedSignersFile needs to be configured`.** Set `git config --global gpg.ssh.allowedSignersFile ~/.config/git/allowed_signers` and ensure the file exists. -**`signing failed: no allowed signers`** - The `allowed_signers` file exists but doesn't contain a line matching `user.email` + a key. Re-run the `echo $(git config user.email) namespaces="git" $(cat ~/.ssh/id_ed25519.pub) >> ...` snippet. +**`signing failed: no allowed signers`.** The `allowed_signers` file exists but doesn't contain a line matching `user.email` + a key. Re-run the `echo $(git config user.email) namespaces="git" $(cat ~/.ssh/id_ed25519.pub) >> ...` snippet. -**Verifies on the host but not in the container** - The bind-mount source path differs. `${localEnv:HOME}` resolves on Linux/macOS hosts; on Windows hosts (WSL2 backend) the `${localEnv:USERPROFILE}` fallback in `devcontainer.json` handles it. Check the actual mount with `mount | grep ssh` inside the container. +**Verifies on the host but not in the container.** The bind-mount source path differs. `${localEnv:HOME}` resolves on Linux/macOS hosts, and on Windows hosts (WSL2 backend) the `${localEnv:USERPROFILE}` fallback in `devcontainer.json` handles it. Check the actual mount with `mount | grep ssh` inside the container. -**SSH agent says "could not open a connection"** - The host's agent isn't running. Linux: `systemctl --user start ssh-agent.socket`. macOS: open a new terminal so launchd starts the agent. +**SSH agent says "could not open a connection".** The host's agent isn't running. Linux: `systemctl --user start ssh-agent.socket`. macOS: open a new terminal so launchd starts the agent. diff --git a/docs/token-cost.md b/docs/token-cost.md index a949391..405698b 100644 --- a/docs/token-cost.md +++ b/docs/token-cost.md @@ -19,7 +19,7 @@ A session's context grows monotonically and every request re-reads all of it at | Output | $528 | 10.4% | | Fresh input | $9 | 0.2% | -**Output verbosity is not the lever.** Output was 0.30% of prompt volume and 10.4% of cost, so instructing an agent to write less prose targets a tenth of the bill at best. The cache is also working correctly at a 98.4% read share - the problem is the volume being re-read, not the hit rate. +**Output verbosity is not the lever.** Output was 0.30% of prompt volume and 10.4% of cost, so instructing an agent to write less prose targets a tenth of the bill at best. The cache is also working correctly at a 98.4% read share. The problem is the volume being re-read, not the hit rate. ## The Root Cause: Sessions Outliving Their Task @@ -63,9 +63,9 @@ Two measured attributions. Both **overlap** with the counterfactual above rather | `CODESTYLE.md` | 56 | $32 | | `AUDIT.md` | 27 | $17 | -Median 199 requests remained in the session after such a read, p90 6,005, and only 36.9% of all reads used a range. `AGENTS.md` was **not** auto-loaded by any harness - the session baseline was 33,000 to 37,000 tokens with none of its text present - so the whole cost came from explicit whole-file reads. +Median 199 requests remained in the session after such a read, p90 6,005, and only 36.9% of all reads used a range. `AGENTS.md` was **not** auto-loaded by any harness (the session baseline was 33,000 to 37,000 tokens with none of its text present), so the whole cost came from explicit whole-file reads. -The rule text could not simply be cut: 16 of 18 sections were declared `fidelity: verbatim` and carried byte-identically to the fleet. So the file was split instead - `AGENTS.md` from 87,457 to 7,789 bytes as a router, with the rule text in [`GOVERNANCE.md`][governance] - and reading one section now costs about 3,200 bytes against 87,457. +The rule text could not simply be cut: 16 of 18 sections were declared `fidelity: verbatim` and carried byte-identically to the fleet. So the file was split instead (`AGENTS.md` from 87,457 to 7,789 bytes as a router, with the rule text in [`GOVERNANCE.md`][governance]), and reading one section now costs about 3,200 bytes against 87,457. ### GitHub orchestration: $938, 27% of the cache-read bill diff --git a/spec/fidelity-model.md b/spec/fidelity-model.md index c76acb4..bd56a25 100644 --- a/spec/fidelity-model.md +++ b/spec/fidelity-model.md @@ -4,7 +4,7 @@ How faithfully each carried unit must survive the carry, and how that is verifie ## The Fixed and the Overridable -Carried content is a class with virtual functions. The **fixed** part is the interface - when a thing is invoked, what it is named, and where it is wired. The **overridable** part is the implementation body, which a repo replaces to fit its own targets. Validation must allow the override while detecting a change to the interface or to content meant to stay fixed. Integrity is by **content hash, never a version number** - a version stamp is a claim a repo can keep while editing the body, so it is never trusted for detection. +Carried content is a class with virtual functions. The **fixed** part is the interface: when a thing is invoked, what it is named, and where it is wired. The **overridable** part is the implementation body, which a repo replaces to fit its own targets. Validation must allow the override while detecting a change to the interface or to content meant to stay fixed. Integrity is by **content hash, never a version number**. A version stamp is a claim a repo can keep while editing the body, so it is never trusted for detection. ## The Four Fidelity Levels @@ -12,14 +12,14 @@ Each [`spec/files.json`][files] entry declares one `fidelity`, defaulting to `pr - **presence** - the unit exists (a file, or a Markdown section heading). The audit's baseline check. - **intent** - carried faithfully but judged by meaning, not bytes. A downstream copy legitimately differs (a governed divergence or a paraphrase), and equivalence is a human call via `intentRef`. The audit asserts nothing beyond presence. -- **verbatim** - byte-identical to the hub's canonical after line-ending, action-pin, and job-needs normalization. The audit content-hashes the downstream copy against canonical. It applies to a whole file, a workflow job region (a job selected by key), or a Markdown section region (a `## heading` block selected by name). The section granularity lets one file be **intent overall while a few of its sections are verbatim** - a universal rule block stays byte-identical fleet-wide even though the rest of the document is a repo-adapted paraphrase, so a stale section or a missing rule is caught while its heading still passes the presence check. +- **verbatim** - byte-identical to the hub's canonical after line-ending, action-pin, and job-needs normalization. The audit content-hashes the downstream copy against canonical. It applies to a whole file, a workflow job region (a job selected by key), or a Markdown section region (a `## heading` block selected by name). The section granularity lets one file be **intent overall while a few of its sections are verbatim**. A universal rule block stays byte-identical fleet-wide even though the rest of the document is a repo-adapted paraphrase, so a stale section or a missing rule is caught while its heading still passes the presence check. - **interface** - an overridable body that must honor a named contract. The audit checks the contract by name and wiring, never the body. Fidelity is a declared field defaulting to `presence`, never inferred from `whole`/`placeholders`. `.editorconfig` and `.markdownlint-cli2.jsonc` are both whole with no placeholders yet sit at opposite fidelity, because the discriminator is governance, not field shape. ## Why Each Unit Sits Where It Does -- **verbatim** - `.markdownlint-cli2.jsonc` (fleet-generic, no governed divergence), the `github-release` job region of the release task (the canonical orchestration a repo must not fork), and the universal rule sections of `AGENTS.md` - fleet-law with no repo-specific content (no SHAs, no `ptr727/` references), where a paraphrase or a missing rule is a defect, not an adaptation. Which `AGENTS.md` sections are verbatim and which stay intent (the few that describe one repo) is defined in [section-model.md][section-model]. +- **verbatim** - `.markdownlint-cli2.jsonc` (fleet-generic, no governed divergence), the `github-release` job region of the release task (the canonical orchestration a repo must not fork), and the universal rule sections of `AGENTS.md`, which are fleet-law with no repo-specific content (no SHAs, no `ptr727/` references) and where a paraphrase or a missing rule is a defect, not an adaptation. Which `AGENTS.md` sections are verbatim and which stay intent (the few that describe one repo) is defined in [section-model.md][section-model]. - **interface** - the release and PR workflows. Their fixed contract is the job and check names plus the artifact handoff, while the leaf build jobs are owned. See the override seam in [`GOVERNANCE.md`][governance]. - **intent** - `.editorconfig` and `.gitattributes` (the `[*] end_of_line` default and path pins vary by platform), `cspell.json` (the words list and file scope vary), `CODESTYLE.md` / `WORKFLOW.md` / `AUDIT.md` / `.github/copilot-instructions.md` (carried docs judged by meaning), and the ruleset payloads (whose live state is diffed separately). - **presence** - `README.md`, `HISTORY.md`, `.gitignore`, and the per-repo config that only needs to exist. @@ -30,11 +30,11 @@ The fixed interface of a workflow is stated in [`GOVERNANCE.md`][governance] ("O ## Normalization -A verbatim check compares content by hash after **line-ending, action-pin, and job-needs normalization** - EOL variance is governed by the line-ending rules, a `uses: @` pin (with its trailing `# vN` comment) is Dependabot-owned and bumped per repo, and a job's `needs:` list is pruned per repo to its vendored targets (an unvendored name fails the workflow to load), so all three are governed drift rather than a fidelity deviation. This keeps a verbatim workflow job region (the `github-release` job) from flagging on a routine action bump or a legitimate needs-prune while still catching a real structural fork. It does **not** mask placeholders: a verbatim unit carries none. The files that declare a `placeholders` list (for example `.github/copilot-instructions.md` with ``, ``, ``) are fidelity `intent`, judged by hand and never hashed. Masking could not serve a hash anyway - a downstream copy holds the substituted value (`ptr727`), not the token (``), so masking the token in the canonical alone would guarantee a mismatch. A verbatim unit that ever needed a per-repo substitution would require template-matching (the canonical as a pattern, the copy as an instance), not this content hash. None does today. +A verbatim check compares content by hash after **line-ending, action-pin, and job-needs normalization**. EOL variance is governed by the line-ending rules, a `uses: @` pin (with its trailing `# vN` comment) is Dependabot-owned and bumped per repo, and a job's `needs:` list is pruned per repo to its vendored targets (an unvendored name fails the workflow to load), so all three are governed drift rather than a fidelity deviation. This keeps a verbatim workflow job region (the `github-release` job) from flagging on a routine action bump or a legitimate needs-prune while still catching a real structural fork. It does **not** mask placeholders: a verbatim unit carries none. The files that declare a `placeholders` list (for example `.github/copilot-instructions.md` with ``, ``, ``) are fidelity `intent`, judged by hand and never hashed. Masking could not serve a hash anyway. A downstream copy holds the substituted value (`ptr727`), not the token (``), so masking the token in the canonical alone would guarantee a mismatch. A verbatim unit that ever needed a per-repo substitution would require template-matching (the canonical as a pattern, the copy as an instance), not this content hash. None does today. ## Stale Versus Modified -A verbatim mismatch is one of two things, told apart **by hash, not by a version**. The audit hashes each past revision of the hub's canonical from its own git history. If the downstream copy matches a **past** canonical revision, the base advanced and the copy is **stale** - re-vendor it. If it matches **no** revision the base ever produced, the repo **modified fixed content** - review it. A version stamp could claim to be current while being neither, so it is demoted to a human-facing label and never consulted for integrity. +A verbatim mismatch is one of two things, told apart **by hash, not by a version**. The audit hashes each past revision of the hub's canonical from its own git history. If the downstream copy matches a **past** canonical revision, the base advanced and the copy is **stale**, so re-vendor it. If it matches **no** revision the base ever produced, the repo **modified fixed content**, so review it. A version stamp could claim to be current while being neither, so it is demoted to a human-facing label and never consulted for integrity. [audit]: ./audit.py diff --git a/spec/readme-structure.md b/spec/readme-structure.md index 96e5912..2d5a070 100644 --- a/spec/readme-structure.md +++ b/spec/readme-structure.md @@ -1,6 +1,6 @@ # README Structure -The preferred `README.md` shape for a fleet project. The audit's `readme-structure` dimension checks a repo's README against this: the sections below, in this order (to the letter where a section applies, to intent where a project legitimately has nothing to say). A section that does not apply to a project type is N/A, not a defect (a library has no running-app "Installation"; a source-only repo has no "Usage"). +The preferred `README.md` shape for a fleet project. The audit's `readme-structure` dimension checks a repo's README against this: the sections below, in this order (to the letter where a section applies, to intent where a project legitimately has nothing to say). A section that does not apply to a project type is N/A, not a defect (a library has no running-app "Installation", and a source-only repo has no "Usage"). ## Sections and Order diff --git a/spec/scope-model.md b/spec/scope-model.md index 3747fcd..ae89fd4 100644 --- a/spec/scope-model.md +++ b/spec/scope-model.md @@ -4,10 +4,10 @@ How every governance rule is scoped, so the carried docs are granular single-sco ## Two Axes -A rule has a physical home, and - if it is a repo rule - a reach. +A rule has a physical home, and a repo rule has a reach as well. -- **Axis A, home.** A rule lives on the **host** (per-machine, `~/.claude`, `host-setup/` - it loads in every session regardless of repo and covers ad-hoc work outside any project) or in the **repo** (it travels with a repo and can assume repo context). A rule that must hold in both places is stated in both and kept in sync deliberately, because the populations differ - the write-safety rules are the worked example, living in the host `~/.claude/CLAUDE.md` and the carried `AGENTS.md` at once. -- **Axis B, reach** (repo rules only). A repo rule is **hub-only** (meaningful only in this coordinator repo - the registry, the spec, the audit, fleet coordination), **all-downstream** (every derived repo), or **type-specific** (only repos matching a selector). Hub-only rules are simply absent from the carried baseline. All-downstream and type-specific rules are carried, gated by an `appliesTo` selector. +- **Axis A, home.** A rule lives on the **host** (per-machine, `~/.claude`, `host-setup/`, loading in every session regardless of repo and covering ad-hoc work outside any project) or in the **repo** (it travels with a repo and can assume repo context). A rule that must hold in both places is stated in both and kept in sync deliberately, because the populations differ. The write-safety rules are the worked example, living in the host `~/.claude/CLAUDE.md` and the carried `AGENTS.md` at once. +- **Axis B, reach** (repo rules only). A repo rule is **hub-only** (meaningful only in this coordinator repo: the registry, the spec, the audit, fleet coordination), **all-downstream** (every derived repo), or **type-specific** (only repos matching a selector). Hub-only rules are simply absent from the carried baseline. All-downstream and type-specific rules are carried, gated by an `appliesTo` selector. ## Selectors @@ -20,14 +20,14 @@ A selector is one token from one of four **disjoint** namespaces. Because the na | release trigger | `two-phase` `publish-on-merge` `dispatch-only` `none` | [`registry/repos.schema.json`][repos-schema] | | consumer model | `push` `pull` | [`registry/repos.schema.json`][repos-schema] | -A repo's **selector set** is its `types` plus its `workflowModel`, `releaseTrigger`, and `consumerModel`. `workflowModel` and `releaseTrigger` resolve as the repo value, then `defaults`, then the fleet default (`release`, `two-phase`). `consumerModel` has no fleet default - [`spec/validate.py`][validate] requires it on every cataloged repo, so a cataloged repo always contributes one. `validate.py` also enforces that every `appliesTo` token resolves to a known selector and that no project type collides with a reserved token, and [`spec/audit.py`][audit] resolves the set in `repo_selectors`. +A repo's **selector set** is its `types` plus its `workflowModel`, `releaseTrigger`, and `consumerModel`. `workflowModel` and `releaseTrigger` resolve as the repo value, then `defaults`, then the fleet default (`release`, `two-phase`). `consumerModel` has no fleet default. [`spec/validate.py`][validate] requires it on every cataloged repo, so a cataloged repo always contributes one. `validate.py` also enforces that every `appliesTo` token resolves to a known selector and that no project type collides with a reserved token, and [`spec/audit.py`][audit] resolves the set in `repo_selectors`. ## appliesTo Semantics `appliesTo` appears on a [`spec/files.json`][files] entry (which files a repo carries) and, per the section-object form in [`spec/files.schema.json`][files-schema], on an individual `sections` element (which sections within a carried file apply). - **`*`** means all repos. -- A list is **disjunctive (any-of)**: `["csharp", "operational"]` reads "csharp OR operational". Cross-axis **AND is not expressible**, and that is deliberate - a single-scope piece carries one selector, so the need for AND is the signal to split the piece further, not to write a two-token entry. +- A list is **disjunctive (any-of)**: `["csharp", "operational"]` reads "csharp OR operational". Cross-axis **AND is not expressible**, and that is deliberate. A single-scope piece carries one selector, so the need for AND is the signal to split the piece further, not to write a two-token entry. - Entry-level and section-level `appliesTo` compose with **AND**: a section applies only if its file is carried by the repo *and* the section's own selector matches. ## Documenting a Whole-Carried File's Section Scopes diff --git a/spec/type-model.md b/spec/type-model.md index 7d4db59..b1d09e6 100644 --- a/spec/type-model.md +++ b/spec/type-model.md @@ -1,28 +1,28 @@ # Project Type Model -Companion to [section-model.md][section-model] and [fidelity-model.md][fidelity-model]. Those define how carried *content* is verified. This one defines how a repo's **types** - what it is built from and for - are declared, validated, and checked. It is the ground truth an agent or human consults before adding a type, a profile, or a type check, not a judgment re-derived each session. +Companion to [section-model.md][section-model] and [fidelity-model.md][fidelity-model]. Those define how carried *content* is verified. This one defines how a repo's **types** (what it is built from and for) are declared, validated, and checked. It is the ground truth an agent or human consults before adding a type, a profile, or a type check, not a judgment re-derived each session. **Rollout status.** This model is being wired in stages. Where a rule below names a check `audit.py` does not yet run, or a schema field not yet defined, this doc is the contract that implementation realizes. ## Declaration is the source of truth -A repo's types are **declared** in its [registry/repos.json][repos] entry (`types`), and a language type may also declare a **profile** (below). The audit runs the checks for each declared type plus the cross-cutting dimensions. Declaration - not inference - is authoritative: the registry states what the repo *is*, and [project-types.json][types] holds each type's requirements and checks. +A repo's types are **declared** in its [registry/repos.json][repos] entry (`types`), and a language type may also declare a **profile** (below). The audit runs the checks for each declared type plus the cross-cutting dimensions. Declaration, not inference, is authoritative: the registry states what the repo *is*, and [project-types.json][types] holds each type's requirements and checks. This mirrors the fleet principle that *the registry is ground truth about reality, not intent* (the `setup.driftnotes.current` check, over the registry `driftNotes` field): a declaration is a claim about the repo that must match what the repo actually contains. ## Detection validates, it does not classify -Each type in `project-types.json` carries `detect` patterns (files or markers that evidence the type). Detection is a **validator and a discovery aid**, never an auto-classifier - it checks declarations against reality and surfaces candidates, but it does not decide a repo's types on its own. The two axes give four cases: +Each type in `project-types.json` carries `detect` patterns (files or markers that evidence the type). Detection is a **validator and a discovery aid**, never an auto-classifier. It checks declarations against reality and surfaces candidates, but it does not decide a repo's types on its own. The two axes give four cases: | | detected | not detected | | --- | --- | --- | -| **declared** | consistent - the checks run | **false declaration** - a finding (e.g. `cpp` declared, no C/C++ files present) | -| **not declared** | **discovery advisory** - declare it or mark it ignored | nothing to do | +| **declared** | consistent, so the checks run | **false declaration**, a finding (e.g. `cpp` declared, no C/C++ files present) | +| **not declared** | **discovery advisory**, so declare it or mark it ignored | nothing to do | The undeclared-but-detected advisory has three honest resolutions, all explicit intent, never silent: -- **declare** it, so its checks run; -- **ignore** it - an explicit suppression carrying a reason (the driftNote shape), for a language deliberately not tracked (vendored third-party code, an incidental snippet); +- **declare** it, so its checks run. +- **ignore** it with an explicit suppression carrying a reason (the driftNote shape), for a language deliberately not tracked (vendored third-party code, an incidental snippet). - leave it as a standing advisory until decided. A false declaration is always a finding: a claim the repo does not back is drift, the same way a stale driftNote is. @@ -34,19 +34,19 @@ A language type is present at one of two **depths**, declared as its `profile`: - **build** - the language is compiled, tested, and/or packaged in this repo. Its full check set applies (style, type-check, tests, coverage, packaging). - **lint-only** - the language is present and style-checked here, but not built: there is no build/test/package for it in this repo. Only its lint/style/type-check checks apply. Build, test, coverage, and packaging checks are N/A. -Each check may declare the **minimum profile** it needs via a `minProfile` field. A check without one applies at every profile, and a check with `minProfile: build` applies only at `build`. So lint/style/type-check checks omit it, while build/test/coverage/package checks set `build`. The audit uses the declared profile to hold the coverage requirement - the CODECOV_TOKEN secret and the codecov.yml file - N/A for a lint-only language, replacing the older per-check "N/A for the SCRIPTS profile" prose. +Each check may declare the **minimum profile** it needs via a `minProfile` field. A check without one applies at every profile, and a check with `minProfile: build` applies only at `build`. So lint/style/type-check checks omit it, while build/test/coverage/package checks set `build`. The audit uses the declared profile to hold the coverage requirement (the CODECOV_TOKEN secret and the codecov.yml file) as N/A for a lint-only language, replacing the older per-check "N/A for the SCRIPTS profile" prose. -The profile is **declared and validated**, not merely detected. `python` already reads its shape structurally from `pyproject.toml` (a uv PROJECT with tests and a lockfile, versus stdlib SCRIPTS tooling). That structural read becomes the profile **validator** - a declared `python` profile that contradicts the pyproject shape is a false declaration. One concept (the declared profile), checked by detection, rather than two ways to classify. +The profile is **declared and validated**, not merely detected. `python` already reads its shape structurally from `pyproject.toml` (a uv PROJECT with tests and a lockfile, versus stdlib SCRIPTS tooling). That structural read becomes the profile **validator**. A declared `python` profile that contradicts the pyproject shape is a false declaration. One concept (the declared profile), checked by detection, rather than two ways to classify. ### Consequence for cross-cutting checks -A cross-cutting check that presumes a built, tested language must respect the profile. In particular the coverage checks - the `CODECOV_TOKEN` secret and the `codecov.yml` file presence - are **profile-aware**: they are N/A for a language whose declared profile has no tests. A lint-only language must never manufacture a coverage finding. +A cross-cutting check that presumes a built, tested language must respect the profile. In particular the coverage checks (the `CODECOV_TOKEN` secret and the `codecov.yml` file presence) are **profile-aware**: they are N/A for a language whose declared profile has no tests. A lint-only language must never manufacture a coverage finding. ## Languages Language types carry the style and type-check requirements for their language, gated by profile. A language that is only ever linted in the fleet is `lint-only` by nature and defines no build/test/package checks: -- **cpp** - C/C++ present for style only. The check of record is **clang-format** (a shared config driving the editor, the CLI, and CI, a `parity.lang` arm), feeding the operational lint CI. Deeper semantic and static analysis is intentionally out of scope here - for a codegen or config repo the C++ is scaffolded and completed by its downstream toolchain (an ESPHome compile), which does the compilation-time checking, and clang-tidy would need a compile database the repo does not have. A repo's `.h` is read as C++ by context (Arduino/ESPHome), since the extension alone is ambiguous. +- **cpp** - C/C++ present for style only. The check of record is **clang-format** (a shared config driving the editor, the CLI, and CI, a `parity.lang` arm), feeding the operational lint CI. Deeper semantic and static analysis is intentionally out of scope here. For a codegen or config repo, the C++ is scaffolded and completed by its downstream toolchain (an ESPHome compile), which does the compilation-time checking, and clang-tidy would need a compile database the repo does not have. A repo's `.h` is read as C++ by context (Arduino/ESPHome), since the extension alone is ambiguous. ## Generators