diff --git a/.claude/commands/deslop-reborn.md b/.claude/commands/deslop-reborn.md index 09fbb07b6ca..9e35ba2aee2 100644 --- a/.claude/commands/deslop-reborn.md +++ b/.claude/commands/deslop-reborn.md @@ -1,7 +1,7 @@ --- description: One iteration of the IronClaw Reborn de-slop loop — take ONE Reborn crate, fan out parallel review sub-agents (thermo-nuclear quality, paranoid architect, interface/contract/invariants, test-coverage/wiring), synthesize, apply fixes/refactors/missing tests, open a PR. Stop. disable-model-invocation: true -allowed-tools: Bash(cargo fmt:*), Bash(cargo clippy:*), Bash(cargo test:*), Bash(cargo build:*), Bash(git:*), Bash(gh:*), Bash(grep:*), Bash(rg:*), Bash(ls:*), Bash(wc:*), Bash(scripts/check-boundaries.sh:*), Bash(scripts/reborn-e2e-rust.sh:*), Read, Grep, Glob, Edit, Write, Agent +allowed-tools: Bash(cargo fmt:*), Bash(cargo clippy:*), Bash(cargo test:*), Bash(cargo build:*), Bash(git:*), Bash(gh:*), Bash(grep:*), Bash(rg:*), Bash(ls:*), Bash(wc:*), Bash(scripts/reborn-e2e-rust.sh:*), Read, Grep, Glob, Edit, Write, Agent argument-hint: "[crate name, e.g. ironclaw_turns]" --- diff --git a/.claude/skills/ironclaw-reborn-architecture-review/SKILL.md b/.claude/skills/ironclaw-reborn-architecture-review/SKILL.md index 30568bd0c7a..627007be00d 100644 --- a/.claude/skills/ironclaw-reborn-architecture-review/SKILL.md +++ b/.claude/skills/ironclaw-reborn-architecture-review/SKILL.md @@ -28,6 +28,6 @@ Layer discipline here is enforced by machines, not vibes: `cargo test -p ironcla ## Verify -`cargo test -p ironclaw_architecture_tests` · `bash scripts/check-boundaries.sh` (legacy inventory; may be noisy on HEAD, don't treat as the Reborn architecture gate) · `cargo clippy -p --all-targets --all-features -- -D warnings` · if routes changed: `cargo test -p ironclaw_webui --test webui_v2_descriptors_contract`. +`cargo test -p ironclaw_architecture_tests` · `cargo clippy -p --all-targets --all-features -- -D warnings` · if routes changed: `cargo test -p ironclaw_webui --test webui_v2_descriptors_contract`. **Worked good/bad examples** (before/after shapes, live exemplars, re-verify commands): [references/worked-examples.md](references/worked-examples.md) — the living curriculum; update it as the code evolves. diff --git a/.claude/skills/ironclaw-reborn-skill-maintainer/SKILL.md b/.claude/skills/ironclaw-reborn-skill-maintainer/SKILL.md index 6f9384b897f..f473a807e2d 100644 --- a/.claude/skills/ironclaw-reborn-skill-maintainer/SKILL.md +++ b/.claude/skills/ironclaw-reborn-skill-maintainer/SKILL.md @@ -19,7 +19,7 @@ Every rule below counters a rot pattern this repo's guidance is prone to. Guidan 2. **Verify every concrete reference at write time — and make it re-verifiable.** Branches die silently; prefer in-tree worked examples over branch/PR refs. For each cited path/symbol, keep a one-line grep a maintainer can re-run. 3. **No universal claims without a count.** Avoid absolute docs coverage claims. Write "most; fall back to Cargo.toml + lib.rs" or generate the list. 4. **Triggers live in frontmatter `description`, nowhere else.** The runtime surfaces only frontmatter for selection; a "when to use" buried in the body is invisible. Description = triggering conditions only — never a workflow summary (agents will follow the summary and skip the body). -5. **Never claim enforcement that doesn't exist.** Before writing "enforced by X", run X. If you add a check to `scripts/pre-commit-safety.sh` or `scripts/check-boundaries.sh`, add its self-test — and know there are two hook install paths (`.githooks/` vs `scripts/dev-setup.sh` symlink); a check is only real if both run it. +5. **Never claim enforcement that doesn't exist.** Before writing "enforced by X", run X. If you add a check to `scripts/pre-commit-safety.sh`, add its self-test — and know there are two hook install paths (`.githooks/` vs `scripts/dev-setup.sh` symlink); a check is only real if both run it. 6. **After any extraction/move/rename, grep the guidance layer** for old paths in the same PR: `.claude/`, `AGENTS.md`, `CLAUDE.md`, `crates/AGENTS.md`, `docs/reborn/contracts/`, skill bodies. 7. **Runtime-skill spec must track the parser.** `crates/domains/ironclaw_skills/src/types.rs` supports `requires.config`, `requires.skills`, `activation.setup_marker`, and silently truncates >20 keywords / >5 patterns (`enforce_limits`). If you use or change parser behavior, update `.claude/rules/skills.md` in the same PR. 8. **Codex parity check.** Codex cannot load Claude skills; it reads AGENTS.md. When a skill carries a rule Codex must also follow, the AGENTS.md hierarchy needs the pointer *with enough inline substance* ("read `.claude/skills//SKILL.md`" works — skills are plain markdown). diff --git a/.claude/skills/ironclaw-reborn-testing/SKILL.md b/.claude/skills/ironclaw-reborn-testing/SKILL.md index ba92857a624..d87563ea0d8 100644 --- a/.claude/skills/ironclaw-reborn-testing/SKILL.md +++ b/.claude/skills/ironclaw-reborn-testing/SKILL.md @@ -19,7 +19,7 @@ Pick the tier first; everything else follows. The repo's tier knowledge lives in - **Regression-per-fix is mechanically checked for conventionally marked fix/high-risk changes** (commit-msg hook + `regression-test-check.yml`). Escape hatch `[skip-regression-check]` exists — using it on a real fix will be questioned in review. - **Consolidate, don't proliferate**: extend the existing test that already drives the path (a case, a scripted turn, an assertion) before standing up a new file. Say why an existing test couldn't absorb a genuinely new scenario. -- **Persistence = both backends.** PostgreSQL + libSQL parity where production-facing; the model is `ironclaw_hooks`' dual-backend shape (`crates/loop/ironclaw_hooks/src/postgres_backend/` + `crates/loop/ironclaw_hooks/src/libsql_backend/`, proved equivalent by `crates/loop/ironclaw_hooks/tests/parity_matrix.rs` and `crates/loop/ironclaw_hooks/tests/multi_host_adversarial.rs`). Feature-gate integration tests (`check-boundaries.sh` enforces the gating for root `tests/`). +- **Persistence = both backends.** PostgreSQL + libSQL parity where production-facing; the model is `ironclaw_hooks`' dual-backend shape (`crates/loop/ironclaw_hooks/src/postgres_backend/` + `crates/loop/ironclaw_hooks/src/libsql_backend/`, proved equivalent by `crates/loop/ironclaw_hooks/tests/parity_matrix.rs` and `crates/loop/ironclaw_hooks/tests/multi_host_adversarial.rs`). Feature-gate integration tests. - **The backend-integration tier is NOT a PR gate unless the workflow says so** (re-verify: `grep -n integration .github/workflows/platform-and-compat.yml`): full Postgres coverage may run post-merge or nightly. A green PR does not prove the tier ran; run it locally when your change is DB/runtime-shaped — crate-level, e.g. `cargo test -p ironclaw_hooks --features integration,test-support` (the workspace-root `integration` feature is empty; a bare root `cargo test --features integration` runs nothing extra). - **Never add a silent self-skip to PR-gated tests.** `if docker_missing { return }` hides the suite from CI. Existing Docker sandbox canaries still need migration; new tests should skip loudly via feature gates or explicit env opt-outs. - **Capability results and terminal errors have their own test shape** — recoverable failures must remain model-visible outcomes, while host failures are terminal; test the caller against the capability-access contract and its real redaction/evidence validation. diff --git a/.claude/skills/ironclaw-reborn-testing/references/exemplar-tests.md b/.claude/skills/ironclaw-reborn-testing/references/exemplar-tests.md index d18cf01c096..49132941882 100644 --- a/.claude/skills/ironclaw-reborn-testing/references/exemplar-tests.md +++ b/.claude/skills/ironclaw-reborn-testing/references/exemplar-tests.md @@ -38,7 +38,7 @@ When a predicate selects what goes out the wire, the test must construct the rea **BAD for PR-gated coverage**: `if docker_unavailable { return }` — the container security suite silently vanishes from CI and no gate notices. Existing Docker sandbox canaries still use soft skips; don't copy that pattern into new gate coverage. -**GOOD**: make absence loud — feature-gate the test (`#![cfg(all(feature = "postgres", feature = "integration"))]`, which `scripts/check-boundaries.sh` enforces for root `tests/`), or require an explicit opt-out env var and *fail* when the dependency is missing without it. A skipped security test that doesn't announce itself is indistinguishable from coverage. +**GOOD**: make absence loud — feature-gate the test (`#![cfg(all(feature = "postgres", feature = "integration"))]`), or require an explicit opt-out env var and *fail* when the dependency is missing without it. A skipped security test that doesn't announce itself is indistinguishable from coverage. ## 6. Naming your contract's tests diff --git a/.coderabbit.yaml b/.coderabbit.yaml index b2ba1c6af34..d0b92ba1f1f 100644 --- a/.coderabbit.yaml +++ b/.coderabbit.yaml @@ -107,7 +107,8 @@ reviews: keep ironclaw_hooks_postgres / ironclaw_hooks_libsql in parity via hooks_parity). Multi-step DB operations must be wrapped in a transaction. Driver types (tokio_postgres::, libsql::) must not leak outside the allowed modules - (scripts/check-boundaries.sh). + (enforced by reborn_persistence_driver_boundary in + crates/app/ironclaw_architecture_tests). - path: "**/*.wit" instructions: | Hand-written WIT contracts define the trusted/untrusted WASM ABI. Interface changes diff --git a/crates/AGENTS.md b/crates/AGENTS.md index c43449c0c81..22e1528f9d6 100644 --- a/crates/AGENTS.md +++ b/crates/AGENTS.md @@ -142,9 +142,8 @@ workspace gate (`cargo fmt`, workspace clippy, `cargo test`) is the root `AGENTS.md`'s; test tiers and the regression rules are `.claude/rules/testing.md`. -> **Do not reach for `scripts/check-boundaries.sh`.** Measured 2026-08-05: it -> fails on a clean tree (its check 5 grep false-positives on live test files) -> and checks 1/2/3/6 target the deleted v1 `src/` tree, passing vacuously. +> The legacy `check-boundaries.sh` script is deleted (measured 2026-08-05: it +> failed on a clean tree and its v1-targeted checks passed vacuously). > Boundary enforcement for `crates/` is the architecture suite above. ## Cross-family change routes diff --git a/crates/app/ironclaw_architecture_tests/tests/reborn_capability_dto_collapse_ratchet.rs b/crates/app/ironclaw_architecture_tests/tests/reborn_capability_dto_collapse_ratchet.rs index 9afd21ec592..13c7583615e 100644 --- a/crates/app/ironclaw_architecture_tests/tests/reborn_capability_dto_collapse_ratchet.rs +++ b/crates/app/ironclaw_architecture_tests/tests/reborn_capability_dto_collapse_ratchet.rs @@ -9,26 +9,18 @@ //! that **"the §10 mirror-DTO ratchet's allowlist is what makes this safe: the //! old [shapes] are frozen entries that may only disappear."** //! -//! This is that ratchet. It freezes the current set of collapse-target DTOs and -//! fails on any change: +//! The collapse COMPLETED (#6447 retired the last request DTOs), so this file +//! is no longer the shrinking freeze that §9 describes — it is the permanent +//! zero-gate that survives it: [`RETIRED_COLLAPSE_DTOS`] names the retired +//! mirror shapes, and the test fails if any of them is ever re-declared +//! (the exact §1.1 Mechanism 1 failure, attempted after the fact). //! -//! - a **second definition** of a frozen name (a downstream crate re-declaring an -//! upstream request "for decoupling" — the exact §1.1 Mechanism 1 failure) — -//! flagged by the multiplicity check; -//! - **deleting** one without trimming [`FROZEN_COLLAPSE_DTOS`] also fails — so -//! the allowlist shrinks in lock-step as the collapse lands (§10: compare set -//! membership, never a count), and reviewers watch it get shorter toward the -//! §3 end state (`Invocation` + the surviving per-lane requests + the five -//! result channels — zero mirrors). -//! -//! Definition of done for this axis: every entry below is deleted (its fields -//! now carried by `Invocation`/`Authorized`, its result variants by -//! `Resolution`), and this file is deleted with the last of them (enforced — -//! the test fails on an empty allowlist). -//! -//! Owner: the §3 capability-path collapse slice series under the #6168 -//! umbrella (authorize → dispatch → result-channel migration) trims this list -//! in the same PRs that delete the types; reviewers hold additions to zero. +//! Two of the ten originally-frozen names are deliberately NOT here: +//! `CapabilityOutcome` (deleted by #6299 before the retired-list conversion) +//! and `CapabilityDispatchRequest` (its removal was a blessing, not a +//! deletion — it survives as the canonical port type in +//! `ironclaw_host_api::dispatch`). Reintroducing the former fires nothing; +//! add it below if that ever becomes a live hazard. //! //! Scanner semantics (shared with the other §10 ratchets — see //! [`ratchet_support`]): comments/strings stripped before matching; covers diff --git a/crates/app/ironclaw_architecture_tests/tests/reborn_composition_boundaries.rs b/crates/app/ironclaw_architecture_tests/tests/reborn_composition_boundaries.rs index da4f02d2c20..6cb410fcf60 100644 --- a/crates/app/ironclaw_architecture_tests/tests/reborn_composition_boundaries.rs +++ b/crates/app/ironclaw_architecture_tests/tests/reborn_composition_boundaries.rs @@ -20,7 +20,6 @@ use ratchet_support::{crate_path, workspace_root}; const SUBSTRATE_CRATES: &[&str] = &[ "ironclaw_auth", "ironclaw_host_api", - "ironclaw_storage", "ironclaw_filesystem", "ironclaw_event_log", "ironclaw_event_projections", @@ -28,7 +27,6 @@ const SUBSTRATE_CRATES: &[&str] = &[ "ironclaw_extension_registry", "ironclaw_authorization", "ironclaw_approvals", - "ironclaw_approvals", "ironclaw_resources", "ironclaw_trust", "ironclaw_capabilities", @@ -47,7 +45,6 @@ const SUBSTRATE_CRATES: &[&str] = &[ "ironclaw_openai_compat", "ironclaw_telegram_extension", "ironclaw_assistant", - "ironclaw_assistant", "ironclaw_triggers", ]; @@ -55,8 +52,14 @@ const SUBSTRATE_CRATES: &[&str] = &[ fn no_substrate_crate_depends_on_composition_root() { let dependencies = workspace_dependencies(); for substrate in SUBSTRATE_CRATES { + // Fail closed on an unresolvable entry: the silent `continue` this + // replaces let `ironclaw_storage` sit in this list long after the + // crate was deleted — a row that resolves to nothing polices nothing. let Some(actual) = dependencies.get(*substrate) else { - continue; + panic!( + "{substrate} is listed in SUBSTRATE_CRATES but is not a workspace package; \ + remove the stale entry (or fix the name) so this list keeps matching the tree" + ); }; assert!( !actual.iter().any(|dep| dep == COMPOSITION_CRATE), diff --git a/crates/app/ironclaw_architecture_tests/tests/reborn_dependency_boundaries.rs b/crates/app/ironclaw_architecture_tests/tests/reborn_dependency_boundaries.rs index 92d3917bdb3..58d1e371592 100644 --- a/crates/app/ironclaw_architecture_tests/tests/reborn_dependency_boundaries.rs +++ b/crates/app/ironclaw_architecture_tests/tests/reborn_dependency_boundaries.rs @@ -594,29 +594,94 @@ fn reborn_crate_dependency_boundaries_hold() { /// with slack is an unclaimed budget for the specific debt it names (#7147); a /// line ceiling with slack just means the crate got smaller, which is the /// direction wanted, and equality here would red the build on every routine -/// deletion. What must not happen silently is *growth*, so each ceiling sits a -/// bounded `TOLERANCE` above the measured count and a crate that eats through it -/// forces a reviewer to say so in writing. +/// deletion. /// -/// The ceiling is also bounded from *below*: a crate more than one tolerance -/// window under its ceiling has banked slack, which is how a ratchet goes inert -/// (the exact way `composition-budget.toml`'s share ceiling did — 17.4pp of -/// slack, constraining nothing, #7151). Re-capture at every wave close. +/// Each ceiling is **pinned at the measured count** ("set to current, not +/// padded") and the pass window extends [`GROWTH_TOLERANCE`] above it and +/// [`TOLERANCE`] below it. The upward slack exists because a hard cap at the +/// observed count reds **every open branch** the moment anyone lands a line +/// in a contracts crate on main — measured, not hypothetical: PR #7157 +/// re-captured `ironclaw_loop_contracts` four times, roughly once per fold +/// onto main, every delta ≤105 lines (gate audit 2026-08, +/// `docs/internal/gate-audit-2026-08.md` §3.2). The tolerance is sized to +/// absorb that routine drift while staying far under the growth this gate +/// exists to force into review (the reviewed raises it has caught were +/// +1,069 and +1,214 lines; `composition-budget.toml` uses the same 150). +/// Growth past the window is the reviewed decision; re-pin to the new +/// measured count in the same PR, with the reason, and keep the dated +/// re-capture notes below **append-only** — an overwritten rationale is a +/// lost audit trail. +/// +/// The ceiling is also bounded from *below*: a crate more than one +/// [`TOLERANCE`] window under its ceiling has banked slack, which is how a +/// ratchet goes inert (the exact way `composition-budget.toml`'s share +/// ceiling did — 17.4pp of slack, constraining nothing, #7151). Re-capture at +/// every wave close. /// /// Measured through [`production_rust_files`], the suite's single definition of /// a production source file, so the numbers agree with every sibling gate /// rather than with a private walk. +/// The banked-slack window below each ceiling: a crate sitting more than +/// this far under its ceiling forces a re-capture (anti-inertness). +const TOLERANCE: usize = 400; + +/// Working slack ABOVE each pinned count, so routine drift — a fold from +/// main, a few lines of wiring — passes while real growth still forces a +/// reviewed re-pin. Before 2026-08-07 this direction had NO tolerance +/// (the check was a bare `lines > ceiling`), which combined with +/// "set to current" pins to red every open branch on any main-side +/// contracts-crate growth (see the module doc and the gate audit). +const GROWTH_TOLERANCE: usize = 150; + +/// One crate's measured production line count judged against its pinned +/// ceiling. The pass window is `[ceiling - TOLERANCE, ceiling + +/// GROWTH_TOLERANCE]`; both jaws are enforced by the size-ceiling gate and +/// the arithmetic is pinned by `contracts_size_ceiling_window_edges_hold`. +#[derive(Debug, PartialEq, Eq)] +enum CeilingVerdict { + /// Inside the window — the routine-drift case the working slack exists + /// for. + Within, + /// Growth past the working slack: a reviewed re-pin is required. + Over, + /// More than one banked-slack window under the ceiling: re-capture in + /// the PR that shrank the crate (anti-inertness). + Banked, +} + +fn contracts_ceiling_verdict(lines: usize, ceiling: usize) -> CeilingVerdict { + if lines > ceiling + GROWTH_TOLERANCE { + CeilingVerdict::Over + } else if ceiling.saturating_sub(lines) > TOLERANCE { + CeilingVerdict::Banked + } else { + CeilingVerdict::Within + } +} + #[test] fn reborn_contracts_crates_carry_a_checked_size_ceiling() { - /// How far above the measured count each ceiling sits, and the width of - /// the banked-slack window below it. - const TOLERANCE: usize = 400; - /// `(crate, production line ceiling)` — captured 2026-08-05 by running this /// test with every ceiling at `0` and reading the counts out of its own /// failure message. Never counted by eye. + /// Re-pinned 2026-08-07 (gate audit): all six set to the counts this + /// test reported with every ceiling at 0 on this tree. Three had been + /// seeded at measured+400 (common, loop_contracts, prompt_envelope) — + /// the maximum non-tripping pad, contradicting the capture rule above — + /// and three at measured+0. With `GROWTH_TOLERANCE` carrying the working + /// slack, every pin now follows the one rule: set to current. + /// ✎ Union re-captured on the #7373 merge (2026-08-08): every value below + /// is the merged tree's own report (ceilings-at-0 procedure); #7157's and + /// this audit's chains fold together, and product_contracts ratchets down. + /// ✎ Union re-captured on the 2026-08-12 refresh merge of main: the + /// ceilings-at-0 procedure on the merged tree reports common 3_393, + /// extension_contracts 7_892, host_api 19_017, loop_contracts 13_345, + /// product_contracts 16_024, prompt_envelope 432 — five rows' surviving + /// pins already equal the merged tree's report exactly; prompt_envelope + /// re-pins from main's 832 (the last row still carrying the +400 seed + /// pad) to its measured count per the capture rule above. const SIZE_CEILINGS: &[(&str, usize)] = &[ - ("ironclaw_common", 3_793), + ("ironclaw_common", 3_393), // 7_727 -> 7_748 (2026-08-05, #7157): +21 lines for the // `ActivePreferenceTargetCodecs` port beside its sibling // `PreferenceTargetCodec` — a trait plus a test-shape blanket impl, @@ -776,10 +841,15 @@ fn reborn_contracts_crates_carry_a_checked_size_ceiling() { // is bounded, output-only read-view descriptors. Capture, retention, // authorization, and transport behavior remain in their owning // non-contract crates. + // 15_758 -> 15_715 (2026-08-08, #7373 merge re-capture): the crate came + // back 43 lines lighter from main-side work, so the pin ratchets DOWN + // with it. Count read from this test's own failure message. // Raised 15_758 -> 15_800 by #7228 (audited admin thread scraping): the // growth is the three admin scrape request DTOs and the wire // `RebornListThreadsResponse` reuse — declarations only; authorization, - // audit, and artifact building stay in ironclaw_assistant. + // audit, and artifact building stay in ironclaw_assistant. Folded with + // the ratchet-down above on the second #7373 merge (2026-08-08); the + // value below is the merged tree's own report. // Raised 15_800 -> 15_840 by the web-push channel: the browser // enrollment wire DTO family (`RebornWebPush*` status/subscribe/ // unsubscribe shapes) — declarations only; validation and storage stay @@ -802,7 +872,10 @@ fn reborn_contracts_crates_carry_a_checked_size_ceiling() { // and transport-consumed descriptors; validation, storage, and request // enforcement stay in owning crates. ("ironclaw_product_contracts", 16_024), - ("ironclaw_prompt_envelope", 832), + // 832 -> 432 (2026-08-12, #7373 refresh merge): re-pinned to the + // measured count — this row still carried the +400 seed pad the + // 2026-08-07 re-pin removed from its siblings; main never moved it. + ("ironclaw_prompt_envelope", 432), ]; let root = workspace_root(); @@ -827,16 +900,18 @@ fn reborn_contracts_crates_carry_a_checked_size_ceiling() { }) .sum(); - if lines > *ceiling { - over.push(format!( - "{crate_name}: {lines} production lines over a ceiling of {ceiling}" - )); - } else if ceiling.saturating_sub(lines) > TOLERANCE { - banked.push(format!( + match contracts_ceiling_verdict(lines, *ceiling) { + CeilingVerdict::Over => over.push(format!( + "{crate_name}: {lines} production lines over a ceiling of {ceiling} \ + (+{GROWTH_TOLERANCE} working slack -> effective {})", + ceiling + GROWTH_TOLERANCE + )), + CeilingVerdict::Banked => banked.push(format!( "{crate_name}: {lines} production lines against a ceiling of {ceiling} \ ({} of slack, window is {TOLERANCE})", ceiling - lines - )); + )), + CeilingVerdict::Within => {} } } @@ -860,6 +935,51 @@ fn reborn_contracts_crates_carry_a_checked_size_ceiling() { ); } +/// Regression pin for the 2026-08-07 zero-slack repair (gate audit §3.2): +/// before it, the growth check was a bare `lines > ceiling` — `TOLERANCE` +/// was consulted only for the banked-slack direction — so every "set to +/// current" pin was a hard cap at the observed count, and one line landed on +/// main in any contracts crate redded every open branch at its next fold +/// (measured on #7157: four loop_contracts re-captures, roughly one per +/// fold). This fixture drives the REAL comparison the gate runs at all four +/// window edges so the asymmetry cannot silently return. +#[test] +fn contracts_size_ceiling_window_edges_hold() { + const CEILING: usize = 10_000; + // Upward jaw: the last line of working slack passes; one more is growth + // that demands a reviewed re-pin. + assert_eq!( + contracts_ceiling_verdict(CEILING + GROWTH_TOLERANCE, CEILING), + CeilingVerdict::Within + ); + assert_eq!( + contracts_ceiling_verdict(CEILING + GROWTH_TOLERANCE + 1, CEILING), + CeilingVerdict::Over + ); + // Downward jaw: the last line of the banked window passes; one more is + // slack the ceiling must re-capture. + assert_eq!( + contracts_ceiling_verdict(CEILING - TOLERANCE, CEILING), + CeilingVerdict::Within + ); + assert_eq!( + contracts_ceiling_verdict(CEILING - TOLERANCE - 1, CEILING), + CeilingVerdict::Banked + ); + // The pin itself sits inside the window (the audit sabotage log's ±1 + // probes, as arithmetic). + assert_eq!( + contracts_ceiling_verdict(CEILING, CEILING), + CeilingVerdict::Within + ); + // A scan that measured nothing against a real pin reads as banked slack, + // never as a silent pass — the suite's fail-closed doctrine. + assert_eq!( + contracts_ceiling_verdict(0, CEILING), + CeilingVerdict::Banked + ); +} + /// PROPOSAL §11.2.3, external half: a contracts crate never acquires a /// framework, driver, or runtime client. The internal-dep allowlist above /// cannot see these — every metadata helper in this file filters to `ironclaw_*` diff --git a/crates/app/ironclaw_architecture_tests/tests/reborn_extension_specificity.rs b/crates/app/ironclaw_architecture_tests/tests/reborn_extension_specificity.rs index 8571a83a6ac..656fba88fc8 100644 --- a/crates/app/ironclaw_architecture_tests/tests/reborn_extension_specificity.rs +++ b/crates/app/ironclaw_architecture_tests/tests/reborn_extension_specificity.rs @@ -691,10 +691,13 @@ const PATH_TERM_COLLISIONS: &[(&str, &str, &str)] = &[ /// structural reason, mirroring `reborn_retired_taxonomy.rs`: the one-time /// forward data migrations name what they fold forward. const SANCTIONED_PATHS: &[&str] = &[ - "extension_host/extension_installation_store.rs", // One-release legacy webhook-path aliases (MIG-5): the compatibility // table names the concrete legacy paths it forwards; each entry carries // its own removal note. + // (A second fragment, `extension_host/extension_installation_store.rs`, + // sat here after #6430 deleted that file — matching nothing. Unlike the + // taxonomy twin, this list has no staleness check, so keep it pruned by + // hand when the named files go away.) "product_auth/durable/", ]; diff --git a/crates/app/ironclaw_architecture_tests/tests/reborn_manifest_reparse_gate.rs b/crates/app/ironclaw_architecture_tests/tests/reborn_manifest_reparse_gate.rs index e34c7738574..1d3f144b393 100644 --- a/crates/app/ironclaw_architecture_tests/tests/reborn_manifest_reparse_gate.rs +++ b/crates/app/ironclaw_architecture_tests/tests/reborn_manifest_reparse_gate.rs @@ -47,7 +47,7 @@ const ALLOWLIST: &[(&str, usize, ReparseCategory, &str)] = &[ "crates/ironclaw_host_runtime/src/memory_native_extension.rs", 1, ReparseCategory::BundledAsset, - "native_memory_first_party_package — compiles the bundled ironclaw.memory manifest asset (include_str! of assets/memory_native/manifest.toml); a host-bundled descriptor has no installed resolved record to project from", + "native_memory_first_party_package — compiles the bundled ironclaw.memory manifest asset (include_str! of crates/extensions/packages/memory-native/manifest.toml since the WS2 colocation, #7037); a host-bundled descriptor has no installed resolved record to project from", ), ]; diff --git a/crates/app/ironclaw_architecture_tests/tests/reborn_memory_retired_vocabulary.rs b/crates/app/ironclaw_architecture_tests/tests/reborn_memory_retired_vocabulary.rs index 6c0ed142c6b..5f44e8a3ddb 100644 --- a/crates/app/ironclaw_architecture_tests/tests/reborn_memory_retired_vocabulary.rs +++ b/crates/app/ironclaw_architecture_tests/tests/reborn_memory_retired_vocabulary.rs @@ -54,6 +54,16 @@ const SANCTIONED_PATHS: &[&str] = &[ "reborn_memory_retired_vocabulary.rs", ]; +/// Sanity floor for the scan, mirrored from `reborn_retired_taxonomy.rs` (this +/// gate's twin — which had the floor from birth while this file did not). An +/// *unreadable* path already fails the walk outright, so what the floor guards +/// is the shape no I/O error can reveal: a `crates/` that exists and holds a +/// fraction of the tree — the partial family move — where "no retired +/// vocabulary found" is indistinguishable from "almost nothing was looked at" +/// (CHECKLIST WS0, #6963). Far below the real count (~4000) and never expected +/// to bind. +const MIN_SCANNED_FILES: usize = 500; + fn is_sanctioned(path: &str) -> bool { SANCTIONED_PATHS .iter() @@ -210,7 +220,13 @@ fn the_sanctioned_path_resolver_refuses_stale_and_ambiguous_entries() { #[test] fn reborn_code_never_references_retired_memory_vocabulary() { - let (hits, _) = scan_workspace(&workspace_root()); + let (hits, scanned) = scan_workspace(&workspace_root()); + assert!( + scanned.len() >= MIN_SCANNED_FILES, + "scanned only {} files (floor {MIN_SCANNED_FILES}) — a partial tree must not \ + read as a clean one", + scanned.len() + ); assert!( hits.is_empty(), "retired memory vocabulary reintroduced (the bound provider's manifest \ @@ -219,3 +235,35 @@ fn reborn_code_never_references_retired_memory_vocabulary() { hits.join("\n") ); } + +/// The floor's premise, pinned on a fixture: a readable partial tree scans +/// clean (empty hit list) with a file count the floor rejects — exactly the +/// state that must never read as green. +#[test] +fn a_partial_tree_scans_clean_and_hits_the_floor() { + let temp = tempfile::tempdir().expect("tempdir"); + let root = temp.path(); + let family = root.join("crates/domains/ironclaw_memory/src"); + std::fs::create_dir_all(&family).expect("family tree"); + for index in 0..10 { + std::fs::write(family.join(format!("m{index}.rs")), "pub fn f() {}\n").expect("source"); + } + + let mut hits = Vec::new(); + let mut scanned = Vec::new(); + scan_dir(root, &root.join("crates"), &mut hits, &mut scanned) + .expect("a readable partial tree scans"); + assert_eq!( + scanned.len(), + 10, + "expected the partial tree, got {scanned:?}" + ); + assert!( + hits.is_empty(), + "a partial scan also yields an empty hit list — that is the whole problem" + ); + assert!( + scanned.len() < MIN_SCANNED_FILES, + "the floor must reject this scan" + ); +} diff --git a/crates/app/ironclaw_architecture_tests/tests/reborn_ratchet_support_scanners.rs b/crates/app/ironclaw_architecture_tests/tests/reborn_ratchet_support_scanners.rs index 9497ca2a564..abbb1ca3d0f 100644 --- a/crates/app/ironclaw_architecture_tests/tests/reborn_ratchet_support_scanners.rs +++ b/crates/app/ironclaw_architecture_tests/tests/reborn_ratchet_support_scanners.rs @@ -12,10 +12,16 @@ //! //! `ratchet_support` itself cannot hold them: it is a `mod` compiled into ~37 //! separate integration-test binaries, so a `#[test]` there would be collected -//! and run 37 times and would report a helper regression 37 times over. One -//! binary, named `reborn_*` so `code_style.yml`'s `cargo test -p -//! ironclaw_architecture_tests reborn` name filter sees it (CHECKLIST WS10's -//! loud-inventory row, defect 6). +//! and run 37 times and would report a helper regression 37 times over. +//! +//! (An earlier version of this comment claimed the `reborn_*` FILE name puts +//! this binary in `code_style.yml`'s `cargo test -p ironclaw_architecture_tests +//! reborn` smoke lane. It does not: that argument is a TEST-NAME filter — the +//! sibling `reborn_contracts_vendor_census.rs` documents the measurement — and +//! none of this file's `#[test]` fns carries the substring, so the smoke lane +//! runs 0 of them. These fixtures run in the reborn-tests full plan, which is +//! the lane that matters; noted here so nobody trusts the file name for lane +//! coverage again.) //! //! **Two `#[cfg(test)]` strippers, deliberately.** The measurement that //! motivated splitting them is in `ratchet_support`'s own module comment; what diff --git a/crates/app/ironclaw_architecture_tests/tests/reborn_restructure_baselines.rs b/crates/app/ironclaw_architecture_tests/tests/reborn_restructure_baselines.rs index c9cda53de93..2c056571f49 100644 --- a/crates/app/ironclaw_architecture_tests/tests/reborn_restructure_baselines.rs +++ b/crates/app/ironclaw_architecture_tests/tests/reborn_restructure_baselines.rs @@ -125,6 +125,9 @@ const WS0_COMPOSITION_SHARE_BP: usize = 658; /// observed value move with this record so the increase is explicit. /// ✎ Union re-measured 40_432 → 40_747 on 2026-08-07 after merging #7157's /// delivery refactor with #7214's sandbox profile and binding assembly. +/// ✎ Union re-measured on the #7373 merge (2026-08-08): the gate audit's +/// independent re-equalization (40_423 → 40_524, same drift class) folds into +/// the merged-tree figure, recorded with `[gate].loc_ceiling`/`loc_observed`. /// Re-measured on the MERGED tree (web-push channel assembly + #7171 skills /// assembly) with `bash scripts/ci/check-composition-budget.sh` -> 41_509. /// Paired with `[gate].loc_ceiling` in scripts/ci/composition-budget.toml -- @@ -141,6 +144,12 @@ const WS0_COMPOSITION_SHARE_BP: usize = 658; /// manifest ceiling (41_810, seeded from the merge-queue commit where /// concurrent mainline growth adds ~79 LOC on top of this tree) stays within /// the nudge window of this record. +/// ✎ Union re-measured 41_731 → 41_820 on 2026-08-12 (#7373 refresh merge +/// of main): the audit branch's record (40_804) and main's chain fold; +/// measured on the merged tree with `bash +/// scripts/ci/check-composition-budget.sh --print`, and the manifest's +/// `loc_ceiling`/`loc_observed` re-equalize to the same figure in this +/// commit. /// ✎ Re-ratcheted 41_731 → 41_533 on 2026-08-12 for #7185: the memory-save /// guidance and its content pins moved out of composition into the /// memory-native package that owns them, and the prompt tests split out of the diff --git a/crates/app/ironclaw_architecture_tests/tests/reborn_transport_product_boundary.rs b/crates/app/ironclaw_architecture_tests/tests/reborn_transport_product_boundary.rs index 0366d5ed2b8..b69c33e0f87 100644 --- a/crates/app/ironclaw_architecture_tests/tests/reborn_transport_product_boundary.rs +++ b/crates/app/ironclaw_architecture_tests/tests/reborn_transport_product_boundary.rs @@ -390,13 +390,57 @@ fn product_symbols_in(source: &str) -> BTreeSet { }; let tail = tail.trim_start(); if let Some(group) = tail.strip_prefix('{') { - let Some(close) = group.find('}') else { + // Balanced walk, splitting elements only at depth-0 commas. The + // previous `group.find('}')` closed at the FIRST closing brace, so + // a nested group (`use ironclaw_assistant::{m::{X}};`) truncated + // mid-element and recorded NOTHING — a sabotage-verified fail-open + // (gate audit 2026-08): a brand-new product import spelled that + // way passed this gate silently. + let mut depth = 0usize; + let mut element_start = 0usize; + let mut elements = Vec::new(); + let mut close = None; + for (index, ch) in group.char_indices() { + match ch { + '{' => depth += 1, + '}' => { + if depth == 0 { + close = Some(index); + break; + } + depth -= 1; + } + ',' if depth == 0 => { + elements.push(&group[element_start..index]); + element_start = index + 1; + } + _ => {} + } + } + let Some(close) = close else { continue; }; - for raw in group[..close].split(',') { - let name = raw.split(" as ").next().unwrap_or(raw).trim(); - if is_rust_identifier(name) { - names.insert(name.to_string()); + elements.push(&group[element_start..close]); + for raw in elements { + let element = raw.trim(); + let leading: String = element + .chars() + .take_while(|ch| ch.is_ascii_alphanumeric() || *ch == '_') + .collect(); + let rest = element[leading.len()..].trim_start(); + if rest.starts_with("::") { + // Qualified or nested element (`module::X`, `module::{X}`): + // record the leading path segment, the same key the + // single-path branch below records for + // `ironclaw_assistant::module::X`. + if is_rust_identifier(&leading) { + names.insert(leading); + } + } else { + let name = element.split(" as ").next().unwrap_or(element).trim(); + if is_rust_identifier(name) { + names.insert(name.to_string()); + } } } } else { @@ -574,6 +618,8 @@ fn import_scanner_reads_symbols_out_of_real_use_shapes() { fn f(x: &ironclaw_assistant::Qualified) -> ironclaw_assistant::AlsoQualified { } use ironclaw_product_contracts::surface::NotProduct; use ironclaw_product_contracts::inbound_requests::{AlsoNotProduct}; + use ironclaw_assistant::{nested_module::{NestedSymbol}}; + use ironclaw_assistant::{qualified_module::QualifiedInGroup, FlatMate}; use my_ironclaw_product::NotOurs; // use ironclaw_assistant::Commented; #[cfg(test)] @@ -613,6 +659,24 @@ fn import_scanner_reads_symbols_out_of_real_use_shapes() { !found.contains("Renamed"), "the alias is local; the residue list is keyed by the exported name: {found:?}" ); + assert!( + found.contains("nested_module"), + "a nested use group names its module segment — the first-`}}` truncation \ + recorded nothing here (sabotage-verified fail-open, gate audit 2026-08): {found:?}" + ); + assert!( + !found.contains("NestedSymbol"), + "the leaf of a nested group is attributed to its module row, matching the \ + single-path branch: {found:?}" + ); + assert!( + found.contains("qualified_module") && !found.contains("QualifiedInGroup"), + "a qualified element inside a group records its leading segment: {found:?}" + ); + assert!( + found.contains("FlatMate"), + "a flat member sharing a group with a qualified element is still recorded: {found:?}" + ); } /// CHECKLIST WS5's `webui` row ("gains pairing routes") and its WS2 twin diff --git a/crates/app/ironclaw_architecture_tests/tests/telegram_extension_gates.rs b/crates/app/ironclaw_architecture_tests/tests/telegram_extension_gates.rs index a74cc929dc4..1ed697e4482 100644 --- a/crates/app/ironclaw_architecture_tests/tests/telegram_extension_gates.rs +++ b/crates/app/ironclaw_architecture_tests/tests/telegram_extension_gates.rs @@ -358,11 +358,6 @@ fn no_retired_taxonomy_telegram_identifiers() { if display.contains("telegram_extension_gates.rs") { continue; } - // `crates/ironclaw_gateway/static` is the v1 monolith's embedded UI, - // retained until the monolith retires — not reborn context. - if display.contains("ironclaw_gateway/static") { - continue; - } let Ok(contents) = std::fs::read_to_string(&file) else { continue; }; @@ -392,9 +387,7 @@ fn reborn_context_free_of_v1_pairing_routes() { let mut offenders = Vec::new(); for file in rust_and_frontend_files(&root.join("crates")) { let display = file.display().to_string(); - if display.contains("telegram_extension_gates.rs") - || display.contains("ironclaw_gateway/static") - { + if display.contains("telegram_extension_gates.rs") { continue; } let Ok(contents) = std::fs::read_to_string(&file) else { diff --git a/crates/product/AGENTS.md b/crates/product/AGENTS.md index b42978f0578..b6a87a545c3 100644 --- a/crates/product/AGENTS.md +++ b/crates/product/AGENTS.md @@ -83,9 +83,9 @@ an at-most-once reservation it does not own (`ironclaw_outbound`). `cargo test -p ironclaw_architecture_tests --test reborn_dependency_boundaries reborn_crate_dependency_boundaries_hold` and `--test reborn_same_layer_edge_inventory` - **Gate-pinned guidance.** Two crate files are contracts, not prose: - `ironclaw_webui/CONTRACT.md` (route table + 19-owner `handlers.rs` charter - map; `cargo test -p ironclaw_webui --test handlers_module_charter`) and - `ironclaw_assistant/AGENTS.md` (19-sub-owner `reborn_services` charter map; + `ironclaw_webui/CONTRACT.md` (route table + `handlers.rs` charter map; + `cargo test -p ironclaw_webui --test handlers_module_charter`) and + `ironclaw_assistant/AGENTS.md` (`reborn_services` charter map; `cargo test -p ironclaw_assistant --test reborn_services_module_charter`). Edit them only with the owning suite green; do not reflow or renumber. diff --git a/docs/internal/gate-audit-2026-08.md b/docs/internal/gate-audit-2026-08.md new file mode 100644 index 00000000000..79a473cb0ad --- /dev/null +++ b/docs/internal/gate-audit-2026-08.md @@ -0,0 +1,509 @@ +# Gate & Ratchet Audit — which ones earn their cost? (2026-08-07) + +An audit of every mechanical enforcement surface in this repository — the 37 +architecture-test gate files (27,149 lines), the five in-crate module-charter +gates, ~80 CI scripts, and the committed baselines/budgets they read — prompted +by PR #7157 going red in CI six consecutive times across four distinct gates. + +Method: every number below comes from a command run during the audit (quoted +inline or in §6's sabotage log). Gates were **sabotage-tested** — the violation +each claims to catch was introduced against the live tree, the failure (or the +silent pass) observed, and the tree restored. Git/GitHub history was mined per +gate for catches vs. bookkeeping. Where a claim comes from reading rather than +running, it says so. + +--- + +## 0. The answer + +**Most of these gates are real, armed, and cheap. The pain is not the gates — +it is (a) two LOC ceilings whose counting rule lets test-relocation mint +headroom while their pins sit at zero slack, and (b) a CI shape that reveals +one broken gate per ~1-hour round-trip when a single local command could have +reported all of them in five minutes before the push.** + +The evidence in both directions, compressed: + +- **The gates catch real things.** The WebUI and assistant charter gates + caught five genuine defects on #7157 alone (2 unowned handlers + 3 stale + rows). The same-layer edge inventory has five documented catches of stale + rows the diff authors hadn't noticed. The origin-gate matrix caught memory + manifests shipping without `origin_gate_matrix` (fail-closed, #6345 era). + Two ratchets drove their debt to zero and were deleted (#6430: −3,547 + lines; #7202: last port-inversion exception). The panic gate is + self-ratcheting in both directions and its baseline is clean today + (measured: 0 stale of 50 entries, 1,266 files, 7.3s). +- **The repo's counter-history justifies paranoia.** Twelve-plus + green-but-inert gates have been found and fixed (issue #6963's six + path-keyed gates "green while measuring nothing"; a 204-test module no lane + had ever run with five assertions drifted red, commit `292c83b5a8`; + fail-open reads inside the enforcement crate itself, `16bab10248`). Gates + that are deleted rather than hardened have historically rotted into false + confidence. This audit found and fixed five more instances of that class + (§5). +- **The tax is real and measurable.** 226 commits touched the arch-test + directory in ~3 months; 67 feat/fix commits had to edit existing gate + files as a side requirement, against ~8–10 commits whose message documents + a gate catching a substantive defect. On #7157, the two size gates + extracted **zero production improvement** — both fixes were byte-verified + test-only relocations — at the cost of two full red-CI round-trips. + (The zero-slack half of that tax is repaired in this PR at owner + direction — §3.2; the counting-rule half remains the top open + recommendation.) + +Per-gate verdicts are §2; the ranked shortlist is §3; CI ergonomics +(deliberately separated from gate value) is §4. + +--- + +## 1. What actually happened on PR #7157 (reconstructed from CI logs) + +Six consecutive red runs, four distinct gates (branch `channel-delivery-tool`; +check-run logs were still available and were read directly, so this is exact): + +| Run head | What failed | Fixed by | +|---|---|---| +| `178fc279eb` | **Assistant `reborn_services` charter** — the map still named pre-rename `OUTBOUND_PREFERENCES_SET_*` symbols | `d7251aec68` (map row rewritten) | +| `d7251aec68` | **Contracts size ceiling** — loop_contracts over its ratcheted ceiling after merging main | `cc5a4acd94` (ceiling 13,850 → 13,949, rationale in-file) | +| `cc5a4acd94` | composition behavior test (not a gate) | `374840b284` | +| `374840b284` | **Composition mass budget** (Code Style) + **WebUI handlers charter** (first firing) + fmt/clippy | `df12b4a8ae` | +| `f1e7fafb71` | **Contracts size ceiling again** — a real +105-line fix pushed loop_contracts to 14,032 vs 13,949 | `d95fccf5a7` | +| `d95fccf5a7` | **WebUI handlers charter still red** | `36024374d9` | + +The two size-gate resolutions, verified byte-for-byte: + +- `df12b4a8ae`: `runtime/approval.rs`'s 417-line inline `#[cfg(test)]` module + split verbatim to `runtime/approval/tests.rs`. Production lines 1–241 + identical before/after; moved body token-identical modulo two rustfmt + re-wraps. Composition's measured count dropped 260 lines **with zero + behavior evicted** — and the ceiling was re-ratcheted *down* (40,692 → + 40,432), so the relocation was at least banked. +- `d95fccf5a7`: the 919-line inline test module in + `loop_contracts/src/runtime_context.rs` split identically; production lines + 1–536 identical; ceiling recaptured down 13,949 → 13,115. + +The charter failures, by contrast, were genuine: `get_notification_channels` / +`set_notification_channels` had no owner row, and the row they belong to still +named three handlers the PR deleted. The branch also paid composition-budget +bookkeeping on two *further* commits (`c9fad3be0d`, `d39e18766e`) — three +budget touches on one PR. + +Post-audit coda: after this audit's cutoff the branch folded main again and +the size ceiling fired **again** — main's #7361/#7363 added 66 lines to +`loop_contracts`' `instruction_bundle.rs`, blowing the 13,115 recapture; +re-captured to 13,181 at `5dde7c3370`. Nothing the branch wrote tripped it. +This recurrence is the §3.2 exhibit: the upward check carries no tolerance, +so main-side growth reds every open branch at its next fold, by construction. + +Why the failures surfaced one at a time rather than together is §4.1. + +--- + +## 2. Inventory and verdicts + +Verdict key: **KEEP** (armed, earning its cost) · **KEEP-FIX** (armed, with a +named defect or friction to fix) · **MERGE** (subsumed by a sibling) · +**DEMOTE** (value is documentation, not defense — keep only with that +understanding) · **DELETE**. Sabotage results marked ⚡ are in §6's log; every +gate file below was read end-to-end by the audit. + +### 2.1 Architecture-test gates (`crates/app/ironclaw_architecture_tests/tests/`, 37 files, one CI bucket) + +| Gate | Protects (one line) | Verdict | Key evidence | +|---|---|---|---| +| `reborn_dependency_boundaries.rs` (5,963 ln, 41 tests) | Layer matrix, 36 per-crate `BoundaryRule`s, CLI exact-dep pin, host_api zero-deps, trusted-trigger seals | **KEEP** (workhorse) with 4 sub-findings below | 140 commits/3mo — highest churn in repo; ~730 forbidden entries incl. ~60 deliberate reintroduction pins | +| — `reborn_contracts_crates_carry_a_checked_size_ceiling` | Logic accreting in the contracts tier (§11.2.3), two-sided (growth + banked slack) | **KEEP-FIX** | ⚡ both jaws armed: +1 line in host_api → red; −1 line in common → red. **The tolerance is asymmetric in code**: `TOLERANCE = 400` binds only the banked-slack direction; the growth check is a bare `lines > ceiling`, so "set to current, not padded" makes every ceiling a hard cap at observed — main-side growth reds every open branch at its next fold (#7157 re-captured loop_contracts **four times, ~once per fold**; see §3.2). **5 of 6 crates within single-digit lines of a jaw today** (host_api slack 0; extension_contracts 6; product_contracts 5; common and prompt_envelope at exactly the 400 banked-slack edge). Counter includes inline `#[cfg(test)]` mass — 26–41% of each crate's counted lines (~17.1k relocatable lines across the six, measured with the panic gate's own lexer). 4 ceiling raises in the gate's first 3 days; #7235's raise **overwrote** #7230's rationale comment. **Repaired in this PR** (owner-directed): `GROWTH_TOLERANCE = 150` on the upward jaw + all six pins re-captured to current; ±1-line probes now pass, +151 still fails | +| — boundary-rule workspace-membership check | A rule whose crate leaves the workspace failing open (PR #3212 class) | **KEEP-FIX** | Directory-basename inventory matching skips `ironclaw_slack_extension`/`ironclaw_telegram_extension` (packages live at `packages/slack|telegram/`) — the #3212 fail-open is still open for exactly those two crates (`assert_no_normal_workspace_deps` returns silently on a metadata-absent crate) | +| — `reborn_product_api_crates_do_not_bind_http_ingress` | Product crates binding TCP/serving HTTP | **KEEP-FIX** | Documented-but-standing gap: webui deliberately uncovered (its `lib.rs:226/244` carry `TcpListener::bind`/`axum::serve`); a dangling comment describes a rule entry that was never added; contradicts the webui BoundaryRule's charter text. Needs the owner ruling the gate itself asks for | +| — `hosted_mcp_discovery_...` | Ambient startup reconciliation of hosted MCP | **DEMOTE or harden** | Two exact fn-name strings checked in two files; a rename evades; no vacuity probe | +| — everything else in the file | (per-test) | **KEEP** | Fail-closed floors throughout (`checked >= 30/60`, `> 500 files`); vacuity probes (`ScriptRuntime`/`McpRuntime` markers) | +| `reborn_composition_boundaries.rs` (1,186 ln) | Composition stays assembly: service-shaped API, pub-use snapshot, no prompt content, registrar-only hooks | **KEEP** with 3 sub-findings | 13 scanner self-tests incl. symlink attacks | +| — `no_substrate_crate_depends_on_composition_root` | Nothing below app → composition | **MERGE** (recommend) | Fully subsumed by the layer matrix + ~15 BoundaryRule lists. Held a nonexistent crate (`ironclaw_storage`) and two duplicates, silently skipped — **fixed and armed in this PR** ⚡ | +| — `composition_public_pub_use_entries_name_their_consumer` | Every re-export names its consumer | **DEMOTE** (it is documentation) | The check is substring presence of `consumer:`/`pinned by:` in a comment; neither is verified to exist. Live entries say "pinned by: `ironclaw_cli` build" (free text). Fine as forced documentation; not a defense | +| — `composition_crate_installs_installed_tier_only_through_registrar` | Hook trust-ceiling bypass | **KEEP-FIX** | `strip_test_module` truncates at the FIRST inline `#[cfg(test)]` module — production code placed after one escapes the scan (fail-open by file layout); `use HookTrustClass as T;` alias also evades | +| `reborn_transport_product_boundary.rs` | Transports consume the product boundary, not the product crate (100-row frozen webui residue) | **KEEP-FIX → fixed** | ⚡ **Sabotage-confirmed fail-open**: `use ironclaw_assistant::{m::{X}};` recorded zero symbols (first-`}` truncation) and passed; plain-path spelling failed. **Parser fixed + regression fixtures in this PR**; sabotage re-run now red | +| `reborn_extension_host_port_inversion.rs` (1,131 ln) | WS2 host→product edge death; residue ledger | **KEEP** | Residues at zero; ledger exists because scope "was sized five times and was wrong five times" (#7092/#7143/#7145). Note: blesses the dev-dep product edge the operator gate refuses — the two files never reconcile the policy | +| `reborn_operator_port_inversion.rs` | products→products edge invisible to the matrix | **KEEP** | Strictest of the three (all dep kinds); 28 DTO pins spot-verified | +| `reborn_extension_manager_split.rs` | Host/manager split | **KEEP** | Witness checks are substrings ("presence is not retention" — self-acknowledged) | +| `reborn_loop_port_location_scan.rs` | `Loop*Port` single-home | **KEEP** | All 13 owner rows verified live; naming-anchored (a port not spelled `Loop…Port` is ungoverned — by design) | +| `reborn_extension_contract_location_scan.rs` / `reborn_product_contract_location_scan.rs` | One-home/one-import-path per tier | **KEEP** | All frozen names + collision exemptions verified live; three near-identical copies of the scan machinery (drift risk noted in-file) | +| `reborn_persistence_driver_boundary.rs` | DB-driver cones equal-in-both-directions; event_store driver privacy | **KEEP** | ⚡ armed (probe red with exact line); all four allowlists verified equal to live manifests. One flagged debt row: turn_runner's normal `libsql` dep whose only usage is a test module | +| `reborn_registration_pipeline_boundary.rs` | Hosted-MCP vocabulary out of shared lifecycle | **KEEP** | The suite's fail-closed reference (7 of 11 tests are sabotage fixtures; #6963's "visited zero files" incident drove the rework) | +| `reborn_runner_sheds.rs` | WS3/WS4 model-gateway shed stays shed | **KEEP** | All 29 moved items verified in loop_host, zero in runner | +| `reborn_authorized_seal_ratchet.rs` | Only the kernel implements `CapabilityAuthorizer` | **KEEP-FIX** | Line-based `contains("CapabilityAuthorizer for")` — alias (`use … as A; impl A for`) and line-break evasions its younger sibling (`sealed_evidence_mint`) explicitly closed for its own traits were never retrofitted here | +| `reborn_sealed_evidence_mint_ratchet.rs` (1,938 ln) | Evidence-mint authority (13 closed paths) | **KEEP** (exemplary) | Born from a measured vacuous feature-seal; extended after a planted forgery passed every scan (WS12 F1); collapsed-header + alias-resolving matcher; >1000-impl-header floor | +| `reborn_origin_gate_matrix_ratchet.rs` | The 17-capability ungated allowlist (security) | **KEEP** | Size pin never moved; imports the REAL constant from host_api; manifest floor met exactly at 14 | +| `reborn_service_method_freeze_ratchet.rs` | `ProductSurface` = exactly 3 methods | **KEEP** | ~70 frozen facade methods driven to 3; failed loudly when the trait moved crates (worked as designed) | +| `reborn_deployment_mode_branching_ratchet.rs` | Composition never branches on profile | **KEEP-FIX** | Literal-token scan: `Self::Variant` inside a composition impl (16 such refs live today, currently benign) and `use … as P` aliasing are invisible; the CLI's production variant-mapping is outside the scan root | +| `reborn_deployment_mode_typename_ratchet.rs` / `reborn_standalone_typename_ratchet.rs` | Mode names never become types | **KEEP** | Standalone list ran 37 → 0 (the cleanest ratchet arc in the suite); two stale `RebornLocal*` entries carry an expired retirement note | +| `reborn_capability_dto_collapse_ratchet.rs` | Retired capability mirror-DTOs stay dead | **KEEP** (header **fixed in this PR**) | Header promised an empty-allowlist assertion and file deletion that don't exist; two originally-frozen names silently left governance (recorded in the new header) | +| `reborn_struct_test_support_ratchet.rs` | Frozen dead-code/test-support member census (79 paths / 276 members, equality both ways) | **KEEP-FIX** | Census counts only field/method attributes: item-level `#[allow(dead_code)]` (struct/mod/impl) and free functions escape entirely. The equality conversion (#7170) caught real untracked slack (#7147) | +| `reborn_manifest_reparse_gate.rs` | Raw manifest reparse confined to 2 sites | **KEEP** (stale note fixed in this PR) | 6 → 2 entries historically; literal-needle dodge (`use … as R; R::from_toml`) exists | +| `reborn_memory_retired_vocabulary.rs` | #3537 retired memory vocabulary at zero | **KEEP** (floor **added in this PR**) | Had no partial-tree floor unlike its explicit twin; was born with an already-dead sanctioned path (its own header records the rot) | +| `reborn_retired_failure_vocabulary.rs` / `reborn_retired_taxonomy.rs` | Retired vocabularies at zero | **KEEP** | Taxonomy's sanctioned-path staleness check exists because two paths rotted invisibly pre-#6996 | +| `reborn_contracts_vendor_census.rs` | Vendor names in contracts tier: exact census (16/2, 91/9, 1/1 — re-derived exactly during audit) | **KEEP** (exemplary) | Equality in both directions; the 91-occurrence `llm_costs` row is a recorded owner-decision queue item, not an oversight | +| `reborn_same_layer_edge_inventory.rs` (1,528 ln) | Same-layer edges the matrix can't see (70 rows, equality) | **KEEP** (strongest churn-to-value ratio in evidence) | Five documented real catches by its own arms; one reviewed growth (72→74) paid for a 2,178-line eviction | +| `reborn_crate_inventory.rs` | The path-resolution meta-layer every gate resolves through | **KEEP** | Its predicted disaster (WS7 family move) happened; ~450 literals survived via the inventory | +| `reborn_cross_crate_include_scan.rs` | `include_str!` dependency-graph bypass | **KEEP-FIX** | `REPORT_ONLY = true` since birth; the named flipping PR (#7094) merged without flipping it; only 16 of ~104 findings are ratcheted; the repo-root-asset half is unbounded; the reclassification loophole (drop the target's `Cargo.toml`) already absorbed most of the WS0 inventory once | +| `reborn_extension_specificity.rs` (2,174 ln) | No vendor names in generic code (derived vocabulary; 119-pair debt list, equality) | **KEEP** (stale entry **fixed in this PR**) | Debt list monotonically shrank 130 → 119; 84 permanent carve-outs are path-unbounded for their 13 terms (growth is review-gated only); highest churn tax in the suite (34 commits, mostly move repoints) | +| `reborn_process_storage_scan_gate.rs` | Request paths never enumerate collections (#7050 perf class) | **KEEP** | Matcher hardened after a demonstrated reformat evasion | +| `reborn_restructure_baselines.rs` | The two shell-gate ratchets stay armed (tamper alarm) | **KEEP** | Its nudge assertion demonstrably fired (refused a 371-line unrecorded ceiling move) | +| `reborn_tracing_target_syntax.rs` | `tracing!(target = …)` field-form drift (#7146: 120 wrong sites) | **KEEP** (exemplary) | Measures its language premise through a real subscriber rather than asserting it in a comment | +| `reborn_build_script_roots.rs` | Build scripts deriving repo root by counted parent hops | **KEEP** | Born from a real silent-skill-loss near-miss; 5-spelling denylist (novel spellings pass) | +| `telegram_extension_gates.rs` | Retired telegram taxonomy tombstones + file budget | **KEEP** (dead exclusions **fixed in this PR**) | `payload.rs` at 983 of the 999-line budget — will bind soon; unreadable files skip silently (contrary to suite doctrine) | +| `reborn_ratchet_support_scanners.rs` + `ratchet_support/` | Shared-scanner regression fixtures | **KEEP** (false lane claim **fixed in this PR**) | ~19 sibling gates still roll private walkers (consolidation incomplete, self-noted) | +| `reborn_conversations_threads_attachments.rs` | conversations/threads naming-trap severance | **KEEP** | Statement-scoped re-export matcher exists because CodeRabbit caught the exact-string version failing open on #7018 | + +### 2.2 Module-charter gates (in their crates; run in that crate's CI bucket) + +All five were **re-verified exact against the live tree during the audit: 0 +unassigned, 0 stale, 0 duplicated entries.** Three verified real catches in +~3 days of life on main (MCP's arming closed a live inline-mint violation; +#7235 was forced to add the `inspector` row in-commit; #7157's charter catch +above). Zero observed false fires — every source-only body edit passed silently. + +| Charter | Size | Verdict | Notes | +|---|---|---|---| +| WebUI `handlers_module_charter` | 19 owners / 224 items | **KEEP** | Caught #7157's five. Blind to top-level `mod`/`pub use` (live example at `handlers.rs:17-18`, mitigated by the directory walk). Local run needs `SKIP_FRONTEND_BUILD=1` (~82s warm, measured) | +| Assistant `reborn_services_module_charter` | 20 owners / 551 items | **KEEP** | The most complete scanner (mods charted, cfg-test exempt). Heaviest local compile of the five (pulls composition test-support) for a text-diff test | +| LLM `module_charter` | 10 owners / 48 files | **KEEP-FIX** | File-granular; **no owner-count floor** — could legally collapse to one `everything` row (product siblings pin ≥11) | +| Auth `module_charter` + severance | 4 owners / 45 files | **KEEP** | The severance half (engine ↮ product_auth, real lexer) is a genuine boundary test living outside the arch crate | +| MCP `module_charter` | 6 modules / 2 grandfathered mints | **KEEP-FIX** | Weakest mechanism: idiom-shaped probe (`reason: "` / `reason: format!` only), **non-recursive walk** (a new `src/foo/bar.rs` escapes), `From` re-add checked in one file only | + +Honest framing the charter gates deserve: their dominant value is +**documentation-forcing** — they make ownership decisions get recorded and +stale rows die. They also demonstrably catch. Both product charters have a +`dispatch` catch-all and no entry-quality enforcement (explicitly by design: +"checks coverage and existence, not prose"). Two files copy the armed MCP +charter's "N rules keep the charter honest" phrasing with **no gate behind +them** (`trace_commons/src/contribution/mod.rs`, `capabilities/src/host/mod.rs`) +— a reader pattern-matching the phrase would wrongly assume enforcement. + +### 2.3 Script gates and committed baselines + +| Gate | Verdict | Evidence | +|---|---|---| +| `check_no_panics.py --reborn-baseline` + baseline (50 entries) | **KEEP** (exemplary) | ⚡ armed both directions; stale entries FAIL (self-ratcheting); baseline clean today; 7.3s local; cargo-metadata-derived scope survived the family moves | +| `check-composition-budget.sh` + `composition-budget.toml` | **KEEP-FIX** | ⚡ armed on real growth; ⚡ **fail-open for production code in `*_tests.rs`-named files** (basename exclusion, nothing verifies cfg-gating — the panic gate disagrees on what "test file" means); inline-test mass counts (25.2% of composition's number, 10,199 lines ≈ 68 tolerance-windows of mintable headroom); live tree is **49 lines below the effective ceiling** — the next routine composition PR trips it. History: every gate-fired event resolved by re-seed (+371, +63, ratify-1122, +4), never by eviction-in-response; evictions happened on program schedule. **Pins re-equalized to observed in this PR** (49 LOC / 10 sites of headroom restored to the designed 150/15 windows, per the TOML's own instructions; probes: +100 passes, +160 fails) | +| `reborn-coverage-ratchet.sh` + `coverage-floor.toml` (global 86.96% + 22 crate floors) | **KEEP** | NOT CI-only: `.githooks/pre-push` runs the local ratchet by default (for anyone with hooks installed — nobody, see §4.3); CI enforcement is push-to-main only by owner decision 2026-08-04. #7083 (11 crates silently dropped from both numerator and denominator under enforce=true) is the gate's own inert-precedent | +| `check-target-tree.py` | **KEEP** | 0.2s; shrink-only exceptions; self-tested | +| `check-guidance.py` | **KEEP** | 0.4s; 2,084 path refs verified; this audit leaned on it to make a deletion self-verifying | +| `docs_publication_boundary.py` | **KEEP** | Frozen `.mintignore`, remove-only | +| `regression-test-check.py` + workflow | **KEEP** | Anti-tamper: CI executes the checker from the PR **base** SHA so a PR cannot edit the gate that judges it | +| `critical_mutation_gate.py` (merge-queue) | **KEEP** | Result set must equal the mutant allowlist exactly; zero mutants for a named fn is an error | +| `check-wasm-artifact-freshness.py`, `check-include-str-paths.sh`, `check-hermetic-env.sh`, hermetic runner family, `ws12_workflow_contracts.py`, `ws12_suite_shards.py`, `classify-test-scope.sh`, `package-feature-flags.sh` | **KEEP** | Each self-tested in CI; `ws12_workflow_contracts` is the meta-gate against silently-disconnected lanes | +| `check-test-suite-boundaries.sh`, `check-version-bumps.sh` | **KEEP-FIX** | Gate in CI with **no self-test** (the only gate scripts in that state) | +| `delta_lint.sh` | **DEMOTE** (status quo) | Opt-in, documented fail-open on base detection | +| `quality_gate.sh` | **KEEP** | The local CI-parity gate; notably uses `--no-fail-fast` — the exact flag CI lacks | +| `quality_gate_strict.sh` | **DELETE (recommend)** | Zero references anywhere; superseded by `quality_gate.sh` | +| `check-e2e-matrix-files.sh` | **DELETE — done in this PR** | Zero references; default target `.github/workflows/e2e.yml` was deleted with the v1 monolith (`b6da0272a8`) | +| `check-boundaries.sh` | **DELETE — done in this PR** | Measured broken on a clean tree (recorded 2026-08-05); never run by CI; two guidance files claimed it "enforces" gating it never enforced | +| `test-ci-artifact-naming.sh`, `test_cut_ironclaw_release.py`, `test-import-reborn-run-artifact.py`, `test-reset-extension-state.sh`, `test-pre-commit-safety.sh`, `tests/test_check_reborn_responses_e2e_manifest.py` | **KEEP-FIX (wire or delete)** | Self-tests that run in **no workflow** — the exact shape that produced the 204-dark-test find. `test-pre-commit-safety.sh` is the highest-value wire-up (its subject runs on every hook-installed commit) | + +--- + +## 3. The shortlist that matters (ranked: developer friction × weakness of protection) + +1. **CI reports one broken gate per round-trip. Fix the run shape, weaken + nothing.** (§4.1–4.2; measured: two broken gates → default run reports 1 + in 18s then stops; `--no-fail-fast` reports both in 211s.) One-line-per-lane + change plus a fast-checks aggregation. This is the highest-leverage item in + the audit and it touches no gate's semantics. +2. **The two LOC ceilings measure production+inline-test mass and are pinned + at zero slack.** Composition: ceiling == observed, 150 tolerance, live + headroom 49 lines; contracts: 5 of 6 crates within single digits of a jaw + (+1 line in host_api reds the suite — sabotage-verified). Meanwhile 25–41% + of every counted crate is inline `#[cfg(test)]` mass, so the honest-looking + fix to any trip is a file move that changes nothing (#7157 did it twice; + ~27k relocatable lines remain across composition + the six contracts + crates). Two concrete repairs, both strengthening: + - **Count only non-test-context lines.** `check_no_panics.py` already + contains a self-tested Rust lexer + test-context tracker; this audit used + it to produce every inline-test number above in seconds. Teach + `check-composition-budget.sh` and `production_rust_files`'s line-count + consumer to subtract test-context lines (or shell out to a shared Python + helper), then re-seed all ceilings to the true production counts. This + kills the relocation-minting class outright, closes the `*_tests.rs` + basename fail-open (⚡ §6.2), and makes a trip mean what the gate claims: + behavior accreted. + - **Give the upward check the tolerance the downward check already has.** + The asymmetry is in the code, not just the pins: `TOLERANCE = 400` is + consulted in exactly one direction — the banked-slack check + (`ceiling.saturating_sub(lines) > TOLERANCE`) — while the growth check + is a bare `lines > *ceiling`. Combined with the in-file instruction + "set to current, not padded," every ceiling is a hard cap at the exact + observed count: one line added *anywhere on main* to a contracts crate + reds **every open branch** at its next fold until someone re-captures. + Measured on #7157, this is not hypothetical — the branch re-captured + `loop_contracts` **four times, roughly once per fold onto main** + (14,479 → 13,850 → 13,949 → 13,115 → 13,181; the last, at + `5dde7c3370`, was tripped by main's #7361/#7363 landing 66 lines in + `instruction_bundle.rs`, nothing the branch wrote). The gate has been + generating its own busywork. + **Repaired in this PR at owner direction** (the fold-red recurrence made + the call): the growth check now allows `GROWTH_TOLERANCE = 150` of + working slack above each pin — sized to the composition-budget + precedent, 1.4× the largest routine delta observed (105), and far under + the reviewed raises the gate has caught (+1,069 / +1,214) — and all six + ceilings are re-pinned to the counts the test itself reports, which + also removes the +400 seed padding that had put common/prompt_envelope + one *deleted* line from the banked jaw. Sabotage-verified: +1 line and + −1 line (both red before) now pass; +151 lines still fails with the + effective-ceiling arithmetic in the message. The composition budget got + the matching maintenance in the same push: its pins had drifted to 49 + LOC / 10 `Arc` of live headroom, and were re-equalized to observed + per the TOML's own instructions (probe: +100 LOC now passes, +160 still + fails). The dial is one constant if the owner wants a different width. + Every #7157 recapture would have been absorbed with zero red builds + under this shape. Remaining from this item: the rationale-comment + append-only discipline is now stated in the gate's doc (#7235 overwrote + #7230's +1,214 rationale — that trail is already lossy). +3. **Arch gates are merge-queue-only for most PRs.** The architecture bucket + runs on a PR only when the PR touches the arch crate or `ironclaw_host_api` + — a PR touching only `loop_contracts` (i.e., #7157) meets the size ceiling + for the first time in the queue. The suite costs 4.2 min (measured, warm). + Recommend: the affected-area planner adds the `architecture-misc` bucket + whenever anything under `crates/` changes. +4. **Fix the two remaining sabotage-verified fail-opens not fixed here.** + (a) Slack/telegram BoundaryRules skip silently (inventory basename + mismatch — §2.1); (b) `composition_crate_installs_installed_tier_only_ + through_registrar` truncates at the first inline test module. Both are + contained, mechanical fixes in the same style as this PR's transport-gate + fix. +5. **Retrofit the older literal-token scanners with their younger siblings' + hardening.** `authorized_seal` (alias + line-break evasion, guarding + `Authorized` forgery of all things) and the deployment-branching ratchet + (`Self::Variant` + alias + CLI scope). The mint ratchet contains the + finished implementation to copy. +6. **Decide `cross_crate_include_scan`'s `REPORT_ONLY` flag.** The PR named as + its flipper merged without flipping; the residual 16 are ratcheted but the + ~88 repo-root reach-ins are unbounded. Either flip with the current + inventory as the allowlist, or retitle the gate a census so it stops + promising enforcement. +7. **Charter-gate small arms**: owner-count floors for llm/auth (product + siblings pin ≥11); recursive walk + `From` all-files check for MCP; + an "(unenforced)" marker or a real gate for the two charter-mimicry module + docs. +8. **Wire the orphan self-tests or delete them** (§2.3 last row) — six + checker-tests no lane runs, in a repo whose history includes a 204-test + module in exactly that state. + +Not on the list deliberately: deleting LOC ceilings or charter gates. The +evidence says the charters catch real defects at low cost, and the ceilings — +once they count the right thing — encode a priced, reviewed decision the +dependency gates cannot see (a crate that imports nothing and implements +everything). The budget TOML's own history shows the ceilings *documenting* +growth rather than preventing it (every fire resolved by re-seed), so the +owner should hold them to the §3.2 repair or consciously accept them as +growth-visibility instruments. + +--- + +## 4. CI ergonomics (separate from gate value) + +### 4.1 Why one PR ate six round-trips + +Three mechanisms, all verified against the workflow files: + +1. **Within a bucket, cargo stops at the first failing binary.** All 37 arch + gate files are test binaries of one crate in one bucket + (`architecture-misc`); each bucket runs as a single + `cargo test -p … --all-targets` with no `--no-fail-fast` + (`reborn-tests.yml:411-414`). `--no-fail-fast` appears nowhere in CI — its + only repo occurrence is the *local* `quality_gate.sh`. The same stop-early + shape holds for the root-partition loop, the group-suite loop, the + integration lanes, and the exact-target PR path (`set -euo pipefail` + loops). Measured on this tree with two gates broken (§6.6): default run + reports **1** failing binary in 17.9s and stops; `--no-fail-fast` reports + **both** in 210.7s. +2. **`cancel-in-progress: true`** (`reborn-tests.yml:46-48`): each fix push + cancels the still-running sibling jobs that would have surfaced the other + failures. Serial discovery across *parallel* jobs. +3. **Sequential steps in `code_style` fast-checks:** fmt → … → panic gate → + … → composition budget run as one job's steps; the first failing step hides + every later gate. +4. (Throttling, not truncation: PR runs cap crate buckets at `max-parallel: 3` + and root/integration lanes at **1**.) + +**Recommendation R1 (weakens nothing): add `--no-fail-fast` to the five CI +cargo-test shapes and make fast-checks steps non-short-circuiting (collect and +report at the end).** Cost: a failing run finishes its lane (~+3–4 min for the +arch bucket) instead of aborting. Benefit: N broken gates = 1 round-trip. +Caveat for the implementer: `ws12_workflow_contracts.py` pins load-bearing +literal strings inside these workflows — run its self-test with the edit. + +### 4.2 Local runnability (measured on a warm tree) + +| Gate layer | Command | Time | +|---|---|---| +| Composition budget | `bash scripts/ci/check-composition-budget.sh` | 1.5s | +| Panic baseline (full) | `python3 scripts/check_no_panics.py --reborn-baseline` | 7.3s | +| Target tree / guidance / include_str / hermetic-env / docs boundary | one command each | 0.1–0.5s each | +| Architecture suite (all 37 gates) | `cargo test -p ironclaw_architecture_tests` | 251.9s (nextest would parallelize; not installed on the audited machine) | +| WebUI handlers charter | `SKIP_FRONTEND_BUILD=1 cargo test -p ironclaw_webui --test handlers_module_charter` | 82.0s (compile-dominated; test itself 0.00s) | +| Contracts size ceiling alone | prebuilt binary, single test | 0.2s | + +Every gate that fired on #7157 is locally runnable in one command. Nothing +tells a developer this: the commands live in six different files, and the +coverage-floor file is the only one that documents its local runner. + +### 4.3 The pre-push gauntlet: it exists, and that's the problem + +`.githooks/pre-push` runs `quality_gate.sh` — fmt + clippy + **the full +workspace test suite** — plus the local coverage ratchet (a full instrumented +rebuild) plus a WebUI provider replay that **hard-errors if the Emulate CLI +isn't built**. It is CI-parity maximalism: hours cold, brittle by default, and +**not installed** — this clone (the owner's) has only `.sample` hooks +(verified), `dev-setup.sh` is opt-in, and the repo's two hook-install stories +(`dev-setup.sh` symlinks vs `.githooks/pre-commit`'s `core.hooksPath` +instruction) install *different* pre-commit checks; neither installs both. + +**Proposed: `scripts/preflight-gates.sh`** — included in this PR as unwired +tooling. It runs exactly the deterministic-gate classes above: the script +layer (~10s), the architecture suite with `--no-fail-fast` (~4.2 min warm, +nextest-aware), and the module-charter tests of crates the diff touches +(0–90s). Measured end-to-end on this branch: **402.8s (~6.7 min), exit 0, +"every deterministic gate green"** — that run also recompiled the gate +binaries this audit edited; a no-recompile run bounds at ~4.5 min, and +nextest would cut the suite further. Every gate keeps running after a +failure and the script reports the full list at the end. Run against +#7157's failure set it covers all four gates (budget: script layer; contracts +ceiling: arch suite; both charters: changed-crate charter runs) — i.e., all +six red CI runs were reachable locally in one five-minute command before the +first push. Adoption suggestions for the owner: mention it in +`AGENTS.md`'s build/run block, and consider making it the *default* pre-push +hook with today's heavy hook behind an env flag (inverting the current +defaults, which are so heavy they produced zero installs). + +### 4.4 Lane-coverage oddities worth knowing (not defects) + +- `code_style.yml`'s merge-lane smoke runs + `cargo test -p ironclaw_architecture_tests reborn` — a **test-name** + filter. Whole binaries whose test fns lack the substring run 0 tests there + (conversations 5/5, process-storage 7/7, tracing 3/3, telegram 11/12, + scanner fixtures 11/11, specificity 5/8 incl. its dependency gate). The + full plan still runs them; only the redundancy is lost. Either drop the + filter (the suite is 4 min) or stop treating file names as lane selectors + (one gate file's false claim about this was fixed in this PR). +- Coverage floors + changed-line gate enforce on push-to-main only (owner + decision 2026-08-04, documented in-line in the workflow) — post-merge + enforcement announced via Slack alerts, deliberate and traceable. + +--- + +## 5. Inert / fail-open findings (highest priority, with sabotage evidence) + +Fixed in this PR (each its own commit, evidence in the message): + +1. **Transport-gate nested-use-group fail-open** — sabotage-verified: a + brand-new product import spelled `use ironclaw_assistant::{m::{X}};` + passed the gate; plain-path spelling failed. Parser now does a balanced + walk; fixtures added; the original sabotage re-run goes red. +2. **`ironclaw_storage` phantom row + duplicates, silently skipped** in + `no_substrate_crate_depends_on_composition_root` — the skip is now a + panic naming the stale entry (sabotage-verified with a probe row). +3. **Stale sanctioned path** in `reborn_extension_specificity.rs` exempting a + file deleted by #6430 (the one exclusion list in that gate with no + staleness check). +4. **Dead `ironclaw_gateway/static` exclusions** in both telegram cross-tree + scans (the v1 crate no longer exists). +5. **Missing partial-tree floor** in `reborn_memory_retired_vocabulary.rs` — + its twin had the 500-file floor from birth; this gate would have passed + green over a partially-moved tree (the #6963 class). Floor added + + fixture proving a 10-file tree scans clean and is rejected. +6. **False lane-coverage claim** in the scanner-fixture file's header + (file-name vs test-name filter). +7. **Gate-header fiction** in the DTO-collapse ratchet (promised assertions + that don't exist; two names that silently left governance now recorded). +8. **`check-boundaries.sh`** — measured broken on a clean tree, never run by + CI, cited by two guidance files as "enforcing" gating it never enforced. + Deleted along with every live reference (check-guidance green). +9. **`check-e2e-matrix-files.sh`** — checker for a workflow deleted with the + v1 monolith; zero references. Deleted. + +Found, verified, and left for the owner (mechanical fixes, but each changes a +gate's blast radius): the slack/telegram BoundaryRule skip; the +registrar-gate first-test-module truncation; the webui HTTP-ingress +documented gap; `REPORT_ONLY` on the include scan; the older seal ratchet's +evasion shapes; the six unwired self-tests. + +--- + +## 6. Sabotage log (methods and outputs) + +All probes ran against the live tree with the shipped gate implementations; +arch-gate probes ran the prebuilt test binaries (the gates are runtime file +scanners). Worktree verified clean after each probe. + +1. **Panic gate**, both directions: injected `.unwrap()` into a + shipping-closure production file → exit 1 naming `identity.rs:934` with + the exact fingerprint; bogus baseline row → exit 1 "Stale baseline entries + (remove them to ratchet downward)". Clean baseline confirmed: "OK: Reborn + production panic baseline matches (1266 files, 50 reviewed invariant(s))". +2. **Composition budget**: +200-line production-named file → exit 1 + (40,724 > effective 40,573). Same 200 lines renamed `__audit_probe_tests.rs` + → exit 0, count unchanged (basename fail-open). Live headroom measured: 49 + LOC (abs), 10 `Arc` sites. +3. **Contracts size ceiling**, both jaws: `echo '// comment' >> + host_api/src/lib.rs` → "18785 production lines over a ceiling of 18784", + FAILED. Deleting one line from `common/src/lib.rs` → "3392 … (401 of + slack, window is 400)", FAILED. +4. **Transport product boundary**: nested-group import → `test result: ok` + (fail-open, pre-fix); plain-path import → FAILED with the gate's message. + Post-fix: nested-group import → FAILED (armed). +5. **Persistence driver boundary**: `use deadpool_postgres::Pool as …;` in a + new event_store file → FAILED naming `store.rs:1` and citing §6.3.2. +6. **Fail-fast demonstration**: with the host_api +1 line and the webui + plain-path import planted simultaneously — `cargo test -p + ironclaw_architecture_tests`: **1** failing binary reported, 17.93s, + remaining binaries never ran; `--no-fail-fast`: **2** failing binaries + reported, 210.74s, all 37 ran. +7. **Substrate-list arming** (post-fix): probe row `ironclaw_zzz_probe` → + FAILED "is listed in SUBSTRATE_CRATES but is not a workspace package". +8. **Inline-test mass measurement** (via `check_no_panics.py`'s lexer, + mirroring the budget counter's exclusions): composition 10,199/40,524 + lines (25.2%); contracts crates 26.0–40.5% each (§2.1/§3.2). + +Historical precedent this audit stands on (all verified in git): +issue #6963 + `fa3c95d9c0` (six path-keyed gates hardened after being +reproduced "green while measuring nothing"); `292c83b5a8` (204-test module no +lane ran, five assertions drifted red); `16bab10248` (fail-open reads inside +the enforcement crate); `ca4acb30d0` (two gates born inert in one lane, +measured 0→6 and 0→5); `3c3bf37a04` ("a twelfth green-but-inert gate, found +on the way"). + +--- + +## 7. What this PR changes vs. recommends + +**Changed (eleven fix commits, each with its evidence in the message, plus +this report):** the nine §5 fixes, the charter-count prose fix in +`crates/product/AGENTS.md`, the manifest-reparse allowlist note repoint, and +the proposed-but-unwired `scripts/preflight-gates.sh` (validated end-to-end: +exit 0 on this branch). + +A twelfth commit landed because a gate caught this audit's own PR: the +affected-area planner's fail-closed arm refused +`unmapped test or CI path: scripts/check-boundaries.sh` — precisely the +forced-decision behavior its `PR_STATIC_CONTROL_PATHS` comment block +documents, working as designed against the auditor. Both audit-touched +script paths are now classified per that list's membership rule. + +**Landed after owner direction (2026-08-07, this conversation):** the §3.2 +zero-slack repair itself — `GROWTH_TOLERANCE = 150` on the contracts size +ceiling with all six pins re-captured to current, and the composition +budget's pins re-equalized to observed (record constant moved in the same +commit, as its file requires). These are deliberate loosenings of two +ratchets' *pin states*, made at the owner's call with the sizing rationale +and sabotage evidence in the commits; the gates' catch thresholds for real +growth remain far below every event they exist to catch. Still open from +§3.2: the cfg-test-aware counting rule, which would re-seed these numbers +again and kill the relocation-minting class. + +**Recommended, not changed (owner decisions):** `--no-fail-fast` + +fast-checks aggregation in CI (R1); cfg-test-aware LOC counting + mid-window +pins + append-only rationales (R2); arch bucket on any `crates/` change (R3); +the two remaining fail-open fixes (R4); seal/branching-ratchet retrofits +(R5); the `REPORT_ONLY` decision (R6); charter small-arms (R7); orphan +self-test wiring (R8); `quality_gate_strict.sh` deletion; hook-install-story +reconciliation and preflight adoption (§4.3). diff --git a/scripts/check-boundaries.sh b/scripts/check-boundaries.sh deleted file mode 100755 index e9f7a555d2e..00000000000 --- a/scripts/check-boundaries.sh +++ /dev/null @@ -1,275 +0,0 @@ -#!/usr/bin/env bash -# Architecture boundary checks for IronClaw. -# Run as: bash scripts/check-boundaries.sh -# Returns non-zero if hard violations are found. - -set -euo pipefail - -REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -cd "$REPO_ROOT" - -violations=0 - -echo "=== Architecture Boundary Checks ===" -echo - -# -------------------------------------------------------------------------- -# Check 1: Direct database driver usage outside the db layer -# -------------------------------------------------------------------------- -# tokio_postgres:: and libsql:: types should only appear in: -# - src/db/ (the database abstraction layer) -# - src/workspace/repository.rs (workspace's own DB layer) -# - src/error.rs (needs From impls for driver error types) -# - src/app.rs (bootstraps/initialises the database) -# - src/testing.rs (test infrastructure) -# - src/cli/ (CLI commands that bootstrap DB connections) -# - src/setup/ (onboarding wizard bootstraps DB) -# - src/main.rs (entry point) -# -# Everything else is a boundary violation -- those modules should go through -# the Database trait, not touch driver types directly. -# -------------------------------------------------------------------------- - -echo "--- Check 1: Direct database driver usage outside db layer ---" - -results=$(grep -rn 'tokio_postgres::\|libsql::' src/ \ - --include='*.rs' \ - | grep -v 'src/db/' \ - | grep -v 'src/workspace/repository.rs' \ - | grep -v 'src/error.rs' \ - | grep -v 'src/app.rs' \ - | grep -v 'src/testing.rs' \ - | grep -v 'src/cli/' \ - | grep -v 'src/setup/' \ - | grep -v 'src/main.rs' \ - | grep -v '^\s*//' \ - | grep -v '//.*tokio_postgres\|//.*libsql' \ - || true) - -if [ -n "$results" ]; then - echo "VIOLATION: Direct database driver usage found outside db layer:" - echo "$results" - echo - count=$(echo "$results" | wc -l | tr -d ' ') - echo "($count occurrence(s) -- these modules should use the Database trait)" - violations=$((violations + 1)) -else - echo "OK" -fi -echo - -# -------------------------------------------------------------------------- -# Check 2: .unwrap() / .expect() in production code (heuristic) -# -------------------------------------------------------------------------- -# We cannot perfectly distinguish test vs production code with grep alone -# (test modules span many lines). Instead we: -# 1. Exclude files that are entirely test infrastructure -# 2. Exclude lines that are clearly in test code (assert, #[test], etc.) -# 3. Report a per-file summary so reviewers can focus on the worst files -# -# This is a WARNING, not a hard violation. -# -------------------------------------------------------------------------- - -echo "--- Check 2: .unwrap() / .expect() / assert!() in production code ---" - -# Collect raw matches excluding obvious test-only files and lines. -# Also catches assert!(), assert_eq!(), assert_ne!() but NOT debug_assert variants. -raw_results=$(grep -rnE '\.(unwrap|expect)\(|[^_]assert(_eq|_ne)?!' src/ \ - --include='*.rs' \ - | grep -v 'src/main.rs' \ - | grep -v 'src/testing.rs' \ - | grep -v 'src/setup/' \ - | grep -Ev 'debug_assert|// safety:' \ - || true) - -if [ -n "$raw_results" ]; then - total=$(echo "$raw_results" | wc -l | tr -d ' ') - echo "WARNING: ~$total .unwrap()/.expect()/assert!() calls found in src/ (excluding main/testing/setup)." - echo "Many are in test modules; a per-file breakdown helps triage:" - echo - # Show per-file counts, sorted by count descending, top 15 - file_counts=$(echo "$raw_results" | cut -d: -f1 | sort | uniq -c | sort -rn) - echo "$file_counts" | head -15 - fc_total=$(echo "$file_counts" | wc -l | tr -d ' ') - if [ "$fc_total" -gt 15 ]; then - echo " ... and $((fc_total - 15)) more files" - fi - echo - echo "(This is a warning for gradual cleanup, not a blocking violation.)" - echo "(Many of these are inside #[cfg(test)] modules which is acceptable.)" -else - echo "OK" -fi -echo - -# -------------------------------------------------------------------------- -# Check 3: std::env::var reads outside config/bootstrap layers -# -------------------------------------------------------------------------- -# Sensitive values should come through Config or the secrets module. -# Direct std::env::var / env::var() reads are allowed in: -# - src/config/ (the config layer itself) -# - src/main.rs (entry point) -# - src/setup/ (onboarding wizard) -# - src/testing.rs (test infrastructure) -# - src/cli/ (CLI commands that read env for bootstrap) -# - src/bootstrap.rs (bootstrap logic) -# -------------------------------------------------------------------------- - -echo "--- Check 3: Direct env var reads outside config layer ---" - -results=$(grep -rn 'std::env::var\|env::var(' src/ \ - --include='*.rs' \ - | grep -v 'src/config/' \ - | grep -v 'src/main.rs' \ - | grep -v 'src/setup/' \ - | grep -v 'src/testing.rs' \ - | grep -v 'src/cli/' \ - | grep -v 'src/bootstrap.rs' \ - | grep -v '#\[cfg(test)\]' \ - | grep -v '#\[test\]' \ - | grep -v 'mod tests' \ - | grep -v 'fn test_' \ - | grep -v '//.*env::var' \ - || true) - -if [ -n "$results" ]; then - count=$(echo "$results" | wc -l | tr -d ' ') - echo "WARNING: Direct env var reads found outside config layer ($count occurrences):" - echo "$results" - echo - echo "(Review these -- secrets/config should come through Config or the secrets module)" -else - echo "OK" -fi -echo - -# -------------------------------------------------------------------------- -# Check 4: Test tier gating — external-service tests must use integration flag -# -------------------------------------------------------------------------- -# Files in tests/ that connect to PostgreSQL or use DATABASE_URL must be -# gated behind #![cfg(feature = "integration")]. This ensures `cargo test` -# (no flags) never requires external services. -# -# Heuristic: any test file referencing DATABASE_URL, connect(), PgPool, -# or tokio_postgres should have the cfg gate on the first few lines. -# -------------------------------------------------------------------------- - -echo "--- Check 4: Test tier gating for integration tests ---" - -tier_violations=() -for test_file in tests/*.rs; do - [ -f "$test_file" ] || continue - - # Check if the file actually connects to a database (imports DB types - # or calls pool/connect). Mere string references like "DATABASE_URL" - # in config tests don't count. - needs_gate=false - if grep -q 'PgPool\|tokio_postgres::\|create_pool\|\.connect(' "$test_file" 2>/dev/null; then - needs_gate=true - fi - - if [ "$needs_gate" = true ]; then - # Check first 5 lines for the cfg gate - if ! head -5 "$test_file" | grep -q 'cfg.*feature.*integration' 2>/dev/null; then - tier_violations+=(" $test_file: needs '#![cfg(feature = \"integration\")]'") - fi - fi -done - -if [ ${#tier_violations[@]} -gt 0 ]; then - echo "VIOLATION: Integration tests missing feature gate:" - printf '%s\n' "${tier_violations[@]}" - echo - echo "(Tests requiring external services must be gated behind the 'integration' feature)" - violations=$((violations + 1)) -else - echo "OK" -fi -echo - -# -------------------------------------------------------------------------- -# Check 5: No silent test-skip patterns (try_connect, is_available, etc.) -# -------------------------------------------------------------------------- -# Tests must fail loudly when prerequisites are missing, not silently skip. -# The correct approach is feature-flag gating (#![cfg(feature = "integration")]). -# Patterns like try_connect().is_none() { return; } hide broken tests. -# -------------------------------------------------------------------------- - -echo "--- Check 5: No silent test-skip patterns ---" - -skip_results=$(grep -rn 'try_connect\|is_available.*return\|is_none.*return\|is_err.*return.*//.*skip' tests/ \ - --include='*.rs' \ - || true) - -if [ -n "$skip_results" ]; then - echo "VIOLATION: Silent test-skip patterns found (use feature gates instead):" - echo "$skip_results" - echo - violations=$((violations + 1)) -else - echo "OK" -fi -echo - -# -------------------------------------------------------------------------- -# Check 6: LLM module isolation — no imports from other crate modules -# -------------------------------------------------------------------------- -# src/llm/ should only import from: -# - crate::llm (self-references) -# - external crates (no crate:: prefix) -# It must NOT import from crate::agent, crate::tools, crate::channels, -# crate::safety, crate::config, crate::bootstrap, crate::cli, crate::db, -# crate::workspace, crate::worker, crate::orchestrator, crate::skills, -# crate::hooks, crate::setup, crate::context, etc. -# -# Test-only imports (crate::testing) are excluded since they don't affect -# the runtime dependency graph and won't exist in the extracted crate. -# -------------------------------------------------------------------------- - -echo "--- Check 6: LLM module isolation ---" - -# Match any `crate::` reference (use-imports AND inline paths) that isn't -# crate::llm or crate::testing. Filter out comments. -# We strip inline comments (everything after //) with sed before checking, -# so a line like `real_code(crate::foo); // crate::llm` is still caught. -results=$(grep -rn 'crate::' src/llm/ \ - --include='*.rs' \ - | grep -v '^\s*//' \ - | sed 's|//.*||' \ - | grep 'crate::' \ - | grep -v 'crate::llm' \ - | grep -v 'crate::testing' \ - || true) - -if [ -n "$results" ]; then - count=$(echo "$results" | wc -l | tr -d ' ') - echo "WARNING: src/llm/ has $count reference(s) to modules outside crate::llm:" - echo "$results" - echo - echo "(These are pre-existing; fix them before extracting the crate.)" - echo "(New 'use crate::' imports are hard violations — see below.)" - echo - # Hard-fail only on new `use crate::` imports (easy to avoid in new code). - use_imports=$(echo "$results" | grep '^[^:]*:.*use crate::' || true) - if [ -n "$use_imports" ]; then - echo "HARD VIOLATION: new 'use crate::' imports in src/llm/:" - echo "$use_imports" - violations=$((violations + 1)) - fi -else - echo "OK" -fi -echo - -# -------------------------------------------------------------------------- -# Summary -# -------------------------------------------------------------------------- - -echo "=== Summary ===" -if [ "$violations" -gt 0 ]; then - echo "FAILED: $violations hard violation(s) found" - exit 1 -else - echo "PASSED: No hard violations found (review warnings above)" - exit 0 -fi diff --git a/scripts/ci/check-e2e-matrix-files.sh b/scripts/ci/check-e2e-matrix-files.sh deleted file mode 100755 index eb15c3b9cad..00000000000 --- a/scripts/ci/check-e2e-matrix-files.sh +++ /dev/null @@ -1,71 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -repo_root="$(git rev-parse --show-toplevel)" -workflow="${1:-.github/workflows/e2e.yml}" - -if [[ "${workflow}" != /* ]]; then - workflow="${repo_root}/${workflow}" -fi - -python3 - "${repo_root}" "${workflow}" <<'PY' -import json -import re -import sys -from pathlib import Path - -repo_root = Path(sys.argv[1]) -workflow = Path(sys.argv[2]) -workflow_text = workflow.read_text(encoding="utf-8") - -path_re = re.compile(r"tests/e2e/scenarios/[A-Za-z0-9_./-]+\.py") -paths = set(path_re.findall(workflow_text)) - - -def collect_paths(value): - if isinstance(value, str): - paths.update(path_re.findall(value)) - elif isinstance(value, list): - for item in value: - collect_paths(item) - elif isinstance(value, dict): - for item in value.values(): - collect_paths(item) - - -matrix_assignment_re = re.compile(r"(?m)^\s*([A-Z][A-Z0-9_]*)='(\[[^\n]*\])'\s*$") -parsed_matrix_count = 0 - -for match in matrix_assignment_re.finditer(workflow_text): - matrix_name = match.group(1) - matrix_json = match.group(2) - try: - matrix = json.loads(matrix_json) - except json.JSONDecodeError as exc: - print(f"{workflow}: invalid JSON in {matrix_name}: {exc}", file=sys.stderr) - sys.exit(2) - parsed_matrix_count += 1 - collect_paths(matrix) - -if parsed_matrix_count == 0: - print( - f"{workflow}: no inline JSON matrix assignments found; checked direct path references only", - file=sys.stderr, - ) - -if not paths: - print(f"{workflow}: no tests/e2e/scenarios/*.py references found", file=sys.stderr) - sys.exit(2) - -missing = sorted(path for path in paths if not (repo_root / path).is_file()) -if missing: - print( - "Missing E2E scenario files referenced by .github/workflows/e2e.yml:", - file=sys.stderr, - ) - for path in missing: - print(f" {path}", file=sys.stderr) - sys.exit(1) - -print(f"All referenced E2E scenario files exist ({len(paths)} checked).") -PY diff --git a/scripts/ci/composition-budget.toml b/scripts/ci/composition-budget.toml index ec785628dca..9a97ed8e6bb 100644 --- a/scripts/ci/composition-budget.toml +++ b/scripts/ci/composition-budget.toml @@ -66,8 +66,8 @@ tolerance_bp = 30 # Informational — the observed share when this file was last updated. Lets a # reviewer see the baseline and lets the gate emit a down-ratchet nudge. Not # consulted for the pass/fail decision. -observed_bp = 576 -observed_date = "2026-08-07" +observed_bp = 572 +observed_date = "2026-08-08" # --- Absolute mass (production LOC) --------------------------------------- # The BINDING mass bound. Same numerator as the share metric — production `.rs` @@ -153,6 +153,12 @@ observed_date = "2026-08-07" # with `bash scripts/ci/check-composition-budget.sh`. # Union re-measured 40432 -> 40747 on 2026-08-07 after merging #7157's delivery # refactor with #7214's sandbox profile and binding assembly. +# Union re-measured on the #7373 merge (2026-08-08): the gate-audit branch had +# independently re-equalized 40423 -> 40524 for the same drift class before +# #7157's chain (40692 -> 40432 -> 40747) landed; both chains fold into the +# merged-tree measure below (40804 on the #7373 merge), set to current, not padded. The recurrence class +# itself is repaired in this PR: the contracts size gate now carries upward +# working slack, and this file's loc_tolerance keeps doing that job here. # History: 40747 -> 40811 (#7157 acting-user scope helper), then two features # merged on top on 2026-08-08/09, each service-graph assembly only: # - web-push channel assembly (`assemble_web_push`: subscription-store @@ -173,6 +179,12 @@ observed_date = "2026-08-07" # composition growth consumed the prior tolerance before this branch merged. # Measured by the Fast deterministic checks gate on the GitHub merge commit and # recorded explicitly rather than weakening or bypassing the absolute bound. +# Union re-measured 41810 -> 41820 on 2026-08-12 (#7373 refresh merge of +# main): the audit branch's chain (40804 @ 2026-08-08) and main's chain +# (41810 @ #7471) fold; measured on the merged tree with this gate's +# --print. Set to current, not padded — re-equalized per this file's own +# re-capture instruction so the designed 150-line working window is fully +# live again (main's pin had drifted to 140 lines of effective headroom). # # Re-ratcheted DOWN 41810 -> 41527 on 2026-08-12 by #7365 (memory recall): the # memory-save guidance and its content pins moved out of composition into the @@ -222,6 +234,14 @@ loc_observed_date = "2026-08-12" # baseline without lowering the mass ratchet; future work must shrink again. # 2026-08-05 (program closure): nudge taken, 1122 -> 814 — locked at today's # observed count; the WS0 record (827) stays within the effective ceiling (829). +# 2026-08-07 (gate audit): re-equalized 814 -> 819 — five governed sites of +# merged main-side drift through the 15-site tolerance window since 08-05, +# leaving 10 sites of live headroom. Measured with the gate's --print; set to +# current, not padded; the WS0 record (827) stays within the effective +# ceiling (834). +# 2026-08-08 (#7373 merge): union re-measured 819 -> 826 — the folded #7157 +# delivery lane and siblings carry 7 more governed sites. Set to current; the +# WS0 record (827) stays within the effective ceiling (841). # 2026-08-10 PR #7474 (#7247 truthful connection context): 814 -> 816 for the # two injected context-provider ports (`ExtensionCredentialSetupService` + # channel-connection facade) wired into the communication-context assembly — @@ -229,6 +249,12 @@ loc_observed_date = "2026-08-12" # only wires). Observed on the batch branch: 831 (829 on main pre-batch + these # two); effective ceiling 816 + 15 tolerance = 831 — equal to the observed # count exactly, no slack. -arc_dyn_ceiling = 816 +# 2026-08-12 (#7373 refresh merge): union re-measured — the merged tree +# reports 831 governed sites, exactly the effective ceiling above, i.e. the +# gate was live at ZERO headroom (the recurrence class §3.2 of the gate +# audit names). Re-equalized 816 -> 831 per this file's re-capture +# instruction, restoring the designed 15-site window. The WS0 record (827) +# stays within the effective ceiling (846). +arc_dyn_ceiling = 831 arc_dyn_tolerance = 15 arc_dyn_observed = 831 diff --git a/scripts/ci/reborn_pr_test_plan.py b/scripts/ci/reborn_pr_test_plan.py index a8b33a70baf..98335c4105d 100644 --- a/scripts/ci/reborn_pr_test_plan.py +++ b/scripts/ci/reborn_pr_test_plan.py @@ -389,6 +389,15 @@ def _is_shipped_asset_markdown(path: str) -> bool: "scripts/render-architecture-video.sh", # * `pre-commit-safety.sh` is a local git hook, not a CI lane. "scripts/pre-commit-safety.sh", + # * `preflight-gates.sh` is the local pre-push gate gauntlet proposed by + # the 2026-08 gate audit (docs/internal/gate-audit-2026-08.md §4.3); + # referenced by no workflow, so no lane can be selected for it. + "scripts/preflight-gates.sh", + # * `check-boundaries.sh` was DELETED by the same audit (measured broken + # on a clean tree, run by nothing). The entry stays so the deletion + # diff — and any revert — classifies instead of tripping the + # fail-closed arm; the audit's own PR was the first to hit it. + "scripts/check-boundaries.sh", # * `test-mutation-audit.sh` is the self-test for the mutation audit, # driven by its own lane rather than by a crate/integration selection. "scripts/test-mutation-audit.sh", diff --git a/scripts/preflight-gates.sh b/scripts/preflight-gates.sh new file mode 100755 index 00000000000..f7e487be8ba --- /dev/null +++ b/scripts/preflight-gates.sh @@ -0,0 +1,121 @@ +#!/usr/bin/env bash +# PROPOSED (gate audit 2026-08, docs/internal/gate-audit-2026-08.md §4): the +# cheap deterministic pre-push gauntlet. NOT wired into any hook or workflow — +# run it by hand before pushing: +# +# bash scripts/preflight-gates.sh +# +# Scope: exactly the gate classes that produced PR #7157's six red CI runs — +# the script gates (composition budget, panic baseline, target tree, guidance, +# include_str!, docs boundary), the architecture suite (contracts size +# ceilings and every other reborn_* gate), and the module-charter tests of +# crates your diff touches. It deliberately runs NO ordinary unit/integration +# tests — `.githooks/pre-push` (quality_gate.sh) remains the full CI-parity +# gate; this script is the five-minute version that catches the +# deterministic-gate class. +# +# Measured on a warm target dir (2026-08-07, M-series mac): script layer ~10s, +# architecture suite ~4.2min (cargo) — nextest is used when installed and cuts +# the suite further by running the 37 binaries in parallel. Charter tests are +# compile-dominated (~80s for ironclaw_webui warm, near-zero when unchanged). +# Cold-target first runs pay the usual build cost on top. +# +# Every gate runs even after an earlier one fails (the point is ONE round-trip +# reporting EVERYTHING — CI's bucket shape stops at the first failing binary); +# the script exits non-zero if any gate failed. + +set -uo pipefail + +# Deliberately no `-e`: the gate-running section must keep going after a +# failing gate (one round-trip, full report). The setup below therefore +# checks its own plumbing explicitly — a setup failure must never let the +# script report OK over work it didn't do. +REPO_ROOT="$(git rev-parse --show-toplevel)" || { + echo "preflight-gates: not inside a git repository" >&2 + exit 2 +} +cd "${REPO_ROOT}" || exit 2 + +failures=() + +run_gate() { + local label="$1" + shift + echo "==> ${label}" + if ! "$@"; then + failures+=("${label}") + echo " FAILED: ${label}" + fi +} + +# --- Layer 1: script gates (seconds each) ---------------------------------- +run_gate "fmt check" cargo fmt --all -- --check +run_gate "composition mass budget" bash scripts/ci/check-composition-budget.sh +run_gate "panic baseline (full closure scan)" \ + python3 scripts/check_no_panics.py --reborn-baseline +run_gate "target tree vs documented tree" python3 scripts/ci/check-target-tree.py +run_gate "guidance path references" python3 scripts/ci/check-guidance.py +run_gate "include_str! + Docker COPY coverage" bash scripts/ci/check-include-str-paths.sh +run_gate "hermetic env mutation (delta)" bash scripts/ci/check-hermetic-env.sh +run_gate "docs publication boundary" python3 scripts/ci/docs_publication_boundary.py + +# --- Layer 2: the architecture gate suite ---------------------------------- +if command -v cargo-nextest >/dev/null 2>&1; then + run_gate "architecture suite (nextest)" \ + cargo nextest run -p ironclaw_architecture_tests --no-fail-fast +else + run_gate "architecture suite (cargo; install cargo-nextest to parallelize)" \ + cargo test -p ironclaw_architecture_tests --no-fail-fast +fi + +# --- Layer 3: module-charter tests for crates the diff touches ------------- +# Charter maps live inside their crates, so only changed crates pay the +# compile. Diff base mirrors the pre-push hook's default (origin/main). +# Discovery fails CLOSED: when the base is missing or the diff plumbing +# errors, the fallback widens to all five charters — a broken setup may cost +# compile time, never a silent skip. +all_charter_crates="crates/product/ironclaw_webui crates/product/ironclaw_assistant \ + crates/domains/ironclaw_llm crates/domains/ironclaw_auth \ + crates/lanes/ironclaw_mcp" +base_ref="${IRONCLAW_PREFLIGHT_BASE:-origin/main}" +if git rev-parse --verify --quiet "${base_ref}^{commit}" >/dev/null; then + if merge_base="$(git merge-base HEAD "${base_ref}")" \ + && changed="$(git diff --name-only "${merge_base}...HEAD")"; then + : + else + echo "==> charter tests: cannot diff against ${base_ref}; running all five" + changed="${all_charter_crates}" + fi +else + echo "==> charter tests: base ${base_ref} not found; running all five" + changed="${all_charter_crates}" +fi + +charter() { + local crate_dir="$1" package="$2" test_name="$3" + shift 3 + if grep -q "${crate_dir}" <<<"${changed}"; then + run_gate "charter: ${package}" \ + env "$@" cargo test -p "${package}" --test "${test_name}" + fi +} + +charter "crates/product/ironclaw_webui" ironclaw_webui handlers_module_charter \ + SKIP_FRONTEND_BUILD=1 +charter "crates/product/ironclaw_assistant" ironclaw_assistant \ + reborn_services_module_charter +charter "crates/domains/ironclaw_llm" ironclaw_llm module_charter +charter "crates/domains/ironclaw_auth" ironclaw_auth module_charter +charter "crates/lanes/ironclaw_mcp" ironclaw_mcp module_charter + +# --- Verdict ---------------------------------------------------------------- +echo +if [ "${#failures[@]}" -eq 0 ]; then + echo "preflight-gates: OK — every deterministic gate green" + exit 0 +fi +echo "preflight-gates: ${#failures[@]} gate(s) FAILED:" +for f in "${failures[@]}"; do + echo " - ${f}" +done +exit 1