Skip to content

feat(rules): re-run rule 1's grep on every gate, not once by hand - #188

Merged
wenzowski merged 1 commit into
mainfrom
claude/milestone-1-build-path-s08bil
Aug 9, 2026
Merged

wenzowski merged 1 commit into
mainfrom
claude/milestone-1-build-path-s08bil

Conversation

@wenzowski

Copy link
Copy Markdown
Contributor

Non-negotiable rule 1 — no consumer-specific identifier anywhere in crates/batten — was discharged once, as a manual grep when the crate landed (CLOUD-26). That leaves a standing property carried as prose, which non-negotiable rule 2 calls half a change: nothing re-checks it, so the first violation to land does so green.

Two [[rule]] rows now re-run that grep under batten check, on every gate invocation.

Why the glob is crates/** and not crates/**/*.rs — rule 1 says anywhere. The crate manifest, the changelog and every fixture under tests/ are files a *.rs glob waves straight through.

Why only two of the four literals CLOUD-26's acceptance listed — forbid is a case-sensitive literal substring check, so a literal with an English reading is wrong in both directions at once: it fires on ordinary prose and misses the capitalised form. The vendor name and the predecessor repo's name both already occur in this repo's own text (git grep -Fn button -- crates/ hits src/hook.rs; git grep -in complian hits AGENTS.md and LICENSE-APACHE). What survives is the two shapes with no English reading.

Neither rule id embeds the literal it bans. A finding prints <path>:<line> <rule-id>, so the id reaches the fixture's expected-stdout string — which lives under crates/batten/tests/, inside the glob these rules scan. An id naming its own literal would make the gate fire on the test that proves it works.

The discriminator

The fixture asserts full stdout equality, not a substring, because that is the only form that discriminates. Measured both ways:

state new test the_committed_repo_config_gates_a_repository
both rules deny pass pass
one rule severity = "allow" fail pass

That second row is the gap this closes. The older case seeds only a conflict marker, so its asserted bytes are identical whether these rules are present, absent, mis-globbed or switched off — its comment claimed to catch exactly that, and is corrected here.

Refs: CLOUD-7


Generated by Claude Code

Non-negotiable rule 1 — no consumer-specific identifier anywhere in
crates/batten — was discharged once, as a manual grep when the crate landed
(CLOUD-26). That leaves a standing property carried as prose, which
non-negotiable rule 2 calls half a change: nothing re-checks it, so the
first violation to land does so green.

Two [[rule]] rows now re-run that grep under batten check, on every gate
invocation. The glob is crates/** rather than crates/**/*.rs because rule 1
says *anywhere*: the crate manifest, the changelog and every fixture under
tests/ are files a *.rs glob waves straight through.

Only two of the four literals CLOUD-26's acceptance listed survive the
membership test. forbid is a case-sensitive literal substring check, so a
literal with an English reading is wrong in both directions at once — it
fires on ordinary prose and misses the capitalised form. The vendor name and
the predecessor repo's name both already occur in this repo's own text; what
survives is the two shapes with no English reading. Neither rule id embeds
the literal it bans, because a finding prints its rule id and the fixture's
expected stdout lives inside the glob these rules scan.

The fixture asserts full stdout equality rather than a substring, because
that is the only form that discriminates. Measured: switching one rule to
severity = "allow" fails the new test while
the_committed_repo_config_gates_a_repository stays green. That older case
seeds only a conflict marker, so its asserted bytes are identical whether
these rules are present, absent, mis-globbed or switched off — its comment
claimed to catch exactly that and is corrected here.

Refs: CLOUD-7

Claude-Session: https://claude.ai/code/session_017fSX9B9EL7hzyEMqRYfTaE
@linear-code

linear-code Bot commented Aug 9, 2026 •

Copy link
Copy Markdown
CLOUD-7 Project scaffold and CLI-as-data spine

Foundational setup for the batten crate and the single-source-of-truth command surface.

This group establishes the repo-agnostic crate, the usage-spec-driven CLI, and the derived read-only allowlist.

