Skip to content

docs(adr): ADR 0160 records the public-repo content policy that was being enforced unrecorded - #198

Merged
wshallwshall merged 2 commits into
mainfrom
claude/adr-0160-content-policy
Aug 5, 2026
Merged

docs(adr): ADR 0160 records the public-repo content policy that was being enforced unrecorded#198
wshallwshall merged 2 commits into
mainfrom
claude/adr-0160-content-policy

Conversation

@wshallwshall

Copy link
Copy Markdown
Collaborator

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:

Form Files
docs/releases/... 12
relative (releases/... 15 (invisible to the above)
union 23

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,:13 do cite it, via the relative form. docs/README.md is 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:53 carries "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.md should come out. It cannot: git worktree add cannot 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.md stay? It is process material by the test and 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. This blocks Phase 2.

Three further questions are listed under To resolve on acceptance, including whether docs/Secure_Development_Standards.md is reviewer material that stays (it was deliberately pulled from the public PyPI sdist, which suggests the question was asked once and answered differently).

⚠️ Explicitly not a confidentiality control -- git log retains everything removed, nothing here was ever treated as secret, and anything genuinely sensitive belongs in the vault and always did.

…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
wshallwshall enabled auto-merge (squash) August 5, 2026 04:02
@wshallwshall
wshallwshall merged commit 0d27d23 into main Aug 5, 2026
31 checks passed
@wshallwshall
wshallwshall deleted the claude/adr-0160-content-policy branch August 5, 2026 04:03
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant