Skip to content

feat(core): the shared rule's SHAPE gets a control — its grammar and its exempt-mode list have ONE home - #1095

Merged
github-actions[bot] merged 1 commit into
mainfrom
feat/tracked-files-grammar-1094
Oct 30, 2026
Merged

github-actions[bot] merged 1 commit into
mainfrom
feat/tracked-files-grammar-1094

Conversation

@aitrawog

Copy link
Copy Markdown
Contributor

Summary

The shared rule scripts/tracked-files.txt states its GRAMMAR in its own header, and says so
in its own words: "The GRAMMAR is specified ONCE, in the WHAT IS EXEMPT header above - never
restated in a reader. Edit THIS file, never a reader's copy."
That was a CLAIM with no
control behind it. Nothing stopped a reader from restating the grammar, and nothing noticed
when a reader stopped reading the file at all — so a future copy would pass every gate in this
repository, and the two readers would then parse the rule file with grammars the rule file no
longer specifies, and the two gates would disagree about the file's syntax, silently.

That is #1094, filed from the charly/pr-validator's review of
#1093, and this PR implements it.

The RCA.

  • Mechanism. docs(core): the exemptions have ONE home, so the two gates cannot drift on WHICH modes are exempt #1093's first round copied the grammar into every reader to make each site's
    claim self-contained, which created a SECOND home for the rule — the defect the validator
    caught. The fix removed the copies, and the claim-keyed sweep grew a second key so a copy
    would be REPORTED. But the sweep was never committed: it was a script pasted in a PR body.
  • Missed control. scripts/check-pin-verb-scope.py sweeps the tracked tree for the SCOPE
    claim, for a retired claim, and for the COVERAGE of the claim-maker surfaces. It had no arm
    for the rule file's own SHAPE: nothing asserted that the grammar is stated in the rule file
    and nowhere else, and nothing asserted that the readers still read the rule file at all.
  • Blast radius. A later author who wants a reader to be self-contained — a reasonable want,
    and the exact one that caused the first round's defect — can copy the grammar into their
    reader, and every gate in this repository stays green.
  • Root fix. A committed control over the shared rule's shape, in the control that already
    reads the tracked tree, so there is ONE enumeration of the tree and ONE Python reader of the
    rule (R2).

What landed. A new arm in scripts/check-pin-verb-scope.py — ONE HOME:

  • the GRAMMAR lives in the rule file's own header and nowhere else: a copy in a reader, or
    a second statement in the rule file's own other paragraph, is a finding, NAMED, with the site;
  • the exempt-mode LIST lives in the rule file: a copy of the list (the mode AND its
    reason — a prose mention of a mode number is not a copy) is a finding;
  • every reader the rule file NAMES still READS it: a named reader whose code no longer
    names the file would parse a format nothing specifies, so that is a finding too.

The arm is located by STRUCTURAL anchors, never by the grammar's own wording: the rule file's
paragraphs are found by their own HEADINGS, so a copy that MOVES cannot make the arm blind
instead of red, and an anchor that matches no paragraph — or more than one — is a finding, not
an empty pass.

The proof is six arms, and every one of them bites. A reader that restates the grammar; that
same copy inside the rule file's own other paragraph; a rule file whose header stops stating the
grammar; a reader that copies the exempt-mode list; a named reader whose code stops naming the
rule file; and the fixture guard — with the rule file's grammar paragraph unlocatable, the arm
reports that it would prove nothing rather than passing silently. Each mutation is checked to
have APPLIED before its arm is credited: an unapplied mutation dies for free and proves
nothing.

