diff --git a/.agents/skills/repo-worktree/SKILL.md b/.agents/skills/repo-worktree/SKILL.md index 93ed6901..c3769aab 100644 --- a/.agents/skills/repo-worktree/SKILL.md +++ b/.agents/skills/repo-worktree/SKILL.md @@ -60,13 +60,38 @@ is whatever it last fetched rather than the branch it names. ## Creating a Worktree The fleet layout convention keeps every base clone and every in-flight task visible in one -place, with no owner segment since every repo here is under one owner: +place: ```text ~/repos/ base clone, on its default/working branch ~/repos/worktrees/- one worktree per in-flight task, own branch +~/repos/upstream/- clone of a repo under another owner, not a fork ``` +The top level carries no owner segment because everything in it is the fleet owner's own, an +original repo and a fork alike. A fork is named `-` at fork time, +so a fork of `acme/core` is `acme-core`, and its name identifies the upstream project and stays +unique in the flat namespace without an owner segment of its own. A repository adopted as the +owner's own work rather than kept as a fork is detached from its parent and keeps a plain name, +`widget` rather than `initech-widget`, since it no longer tracks anything upstream. + +A clone of a repository under another owner is neither of those, and flattening one collides +rather than merely reading oddly: `acme/core` joined the way a fork is joined **is** the fork's +name, `acme-core`, while reduced to a bare `core` it names no project and collides with the next +`core` cloned from any other owner. Those clones live one level down under `upstream/`, named by +that same join, so `upstream/acme-core` sits beside the fork it would otherwise land on. The +segment states the relationship rather than the owner, so a reference checkout is told from a +working repo without a `git remote` call, and the names under it never compete with the flat +namespace above. The join is ambiguous in the abstract, since a hyphen in either half means +`acme-labs/core` and `acme/labs-core` produce one name, and it is kept anyway because it is the +fork convention's own join: the ambiguity is inherited from the flat namespace above rather than +introduced here, and it surfaces at clone time as a directory that already exists, where the +second clone takes a hand-picked name. A worktree off one of them keeps the flat worktrees path +under the same name, `~/repos/worktrees/--`. Contributing a change from +such a clone is never a push out of it: fork the upstream first, per the +`upstream-contribution-workflow` skill, and that fork's own clone then belongs in the flat +namespace above, under the name this one already has. + ```sh git -C ~/repos/ fetch origin develop git -C ~/repos/ worktree add ~/repos/worktrees/- -b origin/develop diff --git a/.claude-plugin/fleet-skills/.source-digest b/.claude-plugin/fleet-skills/.source-digest index f0ee8330..3ad055fa 100644 --- a/.claude-plugin/fleet-skills/.source-digest +++ b/.claude-plugin/fleet-skills/.source-digest @@ -1 +1 @@ -e4a4146ddbea9745 +b1c02e77cc574d26 diff --git a/.claude-plugin/fleet-skills/skills/repo-worktree/SKILL.md b/.claude-plugin/fleet-skills/skills/repo-worktree/SKILL.md index 93ed6901..c3769aab 100644 --- a/.claude-plugin/fleet-skills/skills/repo-worktree/SKILL.md +++ b/.claude-plugin/fleet-skills/skills/repo-worktree/SKILL.md @@ -60,13 +60,38 @@ is whatever it last fetched rather than the branch it names. ## Creating a Worktree The fleet layout convention keeps every base clone and every in-flight task visible in one -place, with no owner segment since every repo here is under one owner: +place: ```text ~/repos/ base clone, on its default/working branch ~/repos/worktrees/- one worktree per in-flight task, own branch +~/repos/upstream/- clone of a repo under another owner, not a fork ``` +The top level carries no owner segment because everything in it is the fleet owner's own, an +original repo and a fork alike. A fork is named `-` at fork time, +so a fork of `acme/core` is `acme-core`, and its name identifies the upstream project and stays +unique in the flat namespace without an owner segment of its own. A repository adopted as the +owner's own work rather than kept as a fork is detached from its parent and keeps a plain name, +`widget` rather than `initech-widget`, since it no longer tracks anything upstream. + +A clone of a repository under another owner is neither of those, and flattening one collides +rather than merely reading oddly: `acme/core` joined the way a fork is joined **is** the fork's +name, `acme-core`, while reduced to a bare `core` it names no project and collides with the next +`core` cloned from any other owner. Those clones live one level down under `upstream/`, named by +that same join, so `upstream/acme-core` sits beside the fork it would otherwise land on. The +segment states the relationship rather than the owner, so a reference checkout is told from a +working repo without a `git remote` call, and the names under it never compete with the flat +namespace above. The join is ambiguous in the abstract, since a hyphen in either half means +`acme-labs/core` and `acme/labs-core` produce one name, and it is kept anyway because it is the +fork convention's own join: the ambiguity is inherited from the flat namespace above rather than +introduced here, and it surfaces at clone time as a directory that already exists, where the +second clone takes a hand-picked name. A worktree off one of them keeps the flat worktrees path +under the same name, `~/repos/worktrees/--`. Contributing a change from +such a clone is never a push out of it: fork the upstream first, per the +`upstream-contribution-workflow` skill, and that fork's own clone then belongs in the flat +namespace above, under the name this one already has. + ```sh git -C ~/repos/ fetch origin develop git -C ~/repos/ worktree add ~/repos/worktrees/- -b origin/develop diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index afeb62f1..9457c64b 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -173,7 +173,7 @@ gh pr view --json reviews --jq \ **So an unrecognized shape is a blocking outcome, and its remedy is an issue rather than a judgment call.** When any reader here meets a heading, a collapsed section, a metadata line, a coverage wording or a reviewer login it has no vetted spelling for, the review loop **does not close**, whatever else the digest says. Do not read past it, do not infer what the new wording probably means, and do not treat a body that looks clean as a clean review, because "looks clean" is precisely what a misread review looks like. Two things follow, in this order: -1. **File an issue on the hub, `ptr727/ProjectTemplate`**, which hosts `scripts/pr_review.py` and holds the vetted inventory. Name each unrecognized shape and quote the review body it came from, so the fix is made against the real wording rather than a paraphrase. The issue is filed even when the shape turns out to be cosmetic, since "cosmetic" is a conclusion drawn after reading the body and not before. +1. **File an issue on the hub**, the repository `AGENTS.md` "Fleet Bootstrap" names, which hosts `scripts/pr_review.py` and holds the vetted inventory. Name each unrecognized shape and quote the review body it came from, so the fix is made against the real wording rather than a paraphrase. The issue is filed even when the shape turns out to be cosmetic, since "cosmetic" is a conclusion drawn after reading the body and not before. 2. **The merge decision is the maintainer's**, not the agent's and not the script's. An unrecognized shape does not mean the pull request is bad, it means nothing here can vouch for the review of it. Report the state, hand it over, and stop. `scripts/pr_review.py status --repo /` reports this as `shapes=UNRECOGNIZED`, lists each shape under a marker naming the remedy, and exits `43`. `wait` carries the same code, so a wait cannot end on a clean zero over output nothing read. The vetted inventory lives in that script and is small on purpose: measured over 332 Copilot review bodies on this repository, with fenced blocks dropped and text reduced to ASCII, the whole corpus is seven headings, six `` texts and three metadata labels, and every body carries at least one of them. A body carrying none is itself the unrecognized shape, which is what catches a rewrite that changes everything at once, the refusal wording drifting among it. @@ -317,7 +317,7 @@ After the final push, sweep-resolve stale older threads for removed code paths. ### Disproved Claims -**A disproof is proof about this repository, and the thread it was written in is not where the next round looks.** [GOVERNANCE.md "Every Finding Ends in an Action"](../GOVERNANCE.md#every-finding-ends-in-an-action) closes a false finding by disproving it in the thread, addressed to the reviewer so it does not raise the same thing again, and while the pull request is open that is the right place for it. Afterwards it is the wrong one. The pull request merges, the next round begins with no memory of the last, and the second occurrence reaches a maintainer with no way to tell it from a first. Each entry below is a claim that was tested against this repository and found false, kept so the proof is read rather than built twice. +**A disproof is proof about this repository, and the thread it was written in is not where the next round looks.** [GOVERNANCE.md "PR Review Etiquette"](../GOVERNANCE.md#pr-review-etiquette), which routes to the `pr-review-conduct` Skill, closes a false finding by disproving it in the thread, addressed to the reviewer so it does not raise the same thing again, and while the pull request is open that is the right place for it. Afterwards it is the wrong one. The pull request merges, the next round begins with no memory of the last, and the second occurrence reaches a maintainer with no way to tell it from a first. Each entry below is a claim that was tested against this repository and found false, kept so the proof is read rather than built twice. **An entry names the claim, what was run or read to disprove it, the revision it was proved against, and what ends it.** A disproof is true of one tree at one revision, so an entry whose subject moves is deleted by the change that moves it rather than edited to look current, which is the same sweep the [GOVERNANCE.md "Documentation Style Conventions"](../GOVERNANCE.md#documentation-style-conventions) rule already requires of prose asserting a behavior that has changed underneath it. This is deliberately not a list to append to, since an entry outliving the code it was proved against becomes a reason not to check, and that is strictly worse than proving the claim a second time. diff --git a/docs/fleet-map.md b/docs/fleet-map.md index dc86b6a1..67ae96a6 100644 --- a/docs/fleet-map.md +++ b/docs/fleet-map.md @@ -194,7 +194,7 @@ flowchart LR ### G4: Deletion Sweeps Miss Prose (Closed) - **Gap** - A resync that deletes a carried file greps for the path and finds code uses, not prose describing the file without naming its path. A measured incident left a layout section describing a deleted script. -- **Resolution** - Split by what a pattern can reach. The named-path half is mechanized: the `dead-path` rule in [`scripts/prose_lint.py`][prose-lint] reports a Markdown mention (a backtick span, an inline link target, or a reference definition) of a path git once tracked and the tree no longer holds. Keying on deletion history is what scopes it: a proposed file a backlog names, another repository's layout, and a ref like `origin/develop` each have no history here and stay silent, and a manifest-declared carried path is exempt since the hub's own instance retires to a catalog snippet while docs keep naming the carried file. The rule runs in the default set and in CI, where the checkout fetches full history because the rule stands down loudly in a shallow clone rather than pass blind. The name-shaped half, the description that names no path, is `accepted` as manual: no pattern reaches it, the same judgment the home-path rule records, so the [`RESYNC.md`][resync] section 4 read of the layout and operations sections stands and now names the lint beside it. +- **Resolution** - Split by what a pattern can reach. The named-path half is mechanized: the `dead-path` rule in [`scripts/prose_lint.py`][prose-lint] reports a Markdown mention (a backtick span, an inline link target, or a reference definition) of a path git once tracked and the tree no longer holds. Keying on deletion history is what scopes it: a proposed file a backlog names, another repository's layout, and a ref like `origin/develop` each have no history here and stay silent, and a manifest-declared carried path is exempt since the hub's own instance retires to a catalog snippet while docs keep naming the carried file. A hub-hosted path is exempt for the mirror-image reason, measured downstream rather than predicted: a repository that deleted its copy per a `retire` disposition still names the hub's, which is the pointer [`GOVERNANCE.md`][governance] "Hub-Hosted Tooling" requires of it, and the manifest exemption cannot reach that case because no repository carries `spec/files.json`. That set is a literal in the gate, since the prose-gate action can fetch the one file and no ledger beside it, and a test asserts it against the ledger's `retire` dispositions so a new retirement cannot land on one side only. The rule runs in the default set and in CI, where the checkout fetches full history because the rule stands down loudly in a shallow clone rather than pass blind. The name-shaped half, the description that names no path, is `accepted` as manual: no pattern reaches it, the same judgment the home-path rule records, so the [`RESYNC.md`][resync] section 4 read of the layout and operations sections stands and now names the lint beside it. - **Closing test** - `TestDeadPath` in `scripts/tests/test_prose_lint.py`, including the shallow stand-down and the tree-clean assertion. The rule's first tree-wide run caught a real instance, [`docs/host-setup.md`][host-setup-doc] describing bind-mounts in the deleted `.devcontainer/` definitions, fixed by re-pointing at the catalog snippets. ### G5: Intent-Fidelity Drift Is Invisible (Closed) diff --git a/registry/repos.json b/registry/repos.json index 10b90d77..d93c4fd9 100644 --- a/registry/repos.json +++ b/registry/repos.json @@ -114,7 +114,7 @@ "consumerModel": "pull", "releaseTrigger": "two-phase", "configLayout": { "rulesetsDir": "repo-config", "pythonConfig": null }, - "driftNotes": ["Carries ARCHITECTURE.md and codecov.yml beyond the baseline.", "First csharp+python repo: a C# console app at the root plus a stdlib-only Python tooling subtree (RegressionTests/, uvx scripts profile - no uv.lock, pyproject carries only ruff+mypy config; PlexCleaner#855). python.uvlock.pinned and python.coverage.codecov are N/A for that subtree (no uv project, no tests); codecov.yml stays required for the C# side. Reference for the csharp+python shape (issue #339).", "Branch hygiene: 3 stale Dependabot nuget branches (PRs closed/superseded) and an unmerged feature/727-decouple-release-assets branch linger; main+develop otherwise clean after the 2026-07 sweep."] + "driftNotes": ["Carries ARCHITECTURE.md and codecov.yml beyond the baseline.", "First csharp+python repo: a C# console app at the root plus a stdlib-only Python tooling subtree (RegressionTests/, uvx scripts profile - no uv.lock, pyproject carries only ruff+mypy config; PlexCleaner#855). python.uvlock.pinned and python.coverage.codecov are N/A for that subtree (no uv project, no tests); codecov.yml stays required for the C# side. Reference for the csharp+python shape (issue #339)."] }, { "name": "ESPHome-NonRoot", @@ -170,7 +170,7 @@ "requiredSecrets": [], "consumerModel": "pull", "releaseTrigger": "dispatch-only", - "driftNotes": ["Maintainer config/ops repo (docker-compose stacks, lifecycle scripts, Firewalla configs); Linux-consumed on the Proxmox host, so lineEndings lf.", "Renamed from HomeAutomation for fleet naming consistency (config repos are *-Config). The Vantage controller config is split out to its own Windows/CRLF Vantage-Config repo, not carried here; the legacy Vantage/ subtree is stripped.", "Private; README self-flags previously-committed secrets - secrets-hygiene concern."] + "driftNotes": ["Maintainer config/ops repo (docker-compose stacks, lifecycle scripts, Firewalla configs); Linux-consumed on the Proxmox host, so lineEndings lf.", "Renamed from HomeAutomation for fleet naming consistency (config repos are *-Config). The Vantage controller config is split out to its own Windows/CRLF Vantage-Config repo, not carried here; the legacy Vantage/ subtree is stripped."] }, { "name": "KiCadLibrary", diff --git a/reports/conformance-matrix.md b/reports/conformance-matrix.md index 4be38a37..685ba75c 100644 --- a/reports/conformance-matrix.md +++ b/reports/conformance-matrix.md @@ -14,13 +14,13 @@ The primary shapes are stood up as whole repos. The **composable targets** (`nug | `hugo` + `source-only` + `release` | Blog | not-tested | 2026-08-05 | Hugo static site (#456, #558), stood up 2026-08-01 and cataloged 2026-08-03. Release and deploy are independent surfaces: a dispatch-only publisher cuts the tag, and a separate dispatch deploys to a `self-hosted` filesystem per environment. Reclassified off the interim `source-only`-alone declaration when the type landed. **The 2026-08-05 run is the first to judge the `hugo` checks**, since the 2026-08-03 one predated the type and graded the repo as `source-only` alone. All nine pass, the repo is operational, and the three deviations the first run recorded are closed (ptr727/Blog#27, ptr727/Blog#28, ptr727/Blog#29). Two drift classes stay open, both of them the hub having advanced: seven stale verbatim carries and 17 prose findings. This is the shape `hugo.deploy.retention` was written against: its deploy credential is confined write-only, so the deploy can neither prune nor read the destination back, and the prune is a host-side timer its runbook records as host-owned. The first draft of that check demanded an in-pipeline assertion and would have pushed a correct design to widen a deliberately narrow credential. The run also surfaced a hub defect rather than a repo one, carried as escalation 1 of the report: the template-reference check had no exemption for the byte-locked `Fleet Bootstrap` section, whose first sentence must name the hub, so it fired unclearably on every repo that had carried the current canonical. Fixed since, across all three surfaces that stated the rule: the scan in `spec/audit.py` excises a file's verbatim sections before looking for the name, and `GOVERNANCE.md` "Documentation Style Conventions" and `recurring.norepoxref` both carry the exception and its boundary. It cleared exactly two findings fleet-wide and kept the other eleven. | | `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, and `codecov.yml` stays required for the C# side. Both language rule-sets apply (CODESTYLE.md "Two profiles"). | +| `csharp` + `python` | PlexCleaner | not-tested | 2026-08-15 | 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, and `codecov.yml` stays required for the C# side. Both language rule-sets apply (CODESTYLE.md "Two profiles"). **The 2026-08-15 run is the first to judge the `python` checks**, since the 2026-07-04 one predated the type declaration and graded the repo as `csharp` + `console` + `docker` alone. The repo is operational: every mechanized check passes, and the two workflow divergences still standing (no `changes` paths-filter, a dispatch guard that skips rather than failing fast) are letter misses whose intent holds. Its two open drift items are prose and configuration rather than pipeline, and the pass also settled the two `investigate` gap dispositions the whole fleet was carrying. | | `homeassistant` | - | not-tested | - | Standalone-config conventions (home-assistant/core), scored by the `ha.*` checks. | | `eda` | - | not-tested | - | Data-zip release, pull consumer. | | `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, and CI is lint-only. | -| `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. | +| `operational` config | HomeAutomation-Config | not-tested | 2026-08-15 | `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. HomeAutomation-Config is the first operational repository with a committed report (`reports/homeautomation-config/audit.md`): `source-only` plus `operational`, clean on every mechanized check on `main` after its 2026-08-15 resync and promotion. Its `.editorconfig` and `.gitattributes` are an LF adaptation that the intent advisory keeps flagging by construction. | ## Composable Targets diff --git a/reports/divergences.md b/reports/divergences.md index 93c9ff50..65e77015 100644 --- a/reports/divergences.md +++ b/reports/divergences.md @@ -11,12 +11,10 @@ Generated by `python3 spec/fidelity_honesty.py --report` - do not hand-edit. Cur ### investigate - **pyproject.toml** (manifest gap, carried by Financial-Modeling, aiopurpleair, homeassistant-purpleair) - The hub gained a config-only Scripts-profile pyproject.toml in #388. Decide whether to track it (intent, appliesTo python) after confirming the python repos carry an equivalent. -- **.github/workflows/publish-release.yml** (manifest gap, carried by Blog, ESPHome-Config, ESPHome-NonRoot, Financial-Modeling, HomeAssistant-Config, HomeAutomation-Config, KiCadLibrary, LanguageTags, MediaTools, NxWitness, PhotoCleaner, PlexCleaner, Utilities, VSCode-Server-DotNetCore, Vantage-Config, aiopurpleair, homeassistant-purpleair) - Carried by some repos, absent from others, and varies widely (12 divergent, 5 absent). Needs a fidelity call (interface vs intent) and an appliesTo scope before tracking - it would surface many new findings. -- **.github/workflows/validate-task.yml** (manifest gap, carried by AudioCleaner, Blog, ESPHome-NonRoot, Financial-Modeling, HomeAutomation-Config, LanguageTags, MediaTools, NxWitness, PhotoCleaner, PlexCleaner, Utilities, VSCode-Server-DotNetCore, aiopurpleair) - As publish-release.yml (11 divergent, 9 absent): fidelity plus appliesTo decision pending. ### retire -- **repo-config/configure.sh** (manifest gap, carried by Blog, ESPHome-Config, ESPHome-NonRoot, Financial-Modeling, HomeAssistant-Config, HomeAutomation-Config, LanguageTags, NxWitness, PhotoCleaner, PlexCleaner, Utilities, VSCode-Server-DotNetCore, Vantage-Config, aiopurpleair, homeassistant-purpleair) - The script is hub-hosted rather than carried, per GOVERNANCE.md "Hub-Hosted Tooling", so it is no longer a manifest entry and a downstream copy is retired rather than re-vendored. Every copy the fleet holds is the hub's content with nothing per-repo in it, and the copies predate the payload-driven check mode, which is the drift this removes rather than converges. Delete the copy as each repo is next visited and run the hub's script against the repo instead. The carriers are not enumerated here, because the fleet-wide gap pass names them from live state and a hand-kept list understated them: it read six while fifteen repos carry one. The ruleset payloads and settings.json stay carried, since those are what the repo is audited against and what its own AUDIT.md diffs the live configuration against. +- **repo-config/configure.sh** (manifest gap, carried by ESPHome-NonRoot, Financial-Modeling, HomeAssistant-Config, LanguageTags, NxWitness, PhotoCleaner, Utilities, VSCode-Server-DotNetCore, Vantage-Config, aiopurpleair, homeassistant-purpleair) - The script is hub-hosted rather than carried, per GOVERNANCE.md "Hub-Hosted Tooling", so it is no longer a manifest entry and a downstream copy is retired rather than re-vendored. Every copy the fleet holds is the hub's content with nothing per-repo in it, and the copies predate the payload-driven check mode, which is the drift this removes rather than converges. Delete the copy as each repo is next visited and run the hub's script against the repo instead. The carriers are not enumerated in this ledger entry, because the fleet-wide gap pass names them from live state in the generated report and a hand-kept list understated them: it read six while fifteen repos carry one. The ruleset payloads and settings.json stay carried, since those are what the repo is audited against and what its own AUDIT.md diffs the live configuration against. ### accepted @@ -25,42 +23,45 @@ Generated by `python3 spec/fidelity_honesty.py --report` - do not hand-edit. Cur - **TODO.md** (manifest gap, carried by Blog, HomeAutomation-Config) - A repo's own running backlog, which STANDUP.md section 2 names as one of the four declared destinations for repo-specific content. It is advisory and required by no selector, so the manifest deliberately does not declare it and a repo's copy is its own content rather than a carry. - **.husky/pre-commit** (manifest gap, carried by AudioCleaner, LanguageTags, MediaTools, NxWitness, PhotoCleaner, PlexCleaner, Utilities) - A path collision rather than a carry. Each repo's hook runs that repo's own language formatters, and the hub's runs the doc gates, so the seven downstream copies share the path and none of the content: LanguageTags and Utilities run language formatting and style with no Docker, PlexCleaner runs CSharpier and dotnet format, and the hub runs prose_lint.py and repo_gate.py. Verified by reading the downstream copies on 2026-08-10. - **scripts/README.md** (manifest gap, carried by KiCadLibrary) - A path collision rather than a carry. KiCadLibrary's copy documents its own KiCad tooling (common.py, verify_library.py, build_library.py) beside the scripts it describes, and shares nothing with the hub's fleet-gate documentation. Verified by reading it on 2026-08-10. scripts/ is a generic path, so a repo with its own tooling directory matches this check without carrying anything of the hub's. +- **.github/workflows/publish-release.yml** (manifest gap, carried by Blog, ESPHome-Config, ESPHome-NonRoot, Financial-Modeling, HomeAssistant-Config, HomeAutomation-Config, KiCadLibrary, LanguageTags, MediaTools, NxWitness, PhotoCleaner, PlexCleaner, Utilities, VSCode-Server-DotNetCore, Vantage-Config, aiopurpleair, homeassistant-purpleair) - A path collision rather than a carry, settled 2026-08-15 by reading every cataloged repo's copy on its ground-truth branch (18 copies read, the hub's own included, so 17 downstream repos carry one and 4 carry none). Each repo's publisher is its own. The job keys are per-repo (publish in 15 of the 18, against create-release on homeassistant-purpleair, publish-pypi on aiopurpleair, and an eight-job orchestration on NxWitness), and the triggers run from dispatch-only through dispatch plus schedule plus a paths-filtered push. No interface contract the schema can express fits that spread, and no appliesTo selector separates the carriers from the four that carry none, since both source-only and csharp+console repos sit on either side. What the file must honor is declared elsewhere and needs no second entry: build-release-task.yml carries the interface contract for the release path, and WORKFLOW.md's D2.3, D4.1, and D7.1 are read by hand per AUDIT.md section 5. +- **.github/workflows/validate-task.yml** (manifest gap, carried by AudioCleaner, Blog, ESPHome-NonRoot, Financial-Modeling, HomeAutomation-Config, LanguageTags, MediaTools, NxWitness, PhotoCleaner, PlexCleaner, Utilities, VSCode-Server-DotNetCore, aiopurpleair) - As publish-release.yml, and settled in the same 2026-08-15 read (14 copies read, the hub's own included, so 13 downstream repos carry one and 8 carry none). Every carrier is on: workflow_call over a per-repo job list, in six distinct shapes across the 14 (lint alone on 3, unit-test plus lint on 5, validate alone on 2, lint plus test plus docs on 2, compile-test plus lint on 1, and unit-test plus lint plus codegen-drift on 1), so the only thing every copy shares is the trigger, which no contract field states. The 8 that carry none inline their validation instead, and no selector separates them, since source-only repos again sit on either side. The fixed part is the required check the aggregator reports, which test-pull-request.yml declares at interface fidelity and the ruleset binds by name. ## Untriaged - add a disposition to `spec/divergences.json` -- **AGENTS.md > Fleet Bootstrap** - **not carried** by AudioCleaner, DevKitCIoT, ESPHome-Config, ESPHome-NonRoot, Financial-Modeling, HolidayLights, HomeAssistant-Config, KiCadLibrary, LanguageTags, MediaTools, NxWitness, PhotoCleaner, PlexCleaner, Utilities, VSCode-Server-DotNetCore, Vantage-Config, aiopurpleair, homeassistant-purpleair, so the section never arrived rather than being edited (verbatim canonical) -- **AGENTS.md > Context and Delegation Discipline** - **not carried** by AudioCleaner, DevKitCIoT, ESPHome-Config, ESPHome-NonRoot, HolidayLights, HomeAssistant-Config, KiCadLibrary, LanguageTags, MediaTools, NxWitness, PlexCleaner, Utilities, VSCode-Server-DotNetCore, Vantage-Config, aiopurpleair, homeassistant-purpleair, so the section never arrived rather than being edited (verbatim canonical) -- **AGENTS.md > Where the Rules Live** - **not carried** by AudioCleaner, DevKitCIoT, ESPHome-Config, ESPHome-NonRoot, HolidayLights, HomeAssistant-Config, KiCadLibrary, LanguageTags, MediaTools, NxWitness, PlexCleaner, Utilities, VSCode-Server-DotNetCore, Vantage-Config, aiopurpleair, homeassistant-purpleair, so the section never arrived rather than being edited (verbatim canonical) +- **AGENTS.md > Fleet Bootstrap** - **not carried** by AudioCleaner, DevKitCIoT, ESPHome-NonRoot, Financial-Modeling, HolidayLights, HomeAssistant-Config, KiCadLibrary, LanguageTags, MediaTools, NxWitness, PhotoCleaner, Utilities, VSCode-Server-DotNetCore, Vantage-Config, aiopurpleair, homeassistant-purpleair, so the section never arrived rather than being edited (verbatim canonical) +- **AGENTS.md > Context and Delegation Discipline** - **not carried** by AudioCleaner, DevKitCIoT, ESPHome-NonRoot, HolidayLights, HomeAssistant-Config, KiCadLibrary, LanguageTags, MediaTools, NxWitness, Utilities, VSCode-Server-DotNetCore, Vantage-Config, aiopurpleair, homeassistant-purpleair, so the section never arrived rather than being edited (verbatim canonical) +- **AGENTS.md > Where the Rules Live** - **not carried** by AudioCleaner, DevKitCIoT, ESPHome-NonRoot, HolidayLights, HomeAssistant-Config, KiCadLibrary, LanguageTags, MediaTools, NxWitness, Utilities, VSCode-Server-DotNetCore, Vantage-Config, aiopurpleair, homeassistant-purpleair, so the section never arrived rather than being edited (verbatim canonical) - **GOVERNANCE.md > Representative Data in Agent-Authored Text** - **not carried** by Financial-Modeling, so the section never arrived rather than being edited (verbatim canonical) -- **GOVERNANCE.md > Hub-Hosted Tooling** - **not carried** by Blog, Financial-Modeling, HomeAutomation-Config, PhotoCleaner, so the section never arrived rather than being edited (verbatim canonical) +- **GOVERNANCE.md > Hub-Hosted Tooling** - **not carried** by Financial-Modeling, PhotoCleaner, so the section never arrived rather than being edited (verbatim canonical) ## Mechanical re-vendor (verbatim stale copies) A past hub revision, not the current canonical - the audit already flags these as DRIFT. Copy the current file down. No judgment needed. -- **AGENTS.md > Fleet Bootstrap** (2): Blog, HomeAutomation-Config +- **AGENTS.md > Fleet Bootstrap** (2): Blog, ESPHome-Config - **AGENTS.md > Context and Delegation Discipline** (1): Financial-Modeling -- **AGENTS.md > Where the Rules Live** (4): Blog, Financial-Modeling, HomeAutomation-Config, PhotoCleaner +- **AGENTS.md > Where the Rules Live** (4): Blog, ESPHome-Config, Financial-Modeling, PhotoCleaner - **GOVERNANCE.md > Foundational Principles** (1): Financial-Modeling -- **GOVERNANCE.md > Durable Knowledge and Self-Improvement** (1): Financial-Modeling -- **GOVERNANCE.md > Repository Boundaries and Write Safety** (4): Blog, Financial-Modeling, HomeAutomation-Config, PhotoCleaner -- **GOVERNANCE.md > Git and Commit Rules** (1): Financial-Modeling -- **GOVERNANCE.md > Branching Model** (4): Blog, Financial-Modeling, HomeAutomation-Config, PhotoCleaner -- **GOVERNANCE.md > Release Model** (4): Blog, Financial-Modeling, HomeAutomation-Config, PhotoCleaner -- **GOVERNANCE.md > Operational Repositories** (4): Blog, Financial-Modeling, HomeAutomation-Config, PhotoCleaner -- **GOVERNANCE.md > Pull Request Title and Commit Message Conventions** (1): Financial-Modeling -- **GOVERNANCE.md > Documentation Style Conventions** (4): Blog, Financial-Modeling, HomeAutomation-Config, PhotoCleaner -- **GOVERNANCE.md > Verification Discipline** (4): Blog, Financial-Modeling, HomeAutomation-Config, PhotoCleaner -- **GOVERNANCE.md > PR Review Etiquette** (4): Blog, Financial-Modeling, HomeAutomation-Config, PhotoCleaner -- **GOVERNANCE.md > Communicating with the User** (4): Blog, Financial-Modeling, HomeAutomation-Config, PhotoCleaner -- **GOVERNANCE.md > Workflow YAML Conventions** (4): Blog, Financial-Modeling, HomeAutomation-Config, PhotoCleaner +- **GOVERNANCE.md > Durable Knowledge and Self-Improvement** (4): Blog, ESPHome-Config, Financial-Modeling, PhotoCleaner +- **GOVERNANCE.md > Repository Boundaries and Write Safety** (4): Blog, ESPHome-Config, Financial-Modeling, PhotoCleaner +- **GOVERNANCE.md > Git and Commit Rules** (4): Blog, ESPHome-Config, Financial-Modeling, PhotoCleaner +- **GOVERNANCE.md > Branching Model** (4): Blog, ESPHome-Config, Financial-Modeling, PhotoCleaner +- **GOVERNANCE.md > Release Model** (4): Blog, ESPHome-Config, Financial-Modeling, PhotoCleaner +- **GOVERNANCE.md > Operational Repositories** (4): Blog, ESPHome-Config, Financial-Modeling, PhotoCleaner +- **GOVERNANCE.md > Hub-Hosted Tooling** (2): Blog, ESPHome-Config +- **GOVERNANCE.md > Pull Request Title and Commit Message Conventions** (4): Blog, ESPHome-Config, Financial-Modeling, PhotoCleaner +- **GOVERNANCE.md > Documentation Style Conventions** (4): Blog, ESPHome-Config, Financial-Modeling, PhotoCleaner +- **GOVERNANCE.md > Verification Discipline** (4): Blog, ESPHome-Config, Financial-Modeling, PhotoCleaner +- **GOVERNANCE.md > PR Review Etiquette** (4): Blog, ESPHome-Config, Financial-Modeling, PhotoCleaner +- **GOVERNANCE.md > Communicating with the User** (4): Blog, ESPHome-Config, Financial-Modeling, PhotoCleaner +- **GOVERNANCE.md > Workflow YAML Conventions** (4): Blog, ESPHome-Config, Financial-Modeling, PhotoCleaner - **GOVERNANCE.md > Supported Development Platforms** (1): Financial-Modeling - **GOVERNANCE.md > Editor and Tasks** (1): Financial-Modeling -- **GOVERNANCE.md > Repository Details** (4): Blog, Financial-Modeling, HomeAutomation-Config, PhotoCleaner -- **.markdownlint-cli2.jsonc** (18): Blog, DevKitCIoT, ESPHome-Config, ESPHome-NonRoot, Financial-Modeling, HolidayLights, HomeAssistant-Config, HomeAutomation-Config, KiCadLibrary, LanguageTags, MediaTools, NxWitness, PhotoCleaner, PlexCleaner, Utilities, VSCode-Server-DotNetCore, Vantage-Config, homeassistant-purpleair -- **repo-config/develop.json** (5): Financial-Modeling, MediaTools, PhotoCleaner, PlexCleaner, Utilities -- **repo-config/operational/develop.json** (3): ESPHome-Config, HomeAssistant-Config, Vantage-Config -- **repo-config/main.json** (8): ESPHome-Config, Financial-Modeling, HomeAssistant-Config, MediaTools, PhotoCleaner, PlexCleaner, Utilities, Vantage-Config +- **GOVERNANCE.md > Repository Details** (2): Financial-Modeling, PhotoCleaner +- **.markdownlint-cli2.jsonc** (14): DevKitCIoT, ESPHome-NonRoot, Financial-Modeling, HolidayLights, HomeAssistant-Config, KiCadLibrary, LanguageTags, MediaTools, NxWitness, PhotoCleaner, Utilities, VSCode-Server-DotNetCore, Vantage-Config, homeassistant-purpleair +- **repo-config/develop.json** (4): Financial-Modeling, MediaTools, PhotoCleaner, Utilities +- **repo-config/operational/develop.json** (2): HomeAssistant-Config, Vantage-Config +- **repo-config/main.json** (6): Financial-Modeling, HomeAssistant-Config, MediaTools, PhotoCleaner, Utilities, Vantage-Config ## Promote candidates (intent uniform -> verbatim) diff --git a/reports/homeautomation-config/audit.md b/reports/homeautomation-config/audit.md new file mode 100644 index 00000000..a705896a --- /dev/null +++ b/reports/homeautomation-config/audit.md @@ -0,0 +1,57 @@ +# Audit: HomeAutomation-Config + +- **Audited branch:** main (`22ed137`), after the promotion of `develop` (`c8252c7`) +- **Types:** source-only (from registry), operational workflow model +- **Verdict:** operational on every mechanized check and every hand-judged dimension +- **Date:** 2026-08-15 +- **Run stamp:** `audit run 2026-08-15T15:19:01Z | hub 704223a`, with the pre-promotion pair `audit run 2026-08-15T14:58:02Z | hub d54862a` (main at `e10a2cf`) and `audit run 2026-08-15T14:58:25Z | hub d54862a | branch override develop` kept for the record + +First committed report for this repository. It was resynced against the hub's `main` (`0e84805`) on this date per `RESYNC.md`, the resync landed on `develop` as ptr727/HomeAutomation-Config#51, and the promotion to `main` merged as ptr727/HomeAutomation-Config#52. The pre-promotion runs read `main` at `e10a2cf` with the whole stale-carried-content class open and `develop` clean, and the post-promotion run at `22ed137` reads what `develop` read, so the two branches now grade the same. + +## Develop Drift + +`develop` vs `main`: identical trees after the #52 promotion merge (15 content commits since #36, conflict-free). The commit-count gap in `main`'s favor is the promotion-merge ancestry artifact and is **benign**. No action. + +## Dimensions + +| Dimension | Letter | Intent | Verdict | Evidence (file:line) | +| --- | --- | --- | --- | --- | +| source-only (`sourceonly.release.tagonly`) | pass | pass | pass | `.github/workflows/publish-release.yml:61-69` inlines `softprops/action-gh-release` (SHA-pinned) with `LICENSE` and `README.md` as the only files, so the release is the tag, GitHub's source archive, README, and LICENSE. No `build-*-task.yml` and no `expect_release_assets` | +| source-only (`sourceonly.nbgv.retained`) | pass | pass | pass | `version.json` retained (hub byte form after #51), and `dotnet/nbgv@master` computes the tag inline at `.github/workflows/publish-release.yml:55`, the same unpinned form the hub's own publisher and catalog snippet use by design | +| branch-model | pass | pass | pass | Both rulesets live and matching the carried payloads by normalized diff (`repo-config/main.json`, `repo-config/operational/develop.json`), confirmed by `repo-config/configure.sh check ptr727/HomeAutomation-Config operational` from this checkout and by the repository's own `AUDIT.md` snippets, all reporting in sync | +| carried-scope | pass | pass | pass | `repo-config/configure.sh` deleted in #40, no `retire`-dispositioned hub path remains. Two `investigate`-dispositioned hits stand (`.github/workflows/publish-release.yml`, `.github/workflows/validate-task.yml`), which is a hub triage rather than a repository finding, see Escalations | +| repo-setup | pass | pass | pass | `spec/secrets.json` carries `baseline` plus `note` only, the shape `docs/repo-config-carry.md` states for a source-only repository whose only publish target maps to no mechanism (the dead `targetMechanisms` map dropped in #51). `configure.sh check` reports the Dependabot security features enabled | +| linter-parity | pass | pass | pass | `.github/workflows/validate-task.yml:30-50` runs markdownlint-cli2, cspell (README and HISTORY scope), actionlint, and editorconfig-checker as pinned action wrappers or Docker, one config per linter at the root. `.github/workflows/test-pull-request.yml:31` carries the ruleset-bound `Check pull request workflow status job` | +| recurring-violations | pass | pass | pass | Hub prose lint over the resync diff is clean. LF throughout, per the registry `lineEndings: lf` and the repository's `.editorconfig`. Three pre-existing whole-tree `dead-path` mentions remain (`GOVERNANCE.md:29`, `GOVERNANCE.md:282`, `OPERATIONS.md:306`), the first two inside carried text and all three left for the files' next edit per the fix-as-edited policy | +| readme-structure | pass | pass | pass | The seven README letters the pre-promotion `main` run reported (license shield placement, `-link` suffixes, `github-link` naming, group membership) were closed in #40 and the post-promotion run reports none | +| agent-instruction-set | pass | pass | pass | Every `AGENTS.md` and `GOVERNANCE.md` verbatim section matches the current canonical after #51. Before the re-vendor, each differing line in every stale region was traced to a past hub commit, so no repository-local rule sat inside a verbatim region. `CODESTYLE.md` carries the hub's skill-pointer form with the repository's own shell rules kept as a subsection of Shell. `.github/copilot-instructions.md` carries the current runbook with an empty Disproved Claims ledger | +| workflow (WORKFLOW.md 5A/5B) | pass | pass | pass | Operational model: `.github/workflows/test-pull-request.yml:9-12` runs on `push` to `develop` (advisory) and `pull_request` into `main` (the enforced gate). `.github/workflows/publish-release.yml:4` is `workflow_dispatch` only, matching `releaseTrigger: dispatch-only`. `.github/workflows/merge-bot-pull-request.yml` uses `actions/create-github-app-token` (SHA-pinned) with the client-id input, matching the baseline mechanism note. `.github/dependabot.yml` declares `github-actions` for both `main` and `develop` | + +csharp, nuget, pypi, python, console, docker, hugo: N/A (no packaging, no application code, no site). + +## Defects (most severe first) + +None. The whole class of stale carried content the pre-promotion `main` run reported (2 `AGENTS.md` and 13 `GOVERNANCE.md` verbatim sections, `.markdownlint-cli2.jsonc`, the carried `configure.sh`, and the README letters) was closed by #40, #43, #50, and #51 and reached `main` with #52. + +## Drift Findings + +- `.editorconfig` and `.gitattributes` carry an intent advisory on both branches. Both are the repository's LF adaptation (registry `lineEndings: lf`), and every hub change since the copies' last commit is CRLF-model or hub-tree specific (Python and Dockerfile pins this tree has no files for). Judged current by meaning, and the advisory will keep firing until the hub's `.editorconfig` stops moving, which is a property of the intent tier rather than of this repository. +- `.github/workflows/publish-release.yml` and `.github/workflows/validate-task.yml` report `hub-only` with an `investigate` disposition in `spec/divergences.json`. Both are the repository's own interface workflows honoring the named contract, and neither can be acted on until the hub settles the fidelity and `appliesTo` call the ledger records as pending. + +## Convergence in Flight + +- ptr727/HomeAutomation-Config#51 (merged into `develop` at `c8252c7`): the resync, one commit per drift class, driven to a Copilot review on its head, one finding fixed and resolved. Coverage read 9 of 11 files on both rounds, which the maintainer accepted at merge. +- ptr727/HomeAutomation-Config#52 (merged as `22ed137`): the `develop` to `main` promotion. Copilot reviewed the head, one finding deferred to ptr727/HomeAutomation-Config#53 (Duplicacy credentials on a command line in `Duplicacy/init-storage.sh`, content from #45 rather than from the resync), thread resolved, checks 6/6, coverage 72 of 76 files, which the maintainer accepted at merge. + +## Proposed Registry / Spec Updates + +- Delete the third `driftNote` ("README self-flags previously-committed secrets"): the README no longer mentions secrets, and #46 and #49 moved every real secret out of the checkout to `~/.secrets/`, so the note describes finished work. Applied in the same change. +- Fill the `operational` config row of `reports/conformance-matrix.md` with this repository as its reference, since it is now the first operational repository with a committed report. Applied in the same change. + +## Escalations + +Raised rather than resolved, per AUDIT.md section 9. Each was already on file from a sibling resync the same day, so this run corroborates rather than opens them. + +1. **The hub's canonical `.github/copilot-instructions.md` names `ptr727/ProjectTemplate` in intent-fidelity prose**, in "A Shape Nothing Recognizes Blocks the Loop and Earns an Issue" ("File an issue on the hub, `ptr727/ProjectTemplate`"). The template-reference scan exempts only verbatim sections and the hub itself, so every downstream carry of the current canonical trips `carried:` on its next audit. #51 reworded the line to name the hub through `AGENTS.md` "Fleet Bootstrap", and PlexCleaner's carry holds the same line. The fix belongs in the canonical wording. Tracked as #720. +2. **`spec/divergences.json` still dispositions `.github/workflows/publish-release.yml` and `.github/workflows/validate-task.yml` as `investigate` with no tracking**, so every repository carrying either reports two `hub-only` findings that no repository can act on. The fidelity and `appliesTo` decision the ledger records as pending is the remedy. Tracked as #669. +3. **The `dead-path` prose check flags a carried mention of the hub-hosted `repo-config/configure.sh`** (`GOVERNANCE.md` "Repository Boundaries and Write Safety" is verbatim carried text) in any repository that has deleted its copy, which is now every converged repository. The mention is a pointer to a hub-hosted file, the shape "Documentation Style Conventions" permits, so the check wants the same hub-hosted exemption the template-reference scan gained. Tracked as #721, where it has already failed a downstream promotion gate. diff --git a/reports/photocleaner/audit.md b/reports/photocleaner/audit.md index 7b8ebaa1..f115b10f 100644 --- a/reports/photocleaner/audit.md +++ b/reports/photocleaner/audit.md @@ -1,69 +1,59 @@ # Audit: PhotoCleaner -- **Audited branch:** main (`15b9b5b7411bbba43b8bff8044c4b651355c10a4`) +- **Audited branch:** main (`c457ff3`) - **Types:** csharp, console, docker (from registry) -- **Verdict:** not operational -- **Date:** 2026-08-01 -- **Run stamp:** `audit run 2026-08-01T15:30:19Z | hub 6501479` +- **Verdict:** not operational on `main`, converged on `develop`, promotion in flight +- **Date:** 2026-08-15 +- **Run stamps:** `audit run 2026-08-15T14:28:27Z | hub 0e84805` (main), `audit run 2026-08-15T14:58:12Z | hub d54862a | branch override develop` (develop at `e8b7a81`) -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. +Supersedes the 2026-08-01 snapshot. Everything that snapshot listed as a defect or drift has landed on `develop`: the `AGENTS.md`/`GOVERNANCE.md` router split, the `validate-release` gate, the README structure, and the two 2026-08-13 and 2026-08-15 resyncs against the hub ([#49](https://github.com/ptr727/PhotoCleaner/pull/49), [#50](https://github.com/ptr727/PhotoCleaner/pull/50)). None of it has reached `main` yet, so `main` still measures as the pre-resync state and this report says so rather than reading `develop` as ground truth. ## Develop Drift -`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. +`develop` vs `main`: two content commits ahead (#49, #50), nothing behind. This is convergence awaiting promotion, not divergence. The audit at `main` reports 43 findings, 6 of them letters (README section and reference-name letters, `host-tools.json` absent). The audit at `develop` reports 2, both the `investigate`-dispositioned hub-only files (`publish-release.yml`, `validate-task.yml`) that every repo carries and that are settled in `spec/divergences.json` rather than per repo. + +The promotion [#51](https://github.com/ptr727/PhotoCleaner/pull/51) is open and **blocked by a hub defect, not a repo one**: the prose gate diffs a promotion against `main`, and `prose_lint.py dead-path` flags three mentions of `repo-config/configure.sh` (a file the repo retired per the `retire` disposition), two of them in verbatim hub text the repo cannot reword. Filed as [#721](https://github.com/ptr727/ProjectTemplate/issues/721). Copilot read 18 of 19 files on the promotion, omitting `GOVERNANCE.md`, and generated no comments. ## Dimensions +Judged at `main` unless the row says otherwise. Type-dimension checks are hand-judged per AUDIT.md section 4 and are unchanged from the 2026-08-01 snapshot, since #49 and #50 touched governance and configuration only. + | Dimension | Letter | Intent | Verdict | Evidence (file:line) | | --- | --- | --- | --- | --- | -| 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--*`, 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-`), 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` | +| csharp | pass | pass | pass | `Directory.Build.props` carries the analyzer set and `TreatWarningsAsErrors`, `Directory.Packages.props` centralizes versions, `.editorconfig` carries the shared `[*.cs]` block | +| 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--*`, gated `!smoke` | +| docker | pass | pass | pass | `Docker/Dockerfile` multi-arch, `Docker/README.md` present, `build-docker-task.yml` uses a registry layer cache (`buildcache-`), the image re-pushes on publish | +| branch-model | pass | pass | pass | `repo-config/configure.sh check ptr727/PhotoCleaner release` at hub `0e84805`: both rulesets and every general setting match, nothing to apply | +| repo-setup | pass | pass | pass | Every name in `spec/secrets.json` present in both the Actions and Dependabot stores (`CODECOV_TOKEN` included), no forbidden name | +| linter-parity | pass | pass | pass | `validate-task.yml` runs csharpier check, `dotnet format style --verify-no-changes`, markdownlint, cspell, actionlint, editorconfig-checker, and the hub prose gate. One config per linter | +| recurring-violations | pass | pass | pass | `prose_lint.py --diff origin/develop` clean on #50, `repo_gate.py` clean on `develop` (the `eol-coverage` forward-declaration gap closed in #50) | +| readme-structure | fail | pass | drift | At `main`: no `3rd Party Tools` section, four reference-name letters, an `Internal` link group. All fixed on `develop` in #49 | +| workflow (WORKFLOW.md 5A/5B) | pass | pass | pass | `build-release-task.yml` carries `validate-release` and the re-vendored `github-release` job (interface contract satisfied by name), the two hub-only workflows are `investigate` in the ledger | +| agent-instruction-set | fail | pass | drift | At `main`: `Fleet Bootstrap` and `Hub-Hosted Tooling` absent, 15 verbatim sections stale, `AGENTS.md`/`GOVERNANCE.md`/`CODESTYLE.md`/`WORKFLOW.md`/`copilot-instructions.md` intent copies trail the hub. All re-vendored or reconciled on `develop` in #49 and #50, where the audit reports no instruction-set finding | nuget, pypi, python: N/A (no packaging, no Python). ## Defects (most severe first) -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. +None on `develop`. On `main`, the readme-structure and agent-instruction-set rows above are letter misses whose intent holds (the content exists and is correct on `develop`), so they are drift awaiting promotion rather than defects. ## Drift Findings -- `.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`. +- Every finding the `main` audit reports is closed on `develop` by #49 or #50, and the promotion is #51. +- `publish-release.yml` and `validate-task.yml` are `hub-only` at `investigate` in `spec/divergences.json`, a fleet-wide question rather than this repo's. ## 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 +- [#50](https://github.com/ptr727/PhotoCleaner/pull/50) merged to `develop` at `e8b7a81`: re-vendors the 10 stale verbatim sections, reconciles the intent carries against the hub's history since each last synced (with the repo's own adaptations kept, per the carried-instruction-file-guard probe), claims `CODECOV_TOKEN` in both stores, and clears the `eol-coverage` gap. +- [#51](https://github.com/ptr727/PhotoCleaner/pull/51) `develop -> main` promotion, open, blocked on the hub prose gate ([#721](https://github.com/ptr727/ProjectTemplate/issues/721)). ## Proposed Registry / Spec Updates -- 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. +- The `driftNotes` are unchanged. The first describes the publish shape (multi-arch Docker plus a github-release 7z, two-phase release), which still holds. The second records the private-for-now decision, which still holds (`isPrivate: true` at the time of this run) and still means the GitHub-sourced shields render broken. ## 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. +Two hub findings from this pass, filed rather than patched per repo: -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. +1. [#721](https://github.com/ptr727/ProjectTemplate/issues/721): `prose_lint.py dead-path` cannot recognize a hub-hosted path in a repo that retired the file, so verbatim text naming `repo-config/configure.sh` fails a downstream promotion gate. +2. [#722](https://github.com/ptr727/ProjectTemplate/issues/722): the hub's `.github/copilot-instructions.md` links `GOVERNANCE.md#every-finding-ends-in-an-action`, an anchor that left `GOVERNANCE.md` when PR Review Etiquette was packaged as a Skill. PhotoCleaner re-pointed its copy in #50 after Copilot raised it. diff --git a/reports/plexcleaner/audit.md b/reports/plexcleaner/audit.md index 7566831a..a3a409ca 100644 --- a/reports/plexcleaner/audit.md +++ b/reports/plexcleaner/audit.md @@ -1,71 +1,50 @@ # Audit: PlexCleaner -- **Audited branch:** main (`e84f1426dfa1746777ff470d019e1562632cd9f0`) -- **Types:** csharp, console, docker (from registry) +- **Audited branch:** main (`16f196e`) +- **Types:** csharp, console, docker, python (from registry) +- **Workflow model:** release - **Verdict:** operational -- **Date:** 2026-07-04 +- **Date:** 2026-08-15 +- **Run stamps:** `audit run 2026-08-15T15:13:26Z | hub d54862a` (the hub `develop` head, byte-identical to `main` at `0e84805`), re-run at `audit run 2026-08-15T15:30:07Z | hub d54862a` after the two gap dispositions below were settled + +Supersedes the 2026-07-04 snapshot, which measured the repository before the `AGENTS.md` and `GOVERNANCE.md` split, before `repo-config/configure.sh` was retired, and before the README structure sweep, so it described drift that no longer exists. This report is measured rather than carried forward: every row below was re-read at `16f196e`, and each structural workflow divergence the old snapshot recorded was re-checked rather than assumed. Two of them are closed. + +The repository was resynced against the hub `main` at `0e84805` and promoted on this date (ptr727/PlexCleaner#922, ptr727/PlexCleaner#923, ptr727/PlexCleaner#924). ## Develop Drift -`develop` vs `main`: ahead 13, behind 23 (`gh api repos/ptr727/PlexCleaner/compare/main...develop` -> `status: diverged, ahead_by: 13, behind_by: 23`). **Diverged - a drift finding.** `develop` carries 13 commits `main` lacks (normal unreleased work under the forward-only model) but is also 23 behind. With no `main -> develop` back-merge, `main`-only fixes (e.g. Dependabot bumps merged straight to `main`) have not reached `develop`, so `develop` does not reflect the released pipeline. Not a break; worth reconciling. +`develop` (`57d81f5`) is 0 ahead and 89 behind `main`, and the two trees are **identical** (`git diff main develop` is empty). The count is the promotion-merge ancestry artifact this repository's merge model produces (squash into `develop`, merge commit into `main`), so it is benign and not a finding. The 2026-07-04 snapshot reported `develop` as diverged, which the promotions since have closed. ## Dimensions +The deterministic subset is `spec/audit.py` at the run stamps above. The first reports **0 defect, letter, or error findings** and two `hub-only` drift items, both of them the fleet-wide ledger question rather than this repository's. The second, after that question was settled, reports **clean**. The judgment rows are hand-read at `16f196e`. + | Dimension | Letter | Intent | Verdict | Evidence (file:line) | | --- | --- | --- | --- | --- | -| csharp | pass | pass | pass | `.editorconfig:59` carries the shared `[*.cs]` rule block (matches the template block, plus 17 documented repo-wide CA relaxations `:66-84`); analyzers enforced in `Directory.Build.props:6-9` (`AnalysisLevel=latest-all`, `AnalysisMode=All`, `EnableNETAnalyzers=true`, `TreatWarningsAsErrors=true`). See Drift re CODESTYLE.md:242 | -| nuget | - | - | N/A | no `build-nugetlibrary-task.yml` / `dotnet nuget push`; `Directory.Build.props:11 IsPackable=false` | -| pypi | - | - | N/A | no `pyproject.toml` / `gh-action-pypi-publish` | -| python | - | - | N/A | no Python project | -| console | pass | pass | pass | System.CommandLine app, `PlexCleaner/PlexCleaner.csproj:16 OutputType=Exe`; smoke matrix `["linux-x64","win-x64"]` is a strict subset of the 7-runtime full matrix `build-executable-task.yml:44`; per-runtime `publish--` aggregated by `pattern:` + `merge-multiple:` into one `release-asset--executable` `:91-105`, both the per-runtime upload `:74` and the aggregation job `:85` gated `!smoke` | -| docker | pass | pass | pass | registry layer cache `buildcache-` (never `type=gha`) `build-docker-task.yml:89-92` (`cache-from` both branches, `cache-to` only-branch + only-on-push); trimmed `Docker/README.md` published via `peter-evans/dockerhub-description`, main-only `:102-110`; image always re-pushes on `inputs.push` (`dockerhub && !smoke`), independent of the release-create skip `build-release-task.yml:82`. Publish uses the static `DOCKER_HUB_*` secret (expected for docker, not OIDC) | -| branch-model | drift | pass | drift | both branches protected (rulesets `develop`/`main` `enforcement: active`); the required-status-check `context` `Check pull request workflow status job` is the fleet canonical (adopted template-wide) and matches PlexCleaner's own aggregator `test-pull-request.yml:48`, so the naming is **not** a drift; residual drift is the off-baseline `repo-config/ruleset-{develop,main}.json` filenames (see Drift) | -| repo-setup | pass | pass | pass | actions + dependabot stores both hold `DOCKER_HUB_USERNAME`, `DOCKER_HUB_ACCESS_TOKEN` (docker-hub), `CODEGEN_APP_CLIENT_ID`, `CODEGEN_APP_PRIVATE_KEY` (used by the merge-bot `merge-bot-pull-request.yml:41-42`); no forbidden `NUGET_API_KEY` / `CODEGEN_APP_ID`. `CODECOV_TOKEN` present but unused (see Drift) | -| linter-parity | pass | pass | pass | one `.markdownlint-cli2.jsonc` + `cspell.json` drive editor + CLI + CI (`validate-task.yml:63-78`); csharpier/editorconfig shared via `.config/dotnet-tools.json`; CI runs each linter (csharpier check, `dotnet format style`, markdownlint, cspell, actionlint) `validate-task.yml:54-78` | -| recurring-violations | pass | pass | pass | comments concise; docs ASCII-clean (README/HISTORY/AGENTS/CODESTYLE/WORKFLOW/Docker README grep for em-dash/smart quotes -> none); `cspell.json:3 "language": "en-US"`; real endings compliant (`.gitattributes:6,12-18` `* -text` + LF pins for `*.sh`/`.husky/pre-commit`/`Dockerfile`). See Drift re `.editorconfig` global default | -| readme-structure | drift | pass | drift | Build and Distribution (with Build Status/Releases/Release Notes), Getting Started, Table of Contents, Use Cases, Installation, Configuration, Usage, 3rd Party Tools, License all present and largely ordered, but `## Questions or Issues` sits at `README.md:99` (after the ToC) instead of spec position 9 (after Usage), and `## Development Tooling` `:917` occupies the Development-Environment-Setup slot under a different name; extra sections interleaved | -| workflow (WORKFLOW.md 5A/5B) | drift | pass | drift | all applicable D-guarantees hold by **outcome**; divergences are structural (see 5A/5B): no `changes` paths-filter, single-branch-per-run publisher (no both-branch matrix), dispatch guard skips instead of failing fast, one-directional release gate embedded as a step, NBGV threaded from a single run without `IGNORE_GITHUB_REF` | - -## WORKFLOW.md 5A Static Audit (applicable D-guarantees) - -- **D1.1 / D1.4 (paths-filter):** DRIFT - no `changes` / `dorny/paths-filter` job; `validate` and `smoke-build` run on **every** push including docs-only and `.github/workflows/**`-only (`test-pull-request.yml:25-42`, deliberate per header `:3-10`). Over-builds; the "changed target slips unbuilt" failure mode cannot occur (both targets always built). -- **D1.2 (validation always runs):** PASS - `validate` (unit-test + lint) runs unconditionally `if: !github.event.deleted` (`test-pull-request.yml:25-28`); the aggregator `needs:` it `:50`. -- **D1.3 (smoke uploads/pushes nothing):** PASS - smoke sets `github: false`, `dockerhub: false` (`test-pull-request.yml:40-41`); executable upload gated `!smoke` (`build-executable-task.yml:74,85`), docker push `dockerhub && !smoke` (`build-release-task.yml:82`), github-release `github && !smoke` (`:87`). -- **D1.5 (one aggregator):** PASS - `check-workflow-status`, `needs: [validate, smoke-build]`, `always() && !github.event.deleted`, fails on any non-`success` (`test-pull-request.yml:47-63`). Name is ruleset-bound, the fleet-canonical ` job` (see branch-model). -- **D2.1 / D2.2 (validate-at-entry):** DRIFT - the release gate is the `Verify public release version step` **inside** github-release (`build-release-task.yml:99-111`), not an upfront entry job the builds `needs:`. It strips `+buildmetadata` and refuses a prerelease `-` on `main`, but checks **one direction only** (main-not-prerelease). Safe because `version.json:4` makes `main` the sole public ref, so NBGV structurally prerelease-tags every other branch. -- **D2.3 (dispatch guard):** DRIFT - the publish job `if: github.ref_name == 'main' || github.ref_name == 'develop'` (`publish-release.yml:33`) **silently skips** a dispatch from any other ref rather than failing fast with `::error::`. Skip is safe but non-canonical. -- **D3.1 (version from checked-out branch):** DRIFT (letter) / PASS (intent) - NBGV runs **once** in `get-version` on `inputs.ref` (`get-version-task.yml:46-56`) and threads outputs; no `IGNORE_GITHUB_REF=true`. Correct here because the publisher builds one branch per run with `github.ref` aligned to it (`publish-release.yml:4-7,39`), so no ref leak. -- **D3.2 (default = public, others = prerelease):** PASS - `version.json:4 publicReleaseRefSpec ^refs/heads/main$`; `prerelease: inputs.branch != 'main'` (`build-release-task.yml:149`); gate literal `inputs.branch == 'main'` (`:102`). All name `main`. -- **D3.3 (floor + git height):** PASS - `version.json:3 version "3.19"`; NBGV appends the height. -- **D3.4 (registry versions per classification):** PASS - Docker tags `main => latest`, else `develop`, plus `:SemVer2` (`build-docker-task.yml:84-85`); no nuget/pypi registry. -- **D4.1 (two-phase / both branches):** DRIFT - publisher triggers are `schedule` (weekly, `main` only) + `workflow_dispatch`, **no push trigger** (`publish-release.yml:3-7`), so merges never publish (correct). But it does **not** build both branches via a matrix - it builds only the trigger branch (`:26-44`); `develop` publishes only via a manual dispatch from `develop`. Outcomes are correct; the divergence is structural. -- **D4.2 (tag the built commit):** PASS - `target_commitish: needs.get-version.outputs.GitCommitId` (`build-release-task.yml:148`). -- **D4.3 (release contents):** PASS - tag + `generate_release_notes` + `LICENSE` + `README.md` + `./Publish/*` (the 7z) (`build-release-task.yml:147-154`); `prerelease = branch != main` `:149`; `fail_on_unmatched_files: true` `:150`. -- **D4.4 (no-op republish):** PASS - release-create gated `exists == false || workflow_dispatch` (`build-release-task.yml:143`); Docker still re-pushes (push independent of the existence check). -- **D5 (cleanup):** DRIFT (minor) - every upload sets `retention-days: 1` and there is no blanket `.artifacts[].id` delete (D5.5 pass), but the cross-job `release-asset--executable` consumed by github-release (`build-release-task.yml:114-119`) has **no** consume-delete step; it relies on the retention backstop alone (D5.1 prefers a delete at the consumer). Safe. -- **D6.1 (pattern handoff):** PASS - github-release downloads `pattern: release-asset-${{ inputs.branch }}-*` + `merge-multiple: true` (`build-release-task.yml:117-118`); the leaf uploads `release-asset--executable` (`build-executable-task.yml:104`). Canonical, no `artifact-ids:`. -- **D6.2 / D6.3 (branch drives config, suffixed artifacts):** PASS - leaves key config/tags off `inputs.branch` (`build-executable-task.yml:63`, `build-docker-task.yml:84,95`); artifacts branch-suffixed. -- **D6.4 (target set consistent):** PASS - both targets have a `build-` job and a `github-release` `needs:` entry (`build-release-task.yml:89`); no paths-filter to keep in lockstep (D1 drift). -- **D7.1 (publisher concurrency):** PASS - group `${{ github.workflow }}` (ref-independent), `cancel-in-progress: false` (`publish-release.yml:20-22`). -- **D7.2 (permissions):** PASS - publisher grants `contents: write` at the entry (`publish-release.yml:36-37`); no OIDC needed (docker uses the static secret); merge-bot jobs declare least-privilege `permissions:`. -- **D7.4 (optional-dependency chaining):** PASS - build jobs use `!cancelled() && get-version == 'success' && (validate == 'success' || 'skipped')` (`build-release-task.yml:53,69`). -- **D8.1 (merge-bot):** PASS - auto-merge on `opened`/`reopened`, method by base ref (`develop => --squash`, `main => --merge`) `merge-bot-pull-request.yml:57-65`, disable on maintainer `synchronize` `:70-97`, concurrency keyed on the PR number `:18`. -- **D9.1 (SHA-pinning):** PASS - all actions SHA-pinned with version comments; `dotnet/nbgv@master` is the sanctioned lagging-tag exception (`get-version-task.yml:54-56`). -- **D9.4 (docker cache):** PASS - registry buildcache, per-branch, `cache-to` writes only the built branch and only on push, `cache-from` reads both (`build-docker-task.yml:89-92`). -- **PyPI / NuGet / wrapper 5A addenda:** N/A. - -## WORKFLOW.md 5B Trace (applicable scenarios) - -- **S1** (PR touches a target): validate + smoke-build (amd64-only Docker, 2-runtime executable) run; no push, **no uploads**; release skipped; aggregator success; version prerelease. PASS. -- **S2 / S3** (docs-only / workflow-only PR): smoke-build **runs** (no paths-filter) instead of skipping; aggregator success - safe but over-builds. DRIFT. -- **S4** (PR base = main): smoke versions prerelease; github-release skipped on smoke; promotion not blocked. PASS. -- **S5** (push, opt-in unset): the publisher has **no push trigger**, so no push publishes. PASS (stronger than opt-in). -- **S6** (push to develop, opt-in set): N/A - there is no push-publish path or opt-in variable; develop publishes only via `workflow_dispatch` from `develop`. -- **S7** (scheduled/dispatched publish): builds **only** the trigger branch. Schedule -> `main` -> `X.Y.Z` stable, `latest` image, Docker Hub overview refreshed; dispatch from `develop` -> prerelease, `develop` image. No dangling artifacts (retention backstop). DRIFT (single-branch, not a both-branch matrix); outcomes correct. -- **S8** (dispatch from a non-publishable ref): publish job **skipped** (template fails fast). DRIFT. -- **S9** (re-run, version unchanged): release-create skipped (tag exists, non-dispatch); Docker still re-pushes; no duplicate release. PASS. -- **S10** (branch/version disagree): the main-only `Verify public release version step` fails loud if `main` carries a prerelease suffix (`build-release-task.yml:108-110`). PASS (the develop-plain case cannot arise - NBGV). -- **S11** (wrapper bump): N/A. +| csharp | pass | pass | pass | `Directory.Build.props:6-9` carries `AnalysisLevel=latest-all`, `AnalysisMode=All`, `EnableNETAnalyzers`, and `TreatWarningsAsErrors`, with `ManagePackageVersionsCentrally` at `:12` and versions in `Directory.Packages.props`. `.editorconfig:72` opens the shared `[*.cs]` block, whose repo-wide CA relaxations each carry a reason and are scoped to a console application. Coverage is collected and uploaded at `.github/workflows/validate-task.yml:36,39-40` | +| console | pass | pass | pass | `PlexCleaner/PlexCleaner.csproj:16` is `OutputType=Exe` on `System.CommandLine` (`Directory.Packages.props:17`). The smoke matrix `["linux-x64","win-x64"]` is a strict subset of the seven-runtime full matrix (`build-executable-task.yml:44`), and the per-runtime outputs aggregate into one `release-asset--executable` (`:103-107`), with every upload gated `!inputs.smoke` (`:75,86`) | +| docker | pass | pass | pass | Registry layer cache keyed `buildcache-`, never `type=gha`, reading both branches and writing only the built one on push (`build-docker-task.yml:92-95`). The trimmed overview publishes from `Docker/README.md` on `main` only (`:105-113`). The image push is gated on `inputs.push` alone, independent of the release-create skip | +| python | pass | pass | pass | Lint-only profile: `RegressionTests/pyproject.toml` carries `[tool.ruff]` and `[tool.mypy]` and no `[project]` table, so there is no lockfile to pin. The tools run through `uvx` at pinned versions, `uvx ruff@0.15.22 check .`, `format --check .`, and `uvx mypy@2.3.0 .` (`validate-task.yml:98-100`), which is `python.scripts.uvx` satisfied. `python.uvlock.pinned` and `python.coverage.codecov` are N/A for this subtree, as the registry records | +| branch-model | pass | pass | pass | `repo-config/configure.sh check ptr727/PlexCleaner release` from the hub checkout at `d54862a` reports every ruleset rule and every general setting matching the carried payloads, with nothing to apply | +| carried-scope | pass | pass | pass | `repo-config/configure.sh` is retired from the tree, and no other hub-hosted path is carried. The two `investigate` hits are the fleet-wide question settled in this change rather than a repository finding, see Proposed Registry / Spec Updates | +| repo-setup | pass | pass | pass | Every name in `spec/secrets.json` is present in both stores with no forbidden name, per the mechanized run. `.github/dependabot.yml` declares `nuget` and `github-actions` against both `main` and `develop`, which covers every implied ecosystem (`docker` and `devcontainers` are not implied here). `host-tools.json` declares the .NET 10 SDK alone, with the media tools argued as runtime rather than host dependencies | +| linter-parity | pass | pass | pass | One config per linter drives editor, CLI, and CI: csharpier and `dotnet format style --verify-no-changes` (`validate-task.yml:64-71`), markdownlint-cli2 and cspell (`:73-85`), ruff and mypy (`:91-100`), actionlint and editorconfig-checker (`:102-106`). The `.husky/pre-commit` hook runs the C# half natively and the Markdown half through already-present Docker images | +| recurring-violations | drift | pass | drift | The tree passes the mechanized `eol` check and carries no charset violation in text written since the rules took effect, but a whole-tree run of the hub prose gate reports 772 findings and `repo_gate.py --check eol-coverage` reports one. Neither is swept in CI here, because this repository consumes no prose gate. See Drift Findings 1 and 2 | +| readme-structure | pass | pass | pass | Every required section, shield class, reference name, and third-party-tool entry passes the mechanized `readme.*` checks at `16f196e`, closing the two letters the 2026-07-04 snapshot recorded | +| agent-instruction-set | pass | pass | pass | Every declared `AGENTS.md` and `GOVERNANCE.md` verbatim section matches the current canonical, and no intent carry is flagged. `CODESTYLE.md` carries the General plus Skill-pointer shape with `PlexCleaner .NET Conventions` (`:44`) and `PlexCleaner Python Conventions` (`:63`) as its repository-owned subsections. `OPERATIONS.md` and `host-tools.json` are present | +| workflow (WORKFLOW.md 5A/5B) | drift | pass | drift | Every applicable D-guarantee holds by outcome. Two structural divergences from the letter stand and two are closed, see the re-reads below and Drift Findings 3 and 4 | + +nuget, pypi, hugo, homeassistant, eda, upstream-wrapper, codegen: N/A (no package publish, no site, no upstream tracker, no codegen). + +## WORKFLOW.md 5A Re-Reads (only where the 2026-07-04 snapshot recorded a divergence) + +- **D2.1 and D2.2 (validate at entry, both directions):** now **pass**. `build-release-task.yml:62-88` is a dedicated `validate-release` entry job that strips `+buildmetadata`, fails a `main` version carrying a prerelease suffix **and** a non-`main` version carrying none, and exits early on smoke while the job still reports success. `build-executable` carries it in `needs:` and gates on its result (`:94`). The old snapshot's one-directional gate embedded as a step is closed. +- **D5.1 (delete at the point of consumption):** now **pass**. `build-release-task.yml:201-215` deletes the `release-asset--*` transfer artifacts by pattern once they are attached to the release, gated to the same condition as the create step and `continue-on-error`, with `retention-days: 1` still the backstop. The caller grants `actions: write` (`publish-release.yml:38-39`). +- **D1.1, D1.4, and D1.5 (paths-filter):** still a letter miss. There is no `changes` job, so `test-pull-request.yml:13-14` smoke-builds both targets on every push, a docs-only or workflow-only one included, and the aggregator reads `needs: [validate, smoke-build]` (`:52`) rather than a `changes` job. The failure D1.1 exists to prevent, a changed target slipping through unbuilt, cannot occur when both targets always build, so the cost is runner time rather than coverage. +- **D2.3 (dispatch guard):** still a letter miss. `publish-release.yml:33` gates the publish job on `github.ref_name == 'main' || github.ref_name == 'develop'`, so a dispatch from any other ref **skips silently** where D2.3 wants a fail-fast `::error::`. S8 then reports success on a run that published nothing. +- **D4.1 (gated single-branch publish):** the outcome holds and the canonical shape has moved underneath it. The publisher is `workflow_dispatch` plus a weekly `main` schedule with no push trigger (`publish-release.yml:12-16`), so a human merge never auto-publishes, which is the guarantee. It carries no `publish-plan-task.yml` `plan` job and no code-affecting bot-push path, both of which the current D4.1 names, so a Dependabot bump merged to `main` ships in the next scheduled run rather than immediately. +- **D3.1 (version from the checked-out branch):** unchanged and still correct here. NBGV runs once in `get-version` with no `IGNORE_GITHUB_REF`, which is sound only while one run builds one branch (`publish-release.yml:42-43` pins `github.sha` and passes `github.ref_name`). A future both-branch matrix would need the flag. +- **D7.1, D9.1, and D9.4:** unchanged and passing (a ref-independent concurrency group with `cancel-in-progress: false` at `publish-release.yml:20-22`, every action SHA-pinned with `dotnet/nbgv@master` as the sanctioned exception, and the branch-scoped registry buildcache). ## Defects (most severe first) @@ -73,21 +52,21 @@ None. No applicable check fails both letter and intent. ## Drift Findings -1. **Publisher builds one branch per run, not a both-branch matrix.** `publish-release.yml:26-44` publishes only the trigger branch; the weekly schedule rebuilds `main` only `:3-7`, and `develop` prereleases only on a manual dispatch. WORKFLOW.md D4.1's model schedules both legs via a matrix. Outcomes correct (main -> stable/`latest`, develop -> on-demand prerelease); `develop` gets no scheduled base-image refresh. -2. **No `changes` paths-filter; every push smoke-builds both targets.** `test-pull-request.yml:25-42` runs `validate` + `smoke-build` on every push including docs-only and workflow-only (deliberate, per header `:3-10`). Over-builds; the "changed target slips unbuilt" failure cannot occur with two always-built targets. -3. **Ruleset naming matches the fleet canonical (not a drift).** The check `context` and the aggregator job `name:` are both `Check pull request workflow status job` (`test-pull-request.yml:48`) - the ` job` convention now adopted template-wide. This report predated that adoption; the naming is canonical, not a deviation. -4. **Dispatch guard skips instead of failing fast.** `publish-release.yml:33` silently no-ops a dispatch from a non-`main`/`develop` ref; WORKFLOW.md D2.3 wants a fail-fast `::error::`. -5. **Release gate is one-directional and embedded as a step.** `build-release-task.yml:99-111` verifies only that `main` is not prerelease, inside github-release rather than a dedicated entry job the builds `needs:`. Safe (the reverse case is structurally impossible via `version.json:4`), but weaker than the template's two-direction validate-at-entry. -6. **NBGV threaded from a single run without `IGNORE_GITHUB_REF`.** `get-version-task.yml:46-56` runs NBGV once; no `IGNORE_GITHUB_REF=true`. Correct only because the publisher builds one branch per run with `github.ref` aligned; a future both-branch matrix would misclassify without it. -7. **`release-asset--executable` relies on the retention backstop, no consume-delete.** The transfer artifact consumed by github-release (`build-release-task.yml:114-119`) has no delete step; D5.1 prefers a delete at consumption. `retention-days: 1` reaps it - minor. -8. **`.editorconfig` lacks the canonical global line-ending default.** The `[*]` block (`.editorconfig:17-23`) sets charset/indent but **no** `end_of_line`; CRLF is re-declared per file-type (`:26,31,36,53,60`) - the older per-extension form. The template's own `.editorconfig:20-22` carries `[*] end_of_line = crlf`. `.gitattributes:6,12-18` still enforces endings, so real files are compliant. **(Recurs with Utilities - see Proposed Updates.)** -9. **Repo-wide CA relaxation batch (CODESTYLE.md:242 edge).** `.editorconfig:66-84` turns off 17 `CA*` rules repo-wide; each is documented and most are genuinely N/A for a console app, but a few (`CA1307`/`CA1308`/`CA2007`) are the "push it through" class CODESTYLE.md:242 cautions against. Analyzers remain enforced overall - a note, not a break. -10. **README section order/naming.** `## Questions or Issues` sits at `README.md:99` (after the ToC) rather than spec position 9 (after Usage); `## Development Tooling` `:917` fills the Development-Environment-Setup slot under a different name. -11. **`CODECOV_TOKEN` stored but unused.** Present in both the actions and dependabot stores, but no workflow references codecov (only `coverlet.collector` is a test-time package `Directory.Packages.props:5`). Not forbidden; a stale secret to prune or wire up. -12. **`develop` diverged from `main`** (ahead 13, behind 23). See Develop Drift. +1. **CI runs no prose gate, and the tree carries the backlog that follows.** The repository vendors no prose gate and consumes none, so nothing checks a new line against the fleet prose rules at any point (`validate-task.yml` runs no such step, and `.husky/pre-commit` runs markdownlint and cspell only). A whole-tree run of the hub's `scripts/prose_lint.py` reports 772 findings, most of them `comment-wrap` (342), `comment-case` (135), `semicolon` (120), and `dash` (82), concentrated in the C# sources and in text written before the rules existed. The fleet remedy is the diff-scoped gate rather than a tree-wide sweep, so consuming the hub's `prose-gate` action in `validate-task.yml`, as PhotoCleaner does, bounds the backlog at today's size and leaves the rest to the fix-as-edited policy. Two findings are substantive rather than stylistic and can be fixed on sight: `RegressionTests/RegressionTest.sh:75,361` carry the British `behaviour`, and `PlexCleaner/VideoProps.cs:138` duplicates `not`. +2. **`.gitattributes:29` pins `*.Dockerfile` to LF and no tracked file matches it.** `repo_gate.py --check eol-coverage` fails on exactly this. The repository consolidated on a single `Docker/Dockerfile`, which the `:28` pin already covers, so the `*.Dockerfile` line is either dropped or marked forward-declared the way the `uv.lock` block above it is. +3. **No `changes` paths-filter (D1.1, D1.4, D1.5).** Deliberate, and documented in the workflow header. It over-builds on a docs-only or workflow-only push and cannot under-build. +4. **The dispatch guard skips rather than failing fast (D2.3).** `publish-release.yml:33`. A dispatch from a feature branch is a green run that published nothing, which reads as a successful release. +5. **Two `GOVERNANCE.md` mentions name a path this tree no longer holds.** `GOVERNANCE.md:29` names `repo-config/configure.sh`, which is the hub-hosted pointer that rule requires rather than a stale mention, and is the false positive filed as #721 and fixed in #731. `GOVERNANCE.md:282` names `.vscode/settings.json`, which this repository genuinely deleted, so that one is a real stale mention for the file's next edit. ## Proposed Registry / Spec Updates -- Registry `PlexCleaner` types `["csharp","console","docker"]` and publish (`docker` via `static-secret`, `github-release` via `none`) are accurate; no change. Consider advancing `status` beyond `cataloged` once the structural workflow drifts (D4.1 matrix, D1 paths-filter) are addressed. -- **Spec signal (recurring across two repos):** both PlexCleaner and Utilities show (a) the required-status-check `context` suffixed with `job` (matching the local aggregator name but deviating from the fleet canonical) and (b) the missing global `[*] end_of_line = crlf` default. Two repos sharing both strengthens the case for machine checks - assert the ruleset `context` equals the aggregator job `name:` and flag the fleet-canonical mismatch; assert a global `[*] end_of_line` default exists - in the spec/lint layer, not per-repo notes. -- **Spec gap - merge-bot app secrets:** `spec/secrets.json` models `CODEGEN_APP_CLIENT_ID`/`CODEGEN_APP_PRIVATE_KEY` only under the `codegen-app` mechanism, but here they are required by the **merge-bot** (`merge-bot-pull-request.yml:41-42`) on a repo with no codegen. Consider modeling a `merge-bot` mechanism that requires the App secrets so repo-setup does not read them as orphaned. +- **Delete the branch-hygiene `driftNote`.** It reads "3 stale Dependabot nuget branches (PRs closed/superseded) and an unmerged feature/727-decouple-release-assets branch linger". The Dependabot branches are gone, and the live list now holds seven branches beyond `main` and `develop` rather than four, so the note describes neither the old state nor the new one. Branch hygiene is also not a baseline deviation any check scores, which is what a `driftNote` records, so it is deleted rather than re-snapshotted into prose that rots the same way. Applied in this change. +- **The other two `driftNotes` stand.** `ARCHITECTURE.md` and `codecov.yml` are both carried beyond the baseline, and the csharp-plus-python shape note is accurate down to its details (the `RegressionTests/` subtree, no `uv.lock`, a `pyproject.toml` holding only ruff and mypy config, and the two N/A check ids). +- **`status`, `types`, `publish`, `requiredSecrets`, `consumerModel`, `releaseTrigger`, and `configLayout` are unchanged and accurate.** +- **Settle both `investigate` gap dispositions as `accepted`.** Applied in this change, with the measurement written into the ledger reason. Neither `.github/workflows/publish-release.yml` nor `.github/workflows/validate-task.yml` can be declared. Reading every cataloged repository's copy on its ground-truth branch shows the publisher's job keys are per-repo (`publish` in 15 of the 18 copies read, against `create-release`, `publish-pypi`, and an eight-job orchestration elsewhere) and its triggers run from dispatch-only to dispatch plus schedule plus push, while `validate-task.yml` is uniformly `on: workflow_call` over a per-repo job list in six distinct shapes. The counts here include the hub's own copy, so 17 downstream repositories carry a publisher and 13 carry a validator, which is what the ledger's carrier lists name. No `interface` contract the schema can express fits either file, and no `appliesTo` selector separates the carriers from the non-carriers, since `source-only` repos sit on both sides of each. What governs them is already declared: `test-pull-request.yml` and `build-release-task.yml` carry the interface contracts, and WORKFLOW.md's D-guarantees are read by hand per AUDIT.md section 5. +- **Set the `csharp` plus `python` row's Last audited to 2026-08-15** in `reports/conformance-matrix.md`. Applied in this change. + +## Escalations + +- **#721, fixed in #731 rather than here.** `prose_lint.py --check dead-path` cannot recognize a hub-hosted path in a repository that retired the file, so `GOVERNANCE.md:29` here reports as a dead path. This repository is a second carrier of the finding PhotoCleaner hit at its promotion gate. +- **A changelog legitimately names paths the tree no longer holds, and `dead-path` reports them.** `HISTORY.md:110,137` name three Dockerfiles and a script that later releases removed, which is what a changelog records. The finding fails no gate today, because the gate is diff-scoped and those lines are old, so this is recorded rather than filed. It is the same class as #721 and worth folding in if a third instance appears. diff --git a/scripts/prose_lint.py b/scripts/prose_lint.py index c3ffff92..b2ea8b3a 100755 --- a/scripts/prose_lint.py +++ b/scripts/prose_lint.py @@ -333,6 +333,18 @@ def path_candidate(token: str, in_span: bool = True) -> str | None: return token.removeprefix("./") +# Paths the hub hosts and no repository carries, per GOVERNANCE.md "Hub-Hosted Tooling". +# A mention of one names the hub's copy rather than a file this tree lost. +# Carried text naming a tool is required to name it that way, so the mention is never a dead path. +# The manifest exemption cannot reach this class, since no repository carries `spec/files.json`. +# Downstream that set is empty, and a repository that retired its copy carries the full signature. +# It surfaces at the promotion, whose diff base brings the retirement and its prose into scope. +# That is the gate with the least room to fix it, and a ruleset bypass is the only local remedy. +# Held as a literal because the prose-gate action fetches this one file with no hub tree beside it. +# The `retire` dispositions in `spec/divergences.json` are the source, and a hub test asserts this. +HUB_HOSTED = frozenset({"repo-config/configure.sh"}) + + @functools.cache def carried_paths(root: str) -> frozenset[str]: """Paths the manifest declares as carried, exempt because docs name them as fleet layout. @@ -374,7 +386,9 @@ def dead_path_findings( Requiring a history is what scopes this to the deletion-sweep shape, a file removed with its describing prose left standing. A path another repository holds, a proposed file a - backlog names, and a layout pattern each have no history here, so none is reported. + backlog names, and a layout pattern each have no history here, so none is reported. A + carried path and a hub-hosted one are exempt with a history, since each names a file that + lives elsewhere by design rather than a description this tree left behind. """ m = REF_DEF.match(line) if m: @@ -396,9 +410,13 @@ def dead_path_findings( tracked_rel = (anchor / rel_path).resolve().relative_to(root.resolve()) except ValueError: continue - if str(tracked_rel) in carried_paths(str(root)): + # Both exemption sets are keyed by the posix path the manifest and the ledger hold. + # A git pathspec is posix too, which `rel` already relies on for the diff scope. + # So one key serves both, rather than the platform's separator reaching either. + key = tracked_rel.as_posix() + if key in carried_paths(str(root)) or key in HUB_HOSTED: continue - if once_tracked(str(root), str(tracked_rel)): + if once_tracked(str(root), key): out.append( ( lineno, diff --git a/scripts/tests/test_prose_lint.py b/scripts/tests/test_prose_lint.py index 5d1147a7..a96c7fdc 100755 --- a/scripts/tests/test_prose_lint.py +++ b/scripts/tests/test_prose_lint.py @@ -2795,6 +2795,33 @@ def test_a_manifest_declared_carried_path_is_exempt(self) -> None: self.addCleanup(prose_lint.carried_paths.cache_clear) self.assertEqual([], self.kinds(root, "Run `scripts/gone.py` to apply.\n")) + def test_a_hub_hosted_path_is_exempt(self) -> None: + """A repo that retired its copy still names the hub's, which is the pointer the rule wants. + + The manifest exemption cannot reach this one, since no repository carries `spec/files.json`, + so without the literal set every retirement fails its own promotion gate. + """ + root = self.tmp / "retired" + (root / "repo-config").mkdir(parents=True) + (root / "repo-config" / "configure.sh").write_text("#!/bin/sh\n", encoding="utf-8") + self.git(root, "init", "-q") + self.git(root, "add", "-A") + self.git(root, "commit", "-qm", "base") + self.git(root, "rm", "-q", "repo-config/configure.sh") + self.git(root, "commit", "-qm", "retire") + self.assertEqual([], self.kinds(root, "Run the hub's `repo-config/configure.sh`.\n")) + + def test_the_hub_hosted_set_matches_the_ledger(self) -> None: + """The literal is a copy of the ledger, so a retirement that misses it fails here loudly.""" + ledger = json.loads((REPO / "spec" / "divergences.json").read_text(encoding="utf-8")) + retired = { + e["path"] + for group in ("dispositions", "gaps") + for e in ledger.get(group, []) + if isinstance(e, dict) and e.get("disposition") == "retire" + } + self.assertEqual(retired, set(prose_lint.HUB_HOSTED)) + def test_without_git_the_rule_stands_down(self) -> None: """No history means no deletion signature, so nothing is reported rather than guessed.""" bare = self.tmp / "bare" diff --git a/spec/divergences.json b/spec/divergences.json index dd69f16e..3daf54f0 100644 --- a/spec/divergences.json +++ b/spec/divergences.json @@ -6,13 +6,13 @@ { "path": ".markdownlint-cli2.jsonc", "repos": ["aiopurpleair", "PhotoCleaner", "AudioCleaner"], "disposition": "re-vendor", "reason": "Verbatim config held as a hand-modified copy rather than a past hub revision. Restore the current canonical.", "tracking": null } ], "gaps": [ - { "path": "repo-config/configure.sh", "disposition": "retire", "reason": "The script is hub-hosted rather than carried, per GOVERNANCE.md \"Hub-Hosted Tooling\", so it is no longer a manifest entry and a downstream copy is retired rather than re-vendored. Every copy the fleet holds is the hub's content with nothing per-repo in it, and the copies predate the payload-driven check mode, which is the drift this removes rather than converges. Delete the copy as each repo is next visited and run the hub's script against the repo instead. The carriers are not enumerated here, because the fleet-wide gap pass names them from live state and a hand-kept list understated them: it read six while fifteen repos carry one. The ruleset payloads and settings.json stay carried, since those are what the repo is audited against and what its own AUDIT.md diffs the live configuration against.", "tracking": null }, + { "path": "repo-config/configure.sh", "disposition": "retire", "reason": "The script is hub-hosted rather than carried, per GOVERNANCE.md \"Hub-Hosted Tooling\", so it is no longer a manifest entry and a downstream copy is retired rather than re-vendored. Every copy the fleet holds is the hub's content with nothing per-repo in it, and the copies predate the payload-driven check mode, which is the drift this removes rather than converges. Delete the copy as each repo is next visited and run the hub's script against the repo instead. The carriers are not enumerated in this ledger entry, because the fleet-wide gap pass names them from live state in the generated report and a hand-kept list understated them: it read six while fifteen repos carry one. The ruleset payloads and settings.json stay carried, since those are what the repo is audited against and what its own AUDIT.md diffs the live configuration against.", "tracking": null }, { "path": "LICENSE", "disposition": "accepted", "reason": "Each repo owns its license file. The hub does not standardize license text, so it is intentionally outside the manifest.", "tracking": null }, { "path": "TODO.md", "disposition": "accepted", "reason": "A repo's own running backlog, which STANDUP.md section 2 names as one of the four declared destinations for repo-specific content. It is advisory and required by no selector, so the manifest deliberately does not declare it and a repo's copy is its own content rather than a carry.", "tracking": null }, { "path": ".husky/pre-commit", "disposition": "accepted", "reason": "A path collision rather than a carry. Each repo's hook runs that repo's own language formatters, and the hub's runs the doc gates, so the seven downstream copies share the path and none of the content: LanguageTags and Utilities run language formatting and style with no Docker, PlexCleaner runs CSharpier and dotnet format, and the hub runs prose_lint.py and repo_gate.py. Verified by reading the downstream copies on 2026-08-10.", "tracking": null }, { "path": "scripts/README.md", "disposition": "accepted", "reason": "A path collision rather than a carry. KiCadLibrary's copy documents its own KiCad tooling (common.py, verify_library.py, build_library.py) beside the scripts it describes, and shares nothing with the hub's fleet-gate documentation. Verified by reading it on 2026-08-10. scripts/ is a generic path, so a repo with its own tooling directory matches this check without carrying anything of the hub's.", "tracking": null }, { "path": "pyproject.toml", "disposition": "investigate", "reason": "The hub gained a config-only Scripts-profile pyproject.toml in #388. Decide whether to track it (intent, appliesTo python) after confirming the python repos carry an equivalent.", "tracking": null }, - { "path": ".github/workflows/publish-release.yml", "disposition": "investigate", "reason": "Carried by some repos, absent from others, and varies widely (12 divergent, 5 absent). Needs a fidelity call (interface vs intent) and an appliesTo scope before tracking - it would surface many new findings.", "tracking": null }, - { "path": ".github/workflows/validate-task.yml", "disposition": "investigate", "reason": "As publish-release.yml (11 divergent, 9 absent): fidelity plus appliesTo decision pending.", "tracking": null } + { "path": ".github/workflows/publish-release.yml", "disposition": "accepted", "reason": "A path collision rather than a carry, settled 2026-08-15 by reading every cataloged repo's copy on its ground-truth branch (18 copies read, the hub's own included, so 17 downstream repos carry one and 4 carry none). Each repo's publisher is its own. The job keys are per-repo (publish in 15 of the 18, against create-release on homeassistant-purpleair, publish-pypi on aiopurpleair, and an eight-job orchestration on NxWitness), and the triggers run from dispatch-only through dispatch plus schedule plus a paths-filtered push. No interface contract the schema can express fits that spread, and no appliesTo selector separates the carriers from the four that carry none, since both source-only and csharp+console repos sit on either side. What the file must honor is declared elsewhere and needs no second entry: build-release-task.yml carries the interface contract for the release path, and WORKFLOW.md's D2.3, D4.1, and D7.1 are read by hand per AUDIT.md section 5.", "tracking": null }, + { "path": ".github/workflows/validate-task.yml", "disposition": "accepted", "reason": "As publish-release.yml, and settled in the same 2026-08-15 read (14 copies read, the hub's own included, so 13 downstream repos carry one and 8 carry none). Every carrier is on: workflow_call over a per-repo job list, in six distinct shapes across the 14 (lint alone on 3, unit-test plus lint on 5, validate alone on 2, lint plus test plus docs on 2, compile-test plus lint on 1, and unit-test plus lint plus codegen-drift on 1), so the only thing every copy shares is the trigger, which no contract field states. The 8 that carry none inline their validation instead, and no selector separates them, since source-only repos again sit on either side. The fixed part is the required check the aggregator reports, which test-pull-request.yml declares at interface fidelity and the ruleset binds by name.", "tracking": null } ] }