Files
ironclaw/scripts
Benjamin Kurrek fa3c95d9c0 ci(gates): close #6963 — inventory-driven discovery + fail-closed across the remaining path-keyed gates (#6996)
* ci(gates): add owning-crate path attribution to crate_tree

Shared discovery helper for the #6963 gates: resolve which crate directory
owns a repo-relative path, outermost-wins, from where Cargo.toml files
actually are. Callers that classify production sources need this to stop
keying on the flat crates/ironclaw_* shape.

Refs #6963

* ci(gates): inventory-driven discovery + fail-closed for the script and workflow gates

Closes the script/workflow half of #6963. Every gate below resolved its
scope from the literal flat `crates/ironclaw_*` tree shape and stops
matching at the first family `git mv`; six of them then reported success
having scanned nothing. Each now discovers through the crate inventory
(scripts/ci/lib/crate_tree.py), asserts it measured something, and carries
positive + negative fixtures.

Workflow scope filters (code_style has_reborn_cli, platform-and-compat
has_direct_wasm_abi_risk, ironclaw-stress paths) are matched by crate NAME
at any depth and pinned in scripts/ci/ws12_workflow_contracts.py against
the real inventory, so a renamed, moved or deleted crate fails loudly in
Code Style instead of quietly unhooking a lane.

Two stale terms removed, both matching nothing today:
ironclaw_wasm_product_adapters (crate deleted) from the WASM ABI filter and
ironclaw_run_state (deleted with #6696) from HIGH_RISK_PATTERNS.

Regression tests: test_ws12_workflow_contracts.py (+11 sabotage cases),
test-regression-test-check.sh, test-check-composition-budget.sh (51),
test-build-wasm-extensions.sh (new, 14), test-reborn-changed-coverage.sh
(56), test-critical-mutation-gate.sh (60).

Refs #6963

* fix(ci): repoint the CLI smoke pin on the dist-build scope regex to the crate name

The Reborn CLI smoke contract greps code_style.yml for a `grep -Eq` line
containing the flat literal `crates/ironclaw_reborn_cli/`. Making that scope
regex depth-agnostic broke the needle, and the test failed loudly — which is
the point: it is a fourth pin on the same regex and the only reason a
one-sided edit could not land silently.

Repointed to the crate name (`ironclaw_reborn_cli/`), which survives the
family move for the same reason the regex now does.

Regression coverage: the existing
release_ci_publishes_reborn_without_enabling_legacy_or_docker_paths is the
regression test — it went red on the one-sided edit and green on the repoint,
verified locally.

Refs #6963

* test(architecture): inventory-driven roots + fail-closed censuses in the gate crate

Three slices of the #6963 class inside crates/ironclaw_architecture.

1. reborn_registration_pipeline_boundary (#6963 comment, arrived with #6930).
   workspace_root() walked up a fixed two levels, so under a family move the
   "root" resolved to crates/, the scan targeted crates/crates, and the gate
   passed having visited ZERO files. Its two hardcoded hosted_mcp_ prefixes
   also stopped matching, which would have false-positived against the
   registration pipeline's own files with baseline 0 blocking the fix. Now
   inventory-driven, with measured_scan() asserting inventory size, scanned
   file count, and that every owned scope resolves to at least one real file.
   Its self-test now exercises is_owned(), flat and nested.

2. reborn_sealed_evidence_mint_ratchet. HostProtocolAuthenticator and
   ChannelIngressVerifier are unsealed traits whose mint methods are provided,
   so a bare "impl Trait for X {}" anywhere confers the power to mint
   ProtocolAuthEvidence::Verified. The source census IS the enforcement, and it
   evaded on a multiline impl header, on "use ... as" aliases (plain, braced,
   and raw-identifier), and across a re-export split over two files. Headers
   are now extracted and whitespace-collapsed, in-file aliases resolved,
   matching is identifier-bounded, a re-export guard removes the cross-file
   shape, and a headers-parsed floor keeps the new normalizer from degrading
   silently. Closes the #6995 fail-open; seam origin PR #6981.

3. The shared root idiom. 23 of 24 gate files resolved the workspace root by
   walking up a fixed number of levels. ratchet_support::workspace_root() now
   searches for the nearest ancestor holding both crates/ and Cargo.toml, and
   11 private copies were deleted in its favour. Two gates that went silently
   green under nesting (reborn_authorized_seal_ratchet - worst under a PARTIAL
   move, 1309 -> 45 files scanned with no error; reborn_retired_taxonomy -
   1492 -> 0) gained measurement assertions. Two vacuous
   assert!(!path.exists()) absence checks in telegram_extension_gates now
   require their containing directory to exist.

Five stale entries removed, each matching zero files today and therefore
behavior-free: crates/ironclaw_gateway/ and extension_host/
extension_installation_store.rs from two SANCTIONED_PATHS allowlists (both now
carry stale-entry detection), crates/ironclaw_reborn_api/src and two duplicate
crates/ironclaw_product/src entries from the dependency-boundary roots, and the
deleted repo-root src/ monolith from the manifest reparse scan.

Regression tests: +8 in the family sweep, +7 in the registration boundary, +4
in the sealed-evidence census; every added assertion sabotage-tested red then
green. 26 binaries / 146 passed / 0 failed; clippy -D warnings clean.

Refs #6963, #6995

* docs(checklist): tick the WS0 path-keyed-gate prerequisite and reconcile its neighbours

Row 17 is the WS7-blocking prerequisite; it stayed open until #6963 closed.
#6946 landed the five gates the WS10 row names and #6996 closed the rest, so
the box is ticked citing both PRs and the issue.

The row's prose is reconciled rather than merely ticked: the two staleness
notes it carried (ironclaw_wasm_product_adapters, ironclaw_run_state) are now
historical, and the two places #6963's inventory was wrong are recorded -
build-wasm-extensions.sh already had its empty-set guard, and
check-composition-budget.sh is silently green under a PARTIAL move, which is
the batch shape WS7 will actually use.

Two neighbouring rows described the registration-boundary gate as a silent
trap and are updated to match what it now is: row 124 (the WS6 rename) still
owes it a repoint, but a missed rename now fails loudly; the WS10
loud-inventory row's amendment records what #6996 fixed there and narrows
what remains to the named-path keying the row was always about, across the
20 gates that fail loudly at the git mv.

Residue recorded, not hidden: #6947 stays open, and #6999 was filed for the
server-lifecycle rule's WebChat v2 gap this sweep uncovered. Neither blocks
WS7.

Regression test: docs-only reconciliation of prose whose subject is the gates
landed in the preceding commits of this PR; those gates carry the tests
(scripts/ci/test_ws12_workflow_contracts.py and eight sibling suites, plus 19
new architecture-crate tests).

Refs #6963, #6996, #6999

* fix(ci): make the unattributable-path refusal reachable in the mode CI runs

Review catch on #6996, verified before fixing and worth stating plainly: the
fail-closed check this PR added to the changed-coverage gate could not fire in
production.

git_diff() narrows the diff to per-crate src/ pathspecs, so a Rust file under
crates/ that belongs to no discovered crate was filtered out of the diff text
before parse_diff ever saw it. Only --diff-file, which is handed an un-narrowed
diff, reached reject_unattributable - and that is the mode the self-test used.
The workflow runs --base/--head. Measured on a real git fixture carrying an
orphaned crates/not_a_crate/src/lib.rs: --diff-file refused it; --base/--head
printed "no Reborn production lines added" and exited 0.

screen_unattributable() now walks the unfiltered changed-file list under
crates/ before the narrowing pathspecs are applied, so both modes refuse. A
fail-closed check that cannot fail in the mode that matters is exactly the
defect class this PR exists to close, so it is fixed rather than documented.

Three smaller review items in the same pass:
- both bash callers of crate_tree.py captured stdout with stderr merged in, so
  a Python warning would have been folded into the inventory itself and read as
  a crate directory. Captured separately now.
- crate_tree's memoized inventory sorted longest-first while
  owning_crate_directory documents outermost-wins. Order is provably irrelevant
  today (no entry is a prefix of another), but the code now reads the way the
  rule is written.
- regression-test-check probed for the workspace manifest twice; resolve_prefixes
  owns that decision and main reads its result.

Declined, with reason: high-risk matching keeps `prefix in path` rather than
`startswith`. Switching would narrow the match set, and equivalence with the
pre-existing behavior is this PR's whole contract; the substring form can only
over-match, which makes the gate stricter, never fail-open.

Regression test: "an unattributable path is refused through --base/--head too"
plus its message assertion in test-reborn-changed-coverage.sh, driven through a
real git fixture. Verified red against this PR's own pre-fix gate (rc=0, path
absent from output) and green after. Suite is now 58 cases.

Refs #6963

* fix(architecture): close the gate crate's own fail-open reads and censuses

Review triage on #6996. The headline finding is the embarrassing one: several
of the gates this PR hardens were themselves reading fail-open, which is
precisely the defect class the PR exists to eliminate. Fixed first, and proven.

Fail-open I/O, now fatal (10 sites across 5 gates):

- reborn_sealed_evidence_mint_ratchet.rs: seven `read_to_string(..).
  unwrap_or_default()` plus three swallowed `read_dir`/entry errors. An
  unreadable file contributed no impl headers and no offenders, so it scanned
  exactly like a clean one. This census IS the enforcement for two unsealed
  traits whose mint methods are provided, so a `impl Trait for X {}` it cannot
  see is forged `ProtocolAuthEvidence::Verified`.
- reborn_authorized_seal_ratchet.rs: the same shape on the gate that polices
  the sole minter of `AuthorizationGrant`.
- reborn_registration_pipeline_boundary.rs: two dropped `read_dir` errors and
  one dropped source read, threaded into the `Result<ScanOutcome, String>`
  `measured_scan` already returned.
- reborn_retired_taxonomy.rs: `scan_dir` now propagates, matching its twin
  `reborn_memory_retired_vocabulary.rs`, which already did.
- reborn_manifest_reparse_gate.rs: same.

The floors could not cover any of this: one unreadable crate `src/` tree leaves
every count comfortably above its floor while the gate reports "no violations"
for a subtree it never read. Absent-vs-unreadable is kept distinct — a missing
scan root still fails, and the retired-taxonomy floor test now pins the
*partial* tree (the staged-family-move shape), which is the only thing a floor
can still catch that an I/O error cannot.

Two matcher fail-opens in the evidence census, both verified realizable before
fixing:

- `header_implements` did not skip whitespace before a trait's generic
  arguments. `impl ChannelIngressVerifier <> for Rogue {}` compiles (checked
  against rustc: empty angle brackets after a space are accepted on a
  non-generic trait) and the header collapse *creates* that space whenever a
  line break falls there. Undetected, and it mints.
- `reexports_a_grant_trait` was line-based, so rustfmt's own output for a long
  braced import — `pub(crate) use ..::auth::{\n    ChannelIngressVerifier as
  V,\n};` — evaded it: line 1 has no trait name, line 3 does not start with
  `pub`. That guard is what removes the census's two-file alias blind spot.
  Replaced with a brace-balanced item scan that reports the item's own line.

Both proven red-then-green: sabotage the fix, exactly the self-test that pins
it goes red, the whole-workspace censuses stay green (so both are behavior-free
on today's tree).

Also closed, same class:

- The evidence census walked `crates/` only. `tools/ironclaw_stress` is a
  workspace member that depends on `ironclaw_host_api`, so it can implement a
  witness trait and mint — invisibly, with the `> 500` file floor comfortably
  cleared. Scan roots now come from the root manifest's `members` list
  (1309 -> 1332 files); a new member root joins automatically.
- `node_modules` excluded from both registration-boundary walks.
- The twelfth private `workspace_root()` copy, in the registration-boundary
  gate, deleted in favour of `ratchet_support` — it had survived behind a
  comment claiming it needed one, which was never true. The crate now has
  exactly one definition of the rule, and the CHECKLIST row that claimed
  "11 private copies ... across the whole crate" is corrected to 12 and is now
  true.
- `SANCTIONED_PATHS` fragments in the memory vocabulary gate must resolve to
  exactly ONE scanned file; ambiguity is a refusal, not a silent widening.
  Kept as fragments rather than workspace-relative literals on purpose: a
  literal would re-key the list to the flat `crates/<name>/` depth this PR
  exists to remove.
- `extract_paths_globs` refuses two `paths:` blocks instead of pinning the
  first unconditionally, matching `extract_scope_regex`. Without it a workflow
  that grew a second filter validated GREEN against the wrong block (measured:
  zero errors).
- Both fixture suites derive the crate-discovery floor from `crate_tree.py`'s
  own `MIN_CRATE_DIRECTORIES` instead of copying `24`. Measured: raise the
  floor to 40 and the literal form breaks 37 of 51 cases with an error pointing
  at the fixture; the derived form passes 51/51.

Regression tests: +7, each a negative probe that fails for a deterministic,
platform-independent reason (`read_dir` on a regular file, `read_to_string` on
a directory, a dangling symlink) rather than a chmod that root ignores inside a
container. `cargo test -p ironclaw_architecture` 26 binaries / 146 -> 153
passed / 0 failed — exactly +7, so no pre-existing test changed its verdict.

Declined, with evidence, in the review replies: anchoring high-risk matching
(substring can only over-trigger, which is the fail-closed direction for a
trigger; measured zero delta over all tracked paths, and anchoring would break
this PR's equivalence contract), splitting `validate_crate_scope_filters` for a
Ruff branch-count gate this repo does not have, and making the CLI smoke pin
multiline-safe (it fails loudly, which is the documented intent).

Verification: cargo fmt --all --check clean; cargo clippy -p
ironclaw_architecture --tests --all-features -D warnings clean; composition
budget byte-identical at 6.42% (642 bp) - 43251 / 673642 LOC, 836 Arc<dyn>;
ws12 contracts 25 cases; composition-budget 51; build-wasm 14;
changed-coverage 58; critical-mutation 60; regression-test-check all pass.

Refs #6963

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(architecture): repoint the sanctioned-paths doc at its renamed test

The memory vocabulary gate's SANCTIONED_PATHS doc still named
sanctioned_paths_all_match_real_files after that test became
sanctioned_paths_each_resolve_to_exactly_one_file, and it described only the
stale half of a check that now also refuses ambiguity. Documentation promising
a guarantee has to match the test that enforces it.

Refs #6963

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 11:26:00 -04:00
..