From 7d7f04e1a4925ec8723def57073d248c26097539 Mon Sep 17 00:00:00 2001 From: Brummel Date: Mon, 18 May 2026 15:51:34 +0200 Subject: [PATCH] iter form-a-scalar-param-mode-carveout: scalar-param mode carve-out (docs-honesty tidy) Resolves the M1-fieldtest [friction] + [spec_gap]#1 shared root: form_a.md stated an unconditional 'every (fn ...) param needs an (own/borrow) mode' rule in 4 places, contradicting shipped checker behaviour (scalars take and require bare (con Int); a mode on a scalar trips body-pointing use-after-consume/consume-while-borrowed). All 4 sites rewritten symmetric to the existing return-type carve-out; DESIGN.md gains the bare-scalar export-param rule + a corpus-grounded Form-A snippet; RED-first anti-regrowth pin via FORM_A_SPEC + norm(). Zero language/checker/codegen change. Plan 4c266a6. --- ...ter-form-a-scalar-param-mode-carveout.json | 12 +++ crates/ailang-core/specs/form_a.md | 40 +++++++--- crates/ailang-core/tests/docs_honesty_pin.rs | 37 ++++++++++ docs/DESIGN.md | 17 +++++ ...-iter-form-a-scalar-param-mode-carveout.md | 73 +++++++++++++++++++ docs/journals/INDEX.md | 1 + 6 files changed, 169 insertions(+), 11 deletions(-) create mode 100644 bench/orchestrator-stats/2026-05-18-iter-form-a-scalar-param-mode-carveout.json create mode 100644 docs/journals/2026-05-18-iter-form-a-scalar-param-mode-carveout.md diff --git a/bench/orchestrator-stats/2026-05-18-iter-form-a-scalar-param-mode-carveout.json b/bench/orchestrator-stats/2026-05-18-iter-form-a-scalar-param-mode-carveout.json new file mode 100644 index 0000000..d76e249 --- /dev/null +++ b/bench/orchestrator-stats/2026-05-18-iter-form-a-scalar-param-mode-carveout.json @@ -0,0 +1,12 @@ +{ + "iter_id": "form-a-scalar-param-mode-carveout", + "date": "2026-05-18", + "mode": "standard", + "outcome": "DONE", + "tasks_total": 4, + "tasks_completed": 4, + "reloops_per_task": { "1": 0, "2": 0, "3": 0, "4": 0 }, + "review_loops_spec": 0, + "review_loops_quality": 0, + "blocked_reason": null +} diff --git a/crates/ailang-core/specs/form_a.md b/crates/ailang-core/specs/form_a.md index 77414ae..3f17bb3 100644 --- a/crates/ailang-core/specs/form_a.md +++ b/crates/ailang-core/specs/form_a.md @@ -94,8 +94,10 @@ The `because` MUST be a non-empty justification — empty / whitespace- only `because` is itself an error (`empty-suppress-reason`). `type` is a `(fn-type ...)` (possibly wrapped in `(forall ...)` for -polymorphic defs). All parameters of a `(fn ...)` MUST carry a mode -annotation — see *Modes* below. +polymorphic defs). A *heap-shaped* parameter or return of a +`(fn ...)` MUST carry a mode annotation; a scalar (non-heap-shaped: +`(con Int)`, `(con Bool)`, `(con Unit)`, `(con Str)`) takes **no** +mode and is written bare — see *Modes* below. `params` is a list of bare names that bind the parameters in `body`. The list length must match the number of params in `type`. @@ -227,7 +229,7 @@ TYVAR-NAME ; type variable (e.g. `a`, `T`) annotation: ``` -TYPE ; implicit mode (DO NOT USE in new (fn ...) defs) +TYPE ; no mode — REQUIRED for scalars (Int/Bool/Unit/Str), rejected for heap-shaped (own TYPE) ; caller transfers ownership; callee consumes (borrow TYPE) ; caller retains ownership; callee must not consume ``` @@ -344,12 +346,20 @@ The parser will accept syntactically valid Form-A that violates these; the typechecker will not. Producing Form-A that obeys them yields checked code on the first try. -1. **Mode annotations on every `(fn ...)` parameter.** Every type in - the `(params ...)` clause of a `(fn ...)` definition's - `(fn-type ...)` MUST be wrapped in `(own T)` or `(borrow T)`. The - return type MUST also carry a mode whenever the type is heap-shaped +1. **Mode annotations on every heap-shaped `(fn ...)` parameter.** A + parameter type in the `(params ...)` clause of a `(fn ...)` + definition's `(fn-type ...)`, and the return type, MUST be wrapped + in `(own T)` or `(borrow T)` whenever the type is heap-shaped (i.e. anything other than `(con Int)`, `(con Bool)`, `(con Unit)`, - `(con Str)`). Implicit mode on a `(fn ...)` def is rejected. + `(con Str)`). A scalar (non-heap-shaped) parameter or return takes + **no** mode: write it bare, e.g. `(con Int)`. Scalars are + primitive value types, not linear resources; putting a mode on a + scalar makes the checker hold the primitive to linear discipline + (single use under `own`, no use while a `borrow` is live), so an + ordinary scalar reused in the body (`sample * sample`) trips a + body-pointing `use-after-consume` / `consume-while-borrowed` + rather than a mode error. Implicit (omitted) mode on a + *heap-shaped* `(fn ...)` parameter or return is rejected. 2. **Constructors via `term-ctor`.** `Cons(1, Nil)` becomes `(term-ctor List Cons 1 (term-ctor List Nil))`, never `(app Cons 1 (app Nil))`. @@ -378,9 +388,17 @@ significantly. it is interpreted as a type variable. So `(fn-type (params Int) ...)` parses as "function with one type-variable parameter named Int", which is almost certainly not what was meant. -- **Forgetting mode annotations.** `(fn-type (params (con List)) ...)` - is accepted by the parser but rejected by the checker. Wrap every - `(fn ...)` parameter in `(own ...)` or `(borrow ...)`. +- **Forgetting mode annotations on a heap-shaped parameter.** + `(fn-type (params (con List)) ...)` is accepted by the parser but + rejected by the checker. Wrap every *heap-shaped* `(fn ...)` + parameter in `(own ...)` or `(borrow ...)`. +- **Putting a mode on a scalar parameter.** The inverse trap: + `(fn-type (params (own (con Int))) ...)` is wrong — scalars + (`Int`/`Bool`/`Unit`/`Str`) take no mode, write them bare + `(con Int)`. A mode on a scalar makes the checker treat the + primitive as a linear resource, so an ordinary reuse trips a + body-pointing `use-after-consume` / `consume-while-borrowed` + instead of pointing at the signature. - **Using `app` for constructors.** Constructors are NOT first-class functions. `(app Cons 1 Nil)` is interpreted as "apply variable `Cons` to ...", which then fails because `Cons` is not a fn. diff --git a/crates/ailang-core/tests/docs_honesty_pin.rs b/crates/ailang-core/tests/docs_honesty_pin.rs index c9e7371..59d988a 100644 --- a/crates/ailang-core/tests/docs_honesty_pin.rs +++ b/crates/ailang-core/tests/docs_honesty_pin.rs @@ -9,6 +9,7 @@ //! docs' hard line-wrap. Companion to `effect_doc_honesty_pin.rs` and //! to Sweep 5 of `bench/architect_sweeps.sh`. +use ailang_core::FORM_A_SPEC; use std::fs; fn read(rel: &str) -> String { @@ -98,3 +99,39 @@ fn prose_roundtrip_md_has_no_wunschdenken() { assert!(p.contains("the lowest-common-denominator cycle and always works with any client"), "PROSE_ROUNDTRIP.md must close present-tense on the static-prompt path"); } + +#[test] +fn form_a_scalar_param_carveout_present_and_old_rule_absent() { + // form_a.md is read via the canonical include_str! const, not a + // re-derived path: single source of truth, compile-checked, no + // stale-path bug. norm() collapses the docs' hard line-wrap so + // every asserted substring below is the single-spaced form + // (planner Step-5 item 6: line-wrap is structurally discharged, + // the substrings need not be contiguous in the .md source). + let f = norm(FORM_A_SPEC); + let d = norm(&read("docs/DESIGN.md")); + + // --- ABSENT: the four old unconditional phrasings must be gone --- + assert!(!f.contains("Mode annotations on every `(fn ...)` parameter."), + "form_a.md item 1 must no longer head with the unconditional 'every (fn ...) parameter' rule (site 3)"); + assert!(!f.contains("All parameters of a `(fn ...)` MUST carry a mode annotation"), + "form_a.md ### Function prose must no longer state the unconditional all-params rule (site 1)"); + assert!(!f.contains("(DO NOT USE in new (fn ...) defs)"), + "form_a.md grammar block must no longer blanket-forbid the bare (implicit-mode) form (site 2)"); + assert!(!f.contains("Wrap every `(fn ...)` parameter in `(own ...)` or `(borrow ...)`."), + "form_a.md Pitfalls bullet must no longer state the unconditional 'wrap every parameter' imperative (site 4)"); + + // --- PRESENT: the scalar-parameter carve-out, the four sites --- + assert!(f.contains("A scalar (non-heap-shaped) parameter or return takes **no** mode: write it bare, e.g. `(con Int)`."), + "form_a.md item 1 must state the scalar-parameter carve-out symmetrically with the return-type carve-out (site 3)"); + assert!(f.contains("a scalar (non-heap-shaped: `(con Int)`, `(con Bool)`, `(con Unit)`, `(con Str)`) takes **no** mode and is written bare"), + "form_a.md ### Function prose must carry the scalar carve-out (site 1)"); + assert!(f.contains("; no mode — REQUIRED for scalars (Int/Bool/Unit/Str), rejected for heap-shaped"), + "form_a.md grammar block comment must state the bare form is required for scalars (site 2)"); + assert!(f.contains("Putting a mode on a scalar parameter."), + "form_a.md Pitfalls must carry the inverse pitfall — over-wrapping a scalar (site 4)"); + + // --- PRESENT: the DESIGN.md §\"Embedding ABI (M1)\" mirror --- + assert!(d.contains("Export parameters are written **bare**: a scalar type carries no `own`/`borrow` mode"), + "DESIGN.md §Embedding ABI (M1) must mirror the canonical bare-scalar export-param rule"); +} diff --git a/docs/DESIGN.md b/docs/DESIGN.md index e8f6401..6c8acce 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -2284,6 +2284,23 @@ context parameter that M2 threads through every exported call will change this C signature; do not treat the M1 signature as frozen. The value/record layout freeze is M3. +Export parameters are written **bare**: a scalar type carries no +`own`/`borrow` mode (modes apply only to heap-shaped types, which the +scalar-only rule above forbids at an export boundary anyway). The +canonical M1 export shape: + +``` +(fn step + (export "backtest_step") + (type + (fn-type + (params (con Int) (con Int)) + (ret (con Int)))) + (params state sample) + (body + (app + state (app * sample sample)))) +``` + ## Data model The on-disk JSON-AST is what the toolchain hashes, typechecks, and diff --git a/docs/journals/2026-05-18-iter-form-a-scalar-param-mode-carveout.md b/docs/journals/2026-05-18-iter-form-a-scalar-param-mode-carveout.md new file mode 100644 index 0000000..f79a606 --- /dev/null +++ b/docs/journals/2026-05-18-iter-form-a-scalar-param-mode-carveout.md @@ -0,0 +1,73 @@ +# iter form-a-scalar-param-mode-carveout — form_a.md scalar-parameter mode carve-out (docs-honesty tidy) + +**Date:** 2026-05-18 +**Started from:** 4c266a64b478c6a6106958d577faea5ac30dfa3e +**Status:** DONE +**Tasks completed:** 4 of 4 + +## Summary + +Docs-honesty tidy in the `docs-honesty-lint` class resolving the +fieldtest `[friction]` + `[spec_gap]#1` from +`docs/specs/2026-05-18-fieldtest-embedding-abi-m1.md`: +`crates/ailang-core/specs/form_a.md` and `docs/DESIGN.md` §"Embedding +ABI (M1)" stated an unconditional "every `(fn ...)` parameter MUST +carry a mode" rule that contradicts the shipped checker behaviour +(scalar `Int`/`Bool`/`Unit`/`Str` params take — and require — no +mode, mirroring the existing return-type carve-out). RED-first: the +new pin `form_a_scalar_param_carveout_present_and_old_rule_absent` +was written asserting the post-edit state and failed correctly +(Task 1), then the four form_a.md sites + one DESIGN.md insertion +turned it green (Tasks 2–3). Zero language/checker/codegen change; +exactly three files touched. Workspace suite 622 → 623 (delta = +1, +the new pin), zero failures. + +## Per-task notes + +- iter form-a-scalar-param-mode-carveout.1: RED pin — + `use ailang_core::FORM_A_SPEC;` added + new `#[test] fn + form_a_scalar_param_carveout_present_and_old_rule_absent` + (4 ABSENT + 4 PRESENT form_a + 1 PRESENT DESIGN assertions); + failed at the first ABSENT check (site 3) as designed; 4 + pre-existing pins unaffected (4 passed / 1 failed). +- iter form-a-scalar-param-mode-carveout.2: form_a.md four sites — + site 1 `### Function` prose, site 2 grammar-block comment, site 3 + Schema-invariants item 1 (+ checker-behaviour rationale), site 4 + Pitfalls bullet split into heap-shaped-forget + inverse + scalar-over-wrap. Pin then failed only on the DESIGN.md assertion; + spec_drift all 8 green (not a lockstep partner, confirmed). +- iter form-a-scalar-param-mode-carveout.3: DESIGN.md §"Embedding + ABI (M1)" — inserted the bare-scalar export-param rule + the + corpus-grounded `step` Form-A snippet (byte-identical to + `examples/embed_backtest_step.ail` lines 3-11). Pin fully green; + docs_honesty_pin 5 passed / 0 failed. +- iter form-a-scalar-param-mode-carveout.4: regression gate — + `cargo test -p ailang-core` 112 passed / 0 failed; workspace + TOTAL_PASSED=623 (baseline 622, +1 expected) / 0 failed; zero + src-code diff under ailang-check / ailang-codegen / + ailang-core/src / ailang-surface. + +## Concerns + +(none) + +## Known debt + +(none — site 5 form_a.md:521-524 already-correct few-shot annotation +left verbatim by design; the carve-out text deliberately reuses its +heap-shaped/primitive vocabulary so the document is internally +consistent end to end.) + +## Blocked detail + +(none — DONE) + +## Files touched + +- `crates/ailang-core/specs/form_a.md` (4 carve-out sites) +- `docs/DESIGN.md` (1 §"Embedding ABI (M1)" insertion) +- `crates/ailang-core/tests/docs_honesty_pin.rs` (1 `use` + 1 new `#[test]`) + +## Stats + +bench/orchestrator-stats/2026-05-18-iter-form-a-scalar-param-mode-carveout.json diff --git a/docs/journals/INDEX.md b/docs/journals/INDEX.md index 8c4968a..ea58618 100644 --- a/docs/journals/INDEX.md +++ b/docs/journals/INDEX.md @@ -96,3 +96,4 @@ - 2026-05-18 — iter embedding-abi-m1.1 (Embedding-ABI M1, DONE across 2 dispatches + a Boss plan Repair): a compiled AILang scalar `fn` is now callable in-process from a C/Rust host. Five pipeline layers shipped: additive `FnDef.export: Option` (serde `default`+`skip_serializing_if`, modelled on `FnDef.doc`, ~85-site compile-driven thread incl. all in-source `#[cfg(test)]`/drift/hash literals, hash-stable golden `b4662aa70839f60b`); Form-A `(export "")` modifier (parse/print/`form_a.md` lockstep, byte-identical round-trip); check-side scalar-only+effect-free export gate — two new `CheckError` Error variants `export-non-scalar-signature`/`export-has-effects`, params→ret→effects first-violation-wins, unconditional on `f.export.is_some()` (the feature-acceptance clause-3 discriminator *in code*); codegen `Target::StaticLib` (suppresses `@main`+`MissingEntryMain`, emits one external `@` forwarder per export to `@ail__`); CLI `ail build --emit=staticlib` (`build_staticlib`: `libembed_backtest_step.a` program-only + separate `libailang_rt.a`). Coherent stop PROVEN: `embed_e2e.rs` `cc`-links both archives, `backtest_step(0,3)=9`/`(9,4)=25`, `s==25`, exit 0. Task 1 (fixed-first CLI `MissingEntryMain` baseline pin) green before any staticlib code. Process: Task 4 BLOCKED on the first dispatch — a Boss plan-defect (gate/headline fixtures declared `module ≠ file-stem`; the loader hard-enforces `module==stem` at `workspace.rs:438`, so `ail check` panicked on load before the gate). Boss verified, chose **Option 2 over the orchestrator-recommended Option 1** (keep descriptive `embed_*` filenames, rename fixture *modules* to their stems — the C symbol `backtest_step` stays via `(export …)`, *demonstrating* spec Decision 2's mangling-decoupling rather than violating it; archive→`libembed_backtest_step.a`, internal→`@ail_embed_backtest_step_step`, `host.c` unchanged), Repaired the plan (Design-decision 6 + Task-4 module==stem + Task-5/6/7 dependent strings + folded the no-`*.`-Float-head and `content_hash_fn`→`def_hash` concerns), committed Tasks 1–3 as a verified known-good partial (`818177d`), re-dispatched [4,7]. Independent Boss verification post-completion: E2E 1/1, gate 6/6, lowering 2/2, CLI 2/2, full `cargo test --workspace` 0 failures, `roundtrip_cli`+`design_schema_drift`+`spec_drift` green, Invariant 1 holds (no new dep in `ailang-core`/`ailang-codegen`/`runtime`). 3 behaviour-neutral plan pseudo-vs-reality concerns recorded (`emit_ir` routes via `lower_workspace`; dead `Path` import dropped; `##` vs `###` heading). Bench: `compile_check.py`/`cross_lang.py` exit 0; `check.py` exit 1 = tracked-P2 `*.bump_s`/`*.max_us` known-noise (default-exe codegen/runtime byte-unchanged — audit-adjudicated, not a plan gate). spec `docs/specs/2026-05-18-embedding-abi-m1.md` (grounding-check PASS) → plan `docs/plans/embedding-abi-m1.1.md` (Repaired) → commits `818177d` (partial 1–3) + this completion. Milestone-close `audit` then `fieldtest` (surface-touching — `(export …)` is new authoring surface) are the next pipeline steps. → 2026-05-18-iter-embedding-abi-m1.1.md - 2026-05-18 — audit embedding-abi-m1 (milestone close, CLEAN — carry-on, NO ratify): scope `064599e..e406d07`. Architect `clean` — Invariant 1 (clean core) holds semantically (no finance/`data-server` vocab/logic/dep in `ailang-core`/`ailang-codegen`/`runtime`; `backtest_step` is an opaque author string through generic `FnDef.export`; AILang↔host meeting point correctly M5-deferred), DESIGN.md honesty satisfied (§"Embedding ABI (M1)" + fn-JSON `"export"` + field rustdoc present-tense; "provisional until M3"/"M2 changes the C sig" correctly *labelled* reserved-unimplemented), lockstep intact (`FnDef`↔fn-JSON↔drift/spec-drift carry the field; `@` forwarder additive raw-IR *beside* unchanged `@ail__` mangling — not through `lower_app`; schema exhaustively-constructed, no `_=>`/`..Default`; in-source `missing_entry_main_is_error` byte-unchanged; `_adapter`/`_clos` untouched), Design-decision 6 confirmed correct (fixture module==stem differs from the spec's *illustrative* `(module backtest)`/`(module bad)` by the loader invariant + spec Decision 2 — the fixtures *demonstrate* the C-symbol decoupling, not violate the spec). One `[low]` — ~85 `export: None,` lines brace-column-indented (rustfmt-divergent) — **orchestrator-adjudicated carry-on on evidence** (not pending, not ignored): `cargo fmt --all --check` shows **1137 fmt-divergent locations tree-wide predating M1**, so rustfmt is demonstrably not a project convention and the architect's "cargo fmt would churn later" premise is moot; selectively aligning 85/1137 would impose an absent standard (noise, not tidy). Bench: `compile_check.py` 0/24 + `cross_lang.py` 0/25 EXIT 0; `check.py` EXIT 1, 7 firings (`*.bump_s` trio +12–14%; `explicit_at_rc`/`implicit_at_rc` `p99_9`/`max_us` +36–124%). Bencher causal exoneration **decisive**: 21/21 bench binaries `cmp`-byte-identical `064599e`↔`e406d07` (`bench_list_sum_bump` sha `dce18c288904c9f0` both) — M1's `Target::Executable` default leaves every bench fixture's emitted machine code unchanged ⇒ zero causal mechanism; RC-latency tails reproduce *on the byte-identical oracle itself* (3× run-to-run swing, median/p99 steady = `-n 5` single-sample jitter); fresh re-run collapsed 6/7 firings to ok. Both families map (confirmed) onto the two pre-existing roadmap-P2 tracked items (~4th zero-runtime-change milestone they fire+collapse on). **EXONERATED, NO baseline ratify** (Iron Law forbids ratifying noise against a zero-default-path-byte iter; spec forecloses a zero-codegen baseline bump); the two P2 bench-harness-recalibration items stay orchestrator-owned, NOT bundled (bencher recommendation honoured). M1 milestone audit **CLEAN**; `fieldtest` (surface-touching — `(export "")` is new authoring surface) is the next pipeline step before close → 2026-05-18-audit-embedding-abi-m1.md - 2026-05-18 — fieldtest embedding-abi-m1 (milestone CLOSE, surface-touch — thesis substantiated, 3 non-blocking follow-ups routed): post-audit downstream-LLM-author field test of M1's `(export "")` + `ail build --emit=staticlib` + scalar/effect-free gate, DESIGN.md + public examples only (never compiler source). 4 fresh programs: Int fixed-point EMA `(State,Sample)->State` fold + Float leaky integrator (both author→`--emit=staticlib`→C-host link→typed scalar return, `s==67`/`s==1.875`, exit 0 — **clause-1 confirmed first-try**), IntList-param export (correctly rejected `export-non-scalar-signature`), `!IO` scalar-clean export (correctly rejected `export-has-effects`) — **clause-3 confirmed, both diagnostics precise+self-correcting**. **0 bugs**; the M1 capability + gate are sound and the milestone thesis ("an LLM author makes a scalar fold callable") is empirically substantiated. 3 working + 1 friction + 2 spec_gap. The friction + spec_gap#1 share a root: `crates/ailang-core/specs/form_a.md` "Schema invariants" item 1 + Pitfalls state an *unconditional* "every fn param MUST be `(own/borrow T)`" but scalars require **bare** `(con Int)` (`(own (con Int))`→`use-after-consume`, `(borrow)`→`consume-while-borrowed`); the doc the LLM reads first misdirects M1's headline task into a body-pointing linearity diagnostic — a **pre-existing form_a.md docs-honesty defect M1 made acute, not introduced** (the positive examples only succeeded because the fieldtester imitated the public `embed_*` corpus over the form_a.md prose rule). spec_gap#2: no public `emit-ir --emit=staticlib` though Decision 5 makes kernel-IR readability load-bearing. Boss independently re-verified via the public CLI (positive E2E builds both archives; both rejections fire the documented codes). Orchestrator routing (own judgement, not the fieldtester's recommended-action column): friction+spec_gap#1 → ONE P1 `[todo]` docs-honesty tidy (form_a.md scalar-param carve-out symmetric with the existing return-type carve-out + DESIGN.md mirror; behaviour settled → tidy, no brainstorm; ahead of M2 because it undermines the clause-1 ergonomics of the surface M2–M5 extend); spec_gap#2 → P1 `[feature]` add `--emit=staticlib` to `ail emit-ir` (restore the affordance, NOT narrow DESIGN.md). None are bugs (no debug); none M1-blocking. **embedding-abi-m1 fully ratified and CLOSED**: spec (grounding-check PASS) + plan (Boss-Repaired) + iter (`818177d` partial+Repair / `e406d07` DONE) + audit `425c4eb` CLEAN + fieldtest thesis-substantiated; roadmap P1 `[~]`→`[x]`, 2 follow-ups added P1-ahead-of-M2. Next *milestone* is Embedding ABI — M2 (no spec) = a `/boss` new-milestone bounce-back (not auto-started); the 2 P1 follow-ups are autonomous-eligible tidies/feature but the session is context-deep — surfaced to the user for the session-shape/next-item call. → 2026-05-18-fieldtest-embedding-abi-m1.md +- 2026-05-18 — iter form-a-scalar-param-mode-carveout (M1-fieldtest follow-up #1, docs-honesty tidy in the docs-honesty-lint class, DONE): resolved the M1 fieldtest `[friction]` + `[spec_gap]#1` shared root. `crates/ailang-core/specs/form_a.md` stated an *unconditional* "every `(fn ...)` param MUST carry an `(own/borrow)` mode" rule in four places (`### Function` prose L97, grammar-block comment L230, "Schema invariants" item 1 L347, Pitfalls bullet L382) that contradicts shipped checker behaviour — scalar `Int`/`Bool`/`Unit`/`Str` params take **and require** bare `(con Int)` (a mode on a scalar makes the linearity pass hold the primitive to linear discipline → body-pointing `use-after-consume`/`consume-while-borrowed`, the exact fieldtest misdirection). All four sites rewritten symmetric to the pre-existing return-type carve-out; site 5 (L521 few-shot annotation) already correct, left verbatim by design (carve-out reuses its heap-shaped/primitive vocabulary for end-to-end internal consistency). `docs/DESIGN.md` §"Embedding ABI (M1)" gained the bare-scalar export-param rule + a corpus-grounded `step` Form-A snippet (byte-identical to `examples/embed_backtest_step.ail:3-11`). RED-first anti-regrowth pin `form_a_scalar_param_carveout_present_and_old_rule_absent` in `crates/ailang-core/tests/docs_honesty_pin.rs` (reads the canonical `ailang_core::FORM_A_SPEC` `include_str!` const — single-source, compile-checked — + `read("docs/DESIGN.md")`; `norm()` whitespace-collapse structurally discharges the grep/contains line-wrap family; 4 ABSENT + 4 PRESENT form_a + 1 PRESENT DESIGN). Boss scope calls: 4 form_a sites not 2 (internal-consistency, on the merits — a tidy whose thesis is "consistent current-state mirror" cannot fix some and leave others contradicting); pin idiom = `FORM_A_SPEC` const (semantic single-source, not effort). `spec_drift.rs` recon-confirmed NOT a lockstep partner (anchors are short keyword tokens; `(own`/`(borrow` survive an additive carve-out). Zero language/checker/codegen change by construction; independent Boss verification: docs_honesty_pin 5/0, spec_drift 8/8, ailang-core 112/0, workspace 622→623 (+1 = the pin), zero `crates/**/src/**` diff. spec carrier `docs/specs/2026-05-18-fieldtest-embedding-abi-m1.md` + roadmap P1 entry → plan `docs/plans/form-a-scalar-param-mode-carveout.md` (`4c266a6`). Pure docs+pin tidy — no audit/fieldtest gate (zero authoring-behaviour change; the pin IS the regression coverage). → 2026-05-18-iter-form-a-scalar-param-mode-carveout.md