Files
AILang/docs/plans/design-md-rolesplit.tidy.md
T
Brummel 4f61a7aa4f plan: design-md-rolesplit.tidy — audit drift fix (history->journal, sweep re-scope, clause-3 widen)
7 tasks executing the audit Resolution's 5 points: (1) gate-first
RED — widen design_index_pin.rs clause-3 to a hand-rolled PROVABLE
SUPERSET of architect_sweeps Sweep-1 over design/contracts/ (no
regex dep; subsumption proof inline), verify RED vs the un-stripped
tree; (2-4) sentence-level strip the faithfully-migrated
history/decision-record prose out of typeclasses.md / str-abi.md /
scope-boundaries.md into the decision-record journal, per recon's
per-sentence contract-residue map, each docs_honesty_pin pinned run
guarded contiguous; (5) fix float-semantics.md stale 'see Str ABI
below' -> str-abi.md; (6) re-scope architect_sweeps DESIGN_GLOB to
design/contracts only (models/ is the narrative tier) + lockstep
ailang-architect.md; (7) whole-tree GREEN gate (clause-3 RED->GREEN,
4/4, sweep exit 0, suite >=646/0, acceptance grep CLEAN).
Self-review 8/8.
2026-05-19 13:22:22 +02:00

24 KiB
Raw Blame History

design-md-rolesplit.tidy — Implementation Plan (audit drift fix)

Parent spec: docs/specs/2026-05-19-design-md-rolesplit.md Scope authority: docs/journals/2026-05-19-audit-design-md-rolesplit.md "Resolution" (committed 2ba5e16) — the architect [medium]+[low] drift fix, 5 orchestrator-decided points.

For agentic workers: REQUIRED SUB-SKILL: use skills/implement to run this plan. Steps use - [ ] checkboxes for tracking.

Goal: Close the spirit-vs-letter gap: extract the faithfully-migrated history/decision-record prose out of the 3 affected contract files into the decision-record journal, fix the one stale cross-ref, re-scope the honesty sweeps to design/contracts/ only, and widen design_index_pin.rs clause-3 so the in-code hard gate provably subsumes architect_sweeps.sh Sweep-1 over the contracts scope.

Architecture: TDD by gate-first ordering — Task 1 widens clause-3 and verifies it goes RED against the current (un-stripped) contract files (proving the widened gate catches what the 6 literal markers missed); Tasks 25 strip the history prose (sentence-level per recon's contract-residue map) → clause-3 returns GREEN; Task 6 re-scopes the sweep + its lockstep doc; Task 7 is the whole-tree gate. Build stays green throughout (only one test file's body + Markdown change; the test compiles, fails at runtime until the strip lands). The widened clause-3 is a hand-rolled provable superset of Sweep-1 (no regex dep).