Path correction: the crate landed at crates/batten, not the tools/batten/ path this issue and CLOUD-26's title were written against. The children's titles still carry the old path; the tree is authoritative.


Refinement gate

Children of this epic are gated by the project-level Definition of Ready & Done, in the vocabulary of Batten CLI — the Button house style. Both are attached above. This parent does not restate the eight clauses, and it is not a merge of its children's blocks: each child carries its own per-clause specializations — CLOUD-26 the crate, CLOUD-27 the spec-as-data surface and its drift gate, CLOUD-28 the derived allowlist. What follows is only what is true of the parent and of nothing else.

  • Source of truth (§1). One artifact: SURFACE in crates/batten/src/surface.rs — the command tree declared once as data, each row carrying a verb's path, summary, flags (with their BATTEN_ env equivalents) and its house-style §5 effect. Everything else is derived from that one walk and nothing is re-typed: surface::command() builds the live clap tree, crates/batten/src/spec.rs emits batten spec, batten generate completions produces the committed completions/batten.{bash,zsh,fish}, batten generate schema produces schema/batten.schema.json, and the agent read-only allowlist is spec::read_only_allowlist — filter(effect == read) over the same walk, with no second hand-maintained list. House-style §5 and §11 are cited here, never restated.
  • Computable predicate (§2). The epic is complete when mise run ci exits 0 over a gate carrying all four spine conjuncts, none of them re-derived in this body: completions-check (the committed completions regenerate byte-for-byte from SURFACE), test (the fail-safe assertions in crates/batten/src/spec.rs — every_command_has_a_declared_effect, allowlist_is_exactly_the_read_commands, the_process_spawning_verb_is_never_read_only), batten-check, and — the conjunct this parent supplies — the repo-agnosticism rules in the rule table batten-check evaluates, pinned by the §7 test. The fourth conjunct is what makes the predicate discriminate: the other three pass over a rule table that bans no consumer literal at all, so mise run ci exiting 0 is not by itself evidence the property is enforced, and a reader taking the first three as the whole predicate would close this epic with the property still unmechanised. Non-negotiable rule 1 states that property — no consumer-specific identifier anywhere in crates/batten, "a grep for a specific consumer's names must return zero hits" — and CLOUD-26's acceptance discharged it as a one-time grep over the crate's source, which leaves a standing property carried as prose — half a change by non-negotiable rule 2. This parent converts it into a rule that re-runs on every gate invocation. Policy-engine-first, it lands as [[rule]] entries in the committed root batten.toml, one per banned consumer literal, each kind = "forbid", glob = "crates/**", severity = "deny", scope = "tree", evaluated by batten check → exit 2 on a match, already wired through the batten-check step of the shared hk gate. The set of literals is CLOUD-26's Acceptance list less the two that fail the membership test below; that issue stays the one authority for it, so no copy is re-typed here. The glob covers all of crates/, not only *.rs, because rule 1 says anywhere in crates/batten — git ls-files crates/ | grep -v '\.rs$' names the tracked files a *.rs glob would skip, and crates/** selects them (pinned by glob_double_star_spans_segments in crates/batten/src/rules.rs). Measured with that rule shape over the committed config plus a fixture tree: batten check exits 0 on a clean tree; 2 with sorted, pointer-only <path>:<line> <rule-id> findings once a banned literal appears in a Rust file or a non-Rust one under crates/; and 0 again, silently, with the same rules at severity = "allow". That last reading is the discriminator §7 exists to catch. No engine capability is missing, so there is no capability-gap issue to link and no new mise task: this is the same rule kind as the committed no-conflict-markers rule. Membership in the banned set turns on one question — does the literal name a consumer, or is it a word this repo uses in its own right? The vendor name fails it: it names the vendor, not a consumer, Cargo.toml carries button-inc/batten legitimately, and git grep -n button -- 'crates/**/*.rs' finds only the English noun in "the merge button". compliance fails the same test and is excluded on the same reasoning. forbid is a case-sensitive literal-substring check (forbid_in_file in crates/batten/src/rules.rs), so as a deny rule it exits 2 on an ordinary sentence — measured, a doc comment using the noun — while missing the capitalised form entirely; git grep -in complian finds the noun already in this repo's own prose. Wrong in both directions, and no engine capability repairs it, since word-boundary or case-insensitive matching still matches the English noun: this is a limit of what a literal predicate can decide, not a gap to file. So the banned set holds only literals with no English reading — the entity-path and account-identifier shapes rule 1 names — and git grep -Fn <literal> -- crates/ is the command that decides whether one is present, in place of any count written here. Naming those literals in the committed root config is where they belong rather than a leak of them: rule 1 scopes its grep to crates/batten, the root batten.toml sits outside the crate, and that file already carries this repo's consumer-specific policy for exactly this reason — its [epoch] comment says so in as many words ("This list is consumer-specific ON PURPOSE, and it lives here rather than in crates/batten"). House-style §9 pushes the same way: a rule is inspectable declarative config a reviewer can read, so an obfuscated or hashed pattern would be the worse artifact even if the engine could match one. The rules' glob excludes the root config, so none can match its own definition.
  • Commit / bump (§6). feat(rules) → patch until 0.1.0. The landing is [[rule]] rows in the committed root batten.toml plus the §7 fixture test under crates/batten/tests/, so it does reach crate source; feat(rules) is the type this repo has actually used for a rule landing in that file, and git log --oneline -- batten.toml is the authority — no ci-typed commit appears in it, so the ci(<gate>)-ships-a-gate-as-configuration precedent an earlier draft claimed here is not one. Below 0.1.0 the SemVer arrow does not fire: release-plz bumps the patch whatever the type says, measured on CLOUD-226 and recorded in the gate document's §6. The spine's code already landed through the children, each under its own type.
  • Test obligation (§7). A fixture test in crates/batten/tests/cli.rs, beside the_committed_repo_config_gates_a_repository, loads the committed root batten.toml, writes one file under the rules' glob carrying each banned literal on its own line, and asserts exit 2 with full stdout equality against one <path>:<line> <rule-id> pointer per literal — plus a clean-tree case asserting 0 and empty stdout. Full equality is the load-bearing part: findings are sorted by the (path, line, rule) pointer tuple in crates/batten/src/rules.rs, so the expected bytes are fixed, and a rule that is missing, misspelled, mis-globbed, or set to severity = "allow" changes them. The existing test cannot serve as this obligation and is not offered as it: its fixture writes a single file carrying only a conflict marker, so the stdout it asserts is byte-identical with the new rules present, absent, or switched off — measured against the committed config plus those rules, it still prints crates/x/src/lib.rs:1 no-conflict-markers and exits 2. Its comment claiming "a rule that can never fire fails here" overclaims for that reason, and this parent's landing corrects the comment along with the gap. The banned literals are assembled at runtime rather than written as source text, the way that test already builds its marker from "<".repeat(7): a test file under crates/** that spelled a banned literal would make the real gate fire on the test itself.
  • Blockers (§8). blockedBy CLOUD-6 (closed) — the pre-implementation decision set the spine waited on. It resolved 2026-08-06, and CLOUD-20 (closed) within it recorded the runtime-spec-emission decision that batten spec implements. Nothing else blocks the parent. Child inventory, so the frontier is visible without opening each one:

Review in Linear

@wenzowski
wenzowski marked this pull request as ready for review August 9, 2026 05:27
@wenzowski
wenzowski force-pushed the claude/milestone-1-build-path-s08bil branch from 121628a to c95c69c Compare August 9, 2026 14:52
@wenzowski

Copy link
Copy Markdown
Contributor Author

/fast-forward

@wenzowski
wenzowski merged commit c95c69c into main Aug 9, 2026
15 checks passed
@wenzowski
wenzowski deleted the claude/milestone-1-build-path-s08bil branch August 9, 2026 15:41
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.

2 participants