Skip to content

refactor(capco,engine): PR 4b-D.2 β€” hot-path flip (Engine::project + JoinSemilattice::join)#527

Merged
bashandbone merged 17 commits into
stagingfrom
refactor-006-pr-4b-d-2-hotpath-flip
May 18, 2026
Merged

refactor(capco,engine): PR 4b-D.2 β€” hot-path flip (Engine::project + JoinSemilattice::join)#527
bashandbone merged 17 commits into
stagingfrom
refactor-006-pr-4b-d-2-hotpath-flip

Conversation

@bashandbone
Copy link
Copy Markdown
Collaborator

@bashandbone bashandbone commented May 18, 2026

Summary

The hot-path flip of the marque engine refactor (spec 006). Three production paths move from PageContext-aggregation to lattice-aggregation + closure + page_rewrites:

  1. JoinSemilattice::join for CapcoMarking (crates/capco/src/scheme/marking.rs)
  2. MarkingScheme::project for CapcoScheme (crates/capco/src/scheme/marking_scheme_impl.rs)
  3. Engine::lint and dispatch_page_finalization (crates/engine/src/engine.rs)

Pipeline ordering per lattice-design plan Β§4.7.4: parse β†’ join β†’ closure β†’ page_rewrites β†’ render.

PageContext stays alive feeding RuleContext::page_context for banner-validation rules; deletion is the separate PR 4b-E. Consumer migration of ctx.page_context.expected_* β†’ ctx.page_marking is the separate PR 4b-D.3.

This PR also activates CapcoScheme::closure() (from PR #517 / 4b-D.1) on the production hot path, with the closure-rewrite-application sentinel from lattice-design Β§3 (e.1).

Engine-crate touch authorization (Constitution VII Β§IV)

This PR edits crates/engine/src/engine.rs despite being scheme-adoption work. Authorization derives from the within-006 precedent:

The engine edits here (commits 4, 7, 8) are the same bugfix class: rerouting two call sites (page_marking_arc.get_or_insert_with and dispatch_page_finalization's mirror) from page_context.project() to project_page_marking(&self.scheme, &page_context). No new engine behavior, no new API surface on Engine, no pass-structure changes, no audit-log changes.

Commits

# Hash Subject LOC
1 eac128ee feat(ism): add ProjectedMarking::from_canonical constructor +222 / 0
2 54c57b92 feat(capco): flip JoinSemilattice::join to lattice path +31 / -41
3 6d7eccfe feat(capco): wire CapcoScheme::project to lattice + closure + page_rewrites +552 / -22
4 75f23dc3 feat(engine): drive page_marking through scheme.project on the hot path +62 / -20
5 c906d92f test(capco,engine): Pattern D lattice fixtures + parity retargets + doc sync +449 / -93
6 671aa560 perf(capco): short-circuit CapcoScheme::closure on empty cone triggers +221 / 0
7 6cdaf132 perf(capco,engine): engine-side fast-path for CapcoScheme::project +438 / -153
8 5ea386ba perf(capco,engine): borrow PageContext through the projection pipeline +117 / -27
9 58a46c67 perf(bench): refresh lint_10kb baseline to PR 4b-D.2 post-perf state (superseded by R1 #3 β€” see commit 12) +4 / -4
10 d33e14e1 docs,refactor: address reviewer feedback (4b-D.2) +167 / -40
11 ef3d25fa refactor(scheme,capco): drop impl JoinSemilattice for CapcoMarking; relax MarkingScheme bound (Copilot R1 #1 / D24) +94 / -116
12 b8644eba fix,docs: address Copilot R1 items 2-10 (incl. baseline revert to GHA 912/913/914, new capco/noforn-clears-display-only-to PageRewrite + CAT_DISPLAY_ONLY_TO) +402 / -86
13 d57d1e51 fix(capco): complete NOFORN supersession at apply_fact_add injection site (Copilot R2 suppressed #2 + inverse case) +TBD
14 pending docs,fix: address Copilot R2 nits + G13 sentinel content-ignorance +TBD

Net: ~+TBD across ~20 files.

Decisions captured in specs/006-engine-rule-refactor/decisions.md

  • D22 β€” NOFORN-dominates supersession at FactAdd injection site (amended post-R2): apply_fact_add maintains Β§H.8 p145 across all three axes when NOFORN is inserted into CAT_DISSEM:
    1. Token-axis eviction (commit 3): DissemSet::with_noforn_injected strips dominated Rel / Relido / Displayonly / Eyes tokens from dissem_us.
    2. Country-axis clearing (commit 13): clears attrs.rel_to and attrs.display_only_to country lists so the renderer cannot emit NOFORN ... REL TO USA, GBR.
    3. Inverse-case rejection (commit 13): FactAdd of Rel / Relido / Displayonly / Eyes onto a marking already carrying NOFORN returns ApplyIntentError::IntentInapplicable, preventing the renderer from ever seeing the Β§H.8 p145 violation regardless of FactAdd order.
      All three axes hold at the apply_fact_add injection site; the PageRewrite layer (capco/noforn-clears-{rel-to,fdr-family,display-only-to}) survives as defense-in-depth for any future emission path that bypasses apply_intent.
  • D23 β€” Closure-rewrite-application sentinel placement: scheme-layer (inside CapcoScheme::project between join_via_lattice and closure()); #[cfg(debug_assertions)] snapshot+assert pattern; sibling to the engine's PageFinalization rule-dispatch sentinel. Catches any future refactor that mutates per-portion CanonicalAttrs during closure (violates lattice-design Β§3 (e.1) read-only-attrs invariant). G13 content-ignorance update (commit 14): the snapshot-vs-current divergence panic emits portion counts only, never CanonicalAttrs content β€” mirrors the check_portions_unchanged count-only pattern at engine.rs:4540-4574.
  • D24 β€” Drop impl JoinSemilattice for CapcoMarking; relax MarkingScheme::Marking bound (commit 11, Copilot R1 feat: Phase 1 setup β€” marque-ism crate, dev scaffolding, and review fixesΒ #1): cross-axis CAPCO folds are projections (lossy, multi-axis-aware), not lattice operations (idempotent, single-axis). Keeping a meaningless .join on CapcoMarking would invite misuse. The bound on MarkingScheme::Marking relaxes from JoinSemilattice to Sized + Clone + Send + Sync; per-axis lattices (DissemSet, SciSet, RelToBlock, etc.) still implement JoinSemilattice. DiffInput<M> bound dropped in lock-step.

Performance

Bench-measured tier ladder on WSL2 dev worktree:

Stage lint_10kb median Ξ”
Pre-flip baseline (GHA ubuntu-latest, PR #498) 913 Β΅s β€”
Unmitigated post-flip (WSL2) 1514 Β΅s +600 Β΅s (+65%)
After commit 6 (closure short-circuit) 1510 Β΅s -4 Β΅s (trigger-free bench)
After commit 7 (engine fast-path) 1195 Β΅s -315 Β΅s
After commit 8 (PageContext borrow) 1033 Β΅s -162 Β΅s
Final WSL2 1033 Β΅s +119 Β΅s (+13%) over pre-flip
SC-001 16 ms ceiling β€” unaffected

Lands in the +20% acceptable tier authorized by the project's perf-baseline policy. GHA ubuntu-latest typically reads 5-10% faster than WSL2; expected GHA capture is in the +10% noise band of the original 913Β΅s baseline.

Baseline calibration (commit 9 + commit 12 revert): the perf-mitigation work captured 1033Β΅s on WSL2. Commit 9 (58a46c67) initially refreshed benches/baseline.json::lint_10kb to {1030, 1033, 1036}; Copilot R1 #3 flagged that the CI regression gate consumes the top-level fields and must be calibrated to the CI hardware (GHA ubuntu-latest), not WSL2 (5-10% slower). Commit 12 (b8644eba) reverted the top-level fields back to the pre-PR GHA baseline {912, 913, 914}Β΅s (CI threshold upper_ci_us * 1.10 β‰ˆ 1005Β΅s); the WSL2 capture is preserved in the lint_10kb._wsl2_dev_capture sub-object for cross-host provenance. The first GHA ubuntu-latest re-capture of this branch via scripts/capture-baselines.sh after merge produces the post-flip authoritative numbers.

Parity gate

crates/capco/tests/page_context_lattice_parity.rs: 74/74 tests pass.

  • 71 byte-identity fixtures (PageContext ↔ lattice)
  • 2 fixtures retargeted from _pending_pr_4b_d to post-flip names:
    • fouo_classified_scheme_project_strips_fouo (Β§H.8 p134 FOUO Precedence β€” capco/classification-evicts-fouo page rewrite fires)
    • aea_ucni_classified_scheme_project_strips_and_promotes_noforn (Β§H.6 p116 + p118 DOD/DOE UCNI β€” capco/dod-ucni-evicted-by-classified + capco/dod-ucni-promotes-noforn-when-classified rewrites fire)
  • 3 active documented divergences (classification-axis only, renderer-territory deferred to PR 5+):
    • pure_nato_lattice_vs_pagecontext_diverges (Β§H.7 pp123-125 β€” solely-NATO pages preserve Nato(_) on the lattice path; PageContext flattens to Us(_))
    • joint_unanimous_two_portions (Β§H.3 p56 β€” pure-JOINT pages preserve Joint(_))
    • joint_single_portion_no_us (Β§H.3 p56 β€” solo-JOINT portions preserve Joint(_))
  • relido_plus_nf_noforn_dominates_parity removed from the divergence inventory (already converged in staging; asserts byte-identity)

Direction inversion: post-flip the parity gate's source of truth flips. project_via_scheme (production) now agrees with project_via_lattice; project_via_page_context becomes the divergent side until PR 4b-D.3 / 4b-E migrate consumers and delete PageContext.

Closure runtime activation

CapcoScheme::closure() (landed in PR #517 / 4b-D.1, test-reachable only) is now wired on the production hot path. All 8 closure rules fire through scheme.project(Scope::Page, ...):

  • 7 CLOSURE_NOFORN_* rules (SAR / AEA_RD / UCNI / FGI / ORCON / RSEN_IMCON_DSEN / NONICCONTROLS) per Β§B.3 Table 2 p21
  • 1 CLOSURE_REL_TO_USA_NATO rule per Β§H.7 p127 + Β§G.2 Table 5 p40 (example-derived; D20 layer-separation: Severity::Info silent at lattice layer; S007 at Severity::Suggest visible at text layer)

FDR_DOMINATORS suppressor = {NOFORN, RELIDO, DISPLAY_ONLY, AnyInCategory(CAT_REL_TO), EYES} matches Β§B.3.a p19 canonical FD&R enumeration. EYES inclusion correct per Β§H.8 p157.

Tests

Test surface Pass
Workspace (cargo +stable test --workspace) all green, 0 failures
crates/capco/tests/page_context_lattice_parity.rs 74/74
crates/capco/tests/closure_runtime.rs 22/22 (includes 4 new short-circuit behavioral tests)
crates/capco/tests/category_action_intent.rs + 6 new tests (R2 commit 13): NOFORN clears rel_to/display_only_to country lists; inverse-case rejection (RELIDO/DISPLAY ONLY); idempotence + double-NOFORN at supersession fixed point
crates/engine/tests/closure_hotpath.rs (new) 13/13 (one fixture retargeted via UCNI scenario per R2 #2)
crates/engine/tests/lattice_corpus.rs (new) 4/4
crates/ism/src/projected.rs (from_canonical unit tests) 9/9
cargo +stable clippy --workspace --tests -- -D warnings clean

Constitution VIII citation verification

The CAPCO dissem reviewer re-verified 10 Β§-citations verbatim against crates/capco/docs/CAPCO-2016.md:

  • Β§H.8 p145 (NOFORN-dominates) βœ“
  • Β§B.3 Table 2 p21 (FD&R caveated β†’ NOFORN) βœ“
  • Β§H.6 p116 + p118 (DOD/DOE UCNI Precedence) βœ“
  • Β§H.8 p134 (FOUO Precedence) βœ“
  • Β§H.7 p127 (NATO REL TO Notional Example Page 2) βœ“
  • Β§H.8 pp155-156 (RELIDO observed-unanimity) βœ“
  • Β§H.3 p56 (JOINT banner form) βœ“
  • Β§D.2 Table 3 rows 1-2 (supersession table) βœ“
  • Β§G.2 Table 5 p40 (NATO read-in) βœ“
  • Β§H.8 p157 (EYES ONLY: NSA-only) βœ“

No fabrications, no drift, no propagation defects.

CAPCO-CONTEXT.md Β§3 sync

crates/capco/CAPCO-CONTEXT.md Β§3 updated:

  • Active-divergence count: 3 (was stale at 4-6 from earlier compaction summaries)
  • Two retargeted fixture names reflect post-flip behavior
  • relido_plus_nf_noforn_dominates_parity removed (already converged)
  • Direction inversion documented (scheme/lattice agree; PageContext is the divergent side)
  • JoinSemilattice paragraph rewritten post-D24 (Copilot R1) β€” cross-axis CAPCO folds are projections, not lattice ops

Out of scope (tracked separately)

  • Test helper consolidation: classified_us, project_page, rel_to_contains, dissem_contains duplicated across closure_hotpath.rs + lattice_corpus.rs; deferred to a follow-up test-only PR that adds crates/engine/tests/common.rs
  • CLOSURE_NOFORN_UCNI DoD coverage gap: TOK_DCNI not in trigger list; pre-existing per issue Expand Vocabulary sentinel set + add NNPI + bare-form rewrite fixers (CNWDI / SI-NK / SI-EU)Β #407. The Β§H.6 p116 NOFORN-promotion semantic IS satisfied on the production path via the capco/dod-ucni-promotes-noforn-when-classified page rewrite (PR 4b-C). Closure-layer coverage tracked separately.
  • Β§H.7 pp123-125 citation precision: pre-existing from PR 4b-B; renderer-territory deferred to PR 5+
  • page_context_to_attrs retirement: lone production consumer retired with this PR; function survives behind #[allow(dead_code)] pending PR 4b-E PageContext deletion

PR 4b-D sequence

PR Scope Status
4b-D.1 (#517) CapcoScheme::closure() Kleene-fixpoint override (test-reachable) Merged
4b-D.2 (this PR) Hot-path flip + closure runtime activation + 5 perf commits + 2 Copilot review rounds This PR
4b-D.3 Migrate ctx.page_context.expected_* consumers β†’ ctx.page_marking Pending
4b-E PageContext deletion cleanup Pending

Test plan

  • cargo +stable build --workspace clean
  • cargo +stable clippy --workspace --tests -- -D warnings clean
  • cargo +stable fmt --all --check clean
  • cargo +stable test --workspace all green (0 failures)
  • cargo +stable test -p marque-capco --test page_context_lattice_parity 74/74
  • cargo +stable test -p marque-capco --test category_action_intent includes 6 new R2-coverage tests
  • cargo +stable test -p marque-engine --test closure_hotpath 13/13 (UCNI fixture retargeted per R2 Add Claude Code GitHub WorkflowΒ #2)
  • cargo +stable test -p marque-engine --test lattice_corpus 4/4
  • cargo bench --bench lint_latency -- lint_10kb median 1033Β΅s (WSL2) within +20% acceptable tier; top-level baseline reverted to GHA {912, 913, 914} (CI threshold ~1005Β΅s)
  • CI ubuntu-latest re-capture to confirm GHA bench number lands in +10% band of original 913Β΅s baseline
  • Three-reviewer pre-flight pass (rust-reviewer / general code-reviewer / capco-dissem-validator): all APPROVE WITH NITS β€” 0 CRITICAL, 0 HIGH; all MEDIUMs addressed in commit 10 / 11 / 12 / 13 / 14
  • 10 Β§-citations re-verified verbatim against CAPCO-2016.md
  • Copilot R1 (10 items) addressed in commits 11 + 12
  • Copilot R2 (15 items: 13 inline + 2 suppressed) addressed in commits 13 (correctness 1-3) + 14 (docs 4-13)

Type bridge for PR 4b-D hot-path flip. CapcoScheme::project returns
CapcoMarking (wrapping CanonicalAttrs); the engine consumes
ProjectedMarking through RuleContext::page_marking. This constructor
projects the lattice-path output into the engine-facing shape.

The constructor lives in marque-ism because ProjectedMarking is
#[non_exhaustive] β€” its constructor MUST live in the type's home crate
so cross-crate callers cannot bypass field-addition migrations
(Constitution Principle VII). Both CapcoScheme::project (in
marque-capco) and Engine::lint (in marque-engine) will route through
this single source of truth.

Field mapping is verbatim per-axis from CanonicalAttrs; scope is set
to Scope::Page; provenance is the default (per-portion span
attribution lands when the projection pipeline grows a
contribution-tracking layer, out of scope for 4b-D). CAB-only fields
on CanonicalAttrs (classified_by, derived_from, declass_exemption,
token_spans) are intentionally absent per the type-level "page
aggregate, not a CAB" contract.

CanonicalAttrs::sar_markings is already Option<SarMarking> (singular)
on staging, so the bridge is a direct field move β€” no cardinality
debug_assert needed.

No behavioral change in this commit β€” the bridge is unused until
commit 3 wires CapcoScheme::project to drive through it. 9 unit
tests cover empty round-trip, every supported classification variant
(US / NATO / Joint), SAR singleton preservation, FGI marker
preservation, dissem / non_ic_dissem / rel_to / sci_controls
preservation, declassify_on preservation, and silent drop of CAB-only
fields.

PR 4b-D commit 1/5.
PR 4b-D.2 commit 2/5. CapcoMarking's JoinSemilattice::join impl
delegates to join_via_lattice (the PR 4b-B per-axis lattice
composer) instead of PageContext::add_portion +
page_context_to_attrs. The lattice path is the post-PR-4b-B
authoritative aggregation surface.

The parity gate at crates/capco/tests/page_context_lattice_parity.rs
already enforces byte-identity (or documented Β§-cited divergence)
between the two paths across 74 fixtures. The flip exercises the
same code join_via_lattice already runs; the parity-gate
project_via_lattice helper hits this exact function indirectly
through the fold and the helper continues to pass.

No recursion concern: join_via_lattice's per-axis composition calls
.join on SarSet / FgiSet (per-axis lattice types), never on
CapcoMarking itself.

MeetSemilattice keeps its narrow PageContext-free shape β€” widening
it is independent work outside PR 4b-D.2 scope.

Module-level Phase-B status note updated to reflect the
post-PR-4b-D.2 state (the prior "STILL DELEGATES TO PageContext"
language is replaced with the lattice-path-authoritative framing).
…writes

PR 4b-D.2 commit 3/5. Three changes land atomically:

1. CapcoScheme::project(Scope::Page|Document|Diff) now drives the
   join_via_lattice path β†’ closure operator β†’ declarative
   PageRewrite catalog. Pipeline ordering per
   docs/plans/2026-05-01-lattice-design.md Β§4.7.4:

       parse β†’ join (lattice) β†’ Cl_supp (closure)
                              β†’ PageRewrites β†’ render

   The closure operator and PageRewrites are both monotone;
   PageRewrites operate on the closure's fixed point.

2. Closure-rewrite-application sentinel (decisions.md D23): a
   #[cfg(debug_assertions)]-gated debug_assert verifies the closure
   operator did not mutate the per-portion CanonicalAttrs slice,
   per lattice-design Β§3 (e.1) read-only-attrs invariant. Sibling
   sentinel to the PageFinalization rule-dispatch sentinel in
   engine.rs (PR #490). Ships in cargo test + unoptimized builds;
   zero cost in --release.

3. apply_fact_add NOFORN supersession (decisions.md D22): when
   FactAdd injects NOFORN into CAT_DISSEM, the Β§H.8 p145
   supersession overlay applies at the injection site via
   DissemSet::with_noforn_injected. Pre-fix the path appended Nf
   to dissem_us without re-applying overlays, leaving {Nf,
   Displayonly} or {Nf, Relido} in the bag β€” invalid per Β§H.8 p145.
   Post-fix the injection is correct by construction, idempotent
   under re-insertion, and works equally for closure-driven and
   rule-driven FactAdd paths.

Authority (re-verified 2026-05-17 against crates/capco/docs/
CAPCO-2016.md):
- Β§H.8 p145 (NOFORN: "Cannot be used with REL TO, RELIDO, EYES
  ONLY, or DISPLAY ONLY")
- Β§D.2 Table 3 rows 1-2 (NOFORN dominates FD&R controls)
- Β§H.8 p157 (EYES ONLY: NSA-only marking, parser preserves
  DissemControl::Eyes through lint; supersession table covers it)
- Β§H.7 p127 + Β§G.2 Table 5 p40 (closure NATO row)
- Β§H.8 p163 + Β§D.2 Table 3 rows 25-27 (DISPLAY ONLY axis roll-up
  via tmp_ctx.expected_display_only() β€” parity-gate helper
  updated to match)

13 closure_hotpath integration tests at
crates/engine/tests/closure_hotpath.rs:
- 7 CLOSURE_NOFORN_* rows (SAR, AEA RD, UCNI, FGI, ORCON, RSEN,
  non-IC LIMDIS) firing through the production page projection
- CLOSURE_REL_TO_USA_NATO row firing on bare NATO classification
- Idempotence: classified+ORCON and bare-NATO-at-closure-layer
- Monotonicity sanity on extending fact set
- apply_fact_add NOFORN supersession: strips DISPLAY ONLY at
  injection time via DissemSet::with_noforn_injected
- apply_fact_add NOFORN idempotence under re-injection

DISPLAY ONLY axis wired into the lattice path's join output
mirrors page_context_to_attrs' existing semantic β€” scheme_equivalence's
project_banner_display_only_intersection_matches_pagecontext pins
the per-portion intersection roll-up that the new path now drives.
The parity-gate helper (project_via_page_context) is updated to
match so the 74-fixture byte-identity matrix continues to enforce
both paths producing the same output.

page_context_to_attrs survives behind #[allow(dead_code)] until
PR 4b-E retires the PageContext aggregator entirely; the function
is still referenced by parity-gate prose and unused by production
code.

Authorization: engine-crate touch under Constitution VII Β§IV
within-006 precedent (PR 4b-B Commit 2 / Β§7.B; PR 4b-C bugfix-class
deletions). This PR touches marque-capco only; the engine.rs flip
lands in commit 4.
PR 4b-D.2 commit 4/5. Engine::lint and dispatch_page_finalization now
build the page-marking projection via `scheme.project(Scope::Page, ...)`
instead of `PageContext::project()`. The new `project_page_marking`
helper centralizes the per-portion CapcoMarking conversion and the
`ProjectedMarking::from_canonical` bridge (commit 1) so both call
sites share one source of truth.

Authorization for engine-crate touch: Constitution VII Β§IV
within-006 precedent established by PR 4b-B (PR 4b-B commit 2 / Β§7.B)
and PR 4b-C (bugfix-class deletions in marque-ism). The flip is the
single substantive engine-crate edit; PageContext stays alive as the
transitional page-state accumulator and deletes in PR 4b-E alongside
its supporting machinery.

Pipeline change: pre-flip, `PageContext::project()` produced a
ProjectedMarking via `expected_*` accessors that did NOT run closure
or PageRewrites. Post-flip, the lattice + closure + PageRewrite
pipeline drives the per-page roll-up, so banner-validation rules
that read `ctx.page_marking` see the full Β§B.3 Table 2 closure cone
(NOFORN implications, NATO REL TO injection) and every declarative
PageRewrite (Pattern-B/C strip rows + the seven NOFORN-implication
rows). The 74-fixture parity gate at
`crates/capco/tests/page_context_lattice_parity.rs` enforces both
paths agree on the documented divergence set.

PageContext stays alive: add_portion calls and the per-portion
candidate-loop fields are untouched. RuleContext continues to expose
both ctx.page_context (the PageContext aggregator) and
ctx.page_marking (now derived from scheme.project). PR 4b-E retires
PageContext entirely; this PR's scope is only the projection driver.

Sibling sentinel relocation: the doc-block at the
PageFinalization-dispatch sentinel is updated to describe the
scheme-side sentinel that landed in commit 3
(CapcoScheme::project's read-only-attrs assertion). Both sentinels
fire under #[cfg(debug_assertions)] and together pin the Β§3 (e.1)
read-only-attrs invariant across the engine's two consumer surfaces.

Bench (lint_10kb, ubuntu-latest equivalent on dev WSL2 worktree):
the closure + PageRewrite pipeline now runs once per portion-bounded
page-marking cache miss (~20 times in the 10KB bench input). The
new cost surfaces as a substantive bench regression that exceeds
the +10% noise-band threshold; this is the expected cost of the
flip and the user-approved bench-baseline-staleness pattern routes
the routine refresh through PR 5+. Detailed measurement and
mitigation discussion lives in the PR body.
…oc sync

PR 4b-D.2 commit 5/5. Three pieces of cleanup work landing
atomically:

1. Pattern D corpus coverage at
   crates/engine/tests/lattice_corpus.rs (4 tests + 4 in
   closure_hotpath.rs from commit 3 = 7 Pattern-D scenarios total,
   each carrying its Β§-citation):

   - closure_nato_rel_to_solely_nato (Β§H.7 p127 + Β§G.2 Table 5 p40
     + Β§H.7 pp123-125): solely-NATO page; closure injects USA/NATO
     silently; classification preserved as Nato(_).
   - closure_nato_rel_to_us_plus_nato (Β§H.7 pp123-125 + Β§H.7 p127):
     US+NATO mixed; classification flattens to Us(_) via reciprocal
     raise; closure does NOT inject (TOK_NATO_CLASS absent after
     flatten); NATO survives on FGI axis for S007 to surface.
   - closure_relido_unanimity_all_portions (Β§H.8 pp155-156): unanimous
     RELIDO survives banner roll-up.
   - closure_relido_unanimity_drops_on_disagreement (Β§H.8 pp155-156):
     non-unanimous RELIDO dropped at roll-up.

   The four NOFORN-implication scenarios (SAR / AEA RD / UCNI / FGI /
   ORCON) live in closure_hotpath.rs from commit 3. Together the seven
   tests cover the Pattern D fixture set the PR brief called for.

   The brief's `input.json` + `expected.ndjson` file format was
   considered but rejected: CanonicalAttrs does not derive
   serde::Serialize and adding it would be an out-of-scope cross-crate
   type-system change (Constitution VII). Inline-typed Rust tests
   capture the same Pattern-D coverage with stronger type-checking
   and zero risk of fixture-file format drift; the rationale lives in
   the lattice_corpus.rs module doc-comment.

2. Parity-gate fixture retargets in
   crates/capco/tests/page_context_lattice_parity.rs (PR 4b-D.2):

   - `fouo_classified_pagecontext_and_lattice_both_keep_fouo_pending_pr_4b_d`
     renamed to `fouo_classified_scheme_project_strips_fouo`. Now
     asserts the three-way comparison: per-axis helpers keep FOUO;
     `project_via_scheme` strips it through
     `capco/classification-evicts-fouo` + `capco/fouo-evicted-by-classified`
     PageRewrites (Β§H.8 p134).
   - `aea_ucni_classified_pagecontext_and_lattice_both_keep_ucni_pending_pr_4b_d`
     renamed to `aea_ucni_classified_scheme_project_strips_and_promotes_noforn`.
     Asserts per-axis helpers keep UCNI; `project_via_scheme` strips
     UCNI AND promotes NOFORN via
     `capco/dod-ucni-evicted-by-classified` + `capco/dod-ucni-promotes-noforn-when-classified`
     (Β§H.6 p116).
   - The three active classification-axis divergences
     (`pure_nato_lattice_vs_pagecontext_diverges`,
     `joint_unanimous_two_portions`, `joint_single_portion_no_us`)
     gain a third comparison column: `project_via_scheme` agrees
     with the lattice path on all three (both go through
     join_via_lattice), so the disagreement is now scheme/lattice
     vs PageContext.

3. CAPCO-CONTEXT.md Β§3 sync: removed the stale
   `relido_plus_nf_noforn_dominates_documented_divergence` row
   (converged in staging pre-PR-4b-D.2); renamed the two
   `_pending_pr_4b_d` rows; updated the active divergence count
   from 4 (or 6) to 3 (NATO classification + 2 JOINT cases β€”
   all classification-axis, all renderer-territory pending PR 5+);
   documented the post-flip parity-gate direction inversion
   (scheme/lattice agree; PageContext is the divergent side until
   PR 4b-E retires it). Re-verified all Β§-citations against
   `crates/capco/docs/CAPCO-2016.md` at authoring time per
   Constitution VIII.

4. decisions.md: D22 (NOFORN-supersession at FactAdd injection
   site) and D23 (closure-rewrite-application sentinel placement
   inside CapcoScheme::project) recorded as PR 4b-D.2 decisions.

Parity gate count after retargets: still 74 #[test] fixtures, all
green. The three active divergences are now documented as
classification-axis-only renderer-territory cases pending PR 5+
(no longer "post-PR-4b-D-pending" β€” PR 4b-D.2 IS the flip; the
divergences are inherent to the JointSet / Nato-class variant
preservation choice and won't close until the renderer trait
surface lands).
PR 4b-D.2 commit 6/N. Implements the architect's R-1 mitigation
referenced in the PR brief: skip the snapshot-and-fixpoint loop in
`CapcoScheme::closure` when no catalog rule's trigger fires on the
input marking. The closure is a guaranteed no-op in that case
(should_fire = trigger_fires && !is_suppressed; if trigger_fires is
false for every rule, no rule contributes a fact).

Adds `CapcoScheme::any_closure_trigger_fires` as a `pub(crate)`
inherent method: iterates the catalog and OR's `rule.trigger_fires`.
The closure entry path is gated on this; cost is ≀24 satisfies
calls per projection (8 rules Γ— ≀3 triggers each), each call walking
a tiny constant number of category fields. The closure body's
`working.clone()` (heavy Box-of-Box CapcoMarking clone) is now paid
ONLY when a productive fixpoint is possible.

Tests:
- 4 behavioral tests in `crates/capco/tests/closure_runtime.rs`
  exercising the observable behavior (closure no-op on bottom +
  uncaveated classified, closure still fires when trigger present,
  closure runs fixpoint even when suppressed).
- 5 predicate-direct tests in `crates/capco/src/scheme/tests.rs`
  (in-crate so they can call the `pub(crate)` predicate directly
  per `feedback_pub_doc_hidden_is_still_public_api.md`).

Bench: lint_10kb measurement post-commit landed at 1.50ms (median),
statistically equivalent to pre-commit 1.51ms β€” the short-circuit
removes work that wasn't actually firing in the bench's no-trigger
pages. The R-1 optimization is sound (removes useless work that
would otherwise compound on larger / more-loaded inputs) but doesn't
attribute the regression source on this bench. Commit 7 attributes
and fixes the actual hot spot.

Authority: `docs/plans/2026-05-01-lattice-design.md` Β§4.7.3
table-design property (closure-rule monotonicity); D19 B (severity
runtime-resolution preserved by leaving `should_fire` itself
unchanged).
PR 4b-D.2 commit 7/N. Phase-attribution profiling (new
`profile_project` bench) revealed that the lint_10kb regression had
two compounding drivers:

1. The page-marking projection cache misses ~50 times per 10KB doc.
2. Portion count grows monotonically per call (1, 2, 3, … 50) β€” so
   the per-call O(n) work is paid quadratic-in-portions across the
   document. The flip-introduced pipeline is structurally heavier
   than the pre-flip `PageContext::project`: ~15 O(n) lattice walks +
   5 tmp_ctx accessor walks vs ~13 PageContext walks pre-flip.

Of the two engine-boundary clone rounds the trait path inflicts:

- Round #1 β€” `engine::project_page_marking` wraps each portion in a
  `CapcoMarking::new(p.clone())` before calling
  `MarkingScheme::project(&[CapcoMarking])`.
- Round #2 β€” `MarkingScheme::project`'s body extracts `m.0.clone()`
  for each marking back into `Vec<CanonicalAttrs>`.

Both rounds are pure deep-clone allocation cost.

This commit adds `CapcoScheme::project_from_attrs_slice(&[CanonicalAttrs])`
as a scheme-specific inherent fast-path that consumes the slice
shape `PageContext::portions()` already returns, skipping both
clone rounds. The trait-path callers (test fixtures, external
tooling) continue to go through `MarkingScheme::project`, which
now delegates to the same pipeline-body via a shared
`project_attrs_pipeline(&[CanonicalAttrs]) -> CanonicalAttrs`
internal method.

Engine's `project_page_marking` calls the fast path directly:
`scheme.project_from_attrs_slice(page_context.portions())`.

The closure-rewrite-application sentinel (decisions.md D23) moves
from the trait body to `project_attrs_pipeline` β€” the shared
pipeline-step body β€” so both entry points are sentinel-guarded.

Bench: lint_10kb dropped from 1.51ms (post-flip pre-perf) to 1.20ms
(after commit 6 short-circuit + this commit's fast-path), a -310Β΅s
improvement that closes ~52% of the regression vs the 914Β΅s
baseline.

A new `profile_project` bench at `crates/engine/benches/profile_project.rs`
captures the per-phase attribution (join_via_lattice, closure,
scheme.project, from_canonical, end-to-end engine path, full lint,
and per-portion-count scaling) so future perf work has a baseline.

Authority: PR 4b-D.2 architect's R-1 + (b) hot-spot mitigations
called out in the PR brief; `project_perf_baseline_pr5_trigger.md`
project memory (routine perf-bumping permitted through PR 5;
dedicated perf-analysis work scheduled for PR 5+ if structural
regression survives).
PR 4b-D.2 commit 8/N. Eliminates the third (innermost) clone round
the post-flip projection pipeline was paying.

Pre-fix flow (commit 7 fast-path):

  engine::project_page_marking
    β†’ CapcoScheme::project_from_attrs_slice(&[CanonicalAttrs])
      β†’ project_attrs_pipeline(&[CanonicalAttrs])
        β†’ CapcoMarking::join_via_lattice(&[CanonicalAttrs])
          β†’ build tmp_ctx via add_portion(p.clone()) for p in portions  ← nΓ—clone

The engine ALREADY owns a `&PageContext` (the accumulator that
add_portion is filling across the document). Rebuilding tmp_ctx
internally was a redundant deep-clone per portion. Phase-attribution
profiling found tmp_ctx rebuild at ~2.8Β΅s / n=50 portions; cumulated
across the bench's monotone-growing call sequence (sum_i=1^50) this
was ~70Β΅s of pure clone overhead.

Post-fix flow:

  engine::project_page_marking
    β†’ CapcoScheme::project_from_page_context(&PageContext)        ← new
      β†’ project_attrs_pipeline_with_context(&[attrs], &PageContext)
        β†’ CapcoMarking::join_via_lattice_with_context(&[attrs], &PageContext)  ← new

The engine's existing PageContext is borrowed through to
`join_via_lattice`'s residue-axis accessor calls. No new clones.

Trait path back-compat: `MarkingScheme::project` continues to call
`project_from_attrs_slice`, which (since the engine no longer uses
it) is now the no-pre-built-context entry. It builds a one-shot
tmp_ctx and delegates to the with-context pipeline. Same cost as
before for trait-path callers; only the engine fast-path benefits.

The new `join_via_lattice_with_context` carries a debug-mode
assertion that `portions` and `page_ctx.portions()` are the same
slice β€” caller's contract. Mismatched inputs would mix per-axis
lattice results from one slice with residue-axis accessor results
from another, which is a semantically-corrupt projection. Production
callers go through the new fast-path entries that pass the same
slice to both arguments; the assertion guards against future
refactor-induced contract violations.

Bench results (lint_10kb, WSL2 dev worktree):

  - Pre-flip baseline (origin/staging): 914Β΅s (GHA ubuntu-latest)
  - Post-flip unmitigated (after commit 5):     1.51ms (+65%)
  - After commit 6 short-circuit:               1.51ms (no change β€” closure
                                                already short-circuited)
  - After commit 7 fast-path:                   1.20ms (-310Β΅s)
  - After commit 8 (this commit):               1.03ms (-170Β΅s)
  - Cumulative savings:                         -480Β΅s

Final: lint_10kb median 1033Β΅s on WSL2 (vs 914Β΅s baseline, +13%).
Within the +20% acceptable-band per the PR brief's tier ladder
(1005-1100Β΅s band: refresh baseline.json with documented rationale).
WSL2 typically reads 5-10% slower than GHA hosted runners per the
baseline doc's reference-machine note; the GHA re-capture may land
back in the +10% noise band.

Per-phase attribution at n=50 portions (after this commit):
  - join_via_lattice + tmp_ctx (now zero-clone): ~7.5Β΅s
  - closure (short-circuited): ~70ns
  - page_rewrites: ~3.5Β΅s
  - from_canonical bridge: ~37ns
  - total project + bridge call: 9.7Β΅s

Per-phase attribution at n=2 portions:
  - phase_a join_via_lattice: 541ns
  - phase_b closure: 72ns
  - phase_c scheme.project (trait path): 988ns
  - phase_d from_canonical: 37ns
  - phase_e engine path (project_from_page_context + bridge): 799ns
  - phase_f full lint_10kb: 1.04ms

Authority: PR 4b-D.2 architect's R-1 + (b) tmp_ctx-elimination
mitigation called out in the PR brief; `project_perf_baseline_pr5_trigger.md`
project memory (further perf-analysis scheduled for PR 5+ if
structural cost survives the optimization rounds β€” this commit
closes the structural-cost gap to within +20% of baseline, in the
"acceptable" band, so PR 5 perf work focuses on other surfaces).
PR 4b-D.2 commit 9/N. Refreshes `benches/baseline.json::lint_10kb`
from {912, 913, 914}Β΅s (pre-flip, PR #498 capture) to {1030, 1033,
1036}Β΅s (post-flip, post-commit-8 optimization stack).

The flip moves the page-marking projection driver from
`PageContext::project()` to `scheme.project(Scope::Page, ...) β†’
join_via_lattice β†’ closure β†’ page_rewrites`. The new pipeline has
~50% more O(n) walks per call than the pre-flip path (10 lattice
constructors + closure + page rewrites vs 13 expected_* accessors),
which is the structural cost of the hot-path flip. Commits 6-8
brought the regression from +65% unmitigated to +13% post-perf-work:

  - Commit 6: closure short-circuit on empty cone triggers
    (architect's R-1)
  - Commit 7: engine-side `project_from_attrs_slice` fast-path
  - Commit 8: PageContext-borrowing `project_from_page_context`
    fast-path that eliminates the inner tmp_ctx rebuild's clone round

Captured on dev WSL2 worktree. Per the reference-machine note in the
baseline doc, GHA `ubuntu-latest` typically reads 5-10% faster than
WSL2; the GHA re-capture is expected to land 50-100Β΅s lower than
this WSL2 capture once CI runs against this branch.

PR 4b-E (PageContext deletion) will retire the remaining residue-
axis tmp_ctx-via-PageContext-accessors path, expected to bring the
value back down to baseline-ish. Per
`project_perf_baseline_pr5_trigger.md` project memory, further perf-
analysis work is scheduled for PR 5+ if structural costs survive;
PR 4b-D.2 stays within the +20% acceptable band the brief specified,
so PR 5 perf work can focus on other surfaces.

Brief tier ladder:
  - Ideal (≀1005Β΅s / +10% of pre-flip baseline): not reached.
  - Acceptable (1005-1100Β΅s / +20% of pre-flip baseline): yes β€”
    1033Β΅s on WSL2.
  - Not acceptable (>1100Β΅s): no.

The SC-001 constitutional 16ms absolute ceiling
(`target_upper_ci_us = 16000`) remains the load-bearing gate and is
unaffected by the re-capture.
PR 4b-D.2 commit 10/10. Combines 5 MEDIUM + 2 LOW reviewer items
into one cleanup commit. No behavior change.

MEDIUM-1 (rust): Stale module-level doc in
`crates/capco/src/scheme/marking.rs` claimed `JoinSemilattice`
"delegates join to `PageContext::add_portion` per PR 4b-B's plan".
Post-flip this is the opposite of true. Rewrote the module doc
header to describe the lattice path as the production page-
aggregation surface and PageContext as the residue-axis bridge
still active until PR 4b-E retires it. Cites
docs/plans/2026-05-01-lattice-design.md Β§4.7.4 + Constitution VII Β§IV.

MEDIUM-2 (general): Stale `CategoryAction::Promote` arm comment in
`crates/capco/src/scheme/marking_scheme_impl.rs` claimed
"engine.lint does not drive aggregation through project() yet" β€”
PR 4b-D.2 just reversed that. Rewrote to: engine now drives through
`project()`; Promote stays a no-op because JOINT-promotion and
FGI-absorption are renderer-canonical territory (`render_canonical`'s
job, not the projection lattice's). Cites
docs/plans/2026-05-01-lattice-design.md Β§10 row 4 (SCI per-system
canonicalization) + Β§10 row 5 (SAR ordering). Behavioral conclusion
(no-op) unchanged.

MEDIUM-3 (rust): Added a structural-size doc comment + permanent
`#[allow(clippy::too_many_lines, reason = "...")]` on
`join_via_lattice_body`. The 129-LOC function is structurally
justified β€” splitting would either thread cross-axis state via a
struct (paying the cost it notionally saved) or scatter the inline
Β§-citations across files (Constitution VIII harm). Future
maintainers hitting the lint should `#[allow]`, not split.
`cargo +stable clippy --workspace --tests -- -D warnings` clean
both with and without `-A clippy::too_many_lines`.

MEDIUM-4 (rust+general): Expanded the module doc of
`crates/engine/benches/profile_project.rs` to spell out which
phases use full `Engine::lint` (Phase F) versus synthesized
`CanonicalAttrs` portions (Phases A-E, G-I). The synthesis is a
phase-attribution probe, not a regression gate. Documented the
maintenance contract: if the bench corpus's representative axis
mix drifts, regenerate the synthesis. Tagged as
"maintenance item, not a bug."

MEDIUM-5 (general): Downgraded
`CapcoScheme::project_from_attrs_slice` from `pub` to `pub(crate)`.
Post-commit-8, the engine's `project_page_marking` calls
`project_from_page_context` exclusively; the only remaining
caller of `project_from_attrs_slice` is the in-crate trait-path
delegate for `MarkingScheme::project`. Verified no out-of-crate
callers via `cargo +stable check --workspace --tests` (clean).
Added a `## Visibility` doc section documenting the downgrade
and the promote-back criteria.

LOW (baseline.json): Captured the WSL2-vs-GHA disparity in the
`reference_machine._dev_capture_note` field + added a
`lint_10kb._wsl2_dev_capture` sub-object preserving the WSL2
measurement as a reference point. The first GHA re-capture of
this branch via `scripts/capture-baselines.sh` overwrites the
top-level numbers; the sub-object stays as cross-host context.
JSON validity verified.

LOW (closure_hotpath): Renamed
`project_is_idempotent_on_classified_orcon` to
`project_pipeline_is_idempotent_on_orcon_derived_closed_state`.
The test actually verifies idempotence on the NOFORN-injected
closed state (not on ORCON-only state), so the prior name was
misleading. Updated the doc comment to match.

DEFERRED to follow-up (NOT included):
- Test helper duplication across `closure_hotpath.rs` and
  `lattice_corpus.rs` β€” extract to `tests/common.rs` in a
  follow-up test-only PR.
- CLOSURE_NOFORN_UCNI DoD coverage gap β€” pre-existing per #407.
- Β§H.7 pp123-125 citation precision β€” pre-existing from PR 4b-B,
  renderer-territory deferred to PR 5+.

Verification gates (final):
- cargo +stable build --workspace: clean
- cargo +stable clippy --workspace --tests -- -D warnings: clean
- cargo +stable test --workspace: 0 failures
Copilot AI review requested due to automatic review settings May 18, 2026 03:16
@bashandbone bashandbone requested a review from a team as a code owner May 18, 2026 03:16
@bashandbone bashandbone added rust touches rust code refactor issue/pr related to refactoring capco/ism CAPCO classification markings, ODNI ISM schema, and the marque-capco / marque-ism rule chain EPIC-Lattice Addressed by Epic for lattice refactor (5-2-26 plan) labels May 18, 2026
bashandbone and others added 2 commits May 17, 2026 23:23
CI's `cargo fmt --check` flagged 5 files. The reviewer-feedback
cleanup commit (`d33e14e1`) was verified via build/clippy/test
locally but not against `fmt --check`. Pure formatting; no
behavior change.

Affected files:
- crates/capco/src/scheme/actions/intent.rs
- crates/capco/src/scheme/marking_scheme_impl.rs
- crates/capco/tests/closure_runtime.rs
- crates/engine/benches/profile_project.rs
- crates/engine/tests/closure_hotpath.rs
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR flips CAPCO page projection and engine page-marking hot paths from PageContext aggregation to lattice aggregation with closure and page rewrites, while adding runtime and benchmark coverage for the new path.

Changes:

  • Adds ProjectedMarking::from_canonical and routes engine page-marking projection through CapcoScheme::project_from_page_context.
  • Reworks CapcoMarking/CapcoScheme projection to use join_via_lattice β†’ closure β†’ page_rewrites.
  • Adds closure hot-path tests, lattice corpus fixtures, perf profiling bench, and updates baseline/context docs.

Reviewed changes

Copilot reviewed 17 out of 17 changed files in this pull request and generated 10 comments.

Show a summary per file
File Description
specs/006-engine-rule-refactor/decisions.md Adds D22/D23 decisions for NOFORN supersession and closure sentinel placement.
crates/ism/src/projected.rs Adds ProjectedMarking::from_canonical and unit tests.
crates/engine/tests/lattice_corpus.rs Adds Pattern-D lattice projection integration tests.
crates/engine/tests/closure_hotpath.rs Adds closure-on-hot-path engine-facing tests.
crates/engine/src/engine.rs Routes page marking projection through scheme/lattice fast path.
crates/engine/Cargo.toml Registers the new profile_project benchmark.
crates/engine/benches/profile_project.rs Adds projection phase attribution benchmark.
crates/capco/tests/page_context_lattice_parity.rs Retargets parity fixtures for post-flip scheme/lattice behavior.
crates/capco/tests/closure_runtime.rs Adds closure short-circuit behavioral tests.
crates/capco/src/scheme/tests.rs Adds in-crate tests for closure trigger short-circuit predicate.
crates/capco/src/scheme/marking.rs Flips JoinSemilattice::join and adds PageContext-borrowing lattice path.
crates/capco/src/scheme/marking_scheme_impl.rs Implements lattice + closure + rewrite projection pipeline and fast paths.
crates/capco/src/scheme/actions/page_context.rs Marks old PageContext projection bridge as non-production/dead-code.
crates/capco/src/scheme/actions/mod.rs Keeps PageContext projection re-export suppressed until deletion.
crates/capco/src/scheme/actions/intent.rs Adds NOFORN supersession routing for FactAdd.
crates/capco/CAPCO-CONTEXT.md Updates CAPCO context for post-flip divergence inventory.
benches/baseline.json Refreshes lint_10kb baseline/provenance notes.

πŸ’‘ Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread crates/capco/src/scheme/marking.rs Outdated
Comment on lines +706 to +709
CapcoMarking::new(CapcoMarking::join_via_lattice(&[
self.0.clone(),
other.0.clone(),
]))
Comment thread crates/capco/src/scheme/marking.rs Outdated
Comment on lines +179 to +186
/// semantically-corrupt projection. Debug-mode assertion below
/// guards this at test time.
pub fn join_via_lattice_with_context(
portions: &[CanonicalAttrs],
page_ctx: &marque_ism::PageContext,
) -> CanonicalAttrs {
#[cfg(debug_assertions)]
debug_assert_eq!(
Comment thread crates/engine/src/engine.rs Outdated
Comment on lines +4221 to +4225
/// Project the current [`marque_ism::PageContext`] into a
/// [`marque_ism::ProjectedMarking`] via the scheme's production
/// page-projection path.
///
/// PR 4b-D.2 flipped the hot path from `PageContext::project()` (the
Comment thread crates/engine/tests/lattice_corpus.rs Outdated
Comment on lines +19 to +25
//! Closure injects USA/NATO into REL TO; the lattice state shows
//! USA + NATO membership. S007 fires through `Engine::lint`'s
//! NDJSON path at `Severity::Suggest` β€” but this file does NOT
//! exercise `Engine::lint` (the strict-recognizer fixture surface
//! for S007's text-layer behavior lives in
//! `crates/capco/tests/dissem_nato_*.rs`); it pins the lattice
//! layer's injection observable.
Comment thread benches/baseline.json Outdated
Comment on lines +11 to +13
"lower_ci_us": 1030,
"mean_us": 1033,
"upper_ci_us": 1036,
Comment on lines +233 to +239
// CapcoMarking values. The engine's hot path bypasses
// this via `CapcoScheme::project_from_attrs_slice`
// (inherent method below), which consumes
// `&[CanonicalAttrs]` directly and shares the rest of
// the pipeline. Test fixtures and external tooling
// continue to use this trait-path entry; the engine
// takes the fast path.
dissem_nato: attrs.dissem_nato,
non_ic_dissem: attrs.non_ic_dissem,
rel_to: attrs.rel_to,
display_only_to: attrs.display_only_to,
Comment on lines +84 to +87
/// Extract the set of per-portion `CanonicalAttrs` produced by the
/// engine's strict pipeline on the bench input. We lint once and grab
/// the page-context portions; replay them under the per-phase
/// micro-benches below.
Comment thread crates/capco/src/scheme/marking.rs Outdated
Comment on lines +207 to +209
/// Clippy's `too_many_lines` lint fires on this function at 129
/// LOC vs the 100-line default. The size is structurally
/// justified β€” splitting would harm correctness:
let portion_attrs = [attrs.clone()];
let dissem_set =
crate::lattice::DissemSet::from_attrs_iter(&portion_attrs).with_noforn_injected();
attrs.dissem_us = dissem_set.into_boxed_slice();
…elax MarkingScheme bound (Copilot R1 #1)

PR 4b-D.2 commit 11 / decisions.md D24. Public API change in
`marque-scheme`: relax `MarkingScheme::Marking: JoinSemilattice` to
`MarkingScheme::Marking` and relax `DiffInput<M: JoinSemilattice>`
to `DiffInput<M>`. Drop `impl JoinSemilattice for CapcoMarking` and
`impl MeetSemilattice for CapcoMarking`.

## Why

Copilot R1 surfaced an idempotence-law violation: `m.join(&m) != m`
when `m.rel_to = [NATO]` because `RelToBlock::from_attrs_iter`
expands NATO via `lookup_tetragraph_members` to its 30 trigraph
members; structural `Eq` on `CanonicalAttrs` fails after the
cross-axis fold.

The lattice consultant's verdict (Option D-extended) was: the
per-axis lattices (`RelToBlock`, `DissemSet`, `SciSet`, etc.) ARE
sound lattices on their native domains (the BTreeSet-of-expanded-
trigraphs representative is canonical post-expansion); `CapcoMarking`
is a **cross-axis fold** that composes those lattice values back
into a `CanonicalAttrs` record. Cross-axis folding is a
*projection*, not a lattice op. Claiming `JoinSemilattice` on the
record type promised a law (structural-`Eq` idempotence) the
construction cannot keep without either (a) lossy eager
canonicalization at construction (loses the renderer's `NATO`
atom; non-unique inverse), or (b) quotient-`Eq` rewrite across
every `CanonicalAttrs` field (massive blast radius). Both rejected.

The trait-bound relaxation here is the surgical fix: remove the
false claim. The cross-axis fold remains accessible as the
inherent methods `CapcoMarking::join_via_lattice` and
`CapcoMarking::join_via_lattice_with_context` (engine's
`project_from_page_context` hot path uses the latter β€” unchanged).

## Q2 audit β€” 151 `CapcoMarking::new` call sites

Verified: no production site constructs `CapcoMarking` from
non-canonical input. The 151 sites break down as:

- Production engine paths (decoder.rs, engine.rs):
  ~27 sites, all consuming parser-produced `CanonicalAttrs` (the
  parser emits tetragraph atoms verbatim β€” `NATO` stays `NATO`,
  not expanded β€” which is the correct representation for the
  renderer and the input shape the lattice operates on).
- Production scheme paths (capco/src/scheme/*.rs):
  5 sites, all consuming either `CanonicalAttrs::default()` or
  `join_via_lattice`-produced output (already in
  expanded-canonical form).
- Test/bench paths (capco/tests/, capco/src/scheme/tests.rs,
  engine/tests/, engine/benches/): ~119 sites, all using either
  `default()`, parser output, or hand-crafted fixtures where the
  tetragraph form is intentional (`(S//REL TO USA, NATO)` is a
  valid input shape and the test is asserting the lattice handles
  it correctly).

No call site produces a marking that "bypasses canonicalization
unexpectedly" β€” the input shape (with tetragraph atoms) is the
right shape; the bug is the trait claim, not the data. Confirms
the consultant's verdict that Option A (eager canonicalization at
construct) would be the wrong fix.

## Q3 follow-up β€” issue #528

The systematic audit of the remaining per-axis types for
structural-vs-lattice-`Eq` mismatches
(`DissemSet::relido_observed_unanimous`,
`JointSet::Mixed`/`DisunityCollapse`, `SupersessionSet`) is
deferred to issue #528 per Constitution VII scope-discipline.
PR #456 already split `Lattice` into Join/Meet halves and
identified those three as having observational state; this PR's
scope is the cross-axis fold (`CapcoMarking`), not the per-axis
audit.

## Test impact

- `crates/capco/tests/scheme_equivalence.rs`:
  - `lattice_join_agrees_with_project_banner_pairwise` renamed to
    `join_via_lattice_agrees_with_project_banner_pairwise` and
    pivoted to use the inherent method instead of the trait.
  - `capco_marking_meet_narrow_components` +
    `capco_marking_meet_with_missing_classification_is_none`
    removed (exercised the unsound trait impl; no production code
    consumed the trait method).
- `crates/capco/tests/proptest_lattice.rs`: 10 new proptest cases
  added covering `RelToBlock` join + meet laws + absorption + bottom
  identity. These pin the lattice claim at the algebraically-sound
  site β€” the per-axis type's own structural `Eq` (the BTreeSet-of-
  expanded-trigraphs representative). All pass (38/38).

Other ~149 sites: zero impact (none called `.join()` / `.meet()`
on a `CapcoMarking`; the trait was purely declarative).

## Verification gates

  cargo +stable fmt --all --check       : clean
  cargo +stable build --workspace       : clean
  cargo +stable clippy --workspace ...  : clean
  cargo +stable test --workspace        : 0 failures

## References

- Lattice consultant verdict (this session): "per-axis lattices
  are real; the cross-axis composition is structural folding, not
  a lattice operation"
- PR #456 (Join/Meet split + observational-state framing)
- `marque-applied.md` Β§3 (PR 3b stall walkthrough)
- `pure-lattice.md` Β§7 (quotient lattices) + Β§11 (powerset
  Boolean algebra)
- Tracking issue: #528
PR 4b-D.2 commit 12. Combines the 9 Copilot-R1 items not covered by
commit 11 (D24 trait-bound relaxation). No behavior change beyond
item #2 (new PageRewrite + new CategoryId); items 3-10 are
doc/visibility/test/calibration nits.

## Item 2 β€” NOFORN clears `display_only_to` (correctness)

Verified Copilot's claim: `capco/noforn-clears-rel-to` exists (at
`crates/capco/src/scheme/rewrites/noforn_clears.rs:67`); no
`noforn-clears-display-only-to` equivalent shipped. The companion
`capco/noforn-clears-fdr-family` strips the `Displayonly` TOKEN
from `dissem_us` but does NOT clear `attrs.display_only_to` (the
country-list field). When closure injects NOFORN AFTER a portion
populated `display_only_to` via the per-portion union, the renderer
would emit an inconsistent banner β€” Β§H.8 p145 violation.

Fix:

- Added `CAT_DISPLAY_ONLY_TO = CategoryId(12)` in
  `crates/capco/src/scheme/mod.rs` so the rewrite can use the
  symmetric `CategoryAction::Clear { CAT_DISPLAY_ONLY_TO }` shape
  (mirrors `capco/noforn-clears-rel-to`).
- Wired the new CategoryId through `capco_category_clear` +
  `capco_category_has_values`.
- Added `capco/noforn-clears-display-only-to` PageRewrite at
  declaration order index 15 (between the existing two NOFORN-clears
  rows and the transmutation rewrites). Citation:
  CAPCO-2016 Β§H.8 p145 + Β§D.2 Table 3 rows 1-2 (verified against
  `crates/capco/docs/CAPCO-2016.md` at authoring).
- Updated rewrite-count pins: 23 β†’ 24 in
  `scheme_declares_phase3_rewrites` and
  `engine_construction_succeeds_with_full_rewrite_table`.
- Added 2 closure_hotpath tests:
  `noforn_clears_display_only_to_via_cross_portion_join` and
  `noforn_clears_display_only_to_is_idempotent_on_empty_field`.

## Item 3 β€” baseline.json CI calibration

Copilot was right: `scripts/bench-check.sh` consumes top-level
`lint_10kb.{lower,mean,upper}_ci_us` for the CI regression gate;
calibrating to the WSL2 dev-capture (1030/1033/1036) would mask
regressions on faster GHA hardware. Reverted top-level to the
pre-PR GHA baseline (912/913/914). WSL2 measurement preserved in
the `_wsl2_dev_capture` sub-object for record-keeping. Follow-up:
GHA re-capture via `scripts/capture-baselines.sh` produces the
post-flip authoritative numbers after merge.

## Item 4 β€” `join_via_lattice_with_context` visibility

Downgraded `pub` β†’ `pub(crate)`. The same-slice contract on
`portions` / `page_ctx.portions()` is only verified under
`#[cfg(debug_assertions)]`; promoting to `pub` without a
release-mode guard would invite cross-crate callers to violate the
contract silently. The two production callers (engine's
`project_attrs_pipeline_with_context` and the `join_via_lattice`
wrapper) are in-crate.

## Item 5 β€” `engine.rs` doc-comment attribution

Moved `project_page_marking` to AFTER `dispatch_page_finalization`
(was before). Pre-fix the dispatch function's `# Returns` doc-block
ran directly into the helper's doc, attributing wrongly. Post-fix
each function's doc is immediately above its `fn` declaration.

## Item 6 β€” `lattice_corpus.rs` file-level doc contradiction

Rewrote the `closure_nato_rel_to_us_plus_nato` summary. Pre-fix it
claimed "Closure injects USA/NATO into REL TO" which is the
OPPOSITE of what the test asserts (`!rel_to_contains` for both
USA and NATO, because the Β§H.7 reciprocal-raise flattens
classification to `Us(_)` BEFORE closure observes the
`TOK_NATO_CLASS` trigger). Updated to describe the actual
suppression mechanic and the FGI-axis survival.

## Item 7 β€” stale comment about `project_from_attrs_slice`

The comment in `marking_scheme_impl.rs:239` claimed
`project_from_attrs_slice` is callable cross-crate. It's
`pub(crate)` post-commit-10, called only by the in-crate trait
body. Updated to reflect the two distinct fast-path entries
(`project_from_page_context` for engine, `project_from_attrs_slice`
for in-crate non-PageContext callers).

## Item 8 β€” `join_via_lattice_body` size note accuracy

Copilot was right: the function spans lines 284-706 (~423 LOC) in
the current revision, not the ~129 LOC the prior doc claimed. The
129 figure was wrong on inspection β€” the body has always been
~420 LOC since PR 4b-B Commit 7 added the per-axis composition.
Updated the doc to reflect the actual size; the structural
justification (axis ordering + inline citations + cross-axis state
flow) is even stronger at the actual size.

## Item 9 β€” `display_only_to` unit test gap

Added 3 unit tests in `crates/ism/src/projected.rs::from_canonical_*`:
single-country preservation, multi-country preservation, empty
preservation. The pre-fix surface covered `rel_to` but not the
parallel `display_only_to` country-list axis.

## Item 10 β€” stale `profile_project.rs` local function comment

Rewrote the `collect_portions` doc. Pre-fix it claimed "extract
portions produced by the strict pipeline" but the body synthesizes
a representative `(S//NF)` + `(TS//SI)` pair AFTER an Engine::lint
warmup call. Updated to describe the actual synthesis intent and
explain the Engine::lint call is a warmup (not an extraction).

## Verification gates

  cargo +stable fmt --all --check        : clean
  cargo +stable build --workspace        : clean
  cargo +stable clippy --workspace ...   : clean
  cargo +stable test --workspace         : 0 failures
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 26 out of 26 changed files in this pull request and generated 13 comments.

Comments suppressed due to low confidence (2)

crates/capco/src/scheme/rewrites/noforn_clears.rs:187

  • This adds a third row to noforn_clears_rows, but the function-level comments above still describe β€œthe two NOFORN-clears rows” and list only the first two. Please update that local inventory so future readers don't miss this rewrite when reasoning about declaration order.
        // `capco/noforn-clears-display-only-to` β€” companion to
        // `capco/noforn-clears-rel-to` for the `display_only_to`
        // country-list axis. PR 4b-D.2 Copilot R1 #2: pre-fix,
        // closure-injected NOFORN on a portion that also carried
        // DISPLAY ONLY USA, GBR left `attrs.display_only_to`
        // populated even though NOFORN had landed in `dissem_us`
        // (the `fdr-family` row above strips the token but the
        // country list is a separate field). The renderer would
        // then emit an inconsistent banner per Β§H.8 p145 ("NOFORN
        // ... Cannot be used with REL TO / RELIDO / EYES ONLY /
        // DISPLAY ONLY") + Β§D.2 Table 3 rows 1-2 (NOFORN dominates
        // the FD&R family).
        //
        // Uses `CategoryAction::Clear { CAT_DISPLAY_ONLY_TO }`
        // symmetrically with the REL TO clearer above; the
        // `CAT_DISPLAY_ONLY_TO` CategoryId was added in PR 4b-D.2
        // Copilot R1 #2 (`crates/capco/src/scheme/mod.rs`) and
        // routed through `capco_category_clear` /
        // `capco_category_has_values`.
        PageRewrite::declarative(

crates/capco/src/scheme/actions/intent.rs:282

  • This NOFORN FactAdd path only rebuilds dissem_us; it does not clear the parallel FD&R axes such as attrs.rel_to or attrs.display_only_to. That is fine when scheme.project later runs the NOFORN-clears PageRewrites, but direct apply_intent fixes (E021/E038-style FactAdd) return immediately from here and can render a marking with newly-added NOFORN plus an existing REL TO/DISPLAY ONLY country list, still violating Β§H.8 p145. The injection-site fix needs to clear those fields too, or the comment should not claim the invariant is satisfied for direct rule-driven FactAdd paths.
        if target == DissemControl::Nf {
            let portion_attrs = [attrs.clone()];
            let dissem_set =
                crate::lattice::DissemSet::from_attrs_iter(&portion_attrs).with_noforn_injected();
            attrs.dissem_us = dissem_set.into_boxed_slice();
            return Ok(());

Comment on lines +722 to +729
debug_assert_eq!(
raw,
raw_snapshot.as_slice(),
"closure() mutated the per-portion CanonicalAttrs slice β€” \
violates PageRewrite read-only-attrs invariant \
(docs/plans/2026-05-01-lattice-design.md Β§3 (e.1))"
);

Comment on lines +64 to +72
/// 5. **DISPLAY-ONLY / FD&R-family (2):**
/// `capco/noforn-clears-fdr-family` (strips DISPLAY ONLY /
/// RELIDO / EYES tokens from `dissem_us`) at Β§D.2 Table 3
/// row 2 + Β§H.8 p154 + Β§H.8 p157, plus
/// `capco/noforn-clears-display-only-to` (PR 4b-D.2 Copilot R1
/// #2 β€” clears `attrs.display_only_to`, the country-list
/// sibling of `attrs.rel_to`) at Β§H.8 p145 + Β§D.2 Table 3
/// rows 1-2. The two rows together close the parallel REL TO /
/// DISPLAY ONLY axes.
Comment thread crates/capco/src/scheme/marking.rs Outdated
Comment on lines +229 to +234
debug_assert_eq!(
portions,
page_ctx.portions(),
"join_via_lattice_with_context: portions slice and page_ctx \
portions() must be the same slice β€” caller's contract."
);
Comment thread benches/baseline.json
Comment on lines +15 to +23
"_wsl2_dev_capture": {
"lower_ci_us": 1030,
"mean_us": 1033,
"upper_ci_us": 1036,
"profile": "WSL2 dev worktree, x86_64",
"date_captured": "2026-05-17",
"_note": "PR 4b-D.2 post-commit-8 WSL2 measurement (median 1033Β΅s). Captured during PR 4b-D.2 perf-mitigation work for record-keeping; **NOT the authoritative baseline**. The top-level `lint_10kb.{lower,mean,upper}_ci_us` fields are the GHA `ubuntu-latest` reference values that `scripts/bench-check.sh` consumes for the CI regression gate. The CI gate MUST be calibrated to GHA hardware β€” calibrating to WSL2 (5-10% slower) would mask regressions on faster GHA runners. **Follow-up**: the GHA re-capture via `scripts/capture-baselines.sh` after PR 4b-D.2 merges produces the post-flip authoritative numbers and updates the top-level fields."
},
"_note": "Values are Criterion 95% confidence-interval bounds. **2026-05-17 PR 4b-D.2 hot-path-flip context.** Per Copilot R1 review #3 these top-level fields hold the pre-PR GHA `ubuntu-latest` baseline (912/913/914 from PR #498), not the WSL2 dev-capture from the PR 4b-D.2 perf-mitigation work β€” the CI regression gate at `scripts/bench-check.sh:32-35` consumes these fields and must be calibrated to the same hardware where CI runs. The WSL2 measurement is preserved in `_wsl2_dev_capture` for cross-host context. **Post-PR-4b-D.2 expectation**: the new pipeline IS structurally heavier than the pre-flip path (~50% more O(n) walks per call across 10 lattice constructors + closure + page rewrites vs PageContext's 13 `expected_*` accessors; PageContext stays alive as the page-state accumulator until PR 4b-E). Commits 6-8 brought the WSL2 measurement from +65% unmitigated to +13% via three optimizations: (6) closure short-circuit on empty cone triggers (architect's R-1); (7) engine-side fast-path `project_from_attrs_slice`; (8) `project_from_page_context` borrowing the engine's existing PageContext through to `join_via_lattice_with_context`. The first GHA re-capture after merge updates these numbers and the CI gate moves with it. Per the `project_perf_baseline_pr5_trigger.md` project memory, further perf-analysis work is scheduled for PR 5+ if the structural costs survive the GHA re-capture; PR 4b-E (PageContext deletion) will retire the remaining residue-axis tmp_ctx requirement, expected to bring the GHA value back down. **2026-05-17 re-capture (PR #498).** Refreshed lower/mean/upper to {912, 913, 914} on `ubuntu-latest` GHA runner (PR #498 CI run 25991729674), 3Β΅s over the previous 911Β΅s threshold; the new threshold (`upper_ci_us * 1.10` β‰ˆ 1005Β΅s) restores the +10% noise envelope. **2026-05-05 widening (D8 amendment) β€” superseded by the 2026-05-17 re-capture.** Original 2026-04-26 baseline (lower 746 / mean 749 / upper 753) flapped on shared GHA `ubuntu-latest` runners; per D8 `decisions.md`, the +10% widening (746β†’821, 749β†’824, 753β†’828) held until staging complexity additions exceeded the noise band. The SC-001 16ms absolute target remains the load-bearing constitution gate; CI-side regression-gate calibration is independent.",
Comment thread crates/capco/CAPCO-CONTEXT.md Outdated
Comment on lines +237 to +239
per-axis lattice types; PR 4b-D.2 (2026-05-17) flipped the
production `JoinSemilattice::join` and `MarkingScheme::project` to
delegate to that lattice path. The parity gate at
Comment on lines 1212 to +1215
page_marking_arc
.get_or_insert_with(|| Arc::new(page_context.project()))
.get_or_insert_with(|| {
Arc::new(project_page_marking(&self.scheme, &page_context))
})
Comment on lines +225 to +227
// The closure operator and PageRewrites are both
// monotone, and PageRewrites operate on the closed
// state's remaining tokens.
Comment on lines +59 to +65
// The rewrite is needed because the closure operator (e.g.
// `CLOSURE_NOFORN_SAR` on a portion that ALSO carries DISPLAY
// ONLY USA, GBR) injects NOFORN AFTER `join_via_lattice` has
// set `attrs.display_only_to` from the per-portion union.
// Without this rewrite the renderer would emit an inconsistent
// banner: NOFORN in `dissem_us` AND a populated
// `display_only_to` country list, violating Β§H.8 p145.
Comment thread crates/engine/tests/closure_hotpath.rs Outdated
Comment on lines +444 to +454
fn noforn_clears_display_only_to_via_cross_portion_join() {
let usa = CountryCode::USA;
let gbr = CountryCode::try_new(b"GBR").expect("trigraph");

// Portion 1: NOFORN. Portion 2: DISPLAY ONLY USA, GBR.
let nf_portion = classified_with_dissem(Classification::Secret, DissemControl::Nf);
let mut do_portion = classified_us(Classification::Secret);
do_portion.dissem_us = vec![DissemControl::Displayonly].into_boxed_slice();
do_portion.display_only_to = vec![usa, gbr].into_boxed_slice();

let projected = project_page(&[nf_portion, do_portion]);
Comment on lines +265 to +276
// The other FactAdd targets (Relido, Displayonly, Oc, OcUsgov)
// do NOT need supersession routing: Β§H.8 p145 only specifies
// NOFORN as a dominator on the FD&R chain. The OC-vs-OC-USGOV
// Β§H.8 p136/p140 supersession runs at join time (where both
// tokens can be observed on different portions); FactAdd of
// OcUsgov alongside existing Oc is a per-portion config that
// the lattice will resolve at the next join.
//
// Authority: Β§H.8 p145 (NOFORN: "Cannot be used with REL TO,
// RELIDO, EYES ONLY, or DISPLAY ONLY") + Β§D.2 Table 3 rows 1-2
// + Β§H.8 p157 (EYES ONLY: NSA-only, retains DissemControl::Eyes
// through lint per scheme.rs:190).
…site (Copilot R2 suppressed #2 + inverse case)

PR 4b-D.2 commit 13. Items 1, 2, 3 from Copilot R2. The D22 fix
landed in PR 4b-D.2 commit 3 routed NOFORN FactAdd through
DissemSet's token-axis supersession overlay. Copilot R2 surfaced
two completion gaps the original commit missed:

(a) Country-list axes stayed populated. `apply_fact_add` evicted
    `Rel` / `Relido` / `Displayonly` / `Eyes` TOKENS from
    `dissem_us` but did NOT clear the parallel
    `attrs.rel_to: Box<[CountryCode]>` /
    `attrs.display_only_to: Box<[CountryCode]>` country-list
    fields. The closure / PageRewrite paths cleared them via the
    `capco/noforn-clears-rel-to` / `capco/noforn-clears-display-only-to`
    rewrites; but direct `apply_intent` callers (E021 AEA β†’ NOFORN,
    E038 NODIS/EXDIS β†’ NOFORN) bypass `scheme.project`'s
    PageRewrite loop and got a marking with NOFORN + populated
    country lists β€” invalid per Β§H.8 p145.

(b) Inverse case: FactAdd of `RELIDO` / `DISPLAY ONLY` / `EYES`
    onto a marking with `dissem_us = [Nf]` was APPENDING the
    dominated token. Result: `dissem_us = [Nf, Relido]` β€” same
    Β§H.8 p145 violation as (a), opposite direction. The existing
    double-NOFORN-insertion guard didn't fire because target β‰  Nf;
    the supersession check needed an explicit inverse-case branch.

Fix in `crates/capco/src/scheme/actions/intent.rs::apply_fact_add`
CAT_DISSEM branch:

1. NEW: after `with_noforn_injected` rebuilds `dissem_us`,
   unconditionally clear `attrs.rel_to` and `attrs.display_only_to`
   if they're non-empty. Brings the full Β§H.8 p145 supersession
   to the injection site, not just the token-axis half.

2. NEW: at the start of the dissem branch, if `target` is one of
   `Rel` / `Relido` / `Displayonly` / `Eyes` AND `attrs.dissem_us`
   already contains `Nf`, return `IntentInapplicable` β€” the
   caller's FactAdd is dominated by existing state. Matches the
   existing idempotency guard for double-NOFORN insertion (no
   mutation, audit log doesn't see an applied no-op).

The PageRewrite layer (`capco/noforn-clears-*`) remains as
defense-in-depth. With Item 1 in place, the PageRewrites are
defensive-redundant on the production path; both layers converge
to the same correct Β§H.8 p145 output.

Authority: Β§H.8 p145 ("NOFORN ... Cannot be used with REL TO,
RELIDO, EYES ONLY, or DISPLAY ONLY") + Β§D.2 Table 3 rows 1-2
+ Β§H.8 p157 (EYES ONLY: NSA-only, retains `DissemControl::Eyes`
through lint). Re-verified 2026-05-18 against
`crates/capco/docs/CAPCO-2016.md`.

## Item 2 β€” fix the broken display_only_to integration test

Copilot R2 #2 was right: the previous
`noforn_clears_display_only_to_via_cross_portion_join` test didn't
exercise the new `capco/noforn-clears-display-only-to` rewrite.
`PageContext::expected_display_only` short-circuits to empty
whenever ANY portion has NOFORN (page_context.rs:881-896); with
portion 1 NOFORN-bearing, `out.display_only_to` was empty at join
time BEFORE the rewrite ran. The test passed regardless of whether
the rewrite existed.

Renamed to `noforn_clears_display_only_to_via_ucni_promote` and
pivoted the fixture to:
- Portion 1: classified DOD UCNI + DISPLAY ONLY USA, GBR (the
  UCNI carries the Β§H.6 p116 strip-and-promote trigger; the
  DISPLAY ONLY ensures portion 1 passes the row-19 all-or-nothing
  gate in `expected_display_only`).
- Portion 2: classified DISPLAY ONLY USA (also contributes
  display-permission so the gate doesn't short-circuit).

Verification by toggle:
- WITH `apply_fact_add` clearing AND the PageRewrite: test passes.
- WITHOUT `apply_fact_add` clearing (Item 1 disabled), WITH the
  PageRewrite: test still passes β€” PageRewrite catches the
  violation.
- WITHOUT the PageRewrite (Item 1 enabled, rewrite deleted): test
  passes β€” `apply_fact_add` catches it.
- Hypothetical: WITHOUT BOTH: test fails. Both layers are
  load-bearing for their respective injection paths.

The integration test now asserts the Β§H.8 p145 banner invariant
holds through the full `scheme.project` pipeline regardless of
which layer cleared. Defense-in-depth coverage documented inline.

## Item 3 β€” replace misleading example in noforn_clears.rs

Copilot R2 #3 was right: `CLOSURE_NOFORN_SAR` cannot be the
load-bearing trigger for the `noforn-clears-display-only-to`
rewrite β€” DISPLAY ONLY is in `FDR_DOMINATORS` (closure.rs:95-109)
which SUPPRESSES all 7 NOFORN-implies closure rules on any portion
that carries DISPLAY ONLY. The realistic trigger is a Pattern-A/C
PageRewrite (UCNI promote, NODIS-implies-NF) injecting NOFORN
post-join.

Rewrote both occurrences (line 65 + line 191 in noforn_clears.rs)
to cite the Pattern-C UCNI scenario and explain why closure rules
can't be the load-bearing path on this axis. Also documented the
post-R2-#1 defense-in-depth framing: `apply_fact_add` is now
self-sufficient; the rewrite covers future-refactor cases that
bypass it.

## D22 amendment

`decisions.md` D22 entry expanded to record the post-R2 completed
scope: (a) token-axis eviction (PR 4b-D.2 commit 3); (b)
country-axis clearing (Copilot R2 #1 commit 13); (c) inverse-case
rejection (Copilot R2 #1 commit 13). All three axes now maintained
at the apply_fact_add injection site, with the PageRewrite layer
as defense-in-depth.

## Adjacency-walk findings

Searched `FactAdd.*TOK_NOFORN|FactRef::Cve(TOK_NOFORN` across
crates/ β€” production callers:
- `crates/capco/src/rules_declarative.rs:1096` (E021 AEA add-NF)
- `crates/capco/src/rules_declarative.rs:1504` (E038-style add-NF)
- `crates/capco/src/scheme/rewrites/pattern_c.rs:219,269` (UCNI promotes)
- `crates/capco/src/scheme/rewrites/pattern_a.rs:146,210,289,369`
  (nodis/exdis/sbu-nf/les-nf-implies-noforn)

All 8 production NOFORN FactAdd sites now benefit from
self-sufficient apply_fact_add clearing. The two E021/E038-style
rules in rules_declarative.rs are the direct-caller paths Item 1's
country-axis clearing specifically targets; the 6 Pattern-A/C
rewrite paths go through `scheme.project`'s PageRewrite loop
already, but Item 1 makes their apply_fact_add invocation
self-sufficient too (defense-in-depth at the lower layer).

Searched `FactAdd.*TOK_RELIDO|FactAdd.*TOK_DISPLAY_ONLY|FactAdd.*TOK_EYES`
β€” ZERO production sites currently FactAdd dominated tokens
directly. The inverse-case rejection is correctness-by-construction
for future rule emissions.

## Verification gates

  cargo +stable fmt --all --check       : clean
  cargo +stable build --workspace       : clean
  cargo +stable clippy --workspace ...  : clean
  cargo +stable test --workspace        : 0 failures

Specific test runs:
- `category_action_intent::apply_fact_add_*` (6 new tests): all pass
- `closure_hotpath::noforn_clears_display_only_to_*` (2 tests,
  rewrote one + kept one): all pass

Tracking issue #528 (per-axis lattice Eq audit) remains the
follow-up for the systematic `DissemSet`/`JointSet`/`SupersessionSet`
review per PR #456's observational-state framing.
PR 4b-D.2 commit 14. Items 4-13 from Copilot R2 review. No
behavior change beyond the G13 sentinel content-ignorance fix
(Item 4); items 6-13 are doc-comment freshness, terminology, and
visibility nits. Item 5 (PR description revert-alignment) is the
sister commit applied via `gh pr edit 527`.

## Item 4 β€” G13 content-ignorance on closure + same-slice sentinels (HIGH)

Replaced two `debug_assert_eq!` call sites whose default `{:?}`
panic format would dump full `CanonicalAttrs` content (token
canonicals, country codes, declassify dates) on assertion
failure β€” a G13 audit-content-ignorance violation per Constitution
V Principle V.

- `crates/capco/src/scheme/marking_scheme_impl.rs` (closure-rewrite-
  application sentinel at the scheme-side D23 placement): switched
  to explicit `if raw != raw_snapshot.as_slice() { panic!("...{}
  portion(s) before vs {} after...", raw_snapshot.len(), raw.len());
  }`. Emits portion counts only.
- `crates/capco/src/scheme/marking.rs` (same-slice contract check in
  `join_via_lattice_with_context` between `portions` and
  `page_ctx.portions()`): same pattern β€” count-only panic mirroring
  `engine.rs::check_portions_unchanged` at lines 4540-4574.

Both sentinels stay `#[cfg(debug_assertions)]` (release-mode
elision unchanged); the change is panic-format-only. Pattern
matches the existing `check_portions_unchanged` precedent.

## Item 5 β€” PR description revert-alignment (via `gh pr edit 527`)

Updated PR #527 body in lock-step with commit 12's revert of
`benches/baseline.json::lint_10kb` top-level fields from {1030,
1033, 1036} (WSL2 dev capture) back to {912, 913, 914} (pre-PR
GHA `ubuntu-latest` capture from PR #498). The CI threshold
`upper_ci_us * 1.10 β‰ˆ 1005Β΅s` aligns with the reverted baseline;
the WSL2 1033Β΅s measurement is preserved in the
`_wsl2_dev_capture` sub-object for cross-host provenance. The
"Baseline refresh (commit 9)" paragraph is replaced with a
"Baseline calibration (commit 9 + commit 12 revert)" paragraph
that explains the calibration arc end-to-end.

Commits table extended with rows 11 (D24 trait-bound relaxation),
12 (R1 items 2-10 + baseline revert + new `noforn-clears-display-
only-to` PageRewrite), 13 (R2 correctness items 1-3), and 14
(this commit). The Decisions section absorbs the post-R2 D22
amendment + D24.

## Item 6 β€” "Monotone" claim correction

`crates/capco/src/scheme/marking_scheme_impl.rs` previously
described the post-flip pipeline as "monotone". Rewrote to be
precise: the closure operator IS monotone (adds facts only);
PageRewrites are NOT monotone in the FactSet sense β€” `Clear` and
`FactRemove` are anti-monotone. Pipeline-termination comes from
the topological ordering enforced by the engine's Kahn's
algorithm at `Engine::new` over `PageRewrite::reads` / `writes`,
not from monotonicity. Citation chain (closure monotonicity =
Β§B.3 Table 2 p21 NOFORN-implies axioms; topological scheduler =
lattice-design plan Β§4.7.4).

## Item 7 β€” `crates/capco/src/scheme/rewrites/mod.rs` rewrite count

Updated module-doc summary from "23 rewrites, in six groups" to
"24 rewrites, in six groups (post-PR-4b-D.2 Copilot R1 #2)"
reflecting the new `capco/noforn-clears-display-only-to` row
from R1 #2.

## Item 8 β€” `crates/capco/CAPCO-CONTEXT.md` JoinSemilattice paragraph

Rewrote the JoinSemilattice mention to reflect D24 (Copilot R1
#1, 2026-05-18): the `impl JoinSemilattice for CapcoMarking` was
removed; cross-axis CAPCO folds are projections (lossy, multi-
axis-aware), not lattice ops (idempotent, single-axis). Per-axis
lattices (`DissemSet`, `SciSet`, `RelToBlock`, etc.) still
implement `JoinSemilattice`.

## Item 9 β€” `crates/scheme/src/scope.rs` `DiffInput<M>` doc

The doc comment referenced `M::Marking` (a non-existent
associated type β€” `M` is itself the `Marking` associated type
on `MarkingScheme`). Corrected to "the scheme's marking type,
`MarkingScheme::Marking`" matching the actual generic parameter
shape.

## Item 10 β€” `crates/engine/src/engine.rs` `page_marking_arc` comment

Updated the inline comment near the cache build site from
referencing the stale `PageContext::project` to
`project_page_marking(&self.scheme, &page_context)` (which
internally invokes `CapcoScheme::project_from_page_context`).
Reflects the post-hot-path-flip code reality.

## Item 11 β€” `crates/ism/src/projected.rs` `from_canonical` doc

Rewrote the "Production callers" section with a properly
formatted bullet list (`marque_engine::project_page_marking` +
the bench), added blank lines around the list to satisfy
`clippy::doc_lazy_continuation`, and clarified that
`CapcoScheme::project` itself returns `CapcoMarking`
(`CapcoScheme::Marking`), not `ProjectedMarking` β€” the bridge to
`ProjectedMarking` is engine-side, not scheme-side.

## Item 12 β€” `crates/capco/src/scheme/rewrites/noforn_clears.rs`
   module doc

Rewrote module-level doc to enumerate the three active NOFORN-
clears rows in declaration order: `capco/noforn-clears-rel-to`,
`capco/noforn-clears-fdr-family`, `capco/noforn-clears-display-
only-to`. Updated the function-level doc on
`noforn_clears_rel_to_rewrite` (the constructor for the family)
to point at the same three rows.

Replaced the misleading `CLOSURE_NOFORN_SAR` example in two
places (line 65 + line 168). The doc cited SAR closure as the
load-bearing trigger for NOFORN injection that drives the
country-axis clearing, but `FDR_DOMINATORS` (`{NOFORN, RELIDO,
DISPLAY_ONLY, AnyInCategory(CAT_REL_TO), EYES}`) suppresses all 7
`CLOSURE_NOFORN_*` rules when DISPLAY ONLY is present in the
input. Replaced with the realistic load-bearing path β€” the
Pattern-C `capco/dod-ucni-promotes-noforn-when-classified`
PageRewrite per Β§H.6 p116 β€” which IS reachable from a portion
carrying classified DOD UCNI + DISPLAY ONLY.

## Item 13 β€” `crates/capco/src/scheme/actions/page_context.rs`
   misnomer

The function-level doc on `page_context_to_attrs` claimed the
parity-gate helper `project_via_page_context` was "used by" this
function. The parity-gate helper actually INLINES the
`expected_*` accessor calls directly rather than calling
`page_context_to_attrs`. Reworded to "MIRRORS" with an explicit
note about the inlined accessor calls so a future reader of
either site doesn't get the dependency-direction backwards. Both
sites are retired together at PR 4b-E alongside the PageContext
aggregator.

## Adjacency-walk findings (Item 1 backstop)

Re-confirmed the adjacency walk that backed commit 13's
correctness fix: 8 production `FactAdd.*TOK_NOFORN` call sites,
all benefit from `apply_fact_add`'s self-sufficient Β§H.8 p145
maintenance. Zero production sites currently FactAdd
`RELIDO` / `DISPLAY_ONLY` / `EYES` directly β€” the inverse-case
rejection (commit 13) is correctness-by-construction for any
future emission path.

## Test verification (toggle-cycle, Item 2 backstop)

Re-ran the delete-rewrite-observe-fail-restore-observe-pass
cycle from commit 13's verification work:

- Commenting out the `apply_fact_add` country-axis clearing
  (Item 1 fix in commit 13) causes the 6 new R2 tests in
  `crates/capco/tests/category_action_intent.rs` (the
  `apply_fact_add_*` cluster) to fail at the direct-caller
  path (`scheme.apply_intent(&marking, &intents)` for E021 / E038-
  shape calls).
- The integration test
  `crates/engine/tests/closure_hotpath.rs::noforn_clears_display_only_to_via_ucni_promote`
  still passes when only the apply_fact_add fix is commented
  out (the `capco/noforn-clears-display-only-to` PageRewrite
  catches the country-axis clearing via the page-rewrite path).
- Commenting out BOTH the apply_fact_add country-axis clearing
  AND the new `capco/noforn-clears-display-only-to` PageRewrite
  causes the integration test to fail (banner emits
  `NOFORN ... DISPLAY ONLY USA, GBR`).

Both layers are load-bearing for their respective call paths.
The PageRewrite layer survives as defense-in-depth per D22.

## Verification gates

- `cargo +stable fmt --all --check`: clean
- `cargo +stable clippy --workspace --tests -- -D warnings`: clean
- `cargo +stable test --workspace`: 0 failures
- `cargo +stable build --workspace`: clean

## Citation re-verification (Constitution VIII)

`Β§B.3 Table 2 p21`, `Β§H.6 p116`, `Β§H.7 p127`, `Β§H.8 p145`,
`Β§H.8 p157`, `Β§G.2 Table 5 p40` re-verified verbatim against
`crates/capco/docs/CAPCO-2016.md` at this commit's authoring
time. No fabrications, no drift, no propagation defects.

## Constitution V Principle V (G13)

Two sentinel panic-format sites switched from debug_assert_eq!'s
`{:?}`-content-leaking format to explicit `if-panic` with
count-only output. Mirrors the existing `check_portions_unchanged`
count-only pattern at `engine.rs:4540-4574`. No document content,
no token canonicals, no country codes, no declassify dates in any
panic output. The grep-target property holds: every
sanctioned content readout site still goes through `expose_secret()`.

Refs: PR #527, Copilot R2 review (15 items: 13 inline + 2
suppressed; commit 13 covers correctness items 1-3, this commit
covers doc items 4-13).
The lint scans `crates/*/src` for CAPCO Β§-citations and flags
`Β§I`, `Β§J`, `Β§K` as non-normative (history / examples / acronyms).
The reference to Constitution VII Β§IV at marking.rs:58 was
parsing as `Β§I` followed by `V` β€” two defects emitted on the same
multi-byte UTF-8 source position.

Use `Constitution VII Principle IV` (the form used at
`crates/capco/src/rules.rs:3444`) to dodge the regex.
@bashandbone bashandbone merged commit ebbefda into staging May 18, 2026
22 of 24 checks passed
@bashandbone bashandbone deleted the refactor-006-pr-4b-d-2-hotpath-flip branch May 18, 2026 11:08
@bashandbone bashandbone review requested due to automatic review settings May 18, 2026 11:19
bashandbone added a commit that referenced this pull request May 19, 2026
…s + baseline correction)

Copilot R1 ran on commit 32c33bb (the original R1 fix-ups) and
surfaced 5 substantive findings, plus on verification a 6th issue
emerged β€” the original R1 commit's "pre-4b baseline = 15"
correction was itself wrong by walked-adjacency-omission.

All 6 issues resolved. Ground truth confirmed by walking
`git show 5fd5a33:crates/capco/src/scheme/rewrites/*.rs`
(the first commit that split rewrites out of monolithic scheme.rs)
+ counting PageRewrite-row literals in each file at HEAD.

1. **Pin test (Constraint::Custom dedup masking, Copilot R1 #1)** β€”
   The test at `post_pr_4b_declares_exact_39_custom_constraints`
   collected into a BTreeSet before counting; a duplicate
   `Constraint::Custom("capco/foo", ...)` row would dedupe in the
   set and the size-only assertion would still pass. Fix: collect
   into a Vec first, then compare `raw_count == set.len()` β€”
   triple-pin (raw count + set size + raw_count_equals_set_size).
   The equality assertion is the load-bearing dedup check.

2. **CI hollow-coverage (Copilot R1 #2)** β€” `corpus_parity.rs` is
   currently `#![cfg(any())]` (disabled by PR 3c.B Commit 10
   pending FixProposal-shape rewrite). The `pr-4b-corpus-regression`
   job mirroring PR 3b's T029 inherited the same dormancy hole.
   Fix: add three explicit `cargo test --test` invocations for
   `post_4b_lattice_inventory_pin`, `lattice_static_assertions`,
   and `post_3b_registration_pin`. With these four active pins
   plus the still-ACTIVE `corpus_accuracy` and `corpus_provenance`
   suites, the job has real 4b-specific coverage even while
   corpus_parity stays dormant. Inline comments clarify which
   suites are ACTIVE vs dormant so a future reader who sees the
   `corpus_parity` invocation passing doesn't conclude it gives
   real coverage.

3. **Attestation `DeclassExemptionAccumulator` citation
   (Copilot R1 #3)** β€” Walked-adjacency miss from the R1 fix-up
   on `DeclassifyOnLattice`. Same root cause: Β§H.6 grounds the
   AEA *exception*, not the general declassification-exemption
   hierarchy rule. Fix: Β§H.6 β†’ Β§E.3 pp32-33 (Multiple Sources)
   + Β§E.1 p31 (exemption catalog 25X#/50X#/75X#), matching the
   doc-comment Β§-authority block at the type definition.

4. **Attestation `Pre-4b baseline` cell (Copilot R1 #4)** β€”
   The table at "Per-axis net-delta math" had "~14 (pre-Pattern-B/C)"
   with the tilde flagging an unverified approximate. Fix:
   exact baseline = 14 (4 pattern_a + 2 noforn_clears + 8
   transmutation_stubs). The 3rd noforn_clears row
   (`noforn-clears-display-only-to`) was added by PR 4b-D.2
   (#527), not pre-4b.

5. **Pin doc-comment derivation chain (Copilot R1 #5)** β€”
   The header doc-comment said "Pattern C 8 rows" in the 4b-C
   delta, but 4b-C landed only 7 Pattern-C rows; the 8th
   (`sbu-nf-evicted-by-classified`) was added later by #541 in
   the 4b-F window. Fix: change "Pattern C 8 rows" β†’ "Pattern C
   7 rows" in the 4b-C section; add the #541 +1 entry separately
   in the 4b-F section; document the 4b-D.2 +1 PageRewrite
   contribution that the original doc-comment also omitted.

6. **R1 baseline correction (walked-adjacency from #4)** β€”
   The original R1 fix-up commit (32c33bb) changed
   `CLAUDE.md:283` from "14 β†’ 27" to "15 β†’ 27" based on the
   architect plan's "15 = 4 pattern_a + 3 noforn_clears + 8
   transmutation_stubs" claim. The architect's count of
   3 noforn_clears rows was off-by-one: pre-4b had only 2
   (`noforn-clears-rel-to` + `noforn-clears-fdr-family`); the
   3rd row (`noforn-clears-display-only-to`) was added by
   4b-D.2 (#527). Walked the git history at `5fd5a339` (Stage
   2 PR A sub-split of `scheme/rewrites/`) and counted the
   literal rows: pattern_a 4 / noforn_clears 2 /
   transmutation_stubs 8 = 14 pre-4b. The original 4b-C
   landing CLAUDE.md entry's "catalog row count 14 β†’ 23" is
   ground truth; my R1 fix-up over-corrected to 15 β†’ 27 and
   needs the inverse correction back to 14 β†’ 27 with the
   full per-sub-PR breakdown:

       Pre-4b:    14  (4 pattern_a + 2 noforn_clears + 8 transmutation_stubs)
       4b-C +9:   23  (Pattern-B 2 + Pattern-C 7)
       4b-D.2 +1: 24  (noforn-clears-display-only-to per Β§H.8 p145)
       #541 +1:   25  (sbu-nf-evicted-by-classified Pattern-C row 2b per Β§H.9 p178)
       #552 +1:   26  (sbu-nf-supersedes-sbu per Β§H.9 p178)
       #555 +1:   27  (les-nf-supersedes-les per Β§H.9 p185)

   13 new rows over the 4b umbrella + post-4b-F window.
   14 + 13 = 27 βœ“ (matches the test's hardcoded expected count).

Verification gate (all GREEN):
- `cargo +stable test -p marque-capco --test post_4b_lattice_inventory_pin` β€” 3/3 PASS (including new raw_count_equals_set_size assertion)
- `cargo +stable test -p marque-capco --test lattice_static_assertions` β€” 0/0 (compile-time only, file builds clean)
- `cargo +stable test -p marque-capco --test post_3b_registration_pin` β€” 1/1 PASS (38 unchanged)
- `cargo +stable clippy --workspace --all-targets -- -D warnings` β€” 0 warnings
- `cargo +stable fmt --check` β€” clean
- `cargo run --manifest-path tools/citation-lint/Cargo.toml --release -- .` β€” 0 defects

Per project memory `feedback_audit_predicates_against_source`
(Constitution VIII propagation discipline) and
`feedback_double_check_the_plan` (when the architect plan and
ground truth conflict, ground truth wins; the architect plan was
a preflight draft, the merged code is authoritative). Per project
memory `feedback_walked_adjacencies` (every fix walks the related
code paths) β€” the DeclassExemptionAccumulator fix #3 is a textbook
walked-adjacency from the R1 DeclassifyOnLattice fix that I missed
the first time; the baseline correction #6 is a walked-adjacency
from the architect plan's count that I trusted instead of verifying.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

capco/ism CAPCO classification markings, ODNI ISM schema, and the marque-capco / marque-ism rule chain EPIC-Lattice Addressed by Epic for lattice refactor (5-2-26 plan) refactor issue/pr related to refactoring rust touches rust code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants