diff --git a/.editorconfig b/.editorconfig index 1cb9cf82..aa6be5c3 100644 --- a/.editorconfig +++ b/.editorconfig @@ -13,12 +13,12 @@ # Root config root = true -# Defaults - CRLF is the default; only the LF exceptions below are declared. Most LF pins are shared with -# `.gitattributes` (git-enforced): `*.sh`, the husky pre-commit shebang, `spec/validate.py`, `uv.lock`, and Dockerfiles. -# The workflow-YAML pins (`.github/workflows/*` and the catalog snippet workflows) are `.editorconfig`-only - -# git stays passive on them (`* -text`) and CI (editorconfig-checker) enforces LF. Keep the `[*]` -# `end_of_line = crlf` default - it is required for the Windows-GUI/WSL-engine -# workflow (Windows tooling misbehaves on LF) and every uncovered file type relies on it. +# Defaults: CRLF is the default, and only the LF exceptions below are declared. +# Most LF pins are shared with `.gitattributes`, which git enforces: `*.sh`, the husky pre-commit shebang, `spec/validate.py`, `uv.lock`, and Dockerfiles. +# The workflow-YAML pins (`.github/workflows/*` and the catalog snippet workflows) are `.editorconfig`-only. +# For those, git stays passive (`* -text`) and CI (editorconfig-checker) enforces LF. +# Keep the `[*]` `end_of_line = crlf` default, which the Windows-GUI and WSL-engine workflow requires because Windows tooling misbehaves on LF. +# Every uncovered file type relies on that default too. [*] charset = utf-8 end_of_line = crlf @@ -39,17 +39,19 @@ indent_size = 2 [*.{yml,yaml}] indent_size = 2 -# Workflow YAML is LF: Dependabot and Actions rewrite it with LF, so declaring LF keeps it consistent instead of -# mixed. git still leaves endings alone (`* -text`); this and CI (editorconfig-checker) enforce it. Other YAML is CRLF. +# Workflow YAML is LF, because Dependabot and Actions rewrite it with LF, so declaring LF keeps it consistent instead of mixed. +# Endings are still left alone by git (`* -text`), and this file plus CI (editorconfig-checker) enforce it. +# Other YAML stays CRLF. [.github/workflows/*.{yml,yaml}] end_of_line = lf -# Catalog snippet workflows mirror real workflow files (LF above); keep the snippets LF so a copied snippet -# lands compliant instead of needing conversion. +# Catalog snippet workflows mirror the real workflow files pinned LF above. +# Keep the snippets LF, so a copied snippet lands compliant instead of needing conversion. [catalog/snippets/workflows/*.{yml,yaml}] end_of_line = lf -# The husky pre-commit snippet is a shebang script; keep it LF so a copied snippet runs (a CRLF shebang breaks). +# The husky pre-commit snippet is a shebang script, so keep it LF. +# A copied snippet then runs, where a CRLF shebang would break it. [catalog/snippets/husky/pre-commit] end_of_line = lf @@ -57,42 +59,41 @@ end_of_line = lf [*.sh] end_of_line = lf -# Python is CRLF by the `[*]` default (universal newlines; commonly edited on Windows). Pin LF -# only for a `.py` executed directly via its shebang, by path - here the CI validation entry point -# and the fleet-audit runner. +# Python is CRLF by the `[*]` default, since universal newlines accept it and it is commonly edited on Windows. +# Pin LF only for a `.py` executed directly via its shebang, by path. +# Here that is the CI validation entry point and the fleet-audit runner. [spec/{validate,audit,fidelity_honesty}.py] end_of_line = lf -# The agent-safety kit's Python is shebang-executable tooling run by path (the PreToolUse hook and its -# installer), so pin LF for the same reason as the entry points above - a CRLF shebang breaks direct -# execution on a Unix host. +# The agent-safety kit's Python is shebang-executable tooling run by path, namely the PreToolUse hook and its installer. +# Pin LF for the same reason as the entry points above, because a CRLF shebang breaks direct execution on a Unix host. [host-setup/agent-safety/*.py] end_of_line = lf -# The repo lint/review scripts are shebang-executable tooling run by path, so a CRLF shebang would -# break direct execution on a Unix host. Pinned in .gitattributes to match. +# The repo lint and review scripts are shebang-executable tooling run by path, so a CRLF shebang would break direct execution on a Unix host. +# They are pinned in .gitattributes to match. [scripts/*.py] end_of_line = lf -# uv regenerates uv.lock with LF on every platform, so pin it or an EOL check (editorconfig-checker/CI) -# reds on every `uv lock`/`uv sync` until the file is manually reconverted - same rationale as the -# shebang/Dockerfile pins (a tool owns the ending). A Python repo on the CRLF default carries this; a repo -# with no lockfile is unaffected. +# The uv tool regenerates uv.lock with LF on every platform, so pin it here. +# Without the pin an EOL check (editorconfig-checker or CI) fails on every `uv lock` or `uv sync` until the file is manually reconverted. +# The rationale matches the shebang and Dockerfile pins, where a tool owns the ending. +# A Python repo on the CRLF default carries this, and a repo with no lockfile is unaffected. [uv.lock] end_of_line = lf -# Dockerfiles - CRLF breaks RUN heredocs and line continuations +# Dockerfiles are LF, because CRLF breaks RUN heredocs and line continuations. [{Dockerfile,*.Dockerfile}] end_of_line = lf -# --- .NET-only below: C# and ReSharper style. Everything above is the line-ending -# governance every derived repo carries; a non-.NET repo may drop from here down. --- +# .NET-only below, covering C# and ReSharper style. +# Everything above is the line-ending governance every derived repo carries, and a non-.NET repo may drop from here down. # C# files [*.cs] -# Suppressions follow CODESTYLE.md "Analyzer Diagnostics and Suppressions": prefer a -# [SuppressMessage] attribute or the owning project's .editorconfig; relax a rule -# repo-wide here only when it applies to every project (never a brownfield batch). +# Suppressions follow CODESTYLE.md "Analyzer Diagnostics and Suppressions". +# Prefer a [SuppressMessage] attribute, or the owning project's .editorconfig. +# Relax a rule repo-wide here only when it applies to every project, never for a brownfield batch. dotnet_diagnostic.IDE0055.severity = none csharp_indent_block_contents = true csharp_indent_braces = false diff --git a/.gitattributes b/.gitattributes index 0285142c..08224a0a 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,23 +1,23 @@ -# Default: do not normalize line endings (`* -text`); .editorconfig end_of_line rules guide what the editor writes. -# The exception pins below are git's own enforcement - they force LF for execution-sensitive classes regardless of editor. -# git config --global core.autocrlf false -# git add --renormalize . -# git ls-files --eol +# Default: git does not normalize line endings (`* -text`), and .editorconfig end_of_line rules guide what the editor writes. +# The exception pins below are git's own enforcement, forcing LF for execution-sensitive classes regardless of editor. +# Configure with: git config --global core.autocrlf false +# Renormalize with: git add --renormalize . +# Inspect with: git ls-files --eol * -text -# Exception: scripts must stay LF regardless of the `* -text` default - a CRLF shebang breaks execution. `.editorconfig` -# covers `*.sh`, but extensionless executables match no extension rule, so pin them here so git enforces LF on checkout -# and `--renormalize`. Any repo whose tooling ships extensionless scripts adds the matching path pin, e.g. s6-overlay -# init `Docker/s6-overlay/** text eol=lf` or husky/git hooks `.husky/pre-commit text eol=lf`. +# Exception: scripts must stay LF regardless of the `* -text` default, because a CRLF shebang breaks execution. +# `.editorconfig` covers `*.sh`, but an extensionless executable matches no extension rule. +# Pin those here, so git enforces LF on checkout and on `--renormalize`. +# Any repo whose tooling ships extensionless scripts adds the matching path pin. +# The s6-overlay init is `Docker/s6-overlay/** text eol=lf`, and a husky or git hook is `.husky/pre-commit text eol=lf`. *.sh text eol=lf # The husky pre-commit snippet is an extensionless shebang script (like a copied .husky/pre-commit). catalog/snippets/husky/pre-commit text eol=lf -# Vanilla `.py` follows the CRLF default - Python's universal newlines accept CRLF, and it is -# commonly edited on Windows. Pin LF only for a `.py` executed directly via its shebang, by path - -# here the CI validation entry point, the fleet-audit runner, the agent-safety hook and its -# installer, and the repo lint/review scripts with their tests. Do not re-add a blanket -# `*.py text eol=lf`. +# Vanilla `.py` follows the CRLF default, since Python's universal newlines accept CRLF and it is commonly edited on Windows. +# Pin LF only for a `.py` executed directly via its shebang, by path. +# Those are the CI validation entry point, the fleet-audit runner, the agent-safety hook and its installer, and the repo lint and review scripts with their tests. +# Do not re-add a blanket `*.py text eol=lf`. spec/validate.py text eol=lf spec/audit.py text eol=lf spec/fidelity_honesty.py text eol=lf @@ -30,10 +30,11 @@ scripts/test_prose_lint.py text eol=lf scripts/test_repo_gate.py text eol=lf scripts/test_pr_review.py text eol=lf -# uv regenerates uv.lock with LF on every platform; pin it so git enforces LF on checkout/renormalize and a -# CRLF-default repo does not fight the tool on every `uv lock`/`uv sync`. A repo with no lockfile is unaffected. +# The uv tool regenerates uv.lock with LF on every platform, so pin it here. +# That makes git enforce LF on checkout and renormalize, so a CRLF-default repo does not fight the tool on every `uv lock` or `uv sync`. +# A repo with no lockfile is unaffected. uv.lock text eol=lf -# Dockerfiles must be LF - a CRLF breaks RUN heredocs and line continuations. +# Dockerfiles must be LF, because a CRLF breaks RUN heredocs and line continuations. Dockerfile text eol=lf *.Dockerfile text eol=lf diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index f7bfe16f..18848560 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -18,7 +18,7 @@ Summarized for VS Code's generators. The full rules, rationale, and examples are ## Reviewing Carried Fleet Content -Several of this repository's governance files are carried from a shared template and kept in sync across a fleet of sibling repositories, among them `AGENTS.md`, `CODESTYLE.md`, `WORKFLOW.md`, this file, and the `repo-config/` rulesets. Most of `AGENTS.md` is universal fleet law: every section that states a rule, as opposed to the two that describe this repository's own directory tree and devcontainer, is byte-locked and verified by an automated byte-for-byte match against the template canonical, not by line-by-line review. +Several of this repository's governance files are carried from a shared template and kept in sync across a fleet of sibling repositories, among them `AGENTS.md`, `CODESTYLE.md`, `WORKFLOW.md`, this file, and the `repo-config/` rulesets. Most of `GOVERNANCE.md` is universal fleet law: every section that states a rule, as opposed to the two that describe this repository's own directory tree and devcontainer, is byte-locked and verified by an automated byte-for-byte match against the template canonical, not by line-by-line review. `AGENTS.md` is the thin router and carries two byte-locked sections of its own, with no repository-specific ones. Two constraints follow when reviewing that content. diff --git a/AUDIT.md b/AUDIT.md index bac5e335..7f2e0551 100644 --- a/AUDIT.md +++ b/AUDIT.md @@ -117,6 +117,10 @@ A repo is **operational** only if every applicable check passes. A single applic Write `reports//audit.md` from [`reports/_template.md`][template]: a dimension x {letter, intent, verdict, evidence} table with `file:line` citations (WORKFLOW.md 5A style), a drift section, and a list of proposed registry/spec updates (e.g. a resolved `classificationPending`). Rank findings most severe first. +**The hub authors the report, and a downstream repo does not open a pull request against the hub to write its own.** `reports/` is the hub's evidence that it audited a repo, so a report written by the repo being audited is a claim rather than evidence, and the hub cannot adopt one without checking it. Checking the judgment dimensions **is** the audit, since confirming a verdict like "analyzers enforced" means reading the same files the audit reads, so a submitted report saves only the writing up and not the work. A submitted report is also stale by construction, because it is a snapshot of one hub revision arriving at a later one, and its claims then have to be reconciled against findings that did not exist when it was written. + +What a downstream repo does instead is unchanged and is where its context is worth most: it **files findings about the hub as issues**, which is the opposite of self-certification and is how several hub defects have been found, and it **applies fixes to its own repo** per section 10. Any pull request it does open against the hub follows the same branching model as every other, targeting `develop` and never `main`. + **Findings are a point-in-time snapshot. Stamp them and re-verify before acting.** [`spec/audit.py`][audit-runner] prints a run stamp (`audit run | hub `) and, per repo, the exact commit it read (`@ @`). Anything derived from a run (a report, and especially an **onboarding or conformance issue**) quotes that stamp, so a reader can tell whether it still applies. A convergence issue is generated from the audit, never composed by hand: `spec/audit.py --issue ` emits a ready-to-file title and body from that repo's live findings (grouped into must-fix, converge, and could-not-verify), so the issue content cannot drift from what the audit actually found and regenerates as the repo changes. **Verify a convergence before it is promoted with `--branch`.** `spec/audit.py --branch ` reads that ref instead of the repo's registry `groundTruthBranch`, so a repo can audit its own `develop` while the work is still in flight rather than discovering the gaps after `main` has moved. The registry is not edited, the run is still read-only, and the run stamp names the override so a finding cannot be mistaken for one against ground truth. A ref that does not resolve is a single error naming it, never a baseline's worth of file-absent letters. diff --git a/CODESTYLE.md b/CODESTYLE.md index fd8642d8..dfdfaa9f 100644 --- a/CODESTYLE.md +++ b/CODESTYLE.md @@ -33,9 +33,10 @@ Each language defines a **clean-compile** verification: the combination of build These apply repo-wide, in every directory: -1. **Markdown linting**: All `.md` files must be lint-clean (error and warning free) via the VS Code `markdownlint` extension. [`.markdownlint-cli2.jsonc`][markdownlint-cli2] at the repo root is the single source of truth, and the davidanson `markdownlint` extension and a command-line `markdownlint-cli2` run both read it, so the IDE and CLI stay in lock-step. Rules it deliberately disables (e.g. `MD013` line-length) are **intentional**, so do not "fix" them. `MD033` inline HTML stays **enabled**: HTML comments are permitted (markdownlint does not flag them), HTML elements are flagged, and anything with a native markdown equivalent uses the markdown. Fix violations at the source rather than disabling rules. +1. **Markdown linting**: All `.md` files must be lint-clean (error and warning free) via the VS Code `markdownlint` extension. [`.markdownlint-cli2.jsonc`][markdownlint-cli2] at the repo root is the single source of truth, and the davidanson `markdownlint` extension and a command-line `markdownlint-cli2` run both read it, so the IDE and CLI stay in lock-step. Rules it deliberately disables (e.g. `MD013` line-length) are **intentional**, so do not "fix" them. `MD033` inline HTML stays **enabled**: HTML comments are permitted (markdownlint does not flag them), `details` and `summary` are allowed because a GitHub collapsible has no markdown equivalent, every other element is flagged, and anything with a native markdown equivalent uses the markdown. Fix violations at the source rather than disabling rules. 2. **Spelling**: All spelling must be clean via the CSpell VS Code integration, and words must be correctly spelled in **US English** (the repo-wide convention, per [GOVERNANCE.md][governance]). The shared `cspell.json` sets `"language": "en-US"` so British spellings are flagged, where a bare `"en"` accepts both US and British and silently passes the wrong spelling. Project-specific terms go in the shared `cspell.json` `words` list, the single source of truth the extension, CLI, and CI all read. The `.code-workspace` must **not** carry its own `cspell.words`/`cSpell.words` block, and when externalizing words into `cspell.json`, delete any word list left in the workspace (a leftover one duplicates the list and silently drifts). 3. **Spelling CI scope**: The enforced CI spell-check gate covers **`README.md` and `HISTORY.md` only**, because these are the files every repo visitor sees, so they must be clean. It is deliberately **not** all `**/*.md`: repos carry many markdown files full of technical terms, and gating every one of them would mean endlessly padding `cspell.json` just to keep CI green. Broad, live spell-checking across any file (source, markdown, text) is the **cspell editor extension's** job, so typos still surface to whoever is editing. A repo owner **may** widen their own CI file list, but README + HISTORY are the default; keep the CI workflow, the `Lint: Spelling` VS Code task, and the GOVERNANCE.md cspell one-liner on the same file list. The list is explicit (not a glob), so a repo that ships no `HISTORY.md` (e.g. one with no changelog) must drop it from all three surfaces and gate on `README.md` alone, since cspell errors on a listed file that does not exist. Markdown *linting* (item 1) stays repo-wide `**/*.md`, which does not choke on technical terms. +4. **`HISTORY.md` mirrors the README opening**: `HISTORY.md` is the maintainer-curated changelog and opens as the README's twin, carrying the same `# ` (without the README's ToC-omit comment) and the same intro paragraph copied verbatim, then a `## Release History` section. The mirrored opening keeps the project identity consistent for a reader who lands on the changelog directly. The audit checks that the title and intro match the README, with HTML comments stripped. ## .NET diff --git a/OPERATIONS.md b/OPERATIONS.md new file mode 100644 index 00000000..30e98be3 --- /dev/null +++ b/OPERATIONS.md @@ -0,0 +1,106 @@ +# Operations + +How this repository is run. It ships no application code, so its operations are the fleet audit, the local gates that mirror CI, and the script that applies repository configuration. + +## Runbooks + +### Run the gates the way CI runs them + +CI passes explicit `--check` lists, and a bare `python3 scripts/prose_lint.py [file]` runs `DEFAULT_RULES`, which omits `comment-wrap` and `comment-case`. A bare run therefore under-reports against what CI checks, and a clean result from it proves less than it appears to. Run the CI invocations: + +```sh +python3 scripts/test_prose_lint.py +python3 scripts/test_repo_gate.py +python3 scripts/test_pr_review.py +python3 spec/audit.py --selftest +python3 scripts/repo_gate.py +python3 scripts/prose_lint.py . --check charset --check dupword --check spelling +python3 scripts/prose_lint.py . --check charset-unknown --check semicolon --check dash --check comment-wrap --check comment-case --summary +for f in registry/*.json spec/*.json repo-config/*.json; do jq empty "$f"; done +python3 spec/validate.py +docker run --rm --pull=always -v "$PWD":/check --workdir /check mstruebing/editorconfig-checker:latest +``` + +Two 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. + +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. + +The first prose invocation gates. The second reports the backlog that is corrected as each file is next edited, 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. + +Scope a run to what changed, which matches the correct-as-next-edited rule: + +```sh +python3 scripts/prose_lint.py . --diff origin/develop +``` + +Whole-tree discovery reads only files git tracks, so `python3 scripts/prose_lint.py .` and `--diff` do not see a new file until it is staged, and a clean whole-tree run proves nothing about an unstaged one. An explicit path is always read, tracked or not, so name a new file directly to check it before staging. + +### Audit the fleet + +```sh +python3 spec/audit.py # every cataloged repo +python3 spec/audit.py [RepoName] # one repo +python3 spec/audit.py --issue [RepoName] +``` + +Findings are a point-in-time snapshot read live over the API. Re-run before acting on one, and quote the run stamp in any issue derived from it. The deterministic subset lives here, and the full letter-and-intent verdict is [AUDIT.md](./AUDIT.md). + +### Apply or verify repository configuration + +```sh +repo-config/configure.sh check [owner/repo] [release|operational] +repo-config/configure.sh apply [owner/repo] [release|operational] +``` + +**Always pass the command.** A bare `repo-config/configure.sh` with no arguments defaults to `apply` against the current repo, so an invocation meant to test whether the script runs performs a live write instead. Never run it without a command. + +`check` is read-only and exits non-zero on drift. `apply` is idempotent and drives entirely from the committed payloads, so it is a no-op on a conformant repo. + +`apply` is not a narrow toggle. One run patches every key in `repo-config/settings.json`, sets the default branch, enables both Dependabot features, and creates or updates both branch rulesets. On a repository that has deliberately drifted it silently reasserts the fleet configuration. + +The model argument selects which develop payload is applied, so passing the wrong one applies the wrong ruleset. + +## Backup and Recovery + +The repository is the record, and GitHub holds it. Nothing here keeps state outside git. + +A deleted branch is recoverable from any full clone that still has the commit, which is the recovery path when a branch is deleted while another pull request is based on it: + +```sh +git push origin [sha]:refs/heads/[branch] +``` + +Never use `--depth 1` on a clone that will amend or force-push, because a shallow clone severs the merge base and orphans the branch. + +## Logs and Debugging + +Workflow runs are the log. `gh run list --branch [branch]` and `gh run view [id] --log-failed` reach them. + +A local gate reproduces a CI failure exactly, because CI runs the same commands listed under Runbooks against the same committed configuration. Reproduce locally before reading workflow logs. + +## Tool Usage + +The Docker linters pull `:latest` deliberately, so a local run matches whatever CI resolved: + +```sh +docker run --rm --pull=always -v "$PWD":/workdir --workdir /workdir davidanson/markdownlint-cli2:latest "**/*.md" +docker run --rm --pull=always -v "$PWD":/workdir --workdir /workdir ghcr.io/streetsidesoftware/cspell:latest --no-progress README.md HISTORY.md +``` + +Both commands are the canonical invocations from [GOVERNANCE.md](./GOVERNANCE.md). markdownlint reads every markdown file, while cspell reads `README.md` and `HISTORY.md` only. That narrower spelling scope is deliberate, since gating every markdown file would mean padding `cspell.json` with technical terms without end, and broad live spell-check is the editor extension's job. Widening it here produces noise that no gate acts on. + +The `editorconfig-checker` action is setup-only. Using it alone silently skips the check, so CI invokes the checker itself rather than relying on the action. + +Two `gh` limitations on the current host, both worked around rather than fixed: + +- `gh pr checks` carries no `--json` flag on the installed `gh` 2.46.0, so a watcher built on it prints nothing and a quiet result reads as a passing one. Read the checks from `gh pr view --json statusCheckRollup` instead. +- `gh pr edit --base` fails with a Projects-classic deprecation error. Use `gh api --method PATCH repos/[owner/repo]/pulls/[number] -f base=[branch]` instead. + +## Configuration Layout + +- [spec/](./spec/) is the machine-readable ground truth, holding project types, the file and section baseline, and required or forbidden secrets. +- [registry/repos.json](./registry/repos.json) is the fleet registry, naming every project with its types, publish mechanism, and status. +- [repo-config/](./repo-config/) holds the branch rulesets and the apply script. It sits outside `.github/`, which is Actions-owned. +- [catalog/](./catalog/) holds reference snippets the audit compares implementations against. +- [reports/](./reports/) holds per-repo audit output. +- [scripts/](./scripts/) holds the gates that run in CI and locally. diff --git a/README.md b/README.md index ffd9c874..c7f62d52 100644 --- a/README.md +++ b/README.md @@ -4,8 +4,8 @@ Agent enablement for a fleet of repositories: autonomy and repeatable quality in ## Build and Distribution <!-- omit from toc --> -- **Source Code**: [GitHub][projecttemplate-link] - source, issues, discussions, and CI/CD pipelines. -- **Versioned Releases**: [GitHub Releases][releases-link] - version-tagged source archives. +- **Source Code**: [GitHub][projecttemplate-link] for source, issues, discussions, and CI/CD pipelines. +- **Versioned Releases**: [GitHub Releases][releases-link] for version-tagged source archives. ### Build Status <!-- omit from toc --> @@ -20,7 +20,7 @@ Agent enablement for a fleet of repositories: autonomy and repeatable quality in ### Release Notes <!-- omit from toc --> -**Version: 2.0**: +**Version**: 2.0 **Summary**: @@ -91,10 +91,10 @@ Keeping a fleet of repositories consistent has always been a tax paid in review ProjectTemplate follows the same model it documents, and audits its own rules against itself (it classifies as the source-only project type in [WORKFLOW.md][workflow]). -- **Branching.** Persistent `main` and `develop`, each with its own ruleset. This repo uses the default `release` workflow model: commit on feature branches only, feature branch to `develop` is squash-merged, `develop` to `main` is a merge commit, and `develop` is forward-only (no `main -> develop` back-merges). Live-service config repos instead use the `operational` model (registry `workflowModel`) - direct signed commits to `develop`, promoted to `main` by an occasional PR. See [GOVERNANCE.md "Branching Model"][governance-branching-model]. -- **CI is lint-only.** There is no build or unit test; the PR gate runs markdownlint, cspell, JSON validation (`jq` parses `registry/`, `spec/`, and `repo-config/`, plus the `spec/validate.py` cross-reference and shape checks), and actionlint, and exposes the ruleset-bound `Check pull request workflow status job` aggregator. The same lint configs (`.markdownlint-cli2.jsonc`, `cspell.json`) drive the editor extensions, the CLI, and CI. +- **Branching.** Persistent `main` and `develop`, each with its own ruleset. This repo uses the default `release` workflow model: commit on feature branches only, feature branch to `develop` is squash-merged, `develop` to `main` is a merge commit, and `develop` is forward-only (no `main -> develop` back-merges). Live-service config repos instead use the `operational` model (registry `workflowModel`), with direct signed commits to `develop`, promoted to `main` by an occasional PR. See [GOVERNANCE.md "Branching Model"][governance-branching-model]. +- **CI is lint-only.** There is no build or unit test. The PR gate runs markdownlint, cspell, JSON validation (`jq` parses `registry/`, `spec/`, and `repo-config/`, plus the `spec/validate.py` cross-reference and shape checks), and actionlint, and exposes the ruleset-bound `Check pull request workflow status job` aggregator. The same lint configs (`.markdownlint-cli2.jsonc`, `cspell.json`) drive the editor extensions, the CLI, and CI. - **Review loop.** Every PR is reviewed by GitHub Copilot, and the agent drives the review loop to green and merges only with explicit maintainer permission. See [GOVERNANCE.md "PR Review Etiquette"][governance-pr-review-etiquette]. -- **Release.** A `develop -> main` merge is promoted through a GitHub release (tag plus a source zip, README, and LICENSE); versioning is NBGV-driven from [version.json][version]. See [WORKFLOW.md][workflow]. +- **Release.** A `develop -> main` merge is promoted through a GitHub release (tag plus a source zip, README, and LICENSE). Versioning is NBGV-driven from [version.json][version]. See [WORKFLOW.md][workflow]. ## Rules @@ -103,23 +103,23 @@ A human-readable index of the rules agents enforce, implement, and audit. The au ### Always - Sign every commit (SSH or GPG). -- Branch feature -> develop (squash) -> main (merge commit); develop is forward-only. +- Branch feature -> develop (squash) -> main (merge commit), and develop is forward-only. - Drive every PR through the Copilot review loop and merge only with maintainer approval. - Write US English and ASCII only (no em-dash, straight quotes). -- Write docs and comments in the present tense, describing only the current state - never as a change from a prior one. +- Write docs and comments in the present tense, describing only the current state, never as a change from a prior one. - Keep comments concise and only for the non-obvious, and never grow them on edit. - Follow `.editorconfig` line endings (CRLF default, LF for shell and Docker) and preserve a file's endings on edit. - One logical paragraph per line, with a trailing `\` for an intentional hard break. - Pin every GitHub Action to a commit SHA with a version comment. - Share one lint config per tool across the editor, the CLI, and CI. - Run the repo's whole lint gate before pushing, not just the parts that look relevant. -- Make gates fail loud - a gate that stops gating must error or annotate, never pass silently. +- Make gates fail loud, since a gate that stops gating must error or annotate, never pass silently. - Favor VS Code tasks and launch configs for building, running, and testing over ad-hoc shell scripts. ### Never - Never force-push or rewrite shared history. -- Never treat a merge as a release; publishing is a separate, explicit step. +- Never treat a merge as a release. Publishing is a separate, explicit step. - Never blanket-delete a workflow run's artifacts. - Never store a static key when OIDC Trusted Publishing is available. @@ -129,11 +129,11 @@ A human-readable index of the rules agents enforce, implement, and audit. The au ### If a Python Project -- Configure ruff and a type checker in `pyproject.toml` - pyright strict, or mypy in CI with pyright editor-only; whichever runs in CI is the gate. +- Configure ruff and a type checker in `pyproject.toml`, either pyright strict or mypy in CI with pyright editor-only. Whichever runs in CI is the gate. ### If Both C# and Python -- Both sections above apply; a repo can be both (a C# app plus a Python subtree). The Python is either a full uv project (`uv.lock`, `uv run`) or a stdlib-only `uvx` scripts subtree (no `uv.lock`, `pyproject.toml` carries lint/type config only). See [CODESTYLE.md][codestyle] "Two profiles". +- Both sections above apply, and a repo can be both (a C# app plus a Python subtree). The Python is either a full uv project (`uv.lock`, `uv run`) or a stdlib-only `uvx` scripts subtree (no `uv.lock`, `pyproject.toml` carries lint/type config only). See [CODESTYLE.md][codestyle] "Two profiles". ### If Publishing a Package (NuGet or PyPI) diff --git a/STANDUP.md b/STANDUP.md index a5b57c10..70958f6f 100644 --- a/STANDUP.md +++ b/STANDUP.md @@ -89,10 +89,10 @@ Copy every [`spec/files.json`][files] entry whose `appliesTo` matches the repo's - [`CODESTYLE.md`][codestyle]: the repo's language and formatting conventions beyond the carried rules. - `ARCHITECTURE.md`: how a code repo is built, its module layout, data flow, and design decisions. -- `OPERATIONS.md`: how an operational repo is run, covering runbooks, backup, log and debug procedures, tool-usage notes, and config layout. +- `OPERATIONS.md`: how the repo is run, under the headings `Runbooks`, `Backup and Recovery`, `Logs and Debugging`, `Tool Usage`, and `Configuration Layout`. - `TODO.md`: the repo's running backlog, per [`spec/readme-structure.md`][readme-structure]. It keeps open work out of the README's section order, where it does not belong and changes on a different cadence from everything around it. -**`OPERATIONS.md` is required on an `operational` repo**, not optional, so it appears in the baseline above with `appliesTo: ["operational"]`. It is presence-checked only, the same footing as `README.md` and `HISTORY.md`, so its content is entirely the repo's own and a repo with little to say still carries the file. It is the operational-repo analogue of `ARCHITECTURE.md`, and it is where an `AGENTS.md` split puts the repo-specific half, so real runbooks (a deploy procedure, a rollback, a retention policy, a credential rotation) go there rather than into a carried file. It is agent-instruction content, so it takes the inline-link exception the markdown rules name rather than the reference-style default. `ARCHITECTURE.md` and `TODO.md` stay advisory and are required by no selector, so a repo with nothing to say in one carries no file rather than an empty one. +**`OPERATIONS.md` is required on every repo**, not optional, so it appears in the baseline above with `appliesTo: "*"`. It is presence-checked only, the same footing as `README.md` and `HISTORY.md`, so its content is entirely the repo's own and a repo with little to say still carries the file as a stub, meaning those five headings with no content under them, for which this repo's own `OPERATIONS.md` is the worked example. Do not read the `operational` workflow model into the requirement, because that selector describes where config lives rather than whether the repo has runbooks, and a repo that publishes to a package registry or deploys a site has operational surface under either model. It is the operational analogue of `ARCHITECTURE.md`, and it is where an `AGENTS.md` split puts the repo-specific half, so real runbooks (a deploy procedure, a rollback, a retention policy, a credential rotation) go there rather than into a carried file. It is agent-instruction content, so it takes the inline-link exception the markdown rules name rather than the reference-style default. `ARCHITECTURE.md` and `TODO.md` stay advisory and are required by no selector, so a repo with nothing to say in one carries no file rather than an empty one. Choose the destination while scaffolding rather than after. Repo-specific content left in a carried file is drift, which the audit lists as an undeclared section to reconcile, and reconciling it later means moving prose that downstream readers have already started trusting in the wrong place. diff --git a/TODO.md b/TODO.md index 43e5b4ad..47b192ea 100644 --- a/TODO.md +++ b/TODO.md @@ -2,7 +2,8 @@ Running backlog for this repo, kept in a committed file so the guidance survives across environments where agent memory does not. -- Populate [reports/][reports] for the cataloged repos that still have no audit, since a registry `status` of `cataloged` asserts a result that only a committed report evidences. Eight repos have one. +- Populate [reports/][reports] for the cataloged repos that still have no audit, since a registry `status` of `cataloged` asserts a result that only a committed report evidences. Eight repos have one. This is paced by maintainer capacity rather than blocked on anything, since repos are brought up to spec as they are worked on, so the entry records the outstanding set rather than a defect. +- Revisit automating the audit, which was explored and deliberately deferred, recorded here so the reasoning is not re-derived from scratch. Three shapes were considered: a scheduled hub-driven audit publishing each report as a workflow artifact, the same thing committing the report back the way the codegen bot updates its own files, and a pull-request hook in each downstream repo that audits itself against the current hub. Three things blocked all of them. Until the fleet reaches stasis with every repo onboarded, a scheduled run reports mostly noise, since a repo mid-onboarding is expected to be non-conformant. The hub has to be stable before downstreams can audit against it, because a hub change lands as fleet-wide findings the same day. And the downstream half is a catch-22, since a self-auditing pull-request hook is CI instrumentation the repos that most need it do not yet carry. The agreed outcome was the on-demand audit that [`AUDIT.md`][audit-doc] describes today. Worth reopening once the fleet is onboarded and the hub goes a stretch without carried-content changes, and the artifact shape is the one to try first, since it produces evidence without committing anything and so cannot generate review load while the noise level is still unknown. - Canonicalize Python linter-config placement on `pyproject.toml` (one cataloged repo uses standalone `.ruff.toml` + `pyrightconfig.json`), track as a drift finding, fix downstream. - Consider renaming this repo to reflect the audit-catalog identity (updates badge and link URLs across the fleet). - Adopt the OCI annotation keys (`org.opencontainers.image.*`) for Docker image metadata across the Docker repos, replacing the ad-hoc and `org.label-schema.*` labels (from #363). @@ -11,11 +12,21 @@ Running backlog for this repo, kept in a committed file so the guidance survives - Refresh the README (it has gone stale) and evaluate a lower-maintenance structure, for example a per-section index that points into each doc with a one-line description, keeping the README as the adoption and audit-instruction entry point with pointers to the other docs. A per-section index trades brevity for a sync obligation: it must track what the docs contain. - Add a linter-only Python project type for codegen/boilerplate Python, code that runs during another tool's build to emit generated source (e.g. ESPHome codegen that produces enriched C++ at compile time), so it ships no unit tests and no coverage and needs only the linter. Keep it distinct from the existing `python` type, which is utility code that can and should carry unit tests and coverage (as in PlexCleaner). Until it exists, ESPHome-Config stays `source-only` and its `+python` reclassification is deferred, so accept its one outstanding validation finding meanwhile. - Add a fleet-standard clang-format config for the `cpp` type: a catalog snippet plus a CODESTYLE C++ section defining the style, the C++ analogue of the shared ruff config, so the `cpp` clang-format check references one canonical style rather than each repo inventing its own. Base it on the ESPHome-Config agent's proposed `.clang-format`. -- Sweep the 13 `dash` and `semicolon` findings in `README.md`. They are deferred rather than dropped, because two changes to that file are in flight and a third overlapping edit would conflict with both for no gain. -- Clean the comment shape in `.editorconfig`, `.gitattributes` and `.gitignore` (44 `comment-wrap` and `comment-case` findings). These are the first files every new repo copies, so until they are fixed a new repo learns the shape the rules forbid. -- Sweep the 54 `comment-wrap` and `comment-case` findings in `repo-config/configure.sh`. It is carried `verbatim`, so a downstream copy is byte-matched and cannot fix them locally, which makes this the hub's whole-class sweep rather than a next-edit correction. +- Give [`STANDUP.md`][standup] an explicit branch-bootstrap step, because it currently says nothing about how `main` and `develop` come into being and an agent therefore commits onto whichever branch it finds. The sequence that avoids every cleanup problem is: create `main` and leave it carrying nothing, create `develop` from `main` and leave it carrying nothing, create the first feature branch from `develop` and do the whole standup there, add the repo to GitHub and apply the repo config while still on that branch, then open a normal pull request to `develop` when the repo is ready. Nothing ever has to be cleaned off `main` or `develop`, because nothing ever reached them without review. Note the mechanical wrinkle when writing this up: a git branch cannot exist without a commit, so "carrying nothing" means exactly one signed empty root commit (`git commit --allow-empty`), and section 0's signing window applies to that commit like any other. The alternative of committing onto `develop` and squashing afterwards does not work: `non_fast_forward` is set on both `develop` payloads, so the history cannot be rewritten without disabling the ruleset, and Blog was correctly blocked when it tried. Worth stating that the protection is uneven, since a `release` repo's `repo-config/develop.json` carries a `pull_request` rule that blocks a direct commit outright, while `repo-config/operational/develop.json` carries only `deletion`, `non_fast_forward` and `required_signatures`, so on an operational repo a direct commit succeeds and only the instruction stands between the agent and an unfixable history. The reason it matters on a public repo is that the exploratory standup history is where PII, secrets and noise commits accumulate, and squashing a feature branch is the one chance to leave them out. +- Declare the canonical short description in [`registry/repos.json`][repos] instead of deriving it by parsing the README, which makes every check and every push deterministic. Today [`spec/audit.py`][audit] extracts the intro line after the H1 and compares the GitHub About panel and the Docker Hub short description against it, so the source of truth lives inside prose. That forces anything wanting the value to parse markdown, and PhotoCleaner#32 shows the cost, since a workflow step that reads the intro at publish time needs nine guards against headings, block quotes, all four list markers, ordered lists, HTML, tables, code, links and the length cap, and every one of them fails the release rather than the tagline. Adding a `description` field turns the README intro into a third mirror rather than the source, so the audit compares all three against one declared value, the Docker workflow pushes a literal with no parser, and `repo-config/configure.sh` can set the GitHub About panel from the same field the way it already sets every other repository setting. The 100-character cap stays, since Docker Hub's short description is still the tightest surface. Add the field to `registry/repos.schema.json` as well, which sets `additionalProperties: false`, and make it optional at first so the audit falls back to the README intro while repos adopt it one at a time. +- Rework [`spec/readme-structure.md`][readme-structure] to match the hand-crafted PlexCleaner README, which is the shape the maintainer wants, and make the result auditable rather than advisory. Four concrete divergences are already identified, measured against PlexCleaner `README.md`, this repo's `README.md`, and the current spec. First, the distribution bullet is labeled by deliverable: PlexCleaner ships executables and calls the channel **Binary Releases**, while the spec fixes the label as **Versioned Releases** for every repo, so the label belongs in a per-channel table rather than as one string. Second, the license shield sits in the top **Build Status** block here and at the very bottom of PlexCleaner, inside a closing `## License` section that reads `Licensed under the [MIT License]` followed by the shield, immediately before the link definitions. Third, the Release Notes section closes with `See Release History for complete release notes and older versions.` in PlexCleaner against `See Release History for the full history.` here, and the PlexCleaner form is the wanted one. Note that PlexCleaner writes that link inline, which the reference-style rule forbids, so adopt the wording and keep the reference form. Fourth, the channel bullets and their shields vary by deliverable, meaning GitHub binaries, Docker Hub, NuGet, and PyPI each carry a different bullet label and a different shield set, which is what a per-type table has to encode for the `readme-structure` audit dimension to check a repo against its own declared types. +- Decide whether the canonical README section order follows PlexCleaner, which is a separate question from the four divergences above and affects every repo plus the `readme-structure` audit. PlexCleaner places **Questions or Issues** immediately after the Table of Contents, where the spec orders it ninth, and it carries sections the spec names nowhere, including Performance Considerations, Runtime Metrics, Custom Plugins, Testing, Development Tooling, Feature Ideas, and Sample Media Files. Under the recurrence rule in [`spec/section-model.md`][section-model] those last ones are correctly repo-specific and stay undeclared, so the open question is only the position of the sections the spec already names. +- Declare locally-required secrets the way GitHub-stored ones are already declared, and make a gitignored `secrets/` directory the fleet standard that holds them. [`spec/secrets.json`][secrets] covers only the Actions and Dependabot stores, so a repo that deploys somewhere has no declared way to say what it needs at runtime, and the required set is discoverable only by reading the deploy. The pattern already runs in the fleet in two shapes: HomeAutomation-Config keeps a gitignored secrets directory of env files and Docker secret files, and ESPHome-Config keeps a gitignored `secrets.yaml` beside a committed `_secrets.yaml`. The committed file carries the required names with dummy values, so the shape of the requirement is in git while the values never are, which is the same split the GitHub side already gets from `requiredSecrets[]`. Blog needs it immediately, since it deploys on the proxmox host through HomeAutomation-Config's Docker Compose stack and carries the copy destinations and the internal URI. The hub carries neither the directory nor a `.gitignore` entry for one today, so adopting it here comes first. +- Re-vendor `repo-config/configure.sh` across the fleet. The hub swept it to one sentence per line, and it is carried `verbatim` with `appliesTo: "*"`, so every repo already holding a copy is byte-mismatched against the hub until it takes the new one. <!-- Repo --> +[audit]: ./spec/audit.py +[audit-doc]: ./AUDIT.md [matrix]: ./reports/conformance-matrix.md +[readme-structure]: ./spec/readme-structure.md [reports]: ./reports/ +[repos]: ./registry/repos.json +[secrets]: ./spec/secrets.json +[section-model]: ./spec/section-model.md +[standup]: ./STANDUP.md diff --git a/WORKFLOW.md b/WORKFLOW.md index 10f22837..df2ffff3 100644 --- a/WORKFLOW.md +++ b/WORKFLOW.md @@ -150,7 +150,7 @@ The required behaviors, organized by domain. Each is a **MUST**, stated as input ### D2 - Input/State Validation at Entry - **D2.1 Validate before expensive work.** Output: a dedicated entry job/step asserts each cross-input/derived-state invariant and fails fast before builds. Downstream jobs `needs:` it. -- **D2.2 Release branch matches version classification.** Input: a real (non-smoke) release build. Output: the gate fails loudly if the default branch carries a prerelease suffix **or** a non-default branch carries none. It strips `+buildmetadata` before testing for the prerelease `-` (only a core/prerelease `-` counts), and it is **skipped on smoke** (a detached PR head always versions as prerelease). *Prevents: a non-default leg published as stable; a build-metadata false-positive; the gate blocking every default-base promotion PR.* +- **D2.2 Release branch matches version classification.** Input: a real (non-smoke) release build. Output: the gate fails loudly if the default branch carries a prerelease suffix **or** a non-default branch carries none. It strips `+buildmetadata` before testing for the prerelease `-` (only a core/prerelease `-` counts), and on a smoke build the **check exits early while the job still reports success** (a detached PR head always versions as prerelease). Read that as the validation being skipped rather than the job, because a job-level `if:` would skip the job itself, and a dependent skips with it unless that dependent opts out with `if: always()` and reads the result explicitly, the way the PR aggregator does. `github-release` carries `validate-release` in `needs:` and does **not** opt out, so a job-level skip there would couple the release to smoke through a second path on top of the `if:` it already carries. *Prevents: a non-default leg published as stable; a build-metadata false-positive; the gate blocking every default-base promotion PR.* - **D2.3 Publish only from main or develop.** Input: a dispatch publish. Output: a dispatch from any ref other than `main` or `develop` fails fast. *Prevents: cutting a release from an unintended branch.* - **D2.4 Mutually-exclusive / paired inputs are validated.** Input: a workflow with either/or or must-pair inputs (e.g. the docker-readme task's `repositories` XOR `manifest`+`manifest-jq`). Output: a half-filled or conflicting combination fails fast. *Prevents: a silent fall-through.* diff --git a/registry/repos.json b/registry/repos.json index ac81a910..4f467859 100644 --- a/registry/repos.json +++ b/registry/repos.json @@ -250,7 +250,7 @@ "requiredSecrets": ["DOCKER_HUB_USERNAME", "DOCKER_HUB_ACCESS_TOKEN", "CODECOV_TOKEN"], "consumerModel": "pull", "releaseTrigger": "two-phase", - "driftNotes": ["Mirrors PlexCleaner's publish shape minus the python subtree: multi-arch Docker (linux/amd64,linux/arm64 on main) pushed to Docker Hub via the static DOCKER_HUB_USERNAME/DOCKER_HUB_ACCESS_TOKEN secret, plus a github-release attaching the multi-arch executables as a 7z. Release is two-phase (workflow_dispatch + weekly Mon 02:00 schedule).", "Fully onboarded 2026-07 (rulesets, repo-config/, version.json, WORKFLOW.md, AUDIT.md, dependabot.yml, and the release pipeline all live). The reports/photocleaner/audit.md snapshot predates the docker/release wiring."] + "driftNotes": ["Multi-arch Docker (linux/amd64,linux/arm64 on main) pushed to Docker Hub via the static DOCKER_HUB_USERNAME/DOCKER_HUB_ACCESS_TOKEN secret, plus a github-release attaching the multi-arch executables as a 7z. Release is two-phase (workflow_dispatch + weekly Mon 02:00 schedule). First release 1.0.9 published 2026-07-23.", "Baseline onboarding completed 2026-07 (rulesets, repo-config/, version.json, WORKFLOW.md, AUDIT.md, dependabot.yml, and the release pipeline all live). Not conformance-complete: see reports/photocleaner/audit.md for the open defects.", "Private for now by maintainer decision, going public once the conformance work settles, since public serves prospective users and lowers the GitHub bill. Until that flip the declared github-release channel is not consumer-pullable and shields.io cannot read the repo, so README.md carries Docker Hub shields only and gains the GitHub build and release shields when it goes public."] }, { "name": "MediaTools", diff --git a/reports/conformance-matrix.md b/reports/conformance-matrix.md index 30d92422..957c4429 100644 --- a/reports/conformance-matrix.md +++ b/reports/conformance-matrix.md @@ -11,6 +11,7 @@ The primary shapes are stood up as whole repos; the **composable targets** (`nug | Shape | Reference repo | Cold-standup | Last audited | First gap / notes | |---|---|---|---|---| | `python` + `source-only` | Financial-Modeling | not-tested | - | Reference for the source-release (dispatch-only) profile; the downstream standup issue is open. | +| `source-only` + `release` | Blog | not-tested | - | Hugo static site (#456), the next standup and the first cold test of `STANDUP.md` step 0. Its deploy target has no declared type yet, so `releaseTrigger` is `none` and `publish[]` is empty until a deploy has actually run. | | `csharp` + `console` | - | not-tested | - | | | `csharp` + `docker` | - | not-tested | - | | | `csharp` + `python` | PlexCleaner | not-tested | - | First mixed-language shape (#339). Python is a stdlib-only `uvx` **scripts** profile subtree (`RegressionTests/`): no `uv.lock`, `pyproject.toml` lint/type config only, mypy checker, `python.uvlock.pinned` + `python.coverage.codecov` N/A; `codecov.yml` stays required for the C# side. Both language rule-sets apply (CODESTYLE.md "Two profiles"). | @@ -19,7 +20,7 @@ The primary shapes are stood up as whole repos; the **composable targets** (`nug | `upstream-wrapper` | - | not-tested | - | Tag from a committed state file, not SemVer2. | | `codegen` | - | not-tested | - | Deterministic matrix over both branches. | | `docs` | ProjectTemplate | not-tested | - | Governance hub; CI is lint-only. | -| `operational` config | - | not-tested | - | `workflowModel: operational`, direct signed commits to `develop`, promotion-PR gate. Carries a required `OPERATIONS.md` (`appliesTo: ["operational"]`, presence-checked) for its runbooks. Blog (#456) is the next standup and the first cold test of `STANDUP.md` step 0. | +| `operational` config | - | not-tested | - | `workflowModel: operational`, direct signed commits to `develop`, promotion-PR gate. Its `develop` ruleset carries no `pull_request` rule, so the branch discipline rests on the instruction rather than the gate. | ## Composable Targets diff --git a/reports/photocleaner/audit.md b/reports/photocleaner/audit.md index b5a5f36f..7b8ebaa1 100644 --- a/reports/photocleaner/audit.md +++ b/reports/photocleaner/audit.md @@ -1,44 +1,69 @@ # Audit: PhotoCleaner -- **Audited branch:** main (`3b33b98956190b91b8ed1d3e49e8242545ea523c`) +- **Audited branch:** main (`15b9b5b7411bbba43b8bff8044c4b651355c10a4`) - **Types:** csharp, console, docker (from registry) - **Verdict:** not operational -- **Date:** 2026-07-23 -- **Run stamp:** `audit run 2026-07-23T13:51:20Z | hub 614a291` +- **Date:** 2026-08-01 +- **Run stamp:** `audit run 2026-08-01T15:30:19Z | hub 6501479` + +Supersedes the 2026-07-23 snapshot, which predated the release pipeline. Everything that snapshot listed as a defect has landed: both rulesets are live, `repo-config/` is carried, and the publisher cut release `1.0.9` on 2026-07-23 with the multi-arch image and the executable 7z attached. What remains is a different set, created mostly by the hub advancing rather than by the repo regressing. ## Develop Drift -`develop` vs `main`: ahead 1, behind 3 by commit count - **benign**. The three `main`-only commits are `develop -> main` promotion merge commits whose trees match their develop-side parents, an ancestry gap inherent to merge-commit promotions that carries no content `develop` lacks (the audit's content-based branch check is silent on it, and a cherry-pick would be an empty no-op). The one develop-side commit is normal unreleased work. No action. +`develop` vs `main`: the audit reads both and reports the same findings on each, so `develop` carries no conformance content `main` lacks and vice versa. The commit-count gap is the promotion-merge ancestry artifact recorded before and is **benign**. No action. ## Dimensions | Dimension | Letter | Intent | Verdict | Evidence (file:line) | | --- | --- | --- | --- | --- | -| csharp | fail | fail | drift | `.editorconfig:60` carries the repo-wide `dotnet_analyzer_diagnostic.severity = suggestion` CODESTYLE.md forbids (the hub canonical dropped it in ProjectTemplate#400). `PhotoCleaner/PhotoCleaner.csproj:3-4` sets `AnalysisLevel=latest-all` + `EnableNETAnalyzers=true` but **no `TreatWarningsAsErrors` anywhere**, so analyzers are demoted to suggestions and warnings never fail the build. The ProjectTemplate#353 probe measured what this hides: 362 `xUnit1051` + 2 `xUnit1030`, plus pre-existing `NU1903` (SQLitePCLRaw 2.1.10 high-severity advisory) and 4 `CS8625` that warn without failing anything | -| console | fail | pass | drift | a real System.CommandLine console app (`PhotoCleaner/PhotoCleaner.csproj:9 OutputType=Exe`, `PhotoCleaner/PhotoCleaner.csproj:34 System.CommandLine 2.0.5`, net10.0) but no `build-executable-task.yml` and no release pipeline, so the smoke-subset and `release-asset-<branch>-<target>` checks have nothing to bind to | -| docker | fail | fail | drift | `Docker/Dockerfile` present but no docker build/push workflow and no `Docker/README.md` - the Dockerfile is dead weight until a pipeline exists | -| branch-model | fail | fail | defect | **no rulesets at all** - `develop` and `main` both unprotected (audit DEFECT x2). No `repo-config/` payloads to import. Branches diverged (see Develop Drift) | -| repo-setup | pass | fail | drift | secrets fully provisioned in both stores (`CODECOV_TOKEN`, `CODEGEN_APP_CLIENT_ID`, `CODEGEN_APP_PRIVATE_KEY` in actions + dependabot) but nothing consumes the App creds - `.github/dependabot.yml` is absent (no dual-target sync keystone) and there is no merge-bot workflow | -| linter-parity | pass | pass | pass | `validate-task.yml:66-88` runs csharpier check, `dotnet format style --verify-no-changes`, markdownlint (SHA-pinned action), cspell (SHA-pinned), actionlint (SHA-pinned), and editorconfig-checker via docker `:latest`. The unit-test job collects coverage and uploads to codecov v7 (`validate-task.yml:36-40`). Matches the fleet lint architecture | -| recurring-violations | pass | pass | pass | `.gitattributes:3` fleet-standard `* -text` with LF pins for `*.sh` and `.husky/pre-commit`. README em-dash grep clean | -| readme-structure | fail | pass | drift | app-focused and thorough (`## Overview:5`, `## Usage:46`, flow sections, `## Docker:415`, `## Development Tooling:474`, `## License:653`) but not the fleet section set - no Build and Distribution block (nothing to badge pre-release), no Table of Contents, no Questions or Issues, Development-Environment-Setup slot filled by `Development Tooling` | -| workflow (WORKFLOW.md 5A/5B) | fail | pass | drift | no publisher, so the 5A publish guarantees are N/A. The PR gate itself is sound - `validate` runs unconditionally and the aggregator carries the canonical ruleset-bound name `Check pull request workflow status job` (`test-pull-request.yml:34-40`, interface contract passes, no paths-filter/smoke by design per the header `test-pull-request.yml:5`) - but with no ruleset the required check binds to nothing, so the gate is advisory until branch-model lands | +| csharp | pass | pass | pass | `Directory.Build.props` carries the analyzer set and `TreatWarningsAsErrors`, `Directory.Packages.props` centralizes versions, and the repo-wide analyzer relaxation the previous snapshot flagged at `.editorconfig:60` is gone. The shared `[*.cs]` block is present | +| console | pass | pass | pass | System.CommandLine console (`PhotoCleaner/PhotoCleaner.csproj`, `OutputType=Exe`, net10.0). `build-executable-task.yml` aggregates per-runtime output to `release-asset-<branch>-*`, and the smoke matrix is a strict subset | +| docker | pass | pass | pass | `Docker/Dockerfile` multi-arch, `Docker/README.md` present, `build-docker-task.yml` uses a registry layer cache (`buildcache-<branch>`), and the image re-pushes on publish | +| branch-model | pass | pass | pass | Both rulesets live and matching `repo-config/develop.json` and `repo-config/main.json` by normalized diff | +| repo-setup | pass | pass | pass | Every name in `spec/secrets.json` present in the store its mechanism claims, and no forbidden name | +| linter-parity | pass | pass | pass | `validate-task.yml:66-88` runs csharpier check, `dotnet format style --verify-no-changes`, markdownlint, cspell, actionlint, and editorconfig-checker. One config per linter | +| recurring-violations | pass | pass | pass | ASCII clean across the carried docs, and `.gitattributes` is fleet-standard with the LF pins | +| readme-structure | fail | fail | defect | Intro line 150 characters against the 100-character cap (`README.md:3`), and the Docker Hub short description does not mirror it. No Build and Distribution block, no Table of Contents, no Questions or Issues, and the Development-Environment-Setup slot is filled by `Development Tooling` (`README.md:482`) | +| workflow (WORKFLOW.md 5A/5B) | fail | pass | drift | The publisher, build tasks, and PR gate satisfy the D-guarantees, **except** that `build-release-task.yml` carries no `validate-release` job, so D2.2 (branch matches version classification) is unimplemented and scenarios S1, S4, and S10 bind to a job name that does not exist. The `github-release` job body matches no hub revision | +| agent-instruction-set | fail | fail | defect | `GOVERNANCE.md` absent, and `AGENTS.md` is the pre-split single file, so neither verbatim region can be compared and all ten of its sections read as undeclared. `.github/copilot-instructions.md` lacks `Reviewing Carried Fleet Content` | nuget, pypi, python: N/A (no packaging, no Python). ## Defects (most severe first) -1. **Both branch rulesets missing** - `develop` and `main` are unprotected: no signed-commit requirement, no PR gate, no required status check, force-push and deletion possible. Import `repo-config/develop.json` + `main.json` via `configure.sh apply` once `repo-config/` is carried. +1. **The agent instruction set predates the router split.** `GOVERNANCE.md` is absent (the only LETTER-class file finding) and `AGENTS.md` still carries the ten topical rule sections inline, so no verbatim region can be compared and every section reads as undeclared. A downstream agent reading this repo gets rule text that no longer tracks the canonical. +2. **README intro over the cap, and the Docker Hub mirror diverged from it.** The intro is 150 characters against the 100-character Docker Hub cap, so it overruns the tightest surface it feeds. Of the two mirrors GOVERNANCE.md "Repository Details" names, the GitHub About panel still matched the README exactly and only the Docker Hub short description had diverged (`Pre-process media files for import into photo management systems.`), so the repo carried two different canonical sentences. ## Drift Findings -- `AGENTS.md` is an old skeleton: 6 of the carried intent sections missing (Branching Model, Release Model, Pull Request Title and Commit Message Conventions, Documentation Style Conventions, PR Review Etiquette, Workflow YAML Conventions) and all 3 verbatim universal sections absent (Repository Boundaries and Write Safety, Git and Commit Rules, Verification Discipline) - re-vendor from the hub. -- `.markdownlint-cli2.jsonc` hand-modified (matches no hub revision) - re-vendor the canonical. -- Absent baseline files (audit LETTERs): `WORKFLOW.md`, `version.json`, `repo-config/` (all six), `AUDIT.md`, `spec/secrets.json`, `.github/dependabot.yml`, `Docker/README.md`. -- `.editorconfig:60` analyzer relaxation + no `TreatWarningsAsErrors` (see csharp dimension) - the ProjectTemplate#353 downstream item, sequenced as its own PR (362 sites). -- Committed `CLAUDE.md` and `PhotoCleaner.code-workspace` at the repo root - repo-local extras. `AGENTS.md` is the fleet's agent-agnostic doc, so a committed `CLAUDE.md` duplicates that role and can drift from it. +- `.markdownlint-cli2.jsonc` matches a past hub revision, so re-vendor it (the base gained `MD033 allowed_elements` for `details` and `summary`). +- `repo-config/configure.sh` matches a past hub revision, so re-vendor it (the base gained the `per_page` cap guard in `ruleset_id` and explicit failure guards on four reads). +- `build-release-task.yml` is missing the `validate-release` job, and its `github-release` body matches no hub revision. +- `version.json` carries a `nugetPackageVersion` block for a repo that publishes no package (STANDUP.md section 2, "carry only the fields the repo uses"). Not a mechanical finding, since `version.json` is checked at `intent`. + +## Convergence in Flight + +Six pull requests opened 2026-08-01 against `develop`, one per drift class per AUDIT.md section 10, each driven to a Copilot review on its head SHA and left for the maintainer to merge: + +- [#26](https://github.com/ptr727/PhotoCleaner/pull/26) the workspace extension set (`gruntfuggly.todo-tree` to `fanaticpythoner.better-todo-tree`) +- [#27](https://github.com/ptr727/PhotoCleaner/pull/27) re-vendor the two stale verbatim carries +- [#28](https://github.com/ptr727/PhotoCleaner/pull/28) the `AGENTS.md` and `GOVERNANCE.md` split, plus `Reviewing Carried Fleet Content` +- [#29](https://github.com/ptr727/PhotoCleaner/pull/29) the `validate-release` entry gate and the re-vendored `github-release` +- [#30](https://github.com/ptr727/PhotoCleaner/pull/30) the README and HISTORY structure +- [#31](https://github.com/ptr727/PhotoCleaner/pull/31) drop the unused `nugetPackageVersion` block ## Proposed Registry / Spec Updates -- Refresh the stale `driftNotes`: the repo is no longer pre-CI (PR gate + linters live). The current gaps are rulesets, `repo-config/`, `version.json`, governance docs, `dependabot.yml`, and the release pipeline. Applied in the same change as this report. -- `releaseTrigger: none` remains accurate until a publisher exists. +- Refresh the `driftNotes`: the second note asserted this report predated the docker and release wiring, which it no longer does. Applied in the same change. + +## Escalations + +Five spec questions this audit surfaced, raised rather than resolved, per AUDIT.md section 9. Filed as issue [#509](https://github.com/ptr727/ProjectTemplate/issues/509). + +1. **The `HISTORY.md` mirror rule has no carried home.** `spec/readme-structure.md` owns it and the audit enforces it, but that file is hub-only, so a downstream repo cannot point at the rule it is measured against. PhotoCleaner kept the rule as repo-local prose in `CODESTYLE.md` for want of a destination. Either promote it into a carried section, or accept that mechanical-only enforcement is the intent. +2. **The hub's own `.github/copilot-instructions.md` still describes the pre-split file.** `Reviewing Carried Fleet Content` says "Most of `AGENTS.md` is universal fleet law: every section that states a rule, as opposed to the two that describe this repository's own directory tree and devcontainer". After the split those sections live in `GOVERNANCE.md`, and `AGENTS.md` carries exactly two verbatim sections and no repo-specific ones. Every repo carrying this section downstream inherits the stale description. +3. **`CODESTYLE.md` contradicts `.markdownlint-cli2.jsonc` on MD033.** The Markdown-linting item says "HTML elements are flagged", but the canonical config now sets `MD033: { allowed_elements: ["details", "summary"] }`. The prose was not swept when the config changed. +4. **`spec/readme-structure.md` assumes a public repository.** PhotoCleaner is private for now, so shields.io cannot read its GitHub release, build, or commit data and every GitHub-sourced badge renders broken, the pre-existing License shield included. The Build Status and Releases sub-sections state no behavior for that case. The spec question stands even though this repo resolves on its own: a repo is often private while it is being made presentable, which is exactly when its README is being written. +5. **`WORKFLOW.md` D2.2 "skipped on smoke" is ambiguous, and a reviewer misread it.** The phrase names the validation, and scenario S1 confirms it (`validate-release **skipped (smoke), succeeds**`), but a Copilot review read it as the GitHub job status and proposed a job-level `if: !inputs.smoke` that would have coupled `github-release` to smoke through its `needs`. Worth disambiguating in the D-guarantee text. + +A sixth question was raised with the maintainer and is **answered**: the repository is **private** while its Docker image is public, so the declared `github-release` channel with `consumerModel: pull` is not currently satisfiable. That is deliberate and temporary. The repo goes public once the conformance work settles, which serves prospective users and lowers the GitHub bill, and the declared channel becomes correct at that flip rather than being wrong now. The `driftNotes` record it, and the README gains its GitHub build and release shields at the same time. diff --git a/scripts/test_prose_lint.py b/scripts/test_prose_lint.py index a3e567cd..9fc8c983 100644 --- a/scripts/test_prose_lint.py +++ b/scripts/test_prose_lint.py @@ -8,7 +8,7 @@ Run as `python3 scripts/test_prose_lint.py`, or under `python3 -m unittest discover -s scripts`. """ from __future__ import annotations -import contextlib, io, re, subprocess, sys, tempfile, unittest +import contextlib, io, json, re, subprocess, sys, tempfile, unittest from pathlib import Path from unittest import mock @@ -1136,6 +1136,49 @@ def test_the_repo_is_clean_of_british_spellings(self) -> None: self.assertEqual([], found) +class TestCarriedContent(unittest.TestCase): + """Content the fleet copies byte-matched, which only the hub can ever correct. + + GOVERNANCE "Character Set" states the obligation: correct-as-you-next-edit assumes someone + able to edit the file, and a downstream repo cannot edit a verbatim one, since its copy is + byte-matched against the hub's. So the hub sweeps the class and re-vendors. That makes a + finding in a verbatim file different in kind from the tree-wide backlog: it is not a + correction owed by whoever next edits the file, it is one no downstream repo can make at all. + """ + + def verbatim_paths(self) -> list[Path]: + """Every `verbatim` entry in the carry manifest, read live rather than restated here.""" + def entries(node: object): + if isinstance(node, dict): + if 'path' in node: + yield node + for value in node.values(): + yield from entries(value) + elif isinstance(node, list): + for value in node: + yield from entries(value) + + manifest = json.loads((REPO / 'spec' / 'files.json').read_text(encoding='utf-8')) + seen = {e['path'] for e in entries(manifest) if e.get('fidelity') == 'verbatim'} + return sorted(REPO / p for p in seen) + + def test_the_manifest_still_declares_verbatim_content(self) -> None: + """A manifest that stopped declaring any would make the sweep below vacuously pass.""" + self.assertNotEqual([], self.verbatim_paths()) + + def test_every_verbatim_carried_file_is_comment_clean(self) -> None: + """A downstream repo cannot fix one of these, so the hub may not leave one behind.""" + found = [f'{prose_lint.rel(p.relative_to(REPO))}:{ln}: {kind}' + for p in self.verbatim_paths() if p.exists() + for ln, kind, _ in prose_lint.check_file(p, {'comment-wrap', 'comment-case'})] + self.assertEqual([], found) + + def test_every_declared_verbatim_file_exists(self) -> None: + """A manifest naming a file the hub does not carry would exempt it by absence.""" + self.assertEqual([], [str(p.relative_to(REPO)) for p in self.verbatim_paths() + if not p.exists()]) + + class TestSyntaxDispatch(unittest.TestCase): def test_an_extensionless_file_is_read_as_hash_commented(self) -> None: """A shebang script or a config with no suffix is far more often `#` than nothing.""" diff --git a/spec/audit.py b/spec/audit.py index 3c235e77..fd8e889b 100644 --- a/spec/audit.py +++ b/spec/audit.py @@ -621,12 +621,15 @@ def audit_repo(entry, spec, branch=None): findings.append(("DRIFT", f"secrets: {name} in the {store} store is claimed by no applicable mechanism (stale?)")) # --- Carried files must not reference the template repo --- - # The template is private, so a reference 404s for this repo's users and exposes machinery they cannot - # follow. Checks the agent-instruction files, where a stale "report drift upstream" paragraph spread. - for path in ("AGENTS.md", "GOVERNANCE.md", ".github/copilot-instructions.md"): - doc = gh(f"repos/{slug}/contents/{path}?ref={ground}", ok404=True) - if doc and doc.get("content") and HUB_NAME.lower() in base64.b64decode(doc["content"]).decode("utf-8", "replace").lower(): - findings.append(("DRIFT", f"carried: {path} references the template repo by name or link (private - 404s for this repo's readers; state the behavior, not the destination)")) + # The coordination flow is machinery a consumer should not see, so a carried file states the behavior rather than the destination. + # This reads AGENTS.md, GOVERNANCE.md and .github/copilot-instructions.md, where a stale "report drift upstream" paragraph once spread. + # Skip the hub itself, whose own carried files are the source, where naming the repo they live in is correct. + # A downstream repo naming it is still flagged, which is the point. + if entry.get("name") != HUB_NAME: + for path in ("AGENTS.md", "GOVERNANCE.md", ".github/copilot-instructions.md"): + doc = gh(f"repos/{slug}/contents/{path}?ref={ground}", ok404=True) + if doc and doc.get("content") and HUB_NAME.lower() in base64.b64decode(doc["content"]).decode("utf-8", "replace").lower(): + findings.append(("DRIFT", f"carried: {path} references the template repo by name or link (the coordination flow is machinery this repo's readers should not see; state the behavior, not the destination)")) # --- Dependabot ecosystem coverage --- # A repo's tree implies Dependabot ecosystems it must track: github-actions when it ships workflows diff --git a/spec/files.json b/spec/files.json index 120276c7..d261391c 100644 --- a/spec/files.json +++ b/spec/files.json @@ -8,7 +8,7 @@ { "path": "WORKFLOW.md", "fidelity": "intent", "whole": true, "intentRef": "WORKFLOW.md", "appliesTo": "*" }, { "path": "README.md", "appliesTo": "*" }, { "path": "HISTORY.md", "appliesTo": "*" }, - { "path": "OPERATIONS.md", "appliesTo": ["operational"] }, + { "path": "OPERATIONS.md", "appliesTo": "*" }, { "path": ".github/copilot-instructions.md", "fidelity": "intent", "whole": true, "sections": ["Commit Messages and Pull Request Titles", "Reviewing Carried Fleet Content", "GitHub Copilot Review Runbook"], "placeholders": ["<owner>", "<repo>", "<N>"], "appliesTo": "*" }, { "path": ".editorconfig", "fidelity": "intent", "whole": true, "intentRef": "GOVERNANCE.md#line-endings", "appliesTo": "*" }, { "path": ".editorconfig-checker.json", "fidelity": "intent", "whole": true, "intentRef": "GOVERNANCE.md#line-endings", "appliesTo": "*" }, @@ -27,6 +27,7 @@ { "path": "spec/secrets.json", "fidelity": "intent", "intentRef": "docs/repo-config-carry.md", "appliesTo": "*" }, { "path": ".github/dependabot.yml", "appliesTo": "*" }, { "path": ".github/workflows/test-pull-request.yml", "fidelity": "interface", "contract": { "requiredJobKeys": ["check-workflow-status"], "requiredCheckName": "Check pull request workflow status job" }, "intentRef": "GOVERNANCE.md#workflow-yaml-conventions", "appliesTo": "*" }, + { "path": ".github/workflows/merge-bot-pull-request.yml", "fidelity": "interface", "contract": { "requiredJobKeys": ["merge-dependabot", "disable-auto-merge-on-maintainer-push"] }, "intentRef": "WORKFLOW.md#d8---bots--automation", "appliesTo": "*" }, { "path": ".github/workflows/build-release-task.yml", "fidelity": "interface", "contract": { "requiredJobKeys": ["get-version", "validate-release", "github-release"], "artifactNameToken": "release-asset-", "requireTokensInJob": { "github-release": ["pattern:", "merge-multiple:"] }, "forbidTokensInJob": { "github-release": ["artifact-ids:"] }, "verbatimJobs": ["github-release"] }, "reference": "catalog/snippets/workflows/build-release-task.yml", "intentRef": "GOVERNANCE.md#release-model", "appliesTo": ["csharp", "console", "docker", "nuget", "pypi", "eda"] }, { "path": ".vscode/tasks.json", "sections": ["clean-compile task group"], "reference": "catalog/snippets/configs/vscode-tasks.json", "appliesTo": ["csharp"] }, { "path": ".vscode/tasks.json", "sections": ["clean-compile task group"], "reference": "catalog/snippets/configs/vscode-tasks-python.json", "appliesTo": ["python"] }, diff --git a/spec/project-types.json b/spec/project-types.json index 88daaeb5..62703949 100644 --- a/spec/project-types.json +++ b/spec/project-types.json @@ -155,7 +155,7 @@ { "id": "recurring.charset", "verdict": "letter", "assert": "ASCII only in agent-authored text: no em-dash (use a spaced hyphen), no smart quotes, no stray non-ASCII.", "intentRef": "GOVERNANCE.md#character-set" }, { "id": "recurring.spelling", "verdict": "letter", "assert": "US English spelling; the shared cspell.json sets language en-US (a bare en accepts British spellings too).", "intentRef": "CODESTYLE.md#markdown-and-spelling" }, { "id": "recurring.eol", "verdict": "letter", "assert": "Line endings follow .editorconfig, which carries a global [*] end_of_line default plus LF pins for execution-sensitive files (shell, Dockerfiles, shebang-executable .py by path), plus workflow YAML in .github/workflows/* enforced by editorconfig-checker in CI. A per-extension-only form lacking the global [*] default is a drift finding. The global default is CRLF for release repos, or the consuming application's native platform for an operational (config) repo as recorded in the registry lineEndings field (LF for Linux-native/container config e.g. ESPHome/Home Assistant, CRLF for a Windows-native editor e.g. Vantage/Design Center) - do not re-normalize such a repo to CRLF. Edits preserve the file's endings.", "intentRef": "GOVERNANCE.md#line-endings" }, - { "id": "recurring.norepoxref", "verdict": "intent", "assert": "A carried file (AGENTS.md, GOVERNANCE.md, CODESTYLE.md, WORKFLOW.md, .github/copilot-instructions.md, repo-config/README.md, repo-config/develop.json, repo-config/main.json, spec/secrets.json, the carried AUDIT.md) carries no coordination reference: no reference to the template repo in prose or link (private, so it 404s for the carrying repo's readers, and the coordination flow is machinery a consumer should not see - state the behavior, not the destination), and no sibling fleet repo named as an illustrative example of a rule or adoption. A contextually relevant link to a related project is NOT a coordination reference and is expected (the image that consumes this config, a library this depends on) - the test is whether the link serves a reader of this repo's content. The rule governs carried template content. A repo's own README.md and topical docs are its own content. spec/audit.py mechanically checks the two agent-instruction files for the template name.", "intentRef": "GOVERNANCE.md#documentation-style-conventions" } + { "id": "recurring.norepoxref", "verdict": "intent", "assert": "A carried file (AGENTS.md, GOVERNANCE.md, CODESTYLE.md, WORKFLOW.md, .github/copilot-instructions.md, repo-config/README.md, repo-config/develop.json, repo-config/main.json, spec/secrets.json, the carried AUDIT.md) carries no coordination reference: no reference to the template repo in prose or link (the coordination flow is machinery a consumer should not see - state the behavior, not the destination), and no sibling fleet repo named as an illustrative example of a rule or adoption. A contextually relevant link to a related project is NOT a coordination reference and is expected (the image that consumes this config, a library this depends on) - the test is whether the link serves a reader of this repo's content. The rule governs carried template content. A repo's own README.md and topical docs are its own content. spec/audit.py mechanically checks AGENTS.md, GOVERNANCE.md and .github/copilot-instructions.md for the template name, and skips the hub itself, whose copies are the source.", "intentRef": "GOVERNANCE.md#documentation-style-conventions" } ] }, "readme-structure": { diff --git a/spec/readme-structure.md b/spec/readme-structure.md index 3174c221..3cc7f214 100644 --- a/spec/readme-structure.md +++ b/spec/readme-structure.md @@ -24,6 +24,8 @@ The preferred `README.md` shape for a fleet project. The audit's `readme-structu Shields are not a top-level section. They live under **Build and Distribution** as the **Build Status** and **Releases** sub-sections. Each shield is a reference-style link, with the definitions grouped under `<!-- Shields -->` at the bottom of the file. Stack a group's badges by ending each line except the last with a trailing backslash. +**Repository visibility does not change the structure.** A private repo carries the same sections and the same shield set as a public one, written in full. A private repo cannot be read by shields.io, so its GitHub-sourced badges render broken until it goes public, and that is accepted rather than worked around. The alternative, writing a reduced README while private and adding the missing shields at the visibility flip, makes the shape depend on a property that is temporary by design, since a repo is usually private precisely **while** it is being made presentable, which is exactly when its README is written. Carrying the full shape means going public is a visibility change and nothing else, with no README work owed at the flip and no second shape for the audit to accept. + ## Style - Write in the current state, present tense, so the reader needs no knowledge beyond what they are reading (GOVERNANCE.md Documentation Style). @@ -42,8 +44,12 @@ The file is the declared destination rather than a required file, the same footi ## HISTORY.md -`HISTORY.md` is the maintainer-curated changelog and opens as the README's twin: the same `# <Title>` (without the README's ToC-omit comment) and the same intro paragraph, copied verbatim, then a `## Release History` section. The mirrored opening keeps the project identity consistent for a reader who lands on the changelog directly, and the audit checks that the title and intro match the README (HTML comments stripped). +The rule lives in [`CODESTYLE.md`][codestyle] "Markdown and Spelling", which every repo carries, so a repo can read the rule it is measured against. This file states only what the audit does with it: the `readme-structure` dimension checks that the `HISTORY.md` title and intro match the README's, with HTML comments stripped. ## Docker Hub README Docker Hub has two text fields: a **short description** (the tagline, capped near 100 characters) that mirrors the README intro line (item 1), and the longer **overview**. A repo that publishes a Docker image keeps a **separate** `Docker/README.md` for the overview: Docker Hub's description has a much smaller size limit than a project README, so it carries a trimmed overview, not the full README. It is published by the docker-readme workflow task, not copied from the root README. + +<!-- Repo --> + +[codestyle]: ../CODESTYLE.md diff --git a/spec/section-model.md b/spec/section-model.md index 3efaad79..96605c4a 100644 --- a/spec/section-model.md +++ b/spec/section-model.md @@ -59,15 +59,20 @@ The set of sections, and each section's fidelity, is itself governed. ## Where repo-specific content goes -A repo's own content is not carried and is not declared here, so extraction needs a predictable destination rather than a judgment call per repo. Three topical docs take it, chosen by what the content *is*: +A repo's own content is not carried, and the hub declares where it goes rather than what it says, so extraction needs a predictable destination rather than a judgment call per repo. Four topical docs take it, chosen by what the content *is*: - [`CODESTYLE.md`][codestyle]: a repo's language and formatting conventions beyond the carried rules. - `ARCHITECTURE.md`: how a code repo is built, its module layout, data flow, and design decisions. -- `OPERATIONS.md`: how an operational repo is run, covering runbooks, backup, log and debug procedures, tool-usage notes, and config layout. This is the operational-repo analogue of `ARCHITECTURE.md`, and it is where an `AGENTS.md` split puts the repo-specific half. +- `OPERATIONS.md`: how a repo is run, under the headings `Runbooks`, `Backup and Recovery`, `Logs and Debugging`, `Tool Usage`, and `Configuration Layout`. It is the operational analogue of `ARCHITECTURE.md`, and it is where an `AGENTS.md` split puts the repo-specific half. +- `TODO.md`: the repo's running backlog, which keeps open work out of the README's section order where it does not belong and changes on a different cadence from everything around it. -**`OPERATIONS.md` is required for an `operational` repo**, declared in [`files.json`][files] as `appliesTo: ["operational"]` and checked for presence only, the same footing as `README.md` and `HISTORY.md`, so its content is entirely the repo's own. It is mandatory rather than advisory because the convention was already emerging unevenly: of the four operational repos, two wrote one unprompted and the others scattered the same material across ad-hoc names, which is the improvisation these destinations exist to prevent. A repo with nothing to say in it still carries it, since an empty file that names the destination is cheaper than rediscovering the destination. +**`OPERATIONS.md` is required for every repo**, declared in [`files.json`][files] as `appliesTo: "*"` and checked for presence only, the same footing as `README.md` and `HISTORY.md`, so its content is entirely the repo's own. It is mandatory rather than advisory because the convention was already emerging unevenly: of the four operational-model repos, two wrote one unprompted and the others scattered the same material across ad-hoc names, which is the improvisation these destinations exist to prevent. That reasoning never depended on the workflow model. Every repo has operational surface, since publishing to a package registry needs trusted-publisher setup, shipping an image needs registry credentials, and serving a site needs a deploy path and a staging story. A repo with nothing to say still carries the file as a stub, meaning those five headings with no content under them, because a stub names the destination and its shape where a blank file names only the destination. This repo's own [`OPERATIONS.md`][operations] is the worked example. -`CODESTYLE.md` is carried by every repo already. `ARCHITECTURE.md` stays **advisory**, deliberately: "a code repo" is not one selector token, and mandating it would produce empty files in repos whose design needs no separate document. Promote it if the same unprompted-convention evidence appears for it. +**The workflow model and the need for this file are unrelated axes.** `operational` as a `workflowModel` describes where config lives and how a change reaches `develop`, not whether the repo has runbooks. Keying the file to that selector read a sufficient condition as a necessary one, since an operational-model repo certainly has runbooks while a release repo has them too. Reclassifying a repo between models does not change how much operational surface it has, which is the test that showed the selector was wrong. + +**A destination is declared when its content class recurs across repos.** Software architecture recurs, because every code repo has one, so `ARCHITECTURE.md` is declared. A home-device inventory does not recur, so it stays the repo's own file, neither declared nor mandated. The scattering these destinations prevent is the same material landing under different names in different repos, and that has no force for content existing in exactly one repo. Declaring a destination for a one-repo need would grow this list without bound and still lag whatever the next repo invents. + +`CODESTYLE.md` is carried by every repo already. `ARCHITECTURE.md` stays **declared but advisory**: every code repo has an architecture, and how much of it earns a separate document is contextual, so mandating it would produce empty files where the design needs none. Declared and required are separate questions, and only a universal need answers both. `OPERATIONS.md` is agent-instruction content, so it takes the inline-link exception the markdown rules name, not the reference-style default. `ARCHITECTURE.md` is not on that closed list and follows the reference-style rule. @@ -89,4 +94,5 @@ A repo that carried its governance inside `AGENTS.md` before the router split ho [fidelity-model]: ./fidelity-model.md [files]: ./files.json [governance]: ../GOVERNANCE.md +[operations]: ../OPERATIONS.md [validate]: ./validate.py