docs(adr): ADR 0160 records the public-repo content policy that was being enforced unrecorded - #198
Merged
Merged
Conversation
…eing enforced unrecorded The owner stated this policy twice on 2026-08-04: the public repo carries only what someone RUNNING MessageFoundry needs plus what a SECURITY REVIEW needs, with ADRs named explicitly as the second category, and material about how the project is built with Claude Code comes out. It was recorded nowhere. A session grepped every local and remote ref and found nothing, while the coordinating session was citing it as a constraint on four other sessions' work -- which is exactly the standing that same session had REFUSED from a peer earlier the same day, when it declined to arm a PR on a relayed owner ruling. The rule was applied to others and not to itself. That asymmetry is the defect this ADR fixes: to an isolated session, an unrecorded policy is indistinguishable from an invented one. Status is Proposed on purpose, and the split is deliberate. The RULING in D1 is owner-stated and already in force. The APPLICATION in D2/D3 -- which files, in what order -- is mine, and no file moves before ratification. Grounded in measurement at c90dcb5 rather than recollection: docs/ 580 tracked, docs/adr/ 156 (stays), docs/releases/ 101 (not the 96 I had been carrying), scripts/ 61. Also corrected a false alarm before it became a claim: `git check-ignore docs/security` reports NOT IGNORED because the bare directory path does not match the `/docs/security/` pattern -- a path INSIDE it resolves to .gitignore:144 correctly, so the exclusion is intact. D3 records a genuine conflict rather than papering over it. By the policy CLAUDE.md should come out; it cannot, because `git worktree add` cannot deliver an untracked file, so while it was untracked every new worktree came up with zero project conventions loaded. It stays tracked as a named exception with a recorded mechanical cause -- an untracked convention file is a control that silently fails to load, the class ADR 0158 exists to name. D4 states that no enforcement gate is proposed and why: a path-based gate is evaded or wrong, a keyword gate fires on every legitimate use of "session". That is a bounded claim about scope, not a promise of a later mechanism. Phase 1 carries two constraints that would otherwise bite: FEATURE-MAP.md:5 and :13 cite docs/releases/ and must be fixed in the same commit, and the PR must NOT be docs-only, because every doc-drift guard here is gated on code == 'true' so docs-only is the BLIND mode rather than the cheap one (BACKLOG #327). One blocking question is left explicitly open for the owner: whether docs/BACKLOG.md stays. It is process material by the test and simultaneously CI-load-bearing -- backlog_status_check.py plus backlog-hygiene.yml, a required merge context -- so removing it retires a gate rather than editing a doc.
…ot one The first draft named a single inbound citation, docs/FEATURE-MAP.md:5 and :13, because that is what my notes carried. Measured instead of trusted, the surface is 23 files, and the reason the number was wrong is worth recording in the ADR rather than just fixing: docs/releases/... 12 files relative (releases/... or ../releases/ 15 files <- invisible to the above union 23 files FEATURE-MAP's two citations are real, and they use the RELATIVE form -- which is why a `docs/releases` grep does not see them. So verifying my own claim with a single-form grep returned nothing and nearly made me delete a TRUE statement as unfounded. Both forms are required; neither alone is sufficient. docs/README.md is in the union, so the docs front door breaks without the fix. Also promoted from discipline to structure: Phase 1 CANNOT be docs-only. tests/test_cutover_slug_rot.py:53 carries "docs/releases/" as a literal scan-scope entry, tests/test_lint_scope_parity.py:22 cites a plan by name, and harness/load/profiles/closed-loop.toml:7 cites an execution plan. The PR therefore necessarily touches code, which satisfies the separate rule that a doc migration must not be docs-only -- but the ADR now says explicitly not to rely on that, because the guards would run due to a test file this phase happens to touch, not because of any property of the phase.
wshallwshall
enabled auto-merge (squash)
August 5, 2026 04:02
wshallwshall
added a commit
that referenced
this pull request
Aug 5, 2026
…floor that had 23 items of slack (#202) TEN doc-drift modules exist to police documents and every one is reachable ONLY through pytest, which is gated on `code == 'true'`. So on a docs-only PR -- the exact shape they exist to check -- none of them runs. That is the same defect the "Backlog status invariant" step above was added to fix, at ten times the surface, and this change extends that step's own argument rather than inventing a design: "gating it would skip it on exactly the pull requests it exists to police". NOT HYPOTHETICAL. On 2026-08-04 four docs-only PRs merged (#197, #198, #200, #201) and the doc guards ran on none of them. Two carried citation errors found only by reading: an ADR named ONE inbound citation of docs/releases/ where there are 23, and a docs/SECURITY.md route-table row asserted a refusal DELETE /me/mfa does not make. Neither of those two would have been caught by these particular modules -- but they are the evidence that docs-only PRs land unchecked against the guards built to check them. NO NEW REQUIRED CONTEXT, and that is deliberate. The steps ride the existing required `test (ubuntu-latest, py3.14)` job, which already RUNS on every PR -- only its steps are gated. So this needs no branch-protection change and no owner action, unlike a new job. .github/required-contexts.txt is explicit that it is "not the enforcement -- the server is", and adding a required context is all-or-nothing against a 422. `[dev]` rather than a bare `pip install pytest`, and this one would have bitten: pyproject sets asyncio_mode = "auto" (needs pytest-asyncio) and addopts "--timeout=60 --timeout-method=thread" (needs pytest-timeout), so a bare pytest ERRORS on an unknown option before collecting anything -- a step that could never pass, reding every docs-only PR. Still far lighter than the gated install, which adds console + fhir + dicom + x12 + xml that no doc scan touches. THE FLOOR HAD 23 ITEMS OF SLACK. `--min-items 277` against a corpus that has reached 300, in a guard whose entire purpose is to notice the corpus shrinking. Raised to 300 in BOTH places -- and the second place is the finding: the floor also lives as `_MIN_TOTAL_ITEMS` in tests/test_backlog_status_check.py and NOTHING compares them, so the lower copy silently becomes the only floor that binds. That is BACKLOG #1018's duplicated-constant shape. Both sites now name the other; it is not mechanised here because a test parsing workflow YAML to compare an integer is a new coupling, and the honest fix belongs with #1018's helper. 89 of the 241 tests SKIP and that is structural, recorded in the step: they assert against docs/security/THREAT-MODEL.md, which is vault-only and absent here. ADR 0156 records the class and ASVS 15.1.3 is open on it. The step prints the module list BEFORE running and uses -rs so no skip reads as a pass.⚠️ THIS PR CANNOT EXERCISE ITS OWN NEW STEPS. It changes ci.yml and a test, so `code == 'true'` here and the new steps skip. Evidence is a local run of the exact command (152 passed, 89 skipped, 5.12s) plus confirming [dev] supplies both plugins, that "Set up Python" is ungated, and that constraints.lock is tracked. The first docs-only PR is the real test, and it fails CLOSED -- a broken step reds that PR rather than passing silently. Verified: 57 workflow-structure tests pass (incl. test_workflow_shell_syntax and test_ci_venv_pinning), 27 backlog/lint-scope tests pass, ruff check and format clean, and the YAML parses with both floors reading 300.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Records a policy that has been in force and enforced while being written down nowhere.
Why this exists
You stated it twice on 2026-08-04: the public repo carries only what someone running MessageFoundry needs plus what a security review needs, with ADRs named explicitly as the second category, and material about how the project is built with Claude Code comes out.
It was recorded nowhere -- a session grepped every local and remote ref and found nothing -- while I was citing it as a constraint on four other sessions' work. That is exactly the standing I had refused from a peer earlier the same day, when I declined to arm a PR on a relayed owner ruling. I applied the rule to others and not to myself, and to an isolated session an unrecorded policy is indistinguishable from an invented one. That asymmetry is the defect this fixes.
The question is new because the 2026-07-27 cutover retired the publish-time deny-list that used to answer it. It is now answered per file, at authoring time, with no gate and no written rule.
Status is Proposed on purpose
The ruling (D1) is yours and already in force. The application (D2/D3) -- which files, in what order -- is mine, and no file moves before you ratify it.
Measured, not recalled
At
c90dcb5f:docs/580 tracked,docs/adr/156 (stays),docs/releases/101 -- not the 96 I had been carrying --scripts/61.Two findings that changed the ADR
The Phase 1 citation surface is 23 files, not the one I first named. Citations split across two grep forms and neither alone is sufficient:
docs/releases/...(releases/...My own verification of my own claim used the single form, returned nothing, and nearly made me delete a true statement as unfounded --
docs/FEATURE-MAP.md:5,:13do cite it, via the relative form.docs/README.mdis in the union, so the docs front door breaks without the fix.Phase 1 cannot be docs-only, structurally.
tests/test_cutover_slug_rot.py:53carries"docs/releases/"as a literal scan-scope entry, plus a second test and a harness profile. So the PR necessarily touches code -- which satisfies the separate rule that a doc migration must not be docs-only, but the ADR says explicitly not to rely on that, since the guards would run because of a test file this phase happens to touch, not any property of the phase.D3 records a conflict rather than papering over it
By the policy
CLAUDE.mdshould come out. It cannot:git worktree addcannot deliver an untracked file, so while it was untracked every new worktree came up with zero project conventions loaded -- verified on two live worktrees, and it only looked right in the primary because an untracked copy happened to sit on disk. It stays tracked as a named exception with a recorded mechanical cause, because an untracked convention file is a control that silently fails to load.D4 proposes no enforcement gate, and says why
A path gate is evaded or wrong; a keyword gate fires on every legitimate use of "session". That is a bounded claim about scope, not a promise of a later mechanism.
Blocking question for you
Does
docs/BACKLOG.mdstay? It is process material by the test and CI-load-bearing --backlog_status_check.pyplusbacklog-hygiene.yml, a required merge context -- so removing it retires a gate rather than editing a doc. This blocks Phase 2.Three further questions are listed under To resolve on acceptance, including whether
docs/Secure_Development_Standards.mdis reviewer material that stays (it was deliberately pulled from the public PyPI sdist, which suggests the question was asked once and answered differently).git logretains everything removed, nothing here was ever treated as secret, and anything genuinely sensitive belongs in the vault and always did.