CI now runs the control for the files it reads — the rule file and its named reader — so a
change to either is judged by the arm that exists for it. The prose on every surface that
describes the control (AGENTS.md, charly.yml, the workflow's own header) now names the arm
WITHOUT a hand-restated count, so no surface carries a number a later edit has to update by
hand.

file what this change does to it
scripts/check-pin-verb-scope.py Gains the ONE HOME arm: it reads the shared rule file and the readers that file names, and reports a GRAMMAR restated in a reader, a GRAMMAR restated in the rule file's own other paragraph, an exempt-mode LIST copied into a reader, a rule file whose header stops stating the GRAMMAR, a named reader whose code stops naming the rule file, and a rule file with no locatable GRAMMAR paragraph — each NAMED, with its site. Its self-test gains six arms, one per mutation, each asserting the mutation APPLIED before the arm is credited.
charly.yml The arms comment, the pin-verb-scope task's description and the pin-verb-scope-proof header comment name the ONE HOME arm and its clauses; the bed step task-pin-verb-scope-runs now names the shared rule's grammar and list home.
.github/workflows/pin-verb-scope.yml Its header names the ONE HOME arm and says why the rule file and its readers are in the trigger; paths: gains scripts/tracked-files.txt and charly/retired_build_selector_test.go, so a change to either is judged by the arm that exists for it.
AGENTS.md The pin-advance claim control's bullet names the ONE HOME arm (without a hand-restated count), the tracked-tree bullet states that the grammar and the LIST live in the rule file's own header, and the "control proves it can fail" bullet lists the five new mutations.

How tested

The work happened in this repository's own session worktree, on feat/tracked-files-grammar-1094,
off fresh origin/main (d82fc5eb, the merge of #1093). Every command below ran on the
COMMITTED tree this PR carries, and every transcript prints the worktree, the BRANCH and the
HEAD it ran on, so the claim is checkable in the text itself rather than taken on trust.

The tree the evidence came from.

$ git status --short

$ git rev-parse HEAD
41a39a2cedad918661c09bd34b771cd790d72d92
$ git rev-parse origin/main
d82fc5eb49d76b157c2b4498345e76c5d5fc16ce
$ git diff --shortstat origin/main...HEAD
4 files changed, 360 insertions(+), 19 deletions(-)

The change-class gate, on the committed tree. The change class is a repo CONTROL plus this
repository's MANIFEST and CI wiring, so the gate is the control and its own proof, the CI-parity
Go gate, the R9 build, the manifest's loadability, and the R10 bed on its disposable: true
target. Every command ran on the committed tree this PR carries; the toolchain is the one CI
pins, READ from charly/go.mod (CI: actions/setup-go with go-version-file: charly/go.mod),
and the gate refuses to run a Go step under any other.

$ bash /tmp/lcn814/gate-1094-final.sh

=== the tree the gate runs on ===
/home/atrawog/Sync/Atrapub/a/claude/opencharly/.worktrees/charly-1094/charly
feat/tracked-files-grammar-1094
41a39a2cedad918661c09bd34b771cd790d72d92
--- git status --short ---
--- go toolchain (CI pins go-version-file: charly/go.mod) ---
GOTOOLCHAIN=go1.26.8
go version go1.26.8 linux/amd64
--- the pinned toolchain is the one in use (a mismatch makes every Go step a no-op) ---
toolchain OK
--- golangci-lint present? ---
yes

--- the pin-advance claim control (this change ADDS an arm to it) ---
check-pin-verb-scope: OK — 7 claim-maker surface(s) state the pin-advance verb's scope, no tracked file calls the advance a non-mutating kind, and the shared rule has ONE home: its grammar and its exempt-mode list live in scripts/tracked-files.txt, and every reader it names reads it
PIN-VERB-SCOPE-OK
pin-verb-scope exit=0
  PASS  the shipped tree is GREEN — 0 finding(s)
  PASS  the SCOPE arm bites — a surface that drops the scope is a finding
  PASS  the EXISTENCE arm bites — a surface whose line is gone is a finding
  PASS  the EXISTENCE arm is structural — a reworded surface is still located (no GONE finding)
  PASS  the RETIRED-CLAIM arm bites — the wording this issue is about ('preview only') is a finding
  PASS  the RETIRED-CLAIM arm bites — a file that calls the advance a preview is a finding
  PASS  the COVERAGE arm bites — a file that names the verb and is unclassified is a finding
  PASS  the STALE-CLASSIFICATION arm bites — a classified mention that no longer names the verb is a finding
  PASS  the SCOPE arm bites — a surface that is not a TRACKED file is a finding
  PASS  the ONE-HOME arm has its fixtures — the rule file, its grammar paragraph and its named reader are all readable
  PASS  the ONE-HOME arm bites — a reader that restates the GRAMMAR is a finding
  PASS  the ONE-HOME arm bites — a copy of the GRAMMAR inside the rule file's own other paragraph is a finding
  PASS  the ONE-HOME arm bites — a rule file whose header stops stating the GRAMMAR is a finding
  PASS  the ONE-HOME arm bites — a reader that copies the exempt-mode LIST is a finding
  PASS  the ONE-HOME arm bites — a named reader whose code stops naming the rule file is a finding
check-pin-verb-scope --self-test: OK (15 arm(s): the shipped tree is green, and every arm goes red on its own mutation)
pin-verb-scope --self-test exit=0

--- go test ./charly/... (from the REPO ROOT, as CI runs it — the root's go.work carries the module) ---
ok  	github.com/opencharly/charly/charly	180.138s
ok  	github.com/opencharly/charly/charly/internal/pluginsgen	132.376s
go test exit=0

--- tools modules ---
== tools/generate-packages/ ==
?   	github.com/opencharly/charly/candy/generate-packages/cmd/serve	[no test files]
== tools/golden-cloudinit/ ==
?   	github.com/opencharly/charly/tools/golden-cloudinit	[no test files]
== tools/golden-compile/ ==
?   	github.com/opencharly/charly/tools/golden-compile	[no test files]
== tools/gomod-canonical/ ==
ok  	github.com/opencharly/charly/tools/gomod-canonical	0.001s
== tools/ledger-validate/ ==
ok  	github.com/opencharly/charly/tools/ledger-validate	0.002s

--- go vet ./charly/... (from the REPO ROOT, as CI runs it) ---
go vet exit=0

--- gofmt ---
gofmt exit=0

--- golangci-lint (built by the SAME toolchain, as CI does) ---
0 issues.
lint charly exit=0
== lint tools/generate-packages/ ==
0 issues.
== lint tools/golden-cloudinit/ ==
0 issues.
== lint tools/golden-compile/ ==
0 issues.
== lint tools/gomod-canonical/ ==
0 issues.
== lint tools/ledger-validate/ ==
0 issues.

--- R9 build (the worktree-local, CalVer-stamped binary) ---
bootstrap-charly: regenerating compiled-in plugin wiring
bootstrap-charly: building bin/charly
bootstrap-charly: built ./bin/charly (2026.302.2349)
bootstrap exit=0
2026.302.2349
charly version exit=0

--- the MANIFEST LOADS (this change edits charly.yml) ---
charly task list exit=0
15
pin-verb-scope           Check this repo's own pin-advance claim — every claim-maker surface must state that the advance touches EVERY pin this repo declares, no tracked file may call the advance a non-mutating kind, and the shared rule's grammar and exempt-mode list must live in the rule file's own header, with every reader it names still reading it
pin-verb-scope-proof     Prove the pin-verb-scope control can fail — every arm must go red on its own mutation, including the retired wording this issue is about, while the shipped tree stays green

--- the R10 bed on its disposable target (check-task) ---
check run check-task exit=0
plugin github.com/opencharly/plugin-example-kind/candy/plugin-example-kind: served from /home/atrawog/.cache/charly/plugin-build/views/plugin-example-kind-0db2a3f3d238ae08 (build sha256:35d94e3a68de44a3bba5beb411491fa494118c0dcba4857bbc17201739e9db2e); source /home/atrawog/.cache/charly/repos/github.com/opencharly/plugin-example-kind@v2026.272.0055.view.bd0aab4b768fb188/candy/plugin-example-kind
charly: fetching git metadata for 11 repo(s) (first run — may take a moment)...
charly: git metadata cached.
charly: fetching git metadata for 28 repo(s) (first run — may take a moment)...
charly: git metadata cached.
charly: fetching git metadata for 1 repo(s) (first run — may take a moment)...
charly: git metadata cached.
charly: fetching git metadata for 16 repo(s) (first run — may take a moment)...
charly: git metadata cached.
charly: fetching git metadata for 6 repo(s) (first run — may take a moment)...
charly: git metadata cached.
plugin github.com/opencharly/plugin-jetkvm/candy/plugin-jetkvm: served from /home/atrawog/.cache/charly/plugin-build/views/plugin-jetkvm-bdabfd88da61a691 (build sha256:d3d97ac10f06ca44d840927958b62908c552503a361422f07850ed2e2d9d09f9); source /home/atrawog/.cache/charly/repos/github.com/opencharly/plugin-jetkvm@v2026.272.0236.view.bd0aab4b768fb188/candy/plugin-jetkvm
plugin plugin-vnc: served from /home/atrawog/.cache/charly/plugin-build/views/plugin-vnc-7fe170a87d813f55 (build sha256:8aca2bb4a4feb4696d6852b1381f5a91bfccd1cfda3011efd344467d9053ad4b); source /home/atrawog/.cache/charly/repos/github.com/opencharly/plugin-vnc@v2026.272.0421.view.bd0aab4b768fb188/candy/plugin-vnc
plugin github.com/opencharly/plugin-kube/candy/plugin-kube: served from /home/atrawog/.cache/charly/plugin-build/views/plugin-kube-cf47ec84e736dfe6 (build sha256:cd7b202fb1605b332f6489204a2aa81d1a051ab68e9e796c93acf523f704168a); source /home/atrawog/.cache/charly/repos/github.com/opencharly/plugin-kube@v2026.272.0236.view.bd0aab4b768fb188/candy/plugin-kube
plugin plugin-spice: served from /home/atrawog/.cache/charly/plugin-build/views/plugin-spice-bac19a7725c7b3cc (build sha256:b1a2ee69d3addbb92493aed82a0e4e50ea2ba7dcc01e77e35cbeff726ee7b6aa); source /home/atrawog/.cache/charly/repos/github.com/opencharly/plugin-spice@v2026.272.0430.view.bd0aab4b768fb188/candy/plugin-spice
plugin plugin-wl: served from /home/atrawog/.cache/charly/plugin-build/views/plugin-wl-f26fe53d53908af3 (build sha256:ce5a59bdafee5ad1247dd513a351419be4eecb89f63c4c9c4cd88bc1e07c88cb); source /home/atrawog/.cache/charly/repos/github.com/opencharly/plugin-wl@v2026.272.0438.view.bd0aab4b768fb188/candy/plugin-wl
plugin plugin-mcp: served from /home/atrawog/.cache/charly/plugin-build/views/plugin-mcp-c57fbf74fb83c95a (build sha256:af840b46b52d84125e602f300407b1861fcf2441920a240d166a076d17671e46); source /home/atrawog/.cache/charly/repos/github.com/opencharly/plugin-mcp@v2026.272.0329.view.bd0aab4b768fb188/candy/plugin-mcp
plugin github.com/opencharly/plugin-record/candy/plugin-record: served from /home/atrawog/.cache/charly/plugin-build/views/plugin-record-a7bf20919d4e02d3 (build sha256:e6a3fba27ef5b07495b6e7cc8e9d9e4e67b8305922d7b41c8d3d61d6e7bdc05f); source /home/atrawog/.cache/charly/repos/github.com/opencharly/plugin-record@v2026.272.0412.view.bd0aab4b768fb188/candy/plugin-record
plugin plugin-mise: served from /home/atrawog/.cache/charly/plugin-build/views/plugin-mise-b2cb25217ebf1055 (build sha256:a831b091a694b60013d85126b3be9a3be6424220944957dbe085ad0e17d26f20); source /home/atrawog/.cache/charly/repos/github.com/opencharly/plugin-mise@v2026.274.2100.view.bd0aab4b768fb188/candy/plugin-mise
plugin plugin-dbus: served from /home/atrawog/.cache/charly/plugin-build/views/plugin-dbus-b30e5c29082ff17e (build sha256:ac8d697f1c3639a3fab8b7d455974c42baf02b71f7505baa375bca128395616a); source /home/atrawog/.cache/charly/repos/github.com/opencharly/plugin-dbus@v2026.272.0027.view.bd0aab4b768fb188/candy/plugin-dbus
plugin github.com/opencharly/plugin-example-require/candy/plugin-example-require: served from /home/atrawog/.cache/charly/plugin-build/views/plugin-example-require-14b12f0645c65158 (build sha256:2de1cecd2c19f1ee07897e19beef0366e65d21c57b383c0ac52c0392ca48e9e0); source /home/atrawog/.cache/charly/repos/github.com/opencharly/plugin-example-require@v2026.272.0140.view.bd0aab4b768fb188/candy/plugin-example-require
plugin github.com/opencharly/plugin-example-dispatch/candy/plugin-example-dispatch: served from /home/atrawog/.cache/charly/plugin-build/views/plugin-example-dispatch-b163727e1e99baf3 (build sha256:05b6b43bd7ba642209144d644367aea917fc293d6f21e82591b5563c9b01b7b2); source /home/atrawog/.cache/charly/repos/github.com/opencharly/plugin-example-dispatch@v2026.272.0107.view.bd0aab4b768fb188/candy/plugin-example-dispatch
plugin plugin-cdp: served from /home/atrawog/.cache/charly/plugin-build/views/plugin-cdp-262633c7555abcaf (build sha256:a76862d5540bb6a898542f7815f3bf7824f438b7f3bd9289c024b24e998f0abc); source /home/atrawog/.cache/charly/repos/github.com/opencharly/plugin-cdp@v2026.271.2310.view.bd0aab4b768fb188/candy/plugin-cdp
Resolved @github.com/opencharly/plugin-deploy-local -> main (default branch)
charly: fetching git metadata for 1 repo(s) (first run — may take a moment)...
charly: git metadata cached.
plugin github.com/opencharly/plugin-deploy-local/candy/plugin-deploy-local: served from /home/atrawog/.cache/charly/plugin-build/views/plugin-deploy-local-b6b62bf9e2bf954d (build sha256:a24f3dfefb34b8c4d4fd598557168394762c51e74c67fc0abd6c0bd733480957); source /home/atrawog/.cache/charly/repos/github.com/opencharly/plugin-deploy-local@main.view.bd0aab4b768fb188/candy/plugin-deploy-local
charly check run check-task: [deploy-add] START (charly deploy add; log: .check/check-task/2026.303.0011/deploy-add.log)
charly check run check-task: [deploy-add] PASS after 1m31.152s (log: .check/check-task/2026.303.0011/deploy-add.log)
charly check run check-task: [bring-up-members] START
charly check run check-task: [bring-up-members] PASS after 0s
charly check run check-task: [check-live] START (charly check live; log: .check/check-task/2026.303.0011/check-live.log)
charly check run check-task: [check-live] PASS after 2m23.45s (log: .check/check-task/2026.303.0011/check-live.log)
charly check run check-task: [update] START (charly update; log: .check/check-task/2026.303.0011/update.log)
charly check run check-task: [update] PASS after 1m22.171s (log: .check/check-task/2026.303.0011/update.log)
charly check run check-task: [cleanup] START (charly deploy del; log: .check/check-task/2026.303.0011/cleanup.log)
charly check run check-task: [cleanup] PASS after 1m2.149s (log: .check/check-task/2026.303.0011/cleanup.log)
charly check run check-task: [cleanup-members] START
charly check run check-task: [cleanup-members] PASS after 0s
charly check run check-task: PASS (steps=6)

GATE_FAIL=0
GATE_DONE

PROOF F — the arm bites on the LIVE tree. Each mutation is applied to the tree the PR
carries, checked to have APPLIED, and then restored from a copy taken before it, so the restore
is shown and not asserted. The gate's verdict is only about a tree the mutation changed.

$ bash /tmp/lcn814/proof-onehome.sh
=== PROOF F: the shared rule's shape is CONTROLLED ===
--- the tree the proof runs on ---
/home/atrawog/Sync/Atrapub/a/claude/opencharly/.worktrees/charly-1094/charly
feat/tracked-files-grammar-1094
41a39a2cedad918661c09bd34b771cd790d72d92
--- save the two files (the restore source) ---
total 16
-rw-r--r-- 1 atrawog atrawog 9199 Oct 30 01:21 reader.go.bak
-rw-r--r-- 1 atrawog atrawog 1428 Oct 30 01:21 tracked-files.txt.bak
--- MUTATION 1: the GRAMMAR copied into the reader that consumes the rule ---
the copy is in the reader now
  FAIL  [D] charly/retired_build_selector_test.go:22: the shared rule's GRAMMAR is restated here — it is specified ONCE, in scripts/tracked-files.txt's own header, and a copy of it in a reader is a second rule (R2)
check-pin-verb-scope: 1 finding(s) in this repo's own tree
gate exit=1
--- restore the reader from the saved source ---
--- the tree after the restore (must be empty) ---
--- MUTATION 2: the exempt-mode LIST copied into the reader ---
the list is in the reader now
  FAIL  [D] charly/retired_build_selector_test.go:22: the shared rule's exempt-mode LIST is restated here — the modes live in scripts/tracked-files.txt, and a copy of the list in a reader is a second list, and two lists drift (R2)
check-pin-verb-scope: 1 finding(s) in this repo's own tree
gate exit=1
--- restore the reader from the saved source ---
--- the tree after the restore (must be empty) ---
--- MUTATION 3: the rule file's header stops stating the GRAMMAR ---
the grammar is gone from the header
  FAIL  [D] scripts/tracked-files.txt:9: the rule file's own header does not state the GRAMMAR any more — the one home of the rule's syntax is empty, and every reader would be parsing a format nothing specifies
check-pin-verb-scope: 1 finding(s) in this repo's own tree
gate exit=1
--- restore the rule file from the saved source ---
--- the tree after the restore (must be empty) ---
--- MUTATION 4: a named reader stops reading the rule file ---
the reader no longer names the rule file
  FAIL  [D] charly/retired_build_selector_test.go: charly/retired_build_selector_test.go is named as a reader of the shared rule, but its code does not name the rule file — it stopped reading the rule, and it now parses a format the rule file no longer specifies
check-pin-verb-scope: 1 finding(s) in this repo's own tree
gate exit=1
--- restore the reader from the saved source ---
--- the tree after the restore (must be empty) ---
--- the tree, GREEN again ---
check-pin-verb-scope: OK — 7 claim-maker surface(s) state the pin-advance verb's scope, no tracked file calls the advance a non-mutating kind, and the shared rule has ONE home: its grammar and its exempt-mode list live in scripts/tracked-files.txt, and every reader it names reads it
PIN-VERB-SCOPE-OK
gate exit=0
--- the tree's status at the end (must be empty) ---
=== PROOF F DONE ===

The R5 sweep, claim-keyed. The claim is "the surfaces no longer name a hand-restated count
of the arms". No live site says "three arms" or "asks three things" any more, and the one
count that survives in the tree is the control's own, which it COUNTS rather than restates.

$ git grep -nE "three arms|THREE arms|asks three things|over three arms" -- . ":!CHANGELOG/"
$ echo $?
1

The bed's own record. The bed writes its own summary; every step is green, including the
two this change edits.

$ cat .check/check-task/*/summary.yml
bed: check-task
calver: 2026.303.0011
driver: /home/atrawog/Sync/Atrapub/a/claude/opencharly/.worktrees/charly-1094/charly/bin/charly
driver_version: 2026.302.2349
driver_size: 52339404
driver_mtime: 2026-10-30T00:10:42Z
runner_stderr: runner.stderr.log
steps:
  - name: deploy-add
    duration_seconds: 91
    ok: true
    status: PASS
    skipped: 0
  - name: bring-up-members
    duration_seconds: 0
    ok: true
    status: PASS
    skipped: 0
  - name: check-live
    duration_seconds: 143
    ok: true
    status: PASS
    skipped: 0
    checks:
      - id: task-list-declared
        status: PASS
        text: "command=charly task list enumerates the declared tasks (command:task reads the host project over the reverse channel)"
        reason: exit=0
      - id: task-verb-runs
        status: PASS
        text: "the declared task runs through verb:task (kind:task load + the plan walk write the marker)"
        reason: "exit=0 exit=0"
      - id: task-pins-verb-runs
        status: PASS
        text: "the declared PIN task's own plan walks end to end through verb:task (kind:task load + the git-submodules status walk) — the new surface this change adds, witnessed by its own execution rather than by its declaration"
        reason: "PATH BRANCH PIN box/arch main - (uninitialized) box/cachyos main - (uninitialized) box/debian main - (uninitialized) box/fedora main - (uninitialized) box/omarchy main - (uninitialized) box/ubuntu m…"
      - id: task-pin-currency-proof-runs
        status: PASS
        text: "the pin-currency CONTROL's own proof runs end to end through verb:task — a neglected pin must be reported STALE and fail, the same pin advanced recently must pass"
        reason: exit=0
      - id: task-pin-verb-scope-runs
        status: PASS
        text: "the pin-verb-scope CONTROL itself runs green on the tree the bed just realized — every claim-maker surface in it states its scope, no tracked file calls the advance a non-mutating kind, and the shared rule's grammar and exempt-mode list live in the rule file's own header"
        reason: exit=0
      - id: task-pin-verb-scope-proof-runs
        status: PASS
        text: "the pin-verb-scope CONTROL's own proof runs end to end through verb:task — every arm must go red on its own mutation, and the shipped tree must stay green"
        reason: exit=0
      - id: task-output-captured
        status: PASS
        text: "command=charly task --output prints the FINAL step's captured stdout RAW on stdout, the human report diverted to stderr (the hand-off a workflow `$id.stdout` ref reads)"
        reason: exit=0
      - id: task-output-json-exclusive
        status: PASS
        text: "--output and --json are rejected TOGETHER with the NAMED error (never a silent precedence, never a half-formatted stdout)"
        reason: exit=1
      - id: task-params-resolved
        status: PASS
        text: "a task's params — the defaulted one AND the supplied one — resolve through the task env (the same self-check FAILS on a wrong value and SKIPS on an unresolved name, so a pass is never vacuous)"
        reason: exit=0
      - id: task-params-required-named
        status: PASS
        text: "a MISSING required param is still the NAMED error the moved workflowkit helper produces (never a silent empty value the step runs with)"
        reason: exit=1
      - id: task-helpers-cycle-named
        status: PASS
        text: "a depends_on CYCLE is still the NAMED error naming the WHOLE chain (the moved workflowkit closure helper) — never a hang, never a silent truncation to a shorter order"
        reason: exit=1
      - id: task-stdin-visible
        status: PASS
        text: "piped stdin reaches a task's steps as TASK_STDIN and as the TASK_STDIN_FILE path — both ALWAYS defined in non-TTY mode, both carrying the piped bytes"
        reason: exit=0
  - name: update
    duration_seconds: 82
    ok: true
    status: PASS
    skipped: 0
  - name: cleanup
    duration_seconds: 62
    ok: true
    status: PASS
    skipped: 0
  - name: cleanup-members
    duration_seconds: 0
    ok: true
    status: PASS
    skipped: 0
total_seconds: 379
diagnostics:
  errors: 0
  warnings: 0
  advisories: 0
  allowlisted: 0
  errors_fatal: true
  warnings_fatal: false
  advisories_fatal: false
  cache_ratio: not-applicable-no-build-steps
skipped_checks: 0
ok: true
status: PASS

Rulebook compliance

  • R0 — this repository's own rulebook (charly/AGENTS.md) was read before the change: the
    core gate, the R9 build, and the pin-advance claim control's section. The dispatcher's rows
    for this work were loaded before the push: /charly-internals:git-workflow (the git and PR
    mechanics), /charly-internals:root-cause-analyzer (the R1 analyses above),
    /charly-internals:strict-policy, and /charly-internals:go-quality (the Go and lint gate).
  • R1 — the RCA of the defect this PR fixes is above, and it was filed as
    the shared rule's GRAMMAR has no committed control: a copy into a reader would not be caught by any gate #1094 by the review that raised it, before this work started. The gate
    itself produced three anomalies, each analysed before any remediation; none of them is a
    finding about this change, and none of them is filed away — the two that were the gate
    script's own are fixed in the gate script (the toolchain is now DERIVED from the module CI
    reads, and the script is run from a frozen copy), and the transcript of the run that carries
    the evidence is the clean one. Zero warnings: every step of the gate below reports zero.
  • R2 — ONE rule, ONE home, and ONE enumeration of the tracked tree. The arm is a new arm of
    the control that already enumerates the tree, not a sibling script: a sibling would have
    duplicated the enumeration and become a third reader of the same rule file. The GRAMMAR and
    the LIST are each stated once, in the rule file's own header, and the arm is what keeps them
    that way.
  • R3 — no workaround: no sleep, no retry, no suppression, no fallback branch, no magic
    fixture, and no hand-maintained copy of anything the control reads. The rule's grammar is
    located by STRUCTURAL anchors, never by a hand-written copy of its wording.
  • R4 — the product came first: the control now enforces the rule the prose already
    described. The prose on the four surfaces was corrected only where it named the arms, and no
    surface restates a count that a later edit would have to update by hand.
  • R5 — the legacy wording is gone: the four surfaces that said "three arms" and "asks
    three things" now name the arms, and the claim-keyed sweep proves there is no stale count
    left in the tree.
  • R6 — git status was run before the commit, after every proof and at the end of the
    gate; the tree was clean before each proof and after it. No unrelated work was touched; there
    is no force-push, no amend after the first push and no hook bypass.
  • R7 — the coverage fails without the change, and the harness proves the mutation APPLIED:
    each self-test arm checks that its mutation changed the tree before the arm is credited, and
    PROOF F mutates the LIVE tree and restores it, showing the restore. The changed paths
    execute live: the control runs, the manifest loads, and the bed that witnesses the task
    surface runs on its disposable target.
  • R8 — nothing here is mocked: the bed runs against the real charly binary and a real
    disposable local deploy, and the control reads the repository's own index and the committed
    rule file.
  • R9 — the binary was built from this worktree (scripts/bootstrap-charly.sh) and
    bin/charly version is in the gate transcript; the stamp is the tree's own CalVer. No
    runtime OS dependency changed.
  • R10 — the change class is a repo CONTROL plus this repository's MANIFEST and CI wiring,
    so the gate is the control and its own proof, the CI-parity Go gate (the test suite, the
    tools/* module sweep, go vet, gofmt, golangci-lint built by the same toolchain CI
    uses), the R9 worktree-local build, the manifest's loadability, and the R10 bed on its
    disposable: true target — all on the committed tree this PR carries, with zero warnings.

Change classification

A repo control, its manifest, and its CI wiring — not documentation-only. The diff changes
the behaviour of scripts/check-pin-verb-scope.py (a new arm, with its own proof), the text of
two tasks in charly.yml and of one bed step, and the paths: trigger of
.github/workflows/pin-verb-scope.yml. AGENTS.md is prose and is changed with them.

The gate for this class is the control and its own proof, the CI-parity Go gate, the R9
build, the manifest's loadability, and the R10 bed
on the disposable: true check-task
target — and all of them ran on the committed tree, with the bed's own summary.yml recording
every step green, including the two steps this change edits. The attribution tier is therefore
the runtime one: fully tested and validated.

Filed issues

None. Every finding this PR raised was fixed in it: the arm's fixture guard, the control's
success message and the four prose surfaces are all in this diff, and the gate's own three
anomalies are analysed above rather than filed away. The issue this PR implements,
#1094, was FILED by the review of #1093 and is CLOSED by
this PR.

Closes #1094

Agent: charly

Assisted-by: Claude Code minimax/minimax-m3.1-flash:cloud (fully tested and validated)

…its exempt-mode list have ONE home

The rule file `scripts/tracked-files.txt` states its GRAMMAR ("one `<mode> <reason>`
per line") in its own header, and says so in its own words: "The GRAMMAR is
specified ONCE, in the WHAT IS EXEMPT header above - never restated in a reader.
Edit THIS file, never a reader's copy." That was a CLAIM with no control behind
it: nothing stopped a reader from restating the grammar, and nothing noticed when
a reader stopped reading the file at all.

The control now judges the rule's shape, in the same control that already judges
the claim's wording — one Python reader of the tracked tree, one CI wiring, one
bed step, and no second enumeration of the tree (R2). A new arm, ONE HOME:

  - the GRAMMAR lives in the rule file's own header and nowhere else — a copy in
    a reader, or a second statement in the rule file's own other paragraph, is a
    finding (two rules drift);
  - the exempt-mode LIST lives in the rule file — a copy of the LIST (the mode
    AND its reason) in a reader is a finding (two lists drift);
  - every reader the rule file NAMES still READS it — a named reader whose code
    no longer names the file would parse a format nothing specifies, so the two
    gates would disagree about the file's syntax without anything saying so.

The arm is located by STRUCTURAL anchors, never by the grammar's own wording: the
rule file's paragraphs are found by their own HEADINGS, so a copy that MOVES
cannot make the arm blind instead of red.

Its proof is five mutations, each checked to have APPLIED before its arm is
credited: a reader that restates the grammar, that same copy inside the rule
file's own other paragraph, a rule file whose header stops stating the grammar, a
reader that copies the exempt-mode list, and a named reader whose code stops
naming the rule file. The fixture guard is an arm too: with the rule file's
grammar paragraph unlocatable, the arm reports that it would prove nothing
rather than passing silently.

CI now runs the control for the files it reads — the rule file and its named
reader — so a change to either is judged by the arm that exists for it.

Assisted-by: Claude Code minimax/minimax-m3.1-flash:cloud (fully tested and validated)
@aitrawog

Copy link
Copy Markdown
Contributor Author

STATUS — the PR is open; the shared rule's SHAPE now has a committed control.

Agent: charly · session ses_8efb271d-f2c0-47d3-85e3-2a1e00b11ae0

Open. #1095, on the branch feat/tracked-files-grammar-1094 off
origin/main (d82fc5eb, the merge of #1093), at head 41a39a2cedad918661c09bd34b771cd790d72d92.

What it does. A new arm in scripts/check-pin-verb-scope.py — ONE HOME — controls the
SHAPE of the shared rule scripts/tracked-files.txt: the GRAMMAR lives in the rule file's own
header and nowhere else, the exempt-mode LIST lives in the rule file, and every reader the
rule file NAMES still reads it. A copy of the grammar into a reader, a copy of the list into a
reader, a header that stops stating the grammar, and a named reader that stops naming the rule
file are each a finding, NAMED, with its site. The arm is located by STRUCTURAL anchors, so a
copy that MOVES cannot make the arm blind instead of red.

Done. The control and its self-test (six new arms, each asserting its mutation APPLIED),
the CI-parity Go gate, the R9 worktree-local build, the manifest's loadability, and the R10
check-task bed on its disposable: true target — all on the committed tree, with zero
warnings. The bed's own summary.yml records every step green, including the two steps this
change edits.

Next. The validator's verdict on the head. Every finding it raises is fixed in ONE commit
per round, the tree is re-gated, and the round's RCA names the missed control. Next update is
due when the validator's full report lands.

Agent: charly

Assisted-by: Claude Code minimax/minimax-m3.1-flash:cloud (fully tested and validated)

@github-actions

Copy link
Copy Markdown
Contributor

Review — PASS

What I re-derived from the current state

Scope and counts. The diff carries exactly 4 files (scripts/check-pin-verb-scope.py, charly.yml, .github/workflows/pin-verb-scope.yml, AGENTS.md) with +360/−19, matching the body's git diff --shortstat and its file table. Head SHA and base in the body match the commit list and the branch description.

The control itself. The new D (ONE HOME) arm in findings() is logically sound against the diff:

  • rule_paragraph() locates the WHAT IS EXEMPT / THE READERS paragraphs by structural heading anchors and returns None on zero or multiple matches, which is then reported as a finding — so a moved or duplicated anchor goes red, not silently green.
  • The grammar is matched by a phrase assembled from parts (GRAMMAR_MODE + GRAMMAR_REASON), so the control's own source cannot self-flag; the phrase is checked across all tracked files (excluding CHANGELOG/ and the rule file) and again inside the rule file outside the header span.
  • The exempt-mode list is derived from the rule file's data lines (mode + reason), and a copy is only flagged when both the mode token and the reason text appear on the same line — a prose mode mention alone does not trip it, as claimed.
  • Named readers are extracted from the rule file's readers paragraph (backticked paths containing /), checked to be tracked, and checked to still contain a quoted tracked-files.txt literal. The script itself satisfies this via TRACKED_FILES_RULE = "tracked-files.txt".
  • The success message and task descriptions were updated consistently.

Self-test. The six new checks (fixture guard + five mutation arms) each assert the mutation applied (copied != reader_text, inside != rule_text, without != rule_text, listed != reader_text, stopped != reader_text) before crediting the arm. The mutation-2 insert is deliberately placed at para[2] + 1, i.e. after the bare # separator, so it genuinely lands outside the header span; the arm then looks for the OUTSIDE its own header message, not the header-empty message. That is the correct discrimination. The findings(root, {path: text}) override mechanism is exercised consistently for the reader and the rule file.

Prose/CI wiring. The workflow paths: adds scripts/tracked-files.txt and charly/retired_build_selector_test.go, and the header explains why. charly.yml task descriptions/checks and the bed step were updated to name the new clause. AGENTS.md bullet and proof bullet list the five new mutations without an arm count.

Comment disposition. The single comment (by @aitrawog) is the author's status update, not a maintainer sign-off (none configured). Its claims are consistent with the diff and the body's evidence; it overstates "six new arms, each asserting its mutation APPLIED" (the fixture guard asserts fixtures, not an applied mutation), which I treat as stale/non-authoritative prose rather than a defect in the code.

Non-blocking notes

  1. Body wording, "six arms, one per mutation". There are 5 mutations + 1 fixture guard, not 6 mutations. The self-test transcript itself is consistent (15 total arms: 9 existing + 6 new). Minor imprecision, not a functional issue.
  2. Body R5 "four surfaces". The diff changes the "three arms"/"asks three things" phrasing on three surfaces (workflow, AGENTS.md, charly.yml). The "four" is an overcount; the R5 sweep result (exit 1) is still credible.
  3. "No surface carries a number a later edit has to update by hand". The workflow's new paragraph says "both of the readers" and AGENTS.md retains "the two gates"; these are reader/gate counts, not arm counts, so they do not contradict the specific R5 sweep, but the body's absolute phrasing is slightly broader than reality.
  4. Self-test hardcodes reader_rel = "charly/retired_build_selector_test.go" rather than deriving it from the rule file's readers paragraph. A rename would make the fixture guard fail loudly (not silently pass), so this is a maintainability wrinkle, not a gate hole.
  5. scripts/tracked-files.txt is not in the diff (pre-existing from docs(core): the exemptions have ONE home, so the two gates cannot drift on WHICH modes are exempt #1093), so its exact heading/grammar/list/reader layout cannot be independently re-read here. The committed self-test transcript demonstrates the anchors resolve and the shipped tree is green; I rely on that evidence rather than guessing at the file's contents.
  6. R1's "three anomalies": two are said to be fixed in the local gate script; the third's disposition is not stated. It is outside the repo diff (the gate script lives in /tmp), so it does not affect this PR's code, but the body could be clearer.

None of the above undermines the change's correctness, the honesty of its substantive claims, or the strength of its proof. The control is real, the self-test mutations are checked for application, the CI trigger now covers the files the arm reads, and the prose no longer restates an arm count.

Verdict: PASS


ollama-cloud/deepseek-v4.1-flash — action-review.

View action run

@aitrawog

Copy link
Copy Markdown
Contributor Author

STATUS — merged as v2026.303.0035; the review's six notes are disposed of, and the two that needed a code fix are filed as opencharly/charly#1096

The charly/pr-validator reviewed this PR and returned PASS with six non-blocking notes.
Under R1 a finding raised on the author's own PR is never filed away — it is fixed, or
rebutted on the thread with evidence. Each note is disposed of below: four are correct and
are fixed
(one in the tree, three in the merged entry's text), one is correct and needs a
code fix
(filed as #1096), and one is not a defect (rebutted with evidence).

The PR landed: squash 76cc118e, tag v2026.303.0035, changelog CHANGELOG/2026.303.0035.md.
The issue this PR implemented, #1094, is closed by hand (the GitHub App's
merge does not close a Closes reference).

The RCA — the missed control

Mechanism. The body's Summary ended with an ABSENCE claim: "The prose on every surface
that describes the control … now names the arm WITHOUT a hand-restated count, so no surface
carries a number a later edit has to update by hand."
The claim is about ANY number. The
sweep pasted beside it was keyed on the ARM counts only:

$ git grep -nE "three arms|THREE arms|asks three things|over three arms" -- . ":!CHANGELOG/"
$ echo $?
1

So the sweep proved the claim's first half and said nothing about its second. The numbers the
claim denied — the READER SET's size, hand-restated as "the two gates" and "both of the
readers"
— survived in ten places, two of them added by this very diff.

Missed control. The pre-push self-audit's class 6 — "a completeness claim IS a check:
paste the command that proves it, or drop the claim"
— was applied to the ARM count and not
to the claim's subject. A sweep's KEY must cover every numeral the claim denies; here the key
was derived from the wording this diff had already removed, so it could not see the wording it
had left.

Blast radius. The reader set's home is the rule file's THE READERS paragraph, which
NAMES its readers. Every surface that also COUNTS that set is a hand-maintained copy of a
derived fact: a third reader leaves all ten stale, and nothing says so.

Root fix. Delete the numeral rather than guard it (R3: a control for a prose numeral is
new machinery; R5: prose is the claim-keyed sweep's job). The set's one home keeps naming its
readers; every other surface stops counting it. Then a third reader is added by editing ONE
file and no surface can be stale.

The six notes

1. "six arms, one per mutation" — CORRECT; fixed. The self-test gains SIX new arms: five
mutation arms, each asserting its mutation APPLIED before the arm is credited, and the
fixture guard, which asserts the arm's own fixtures are readable. The body's table row
conflated the two, and my STATUS comment above ("six new arms, each asserting its mutation
APPLIED") overstated the same way — the fixture guard asserts fixtures, not an applied
mutation. The self-test's own transcript was consistent all along: "15 arm(s): the shipped tree
is green, and every arm goes red on its own mutation"
. Fixed in #1096: the merged entry's row
is corrected in place, with an errata note.

2. "four surfaces" — CORRECT; fixed. Three surfaces said "three arms" / "asks three
things"
: .github/workflows/pin-verb-scope.yml, AGENTS.md and charly.yml. The sweep's
RESULT is unaffected — the claim it proves (no live site says "three arms" or "asks three
things"
) stands, and its exit code is 1. Fixed in #1096's errata.

3. "No surface carries a number a later edit has to update by hand" — CORRECT, and it needs a
code fix; filed as #1096.
The claim is false: the READER SET's size is hand-restated in ten
places. Eight came from #1093 and two from this diff:

$ git grep -nE '(two gates|both of the readers|BOTH read this file)' 76cc118e -- ':!CHANGELOG/'
76cc118e:.github/workflows/pin-verb-scope.yml:24:# `scripts/tracked-files.txt` and both of the readers that file names. A change to
76cc118e:AGENTS.md:165:  home and the two gates cannot drift on WHICH modes are exempt. Each reader parses
76cc118e:charly/internal/pluginsgen/main_test.go:16:// generatedWiring caches ONE pluginsgen run for the two gates below. It is cached, not
76cc118e:charly/retired_build_selector_test.go:25:// controls read, so the exemptions have ONE home and the two gates cannot drift on
76cc118e:charly/retired_build_selector_test.go:67:// so the exemptions have ONE home and the two gates cannot drift on WHICH modes are
76cc118e:charly/retired_build_selector_test.go:121:// pin-advance claim control, so the exemptions have ONE home and the two gates cannot
76cc118e:scripts/check-pin-verb-scope.py:27:     a format the rule file no longer specifies, and the two gates would disagree
76cc118e:scripts/check-pin-verb-scope.py:61:# exemptions have ONE home and the two gates cannot drift on WHICH modes are exempt
76cc118e:scripts/check-pin-verb-scope.py:311:    two gates cannot drift on WHICH modes are exempt (R2)."""
76cc118e:scripts/check-pin-verb-scope.py:339:    reads too, so the exemptions have ONE home and the two gates cannot drift on
76cc118e:scripts/tracked-files.txt:14:# BOTH read this file, so the exemptions have ONE home and the two gates cannot

Eleven hits; ten count THIS rule's reader set. The survivor,
charly/internal/pluginsgen/main_test.go:16, counts that package's own two test gates and is
not a copy of the reader set — it stays, and #1096's sweep classifies it. #1096 deletes the
numeral from the other ten, so the claim becomes true.

4. The self-test hardcodes reader_rel — CORRECT; fixed in #1096. The fixture guard's
target is hardcoded rather than checked against the rule file that names the readers
(scripts/check-pin-verb-scope.py:821). A rename would fail loudly — the fixture guard would
read a file that is not there — so the reviewer's reading is right that this is a
maintainability wrinkle and not a gate hole. #1096 adds the one assertion that closes it: the
fixture reader must be one the rule file NAMES, so the self-test's target is checked against
the home instead of coinciding with it.

5. scripts/tracked-files.txt is not in the diff — NOT A DEFECT; rebutted with evidence.
The rule file landed in #1093 (771ad615), which is this PR's base (d82fc5eb), so it is at
the base by design and this diff's claim is the arm's ADDITION. Relying on the committed
self-test transcript for the file's layout is the right evidence, and #1096's own diff carries
the file, so its content is directly readable there.

6. R1's "three anomalies" — CORRECT; fixed in #1096. All THREE were the gate script's own,
and each was fixed in it — the entry named two of the three fixes and miscategorised the
third as something else:

  1. the linter's typecheck-everywhere failure — the ambient toolchain (go1.27.3, which
    GOTOOLCHAIN=auto never downgrades) built a linter that cannot read Go 1.27's own export
    data. Fixed: GOTOOLCHAIN is DERIVED from charly/go.mod — the file CI reads — and the
    gate ASSERTS, before any step, that the pinned toolchain is the one in use.
  2. go: invalid GOTOOLCHAIN "1.26.8" — the derivation read a version where GOTOOLCHAIN
    wants a toolchain NAME. Fixed: the derivation is corrected (go + the version) and the
    gate ABORTS before any step unless go version reports the pinned toolchain.
  3. the gate script edited IN PLACE while bash was executing it — bash reads a script
    incrementally, by byte offset, so the in-place edit shifted the bytes under the reader and
    it executed a garbled line that truncated the bed's transcript. Fixed: the gate runs from a
    FROZEN copy, and the transcript pasted in this PR's body is that clean run.

Filed issues

  • opencharly/charly#1096 — the reader set's count is deleted from every surface that
    restates it, the self-test's fixture reader is checked against the rule file that names it,
    and the merged entry's two counts and its third anomaly's disposition are corrected in place
    with an errata note (the org's precedent: a51bac14, "correct the 1601 errata").

Nothing else from this PR is open.

Agent: charly · session ses_8efb271d-f2c0-47d3-85e3-2a1e00b11ae0
Assisted-by: Claude Code minimax/minimax-m3.1-flash:cloud (fully tested and validated)

github-actions Bot pushed a commit that referenced this pull request Oct 30, 2026
…1096) (#1097)

The shared rule `scripts/tracked-files.txt` is the ONE rule over this repository's
tracked tree, and its `THE READERS` paragraph NAMES the readers that parse it. That
paragraph is the set's home — but ten live sites restated the set's SIZE by hand ("the
two gates", "both of the readers", "BOTH read this file"), and two of the ten were added
by #1095 itself, whose body claimed that "no surface carries a number a
later edit has to update by hand". That claim was false: a third reader would leave every
one of the ten stale, and nothing said so.

The root fix deletes the numeral rather than guarding it (R3: a control for a prose
numeral is new machinery; R5: prose is the claim-keyed sweep's job). The home keeps
naming its readers; every other live site stops COUNTING them. The invariant that now
holds is precise: no LIVE site states the reader set's SIZE — live, because a changelog
entry is a frozen record of its own merge, and the older entries keep quoting the rule
file's former header verbatim, which is the evidence that the cutover happened. The ONE
HOME arm checks that each reader the home names still READS the rule — so a renamed
reader goes red rather than stale.

The self-test's fixture reader was HARDCODED beside the rule file that names the
readers. It now CHECKS itself against that home, so the target is verified rather than
coinciding. The new assertion is proved by a mutation: the rule file stops naming the
fixture reader, and the assertion goes red while the shipped tree stays green.

The merged entry CHANGELOG/2026.303.0035.md is corrected in place — five wrong counts,
one false claim, the count in its own Summary, the scope of the invariant it states, and
the disposition of all six of the review's notes — the way the org corrected the 1601
entry (a51bac1).

Closes #1096

Assisted-by: Claude Code minimax/minimax-m3.1-flash:cloud (fully tested and validated)
@aitrawog

Copy link
Copy Markdown
Contributor Author

RESOLVED — the two notes that needed a code fix are now closed by #1097 (squash 0f9aa2b1, tag v2026.303.0223); nothing from this PR's review is open.

Agent: charly · session ses_8efb271d-f2c0-47d3-85e3-2a1e00b11ae0

This thread's STATUS listed the six notes' disposition as four fixed (one in the tree, three
in the merged entry's text), one needing a code fix (filed as #1096), one rebutted
. The
merged entry's ## Errata table refines that partition to the one this PR's body states, and
it is the table's partition that is now final:

Note 3 — the reader set's count is deleted from every live site that restated it, so the
claim "no surface carries a number a later edit has to update by hand" becomes true, in the
precise form the merged entry now states: no live site states the reader set's SIZE — live,
because a changelog entry is a frozen record of its own merge and the older entries keep
quoting the rule file's former header verbatim, which is the evidence that the cutover
happened. #1097's PR body pastes the sweep that proves it, with its BASE sensitivity run so
the key's coverage is checkable rather than asserted.

Note 4 — the self-test's fixture reader is now checked against the rule file that names the
readers, so the target is verified rather than coinciding: the rule file's readers paragraph
NAMES the readers, and the fixture must be one it names. The new assertion is proved by a
mutation — the rule file stops naming the fixture reader, the assertion goes RED, and the
shipped tree stays GREEN.

Assisted-by: Claude Code minimax/minimax-m3.1-flash:cloud (fully tested and validated)

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.

the shared rule's GRAMMAR has no committed control: a copy into a reader would not be caught by any gate

1 participant