Tech Stack: Markdown (design/contracts/*.md, docs/journals/2026-05-19-design-decision-records.md), one Rust test (crates/ailang-core/tests/design_index_pin.rs), bench/architect_sweeps.sh, skills/audit/agents/ailang-architect.md.

Pinned-byte hazard (recon Cross-references — honor in every strip task): each history strip runs adjacent to a docs_honesty_pin.rs verbatim substring pin. norm() collapses whitespace but the pinned run must stay a contiguous token-run:

  • str-abi.md:19 type-installed; codegen is reserved and not yet shippeddocs_honesty_pin.rs:121-122
  • typeclasses.md:317 `io/print_str` is the only built-in direct-output effect-opdocs_honesty_pin.rs:125-126
  • scope-boundaries.md:9 `Diverge` is a reserved effect name with no op and no codegendocs_honesty_pin.rs:112-113 (over-strip guard — NOT in any edit span; do not touch)

Task 1: widen design_index_pin.rs clause-3 (gate-first, RED)

Files:

  • Modify: crates/ailang-core/tests/design_index_pin.rs:130-157 (fn contracts_carry_no_decision_record_prose)

  • Step 1: Replace the clause-3 body with the widened, provable-superset matcher

Replace fn contracts_carry_no_decision_record_prose (current :130-157, the 6-literal-marker markers array + body.contains(m) loop) with this. The other 3 clauses + helpers (root :10-12, read :14-17, norm :19-21, index_tables :23-62, link_target_exists :67-81, design_md_is_gone :83-90, every_index_link_resolves :92-107, every_contract_names_a_resolvable_ratifying_test :109-128) are untouched.

#[test]
fn contracts_carry_no_decision_record_prose() {
    // clause 3 — the conflation tripwire, widened (design-md-rolesplit.tidy)
    // to a PROVABLE SUPERSET of bench/architect_sweeps.sh Sweep-1's
    // history-anchor regex over the design/contracts/ scope.
    //
    // Invariant established by this milestone: clause-3 GREEN ⟹
    // Sweep-1 finds nothing in design/contracts/.
    //
    // Subsumption proof (each Sweep-1 alternative ⊆ a check below):
    //   Sweep-1 = 'Iter [0-9]+[a-z]?(\.[0-9]+)?|Family [0-9]+
    //             |^[^/]*2026-[0-9]{2}-[0-9]{2}|\*\*Status: |pre-[0-9]+[a-z]?
    //             |[0-9]+[a-z]? sketch|21\.g'
    //   - "Iter <n>" / "Family <n>" / "21.g" / "<n> sketch"
    //         ⊆ iter_code_token() (any "iter"/"family"/"milestone"
    //           token followed by a code, case-insensitive)
    //   - "2026-NN-NN"                ⊆ iso_date() (any 20NN-NN-NN)
    //   - "**Status: " / "pre-<n>"    ⊆ PHRASES (literal, ci)
    //   Broader than Sweep-1 by construction; never narrower.
    let dir = root().join("design/contracts");

    // case-insensitive literal decision-record / history phrases
    const PHRASES: &[&str] = &[
        "we rejected", "an earlier draft", "earlier draft committed",
        "why not other", "was retired", "were retired", "rollback plan",
        "previously all", "deliberately deferred", "new-baseline decision",
        "amends the above", "out of scope per", "**status:", "sketch",
        "the journal records when", "unchanged from the original draft",
    ];

    // a token that looks like an iter / milestone / family provenance code:
    // a keyword followed (within 2 words) by an alnum/dot code or a digit.
    fn iter_code_token(low: &str) -> Option<String> {
        for kw in ["iter ", "milestone ", "family ", "axis-", "pre-"] {
            let mut from = 0;
            while let Some(i) = low[from..].find(kw) {
                let p = from + i + kw.len();
                let tail: String = low[p..].chars().take(12).collect();
                // "iter 24.1", "iter str-concat,", "milestone 22",
                // "family 18", "axis-7", "pre-22a"
                if tail.chars().next().map_or(false, |c| {
                    c.is_ascii_alphanumeric()
                }) && tail.chars().take(3).any(|c| {
                    c.is_ascii_digit() || c == '-' || c == '.'
                }) {
                    return Some(format!("{kw}{tail}"));
                }
                from = p;
            }
        }
        None
    }

    // an ISO-8601-ish date: 20NN-NN-NN
    fn iso_date(b: &[u8]) -> Option<String> {
        for w in b.windows(10) {
            if w[0] == b'2' && w[1] == b'0'
                && w[2].is_ascii_digit() && w[3].is_ascii_digit()
                && w[4] == b'-' && w[5].is_ascii_digit() && w[6].is_ascii_digit()
                && w[7] == b'-' && w[8].is_ascii_digit() && w[9].is_ascii_digit()
            {
                return Some(String::from_utf8_lossy(w).into_owned());
            }
        }
        None
    }

    for entry in fs::read_dir(&dir).expect("design/contracts/ exists") {
        let p = entry.unwrap().path();
        if p.extension().and_then(|e| e.to_str()) != Some("md") {
            continue;
        }
        let raw = fs::read_to_string(&p).unwrap();
        let low = norm(&raw).to_lowercase();
        for ph in PHRASES {
            assert!(
                !low.contains(ph),
                "clause-3: history/decision-record phrase {:?} in contract {:?}",
                ph, p.file_name().unwrap()
            );
        }
        assert!(
            iter_code_token(&low).is_none(),
            "clause-3: iter/milestone provenance code {:?} in contract {:?}",
            iter_code_token(&low).unwrap(), p.file_name().unwrap()
        );
        assert!(
            iso_date(raw.as_bytes()).is_none(),
            "clause-3: ISO date {:?} in contract {:?} (history anchor)",
            iso_date(raw.as_bytes()).unwrap(), p.file_name().unwrap()
        );
    }
}
  • Step 2: Verify clause-3 is now RED against the un-stripped tree

Run: cargo test -p ailang-core --test design_index_pin contracts_carry_no_decision_record_prose 2>&1 | tail -8 Expected: FAIL — the assertion fires on the still-present history prose (e.g. clause-3: ISO date "2026-05-13" in contract "str-abi.md" or iter/milestone provenance code "iter 24.1…"). This is the required RED: the widened gate correctly catches what the 6 literal markers missed.

  • Step 3: Verify the other 3 clauses + build still GREEN

Run: cargo test -p ailang-core --test design_index_pin 2>&1 | grep -E '^test (design_md_is_gone|every_index_link_resolves|every_contract_names) ' Expected: those 3 still ok (only clause-3 RED). cargo build -p ailang-core --tests 2>&1 | tail -1Finished (the new matcher compiles; no new dependency).


Task 2: strip history prose from design/contracts/typeclasses.md

Files:

  • Modify: design/contracts/typeclasses.md (:77-78, :182-186, :197-200, :279-289, :291-300, :302-317 — recon's complete scan)

  • Modify: docs/journals/2026-05-19-design-decision-records.md (append at EOF, after :431)

  • Step 1: Append the migrated typeclasses history to the decision-record journal

Append at EOF of docs/journals/2026-05-19-design-decision-records.md:


## Migrated from design/contracts/ at design-md-rolesplit.tidy (2026-05-19)

History/decision-record prose extracted from contract files by the
post-audit tidy (the `###`-whole relocation in iter .1 dragged these
into contracts; they are relitigation-guard content, not contract).

### typeclasses.md — Prelude-classes evolution

An earlier draft committed to a fixed Prelude (Show/Eq/Ord on the
primitives), but the implementation work to wire `int_to_str` as a
heap-allocated-string runtime primitive proved substantively
separable from the typeclass machinery itself, and the LLM-utility
case for primitive `Show` is weak (LLM-natural form is `int_to_str
x`, not `show x`). Milestone 23 added `Ordering`/`Eq`/`Ord` +
primitive instances + the five helpers; operator routing through
`Eq`/`Ord` and `print`-rewire were held out for bench-rebaseline /
post-mq-dispatcher reasons; `Show` shipped in milestone 24 (iters
24.2/24.3). `MethodNameCollision` was retired at iter mq.3
(cross-class method sharing became structurally legal). The
dedicated `KindMismatch` diagnostic was retired at iter ctt.3
(`BareCrossModuleTypeRef` subsumes it).
  • Step 2: Rewrite the 6 typeclasses.md spans present-tense (recon's contract-residue map)

Apply each, keeping every present-tense contract fact, dropping the history clause:

  • :77-78 Three invariants make this work, all installed in milestone 24's iter 24.3 and worth keeping load-bearing:Three invariants make this work:

  • :182-186 drop the parenthetical (MethodNameCollision was retired at iter mq.3. …) opening; keep the present-tense replacement sentence: Cross-class method sharing is structurally legal; ambiguity surfaces at the call site via AmbiguousMethodResolutionor, for class-fn name overlap, via theclass-method-shadowed-by-fn warning. See §"Method dispatch" below.

  • :197-200 keep … is rejected earlier by the canonical-form validator as BareCrossModuleTypeReffis a bare, non-primitive name not declared as aTypeDef in the owning module.; drop so the dedicated KindMismatch diagnostic was retired at iter ctt.3.

  • :279-289 (the "Milestone 22 ships no built-in Prelude classes / An earlier draft committed to…" paragraph) — delete wholesale (pure decision-record; the present-tense state is the m24 prelude below).

  • :291-300 replace the Milestone 23 amends the above: paragraph with present-tense: The prelude ships the OrderingADT, theEqandOrdclasses, primitiveEq Int/Bool/StrandOrd Int/Bool/Strinstances, and the five polymorphic free-fn helpersne/lt/le/gt/ge. Float has neither EqnorOrdinstance per §"Float semantics"; a polymorphic helper invoked at Float firesNoInstance at typecheck with a Float-aware diagnostic cross-referencing this section. (drop "remain out of scope per their original substantive reasons (bench rebaseline, milestone-24 dependency…); Show itself ships in milestone 24" — that relitigation is now in the journal.)

  • :302-317 replace the Milestone 24 amends the above further: opening with present-tense: The prelude ships class Show a where show : (a borrow) -> Strand primitiveShow Int, Show Bool, Show Str, Show Float instances. Float **is** included in Show (unlike Eq/Ord) …keep verbatim and byte-contiguous the tail through :317 `io/print_str` is the only built-in direct-output effect-op (pinned by docs_honesty_pin.rs:125-126) and the :315 print_mono_body_shape.rs cross-reference; drop only Milestone 24 amends the above further, (iter 24.2), shipped in iter 24.3 (→ replace shipped in iter 24.3 with body with with body).

  • Step 3: Verify typeclasses.md pin + clause-3 progress

Run: cargo test -p ailang-core --test docs_honesty_pin 2>&1 | grep -E 'test result:' Expected: PASS (the io/print_str… pinned run intact). Run grep -n 'io/print_str is the only built-in direct-output effect-op' design/contracts/typeclasses.md` Expected: exactly one contiguous match (pin survived).


Task 3: strip history prose from design/contracts/str-abi.md

Files:

  • Modify: design/contracts/str-abi.md (:14, :16, :18, :20, :22-23, :74, :38-43, :45-47)

  • Modify: docs/journals/2026-05-19-design-decision-records.md (append under the tidy section)

  • Step 1: Append str-abi history to the journal

Append under the ## Migrated … section:


### str-abi.md — heap-Str builtin provenance + deferred-operator rationale

Builtin iter provenance: `int_to_str`/`bool_to_str`/`float_to_str`/
`str_clone` shipped iter 24.1; `str_concat` shipped iter str-concat
(2026-05-13); the non-escape static-Str lowering pass landed iter
18b, move-tracking partial-drop iter 18d.3. Operator routing through
classes is deliberately deferred — it would migrate every fixture
and re-baseline the bench corpus (a new-baseline decision, not an
iter detail). `Num` is not in milestone 22: class-based numeric
overloading would invoke literal-defaulting which axis-7 excluded.
  • Step 2: Strip the (iter …) parentheticals + deferred rationale

  • :14 - int_to_str : (borrow Int) -> Str (iter 24.1) — decimal rendering- int_to_str : (borrow Int) -> Str — decimal rendering

  • :16 - bool_to_str : (borrow Bool) -> Str(iter 24.1) —"true"/"false". → drop (iter 24.1)

  • :18 - float_to_str : (borrow Float) -> Str (iter 24.1) — → drop (iter 24.1). Hazard: :19 type-installed; codegen is reserved and not yet shipped. is pinned (docs_honesty_pin.rs:121-122) — keep :19 byte-identical and contiguous with :18's .

  • :20 - str_clone : (borrow Str) -> Str (iter 24.1) — allocates a fresh → drop (iter 24.1)

  • :22-23 - str_concat : (borrow Str, borrow Str) -> Str (iter str-concat,\n 2026-05-13) — combines …- str_concat : (borrow Str, borrow Str) -> Str— combines twoStrvalues into a single ownedStr. (collapse the soft-wrap; drop the whole (iter str-concat, 2026-05-13) — this is the Sweep-1 exit-1 trip)

  • :74 drop (iter 18b) and (iter 18d.3); keep the present-tense mechanism sentence.

  • :38-43 keep `==`, `<`, `<=`, `>`, `>=` REMAIN primitive operators. Class methods are accessed by name (`eq x y`, `lt x y`, …), not via these operators. ; drop (unchanged from the original draft) and the whole Routing operators through classes is deliberately deferred — … new-baseline decision rather than an iter detail.

  • :45-47 keep Arithmetic operators (+, -, *, /) stay primitive and per-type.; drop `Num` is NOT in milestone 22. and Class-based numeric overloading would invoke literal-defaulting which axis-7 already excluded.

  • Step 3: Verify str-abi pin intact

Run: grep -n 'type-installed; codegen is reserved and not yet shipped' design/contracts/str-abi.md Expected: exactly one contiguous match. Run cargo test -p ailang-core --test docs_honesty_pin 2>&1 | grep 'test result:' → PASS.


Task 4: strip history prose from design/contracts/scope-boundaries.md

Files:

  • Modify: design/contracts/scope-boundaries.md (:5-6, :14-17, :36-38)

  • Modify: docs/journals/2026-05-19-design-decision-records.md (append under the tidy section)

  • Step 1: Append scope-boundaries history to the journal


### scope-boundaries.md — retired ops + deferral rationale

Polymorphic-fn-as-value is deferred because it would need one
closure-pair global per instantiation. The per-type print ops
`io/print_int`/`io/print_bool`/`io/print_float` were retired in iter
rpe.1 (the polymorphic `print` is the canonical non-Str output path).
  • Step 2: Rewrite the 3 scope-boundaries spans

  • :5-6 keep Snapshot of the current boundary.; delete Items move out of this list as iterations land; the JOURNAL records when. (process-meta, not a scope contract; not decision-record either — plain delete, not journal-moved).

  • :14-17 keep Polymorphic fns must be **directly called** at the use site. Passing a polymorphic fn as a value (let f = id in f(42)) is not yet supported.; drop — it would need one closure-pair global per instantiation, deferred. (rationale → journal, Step 1).

  • :36-38 keep Effects on function signatures, with do op(args) for direct effect ops (io/print_str). The polymorphic print (§"Polymorphic print") is the canonical output path for non-Str values.; drop The per-type print ops io/print_int, io/print_bool, io/print_float were retired in iter rpe.1; (→ journal).

  • Step 3: Over-strip guard — the Diverge pin must be untouched

Run: grep -n 'Diverge is a reserved effect name with no op and no codegen' design/contracts/scope-boundaries.md Expected: exactly one match at the original location (:9, unedited — it is NOT in any edit span; this guards against collateral deletion). cargo test -p ailang-core --test effect_doc_honesty_pin 2>&1 | grep 'test result:'` → PASS.


Task 5: fix the stale cross-ref in design/contracts/float-semantics.md

Files:

  • Modify: design/contracts/float-semantics.md:100

  • Step 1: Repoint the stale "below" direction

:99-100 verbatim Both allocate a fresh heap-Str slab at the call site (see "Str ABI" below for the dual realisation) and carry ret_mode: Own. Replace (see "Str ABI" below for the dual realisation)(see design/contracts/str-abi.md for the dual realisation). (design/contracts/str-abi.md exists — confirmed recon; INDEX row design/INDEX.md:93.)

  • Step 2: Verify float-semantics pin intact + no stale "below"

Run: grep -n '"Str ABI" below' design/contracts/float-semantics.md || echo "CLEAN: stale below gone" Expected: CLEAN: stale below gone. Run cargo test -p ail --test eq_float_noinstance 2>&1 | grep 'test result:' → PASS.


Task 6: re-scope architect_sweeps.sh honesty sweeps + lockstep doc

Files:

  • Modify: bench/architect_sweeps.sh:25 (+ header :5-7)

  • Modify: skills/audit/agents/ailang-architect.md:77-84

  • Step 1: Narrow DESIGN_GLOB to design/contracts

bench/architect_sweeps.sh:25 verbatim DESIGN_GLOB="design/contracts design/models"DESIGN_GLOB="design/contracts". (Substantive reason, encoded as a comment on the same line: # contracts only — design/models is the narrative tier; "as of milestone N" context is legitimate there.) Header :5-7 prose design/ prose set (design/contracts + design/models)design/contracts (the honesty-bound contract surface; design/models is the narrative tier, out of honesty-sweep scope). The 5 run_sweep regexes (:46-59) are unchanged; the exit-code block (:61-64) is unchanged.

  • Step 2: Lockstep ailang-architect.md

In skills/audit/agents/ailang-architect.md the :77-84 history-anchor-regrowth bullet: append one sentence after the exit-semantics lines: Sweeps 14 scan design/contracts/only —design/models/ is the narrative tier (§ reading-list: "context, not a drift surface"), so a milestone-context phrase there is not a regrowth. (:34 already states models/ is not a drift surface; this makes the sweep scope explicit and consistent.) No other edit; Sweep count stays 5 so docs_honesty_pin.rs:10 //! needs no change.

  • Step 3: Verify the sweep now scans contracts only

Run: bash bench/architect_sweeps.sh; echo "exit=$?" Expected: at this point Tasks 25 have stripped the contract history, so exit=0 and All five sweeps clean. (If exit 1 with a design/contracts/... match, a history span was missed — fix it; if the match is design/models/... the glob narrowing didn't apply — re-check :25.)


Task 7: whole-tree GREEN gate (milestone-close proof)

Files: none (verification only).

  • Step 1: clause-3 now GREEN, design_index_pin 4/4

Run: cargo test -p ailang-core --test design_index_pin 2>&1 | grep -E '^test |test result:' Expected: all 4 okcontracts_carry_no_decision_record_prose flipped RED→GREEN (history stripped), the other 3 unchanged-GREEN.

  • Step 2: the retargeted pins + E2Es still GREEN

Run: cargo test -p ailang-core --test design_schema_drift --test docs_honesty_pin --test effect_doc_honesty_pin 2>&1 | grep 'test result:' Expected: 3× PASS. Run cargo test -p ail --test eq_float_noinstance --test show_no_instance_e2e 2>&1 | grep 'test result:' Expected: 2× PASS.

  • Step 3: whole workspace GREEN + sweep exit 0 + acceptance grep

Run: cargo test --workspace 2>&1 | grep -oE '[0-9]+ passed; [0-9]+ failed' | awk -F'[ ;]' '{p+=$1; f+=$4} END {print "TOTAL passed="p" failed="f}' Expected: failed=0 and passed ≥ 646 (the iter .1 baseline; the widened clause-3 adds no new test count, it replaces a body). Run: bash bench/architect_sweeps.sh; echo "exit=$?" Expected: exit=0. Run: grep -rIn 'DESIGN\.md' . --exclude-dir=.git --exclude-dir=target | grep -vE '(^\./)?(docs/(journals/|specs/|plans/|roadmap\.md|WhatsNew\.md|journal-archive\.md)|bench/orchestrator-stats/)' | grep -v 'design-md-rolesplit' | grep -v 'design_index_pin.rs' || echo "CLEAN" Expected: CLEAN (no live-ref regression from the tidy edits).


Self-review (planner Step 5)

  1. Spec/Resolution coverage: Task 1 = Resolution-4 (clause-3 widen); Tasks 24 = Resolution-1 (history → journal, 3 files); Task 5 = Resolution-2 (stale "below"); Task 6 = Resolution-3 (sweep re-scope + lockstep); Task 7 = Resolution-5 exit state. All 5 points covered. ✓
  2. Placeholder scan: no TBD/TODO/"similar to". Every strip names the exact recon line + verbatim before→after text. ✓
  3. Type/path consistency: design/contracts/str-abi.md, docs/journals/2026-05-19-design-decision-records.md, contracts_carry_no_decision_record_prose used identically across tasks; the journal append section header ## Migrated … (2026-05-19) consistent Tasks 2/3/4. ✓
  4. Step granularity: each step is one file's edit-set or one Run; the largest (Task 2 Step 2, 6 spans) is one mechanical-per-span pass, 25 min. ✓
  5. No commit steps: none. ✓
  6. Pin/replacement contiguity: the three docs_honesty_pin.rs pinned runs (str-abi.md:19, typeclasses.md:317, scope-boundaries.md:9) are called out as hazards in the header AND verified by an explicit grep -n contiguity check in Tasks 2/3/4 Step 3 + the over-strip guard in Task 4 Step 3. The widened clause-3 matcher (Task 1) is a single self-contained function body — no cross-task substring split. ✓
  7. Compile-gate vs deferred-caller: the only Rust edit is one self-contained test fn body (Task 1); it compiles standalone (no signature change, no caller, no new dependency — hand-rolled matcher). Every Run gate is satisfiable at its task: Task 1 Step 2 expects RED (pre-strip), Task 7 expects GREEN (post-strip); no gate depends on a later task's code. Build green throughout (test compiles; only its runtime assertion is RED until the strip lands). ✓
  8. Verification filter strings resolve: --test design_index_pin / design_schema_drift / docs_honesty_pin / effect_doc_honesty_pin / eq_float_noinstance / show_no_instance_e2e all exist (iter .1 created/retargeted them). The clause-3 filter contracts_carry_no_decision_record_prose is the exact fn name. Task 7 Step 3 uses the unfiltered cargo test --workspace with a numeric failed=0 + passed≥646 assertion (cannot masquerade as "nothing ran"). The grep/bash architect_sweeps gates have explicit CLEAN/exit=0 sentinels. ✓

Plan is placeholder-free; the widened clause-3 is a hand-rolled provable Sweep-1 superset (no regex dep). Hand off to implement.