Skip to content

Separate the two enforcement regimes: CI bears trust, authoring-time does not #121

Description

@maxinelevesque

Separate the two enforcement regimes: CI bears trust, authoring-time does not

Starting with the incident I caused

Commit 5581b65f ("Stage-3 REQ-1: the @bv clause tag, parse-gated") — mine — removed three hook entries from .claude/settings.json: the PostToolUse/Read recorder and the PreToolUse/Write|Edit pair invoking spec-discipline.py and anti-pattern-gate.py. The commit message is entirely about the @bv clause tag and never mentions the settings change. The crosslink-generic entries survived; the project-specific ones vanished — the signature of crosslink init regenerating a tracked-but-machine-authored file.

I did not do that deliberately, and I did not notice. Both agent-facing gates were then dormant for the entire Stage-3 arc, while README.md:172 as it stood at that commit ("PreToolUse/PostToolUse events, so they enforce automatically — no setup" — the passage has since been removed, so that line number points elsewhere today), goal.md's R-XLATE-1/2/3, and all four .claude/agents/acto-*.md continued to assert they fired.

#93 caught it, restored the entries byte-identical, and added tooling/control-plane-check.py so the class can't recur silently. That was good work and this issue is not a complaint about it.

But #93 fixed the instance. The instance had a cause, and the cause is still here.

The cause

Two different enforcement regimes are being treated as one.

CI enforcement authoring-time enforcement
runs on the merge candidate one contributor's editing loop
posture adversarial formative
harness must be agnostic inherently harness-specific
blocks merge yes no
bears trust yes never

spec-discipline.py (read goal.md + the design doc + a reference before editing a routed .rs) and anti-pattern-gate.py (regex-ban todo!, unimplemented!, unreachable!, .unwrap(, .expect(, panic!, Arc<Mutex<, Rc<RefCell<, #![allow() are authoring-time mechanisms. They shape how work proceeds inside one editing loop.

They were then documented as though they were gates — "they enforce automatically — no setup." That produced two failures, both realized:

  1. The claim was silently falsifiable, and it silently falsified. An entire stage ran with the asserted enforcement absent. Nothing detected it because the assertion lived in prose and the mechanism lived in a file a tool regenerates.
  2. A contributor on a different harness gets neither regime. No hooks, because wrong editor. And no CI equivalent, because those rules were never expressed as CI. So the rules are simultaneously mandatory in the docs and unreachable in practice for anyone outside one setup.

The rule

Anything load-bearing for a trust claim must be CI-enforced and
harness-agnostic. Authoring-time tooling may exist, may be opinionated, may be
the project default — but it may never be cited as the reason a property
holds.

To be explicit about what this does not say: the repository is entirely free to be opinionated about which authoring-time tools it supports and ships defaults for. That is a separate question from what determines trust, and keeping the two separate is what makes contribution possible for someone running a different setup, or none.

This is not a proposal to remove the ACToR apparatus. It is a proposal to stop letting it carry weight it cannot hold.

Concretely

1. Route the checkable parts to CI. The anti-pattern rules are lint rules. .design/tooling/control-plane.md OQ-2 already records this — "no clippy.toml backstopping the anti-pattern rules." A clippy.toml plus the existing -D warnings in CI enforces them for every contributor, with real semantics instead of regex-on-replacement-text, and with no harness dependency.

2. Demote the rest to opt-in. spec-discipline.py is inherently interactive ceremony — it cannot be a CI check, because "was this file read first" is not a property of a merge candidate. Keep it, default it on if you like, but it stops being a stated precondition for contributing.

3. Fix the file that started it. .claude/settings.json is tracked and machine-authored, which is the actual defect — crosslink init can rewrite it and git will happily record the result as an ordinary edit. Options, roughly in order of preference: generate it from a declared source and untrack the generated artifact; or keep it tracked and have control-plane-check.py verify it against that declared source rather than against prose.

4. Hooks shell out to targets. Where an authoring-time hook and a CI check want the same rule, the hook should invoke the CI target rather than reimplementing it. One definition, two entry points.

A worked example

I've been applying this in a downstream repository, and it's cheap: bulla-systems/bullaCONTRIBUTING.md carries the regime table and the rule, and every trust-bearing check is a make target that CI runs:

make check-fork    every forked file matches the upstream pin, or is a
                   listed divergence with a reason
make image         the image builds from this tree through forge at the pin
make verify        the receipt's bindings re-check against current source
make determinism   two independent builds are byte-identical
make boot-matrix   1/2/4/8 CPUs, AP-start-failure, reboot, under QEMU/OVMF
make check         all of the above

A contributor with no agent tooling satisfies every requirement by running those. The process tooling that repo uses (day/kan) is explicitly classified as authoring-time and non-trust-bearing — someone who doesn't use it can still land changes; they just won't have the process log.

That's the whole shape. It cost one table and one section, and it makes the enforcement claim true by construction rather than by assertion.

Why now

Thermite's thesis is degrade loudly, never launder. The harness had an "asserted enforcement that isn't" failure — the exact shape the harness exists to prevent — and it went undetected for a stage because the claim and the mechanism were in different places with nothing tying them together.

control-plane-check.py now ties them for the hooks that exist. This issue is about making sure the ones that matter aren't hooks at all.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions