workflow: delete docs/journals/ and docs/journal-archive.md

Strict application of the "Future-Use, not Verlauf" criterion to the
two remaining journal artefacts:

- `docs/journals/` (110 files): the per-iter and audit journals from
  2026-05-11 onward. Pure history; no live reader after the previous
  sweep. Entire directory removed.
- `docs/journals/2026-05-19-design-decision-records.md`: the one file
  that had live readers (`docs_honesty_pin.rs:108`, `parse.rs:80`,
  `duplicate_ctor_pin.rs:8`, three roadmap.md mentions) and was framed
  as a "relitigation guard". On re-examination its three asserted
  pinned phrases ("Regions were considered and rejected", the
  "demands annotations *because*" rationale, the prose-render
  placeholder statement) are rationale-prose, not load-bearing
  invariants — the test pinned itself, not a system property. Any
  Decision that still holds lives in the code, `design/contracts/`,
  or `design/models/`. Removed with the rest.
- `docs/journal-archive.md` (pre-2026-05-11 history): content-frozen
  long-tail history with no live reader. Removed; if anyone ever
  needs the pre-cutoff rationale they can `git log --before=2026-05-11
  --grep=<keyword>`.

Live-file consequences:
- `docs_honesty_pin.rs` `design_md_present_tense_anchors_present`:
  the three `records.*` assertions and the `read("docs/journals/…")`
  are dropped; the contract/model anchor pins remain.
- `duplicate_ctor_pin.rs`: the "why-two-overlays rationale lives in
  docs/journals/…" comment is replaced with the rationale inline.
- `parse.rs`: the "form-refinement rationale lives in docs/journals/…"
  comment is dropped (the rule body already states the refinement).
- `roadmap.md`: the decision-records mention in the DESIGN.md →
  design/ entry's body is dropped; the journal-archive.md `context:`
  pointers across ~12 closed entries are either rephrased to point
  at the relevant iter/audit (no path), or simplified to a one-line
  comment when the iter name alone carries the story.
- `CLAUDE.md` Roles section: the `docs/journal-archive.md` slot is
  removed; vocabulary note rephrased.
- `design/INDEX.md`: the Docs bullet drops `journal-archive.md`.
- `skills/README.md` bootstrap-rationale pointer rephrased.

`docs/specs/` and `docs/plans/` (per-milestone specs and per-iteration
plans) are unaffected — they remain the structured-design artefacts
they were before this sweep.

Workspace builds, full test suite green.
This commit is contained in:
2026-05-20 11:25:15 +02:00
parent 8e586f493f
commit 54f0ced148
118 changed files with 55 additions and 30541 deletions
+7 -13
View File
@@ -43,7 +43,7 @@ it measurably improves correctness or removes redundancy.
| `bench/` | Regression harnesses (`check.py`, `compile_check.py`, `cross_lang.py`) and the throughput-and-latency runner (`run.sh`); `bench/reference/` holds the hand-C corpus for cross-language ratios | | `bench/` | Regression harnesses (`check.py`, `compile_check.py`, `cross_lang.py`) and the throughput-and-latency runner (`run.sh`); `bench/reference/` holds the hand-C corpus for cross-language ratios |
| `examples/` | AILang fixtures used by tests and benches | | `examples/` | AILang fixtures used by tests and benches |
| `design/` | The canonical contract ledger — `design/INDEX.md` (sole addressable spine: a typed Contracts + Models table), `design/contracts/` (prose-authoritative test-linked invariants), `design/models/` (onboarding whitepapers) | | `design/` | The canonical contract ledger — `design/INDEX.md` (sole addressable spine: a typed Contracts + Models table), `design/contracts/` (prose-authoritative test-linked invariants), `design/models/` (onboarding whitepapers) |
| `docs/` | Specs and roadmap — `docs/specs/` (per-milestone design specs), `docs/plans/` (per-iteration plans), `docs/roadmap.md` (forward queue), `docs/journal-archive.md` (pre-2026-05-11 history, content-frozen), `PROSE_ROUNDTRIP.md`. Per-iter history lives in `git log`. | | `docs/` | Specs and roadmap — `docs/specs/` (per-milestone design specs), `docs/plans/` (per-iteration plans), `docs/roadmap.md` (forward queue), `PROSE_ROUNDTRIP.md`. Project history lives in `git log`. |
| `skills/` | Project-local skill definitions and their agents. See `skills/README.md` for the skill table, agent roster, and discovery layout. | | `skills/` | Project-local skill definitions and their agents. See `skills/README.md` for the skill table, agent roster, and discovery layout. |
## Skill system ## Skill system
@@ -193,12 +193,11 @@ Work clusters into **milestones**, each subdivided into
→ fieldtest`), skipping rules, and bench-exit-code gating live in → fieldtest`), skipping rules, and bench-exit-code gating live in
`skills/README.md` and the per-skill `SKILL.md` files. `skills/README.md` and the per-skill `SKILL.md` files.
Vocabulary note: legacy entries in `docs/journal-archive.md` Vocabulary note: pre-2026-05-09 git history uses "iter" / "family";
(pre-2026-05-09) use "iter" / "family"; current vocabulary is current vocabulary is "iteration" / "milestone". Old commits are
"iteration" / "milestone". Existing archive entries are not not retroactively renamed.
retroactively renamed.
## Roles of the `design/` ledger, `git log`, `docs/journal-archive.md`, `docs/roadmap.md`, `docs/specs/`, `docs/plans/` ## Roles of the `design/` ledger, `git log`, `docs/roadmap.md`, `docs/specs/`, `docs/plans/`
- **The `design/` ledger** is the canonical specification. It - **The `design/` ledger** is the canonical specification. It
describes what AILang *is*: schema, semantics, invariants, runtime describes what AILang *is*: schema, semantics, invariants, runtime
@@ -222,10 +221,6 @@ retroactively renamed.
with `git log --oneline -30`; per-milestone scope with with `git log --oneline -30`; per-milestone scope with
`git log <prev-milestone-close>..HEAD --format=full`. `git log <prev-milestone-close>..HEAD --format=full`.
- **`docs/journal-archive.md`** is the archived monolithic
decisions log for everything pre-2026-05-11. Content-frozen.
Read it only when chasing long-tail history; do not append.
- **`docs/roadmap.md`** (since 2026-05-10): the priority-ordered - **`docs/roadmap.md`** (since 2026-05-10): the priority-ordered
forward queue — milestones, features, todos, and ideas. The forward queue — milestones, features, todos, and ideas. The
orchestrator owns this file and is responsible for keeping it orchestrator owns this file and is responsible for keeping it
@@ -243,6 +238,5 @@ retroactively renamed.
by `skills/implement`. by `skills/implement`.
Together these answer three questions: "what is the language right Together these answer three questions: "what is the language right
now?" (the `design/` ledger), "how did we get here?" (`git log`, now?" (the `design/` ledger), "how did we get here?" (`git log`),
plus `docs/journal-archive.md` for pre-2026-05-11 context), and and "what's next?" (roadmap).
"what's next?" (roadmap).
@@ -3,11 +3,13 @@
//! consumer of the per-module `env.ctor_index` rebuild in //! consumer of the per-module `env.ctor_index` rebuild in
//! `check_in_workspace`. Env construction is code-SoT (the //! `check_in_workspace`. Env construction is code-SoT (the
//! `env-construction` ledger row in design/INDEX.md is source-link //! `env-construction` ledger row in design/INDEX.md is source-link
//! only); the why-two-overlays rationale this test rests on as the //! only). The two overlays — typecheck-side rebuild + mono-side
//! asymmetry-with-mono-side argument lives in //! narrowed — are deliberately asymmetric: the typecheck-side
//! docs/journals/2026-05-19-design-decision-records.md. If the //! `ctor_index` half exists *for this very diagnostic*, while the
//! per-module rebuild is ever removed without an equivalent //! mono-side overlay is narrowed to types-only because the runtime
//! replacement path, this test goes red. //! ctor lookup is type-driven. If the per-module rebuild is ever
//! removed without an equivalent replacement path, this test goes
//! red.
use ailang_check::check_workspace; use ailang_check::check_workspace;
use ailang_core::ast::{Ctor, Def, Module, TypeDef}; use ailang_core::ast::{Ctor, Def, Module, TypeDef};
+4 -15
View File
@@ -91,21 +91,16 @@ fn design_md_has_no_doc_archaeology() {
#[test] #[test]
fn design_md_present_tense_anchors_present() { fn design_md_present_tense_anchors_present() {
// Each anchor is read from its post-split home (spec Appendix // Each anchor is read from its post-split home. Phrases that only
// relocation map). Three honest-reserved rationale anchors // ever lived in the (now-removed) decision-records annex are
// ("Regions were considered and rejected", the "does not infer // dropped — anything the design/ ledger truly needs lives in
// everything" bullet, the Form-B prose-placeholder state) sat in // design/contracts/ or design/models/.
// `### What this Decision deliberately does not do` / `### What
// this decision does NOT commit to`, which the Appendix routes to
// the decision-record journal — so the journal is part of the
// canonical anchor corpus, not a contract surface.
let honesty = norm(&read("design/contracts/honesty-rule.md")); let honesty = norm(&read("design/contracts/honesty-rule.md"));
let scope = norm(&read("design/contracts/scope-boundaries.md")); let scope = norm(&read("design/contracts/scope-boundaries.md"));
let memory = norm(&read("design/contracts/memory-model.md")); let memory = norm(&read("design/contracts/memory-model.md"));
let pipeline = norm(&read("design/models/pipeline.md")); let pipeline = norm(&read("design/models/pipeline.md"));
let str_abi = norm(&read("design/contracts/str-abi.md")); let str_abi = norm(&read("design/contracts/str-abi.md"));
let prelude_classes = norm(&read("design/contracts/prelude-classes.md")); let prelude_classes = norm(&read("design/contracts/prelude-classes.md"));
let records = norm(&read("docs/journals/2026-05-19-design-decision-records.md"));
// the discriminator meta-subsection -> contracts/honesty-rule.md // the discriminator meta-subsection -> contracts/honesty-rule.md
assert!(honesty.contains("the honesty rule it holds itself to"), assert!(honesty.contains("the honesty rule it holds itself to"),
@@ -115,8 +110,6 @@ fn design_md_present_tense_anchors_present() {
// protected honest-reserved exceptions — over-correction guard // protected honest-reserved exceptions — over-correction guard
assert!(scope.contains("`Diverge` is a reserved effect name with no op and no codegen"), assert!(scope.contains("`Diverge` is a reserved effect name with no op and no codegen"),
"the gold-form honest-reserved anchor must remain in scope-boundaries.md (do not over-strip)"); "the gold-form honest-reserved anchor must remain in scope-boundaries.md (do not over-strip)");
assert!(records.contains("Regions were considered and rejected"),
"the present-tense design-rationale exclusion must remain in the decision-records (do not over-strip)");
assert!(memory.contains("a tiebreaker, not a rationale"), assert!(memory.contains("a tiebreaker, not a rationale"),
"the self-labelled tiebreaker is honest and stays in memory-model.md (do not over-strip)"); "the self-labelled tiebreaker is honest and stays in memory-model.md (do not over-strip)");
// corrected present-tense anchors // corrected present-tense anchors
@@ -124,12 +117,8 @@ fn design_md_present_tense_anchors_present() {
"models/pipeline.md must describe Boehm present-tense, not as 'on the path to retirement'"); "models/pipeline.md must describe Boehm present-tense, not as 'on the path to retirement'");
assert!(str_abi.contains("type-installed; codegen is reserved and not yet shipped"), assert!(str_abi.contains("type-installed; codegen is reserved and not yet shipped"),
"float_to_str must be present-tense honest-reserved in str-abi.md"); "float_to_str must be present-tense honest-reserved in str-abi.md");
assert!(records.contains("AILang demands annotations *because* the LLM author can produce them effortlessly"),
"the 'does not infer everything' bullet must be present-tense in the decision-records (no doc-archaeology)");
assert!(prelude_classes.contains("`io/print_str` is the only built-in direct-output effect-op"), assert!(prelude_classes.contains("`io/print_str` is the only built-in direct-output effect-op"),
"the print-op set must be stated present-tense in prelude-classes.md (post-split home), not as a retirement narrative"); "the print-op set must be stated present-tense in prelude-classes.md (post-split home), not as a retirement narrative");
assert!(records.contains("the render in `crates/ailang-prose/src/lib.rs` is a placeholder and informational only"),
"the Form-B class/instance prose-projection state must be present-tense in the decision-records");
} }
#[test] #[test]
+3 -5
View File
@@ -76,13 +76,11 @@
//! ``` //! ```
//! //!
//! Notes on the form (the binding constraints are in //! Notes on the form (the binding constraints are in
//! design/contracts/authoring-surface.md; the form-refinement //! design/contracts/authoring-surface.md):
//! rationale is in docs/journals/2026-05-19-design-decision-records.md):
//! //!
//! - The `lam` form carries `paramTypes`, a `ret` type, and an //! - The `lam` form carries `paramTypes`, a `ret` type, and an
//! optional `effects` clause. The early form sketch left //! optional `effects` clause. The AST stores them per-lambda and
//! these out; the AST stores them per-lambda and so the form must //! so the form must round-trip them.
//! round-trip them.
//! - The `import` form admits an optional `as` alias to round-trip //! - The `import` form admits an optional `as` alias to round-trip
//! [`ailang_core::ast::Import::alias`]. //! [`ailang_core::ast::Import::alias`].
+3 -5
View File
@@ -56,11 +56,9 @@ evolving in lockstep with the language:
sole addressable spine for canonical state), `design/contracts/` sole addressable spine for canonical state), `design/contracts/`
(test-linked invariants), `design/models/` (onboarding (test-linked invariants), `design/models/` (onboarding
whitepapers). whitepapers).
- **Docs** (`docs/`): `docs/journal-archive.md` (archived monolith - **Docs** (`docs/`): `roadmap.md` (forward queue), `specs/`
for pre-2026-05-11 history, content-frozen), `roadmap.md` (forward (per-milestone design specs), `plans/` (per-iteration
queue), `specs/` (per-milestone design specs), `plans/` (per- implementation plans). Project history lives in `git log`.
iteration implementation plans). Current iter / audit history
lives in `git log`.
- **Tests**: unit tests per crate plus E2E in `crates/ail/tests/e2e.rs`. Every - **Tests**: unit tests per crate plus E2E in `crates/ail/tests/e2e.rs`. Every
new compiler path needs a test, otherwise the feature does not count as done. new compiler path needs a test, otherwise the feature does not count as done.
File diff suppressed because it is too large Load Diff
@@ -1,75 +0,0 @@
# fieldtest — canonical-type-names
**Date:** 2026-05-11
**Branch:** main (orchestrator-direct dispatch; the or.1 branch-per-iter rule lands on the *next* implement run)
**Status:** DONE_WITH_CONCERNS
**Examples:** 5 of 5 authored + committed
**Findings:** 9 (4 working, 4 friction, 1 spec_gap, 0 bugs)
## Summary
Standard skill-pipeline step after audit cleanly closed
canonical-type-names. The fieldtester wrote five `.ailx` examples
that exercise the milestone surface — one happy-path exhibit and
four diagnostic-triggers, one per new diagnostic plus a second
branch of the merged BadCrossModuleTypeRef case. Spec at
`docs/specs/2026-05-11-fieldtest-canonical-type-names.md`; fixtures
under `examples/ct_*`.
All four milestone-scoped findings landed as **working**: the new
diagnostics fire on the LLM-natural authoring mistakes, the
diagnostics carry actionable recipes (BareCrossModuleTypeRef lists
import candidates + names the migration tool), and Form-A
round-tripping stays byte-stable across cross-module type refs.
One milestone-internal friction: `BadCrossModuleTypeRef` collapses
two distinguishable cases (unknown owner vs. known owner / unknown
type name) into one diagnostic shape. A future tidy iter can split
these and, in the known-owner branch, list the available type names
the way `bare-cross-module-type-ref` lists candidates from imports.
Three orthogonal friction items surfaced as pre-existing UX gaps
that every fieldtest invocation will keep tripping on:
- Workspace search confined to the entry module's directory — no
way to organise fieldtest fixtures under
`examples/fieldtest/` while still importing prelude or std.
- `ail check foo.ailx` returns a misleading JSON-parse error
instead of either auto-parsing `.ailx` or pointing to `ail parse`.
- `(import prelude)` is rejected with a clear diagnostic but no
corpus example warns the author that prelude is auto-injected.
One spec_gap: the canonical happy-path exhibit shipped with this
milestone (`examples/compare_primitives_smoke.ail.json`) exists
only as JSON. Decision 6 makes Surface the LLM-author surface; the
milestone should ship a `.ailx` counterpart (or retire the JSON
fixture in favour of `ct_1_ordering_signum.{ailx,ail.json}`).
## Routing
| Finding | Class | Action |
|---|---|---|
| BareCrossModuleTypeRef recipe + JSON ctx | working | carry-on |
| BadCrossModuleTypeRef catches Surface | working | carry-on |
| QualifiedClassName fires on `(class prelude.Eq)` | working | carry-on |
| Form-A round-trip byte-stable | working | carry-on |
| BadCrossModuleTypeRef merges two cases | friction | roadmap P2 |
| Workspace search confined to entry dir | friction | roadmap P2 |
| `ail check` on `.ailx` misleading | friction | roadmap P2 |
| `(import prelude)` corpus doc gap | friction | carry-on (one-line DESIGN.md hint sufficient) |
| No `.ailx` for `compare_primitives_smoke.ail.json` | spec_gap | roadmap P3 |
No bug findings, so no debug dispatch. The five `.ailx` fixtures
stay in the corpus as live exhibits.
## Lesson — orthogonal UX gaps surface together
Three of four friction items are orthogonal to canonical-type-names
proper. They predate the milestone. None would have surfaced in a
brainstorm or planner pass because none touches the milestone's
substance. Fieldtest's role IS to surface this kind of cross-cutting
UX debt — DESIGN.md does not mention `.ailx` extension handling,
workspace-root flags, or the prelude doc-gap, but every downstream
LLM author trips on them within ten minutes of writing their first
program. The roadmap entries below are the persistent backlog this
fieldtest produced.
@@ -1,75 +0,0 @@
# iter 23.4-prep — checker prerequisites for prelude free fns
**Date:** 2026-05-11
**Branch:** iter/23.4-prep
**Status:** DONE
**Tasks completed:** 2 of 2
## Summary
Closes two localised checker gaps that BLOCKED iter 23.4 mid-flight:
- **Gap 2 (Task 1):** `linearity::check_module`'s globals-construction
loop now iterates `Def::Class` methods and registers each method
type in the globals map. Previously, a borrow-mode fn that
forwarded its borrow-mode params into a class-method call fired
`consume-while-borrowed` false positives, because
`callee_arg_modes` defaulted to `Consume` when it could not find
the method's `param_modes`. `Def::Instance` stays a no-op:
instance method bodies are walked separately via `Def::Fn` after
monomorphisation, per the comment in the new arm.
- **Gap 1 (Task 2):** the `Term::Var { name }` lookup ladder in
`synth` grows one new branch between the class-method arm and the
dot-qualified arm. It iterates `env.imports.values()` to find a
free-fn match in any implicitly-imported module's flat-globals
table; on hit, it qualifies any owner-module-local Type::Cons via
the existing `qualify_local_types` helper (mirror of the
dot-qualified arm). Today the prelude is the only implicit
import, so single-match-wins is unambiguous; the code comment
names the future revisit point for multi-implicit-import
ambiguity.
Both fixes are intra-crate and additive — no schema change, no new
diagnostic, no new workspace plumbing. iter 23.4's five prelude
free fns (`ne`/`lt`/`le`/`gt`/`ge`) can now ship against the
existing plan unchanged.
## Per-task subjects
- iter 23.4-prep.1: linearity — register Def::Class method types in globals
- iter 23.4-prep.2: check — bare-name fall-through to implicit-imported free fns
## Concerns
- No E2E fixture added under `examples/`. The plan's own "Plan
self-review" identifies the two RED-first tests (one in
`linearity.rs`, one in `lib.rs`'s `check_workspace`-level test
module) as the iter-23.4-prep-shaped coverage. The natural E2E
surface (a fixture calling bare `ne 1 2` against the prelude)
cannot land until iter 23.4 ships the prelude free fns
themselves — both `examples/prelude.ail.json` and the consumer
fixture are out of scope for this iter. iter 23.4's E2E coverage
is the right place; flagging here so the next iter does not
inherit "no E2E for the prep iter" as unrecorded debt.
## Known debt
- The Task 2 branch's `mod_name` first-match-wins rule remains
ambiguous-by-design as long as `prelude` is the singleton
implicit import. If a second implicit import lands in a future
milestone, this branch needs a duplicate-detection diagnostic
("name `X` resolves through implicit imports `A` and `B`,
qualify the call"). The code comment names the revisit point.
## Blocked detail
N/A — DONE.
## Commits
c10acf0a..aef4ab84
## Stats
bench/orchestrator-stats/2026-05-11-iter-23.4-prep.json
-68
View File
@@ -1,68 +0,0 @@
# iter 23.4 — Mono-Pass Unification
**Date:** 2026-05-11
**Started from:** fab1685
**Status:** DONE
**Tasks completed:** 11 of 11
## Summary
Restructured `crates/ailang-check/src/mono.rs::monomorphise_workspace`
into a single specialiser over every `Type::Forall`-quantified
`Def::Fn` in one fixpoint pass, covering both class-method residuals
and polymorphic free-fn call sites. Removed the codegen-time
specialiser entirely (`lower_polymorphic_call`, `module_polymorphic_fns`,
`mono_queue`, `mono_emitted`, `emit_specialised_fn`, plus the dead
helpers `apply_subst_to_term`, `descriptor_for_subst`, `type_descriptor`).
Codegen now sees only monomorphic `Def::Fn`s — no polymorphic call
sites, no codegen-time queue.
The unified pass produces bit-identical mono-symbol bodies for the
six existing primitive Eq/Ord symbols (hash-stability pin Task 1,
`crates/ail/tests/mono_hash_stability.rs`) and produces new free-fn
mono symbols on demand for fixtures like `cmp_max_smoke` (Tasks 2,
6, 9). All 73 e2e tests + 18 typeclass tests + 81 codegen tests pass
end-to-end against the new architecture.
## Per-task notes
- iter 23.4.1: hash-stability regression pin — `examples/mono_hash_pin_smoke.ail.json` + `mono_hash_stability.rs` with six pinned hashes (`eq__Int`/`Bool`/`Str`, `compare__Int`/`Bool`/`Str`).
- iter 23.4.2: `cmp_max_smoke` fixture + RED workspace-level acceptance gate.
- iter 23.4.3: `MonoTarget` struct → enum with `ClassMethod` / `FreeFn` variants; `mono_target_key` widened with `"class"` / `"free"` first component (guaranteed-disjoint keys).
- iter 23.4.4: free-fn arm in `collect_mono_targets`. NOTE: the plan's Step 4.3 sketch proposed a separate walker over `Term::App` plus a new `env.polymorphic_free_fns` index; instead I extended `synth` itself with a parallel `&mut Vec<FreeFnCall>` channel (mirroring the existing `&mut Vec<ResidualConstraint>` mechanism). One less data flow, one less helper; substitution tracking comes "for free" via fresh metavars + post-synth `subst.apply`. Plan's Step 4.4 (adding `polymorphic_free_fns` to `Env`) is therefore obsolete; not landed.
- iter 23.4.5: `synthesise_mono_fn_for_free_fn` + N-ary `mono_symbol_n`. One substantive deviation from the plan's sketch: I also substitute rigid vars in the BODY via a new `crate::substitute_rigids_in_term` helper. The plan said "passes through unchanged" mirroring the class-method arm — but free-fn bodies (e.g. `std_list.length`) carry inner `Term::Lam`s whose `param_tys` reference outer Forall vars; without body substitution, Phase 3's re-synth would fail unification on unbound rigid `a`.
- iter 23.4.6: `rewrite_class_method_calls` renamed to `rewrite_mono_calls`; new helper `poly_free_fn_names_for_module` builds the per-module set of bare + qualified poly-fn names; `collect_residuals_ordered` walks the AST emitting interleaved slots via new `interleave_slots` helper. The walker and slot collector share the same predicate so the cursor invariant survives across the two channels.
- iter 23.4.7: `workspace_has_typeclasses``workspace_has_specialisable_targets`. NOTE: the plan's expected "FAIL → PASS after generalisation" was incorrect because the auto-injected prelude (iter 23.1) already keeps the old predicate true for every user workspace. The generalisation is principled-correctness only; documented in the predicate docstring.
- iter 23.4.8: codegen cleanup — removed `lower_polymorphic_call`, both call sites, the Emitter fields (`module_polymorphic_fns`, `mono_queue`, `mono_emitted`), `emit_specialised_fn`, Pass-1 poly population, and the unused dead helpers (`apply_subst_to_term`, `descriptor_for_subst`, `type_descriptor`). `is_static_callee` collapsed to its mono-only path. Two follow-on fixes were required beyond the plan: (a) Unit-default fallback for unpinned forall vars in the mono pass — mirrors pre-iter-23.4 codegen behaviour for sites like `is_empty(Nil)` where the elem type is unobservable from the args alone; (b) updated the `ail emit-ir` command pipeline to include `lift_letrecs` + `monomorphise_workspace` (pre-iter-23.4 the codegen-time poly path was masking this dependency — `emit-ir` was broken for poly fixtures without the explicit mono call now that codegen no longer handles it).
- iter 23.4.9: end-to-end verification — `cmp_max_smoke_runs_end_to_end` proves `cmp_max(3, 7)` at Int prints 7; the three existing poly fixtures (`polymorphic_id_at_int_and_bool`, `polymorphic_apply_with_fn_param`, `poly_rec_capture_demo`) still run.
- iter 23.4.10: `docs/DESIGN.md` §"Monomorphisation (post-typecheck, pre-codegen)" amended to describe the two-arm fixpoint, the dedup key with disjoint `"class"` / `"free"` prefixes, the cursor-aligned walker, and the removed codegen-time path.
- iter 23.4.11: bench-regression check — `bench/check.py`, `bench/compile_check.py`, `bench/cross_lang.py` all exit 0; no metrics regressed.
## Concerns
- Plan Step 4.4 was bypassed (no `env.polymorphic_free_fns` field added). The synth-channel approach is structurally cleaner; the plan's index is unneeded. If a future iter needs an O(1) "is this name a poly free fn" predicate at non-synth call sites, the helper `poly_free_fn_names_for_module` (in mono.rs) builds the set on demand.
- Plan Steps 5.3 / 7's "body unchanged" / "early-out fires for class-free workspaces" assumptions were both wrong against the actual codebase shape (free-fn bodies carry rigid vars in inner Lams; prelude is auto-injected so no workspace is class-free). Both were caught and adapted during implementation — no spec defect, just plan-draft drift against the live codebase.
## Known debt
- None tracked; all out-of-scope items from the plan (five prelude free fns ne/lt/le/gt/ge, `examples/prelude.ail.json` edits, the negative E2E fixtures `eq_ord_polymorphic` / `eq_ord_user_adt`, DESIGN.md §"Decision 11" amendment, Float-NoInstance diagnostic wording, roadmap update) remain owned by iter 23.5 as planned.
## Files touched
- `crates/ailang-check/src/lib.rs` — new `FreeFnCall` struct; `synth` signature gains `&mut Vec<FreeFnCall>`; Var arm tracks `(owner_module, unqualified_name)` and pushes `FreeFnCall` for `Type::Forall`-resolved non-local refs; new `substitute_rigids_in_term` helper.
- `crates/ailang-check/src/mono.rs``MonoTarget` struct → enum (`ClassMethod` / `FreeFn`); `mono_target_key` widened; new `mono_symbol_n` (N-ary); new `synthesise_mono_fn_for_free_fn`; new `poly_free_fn_names_for_module`; new `interleave_slots`; rename `rewrite_class_method_calls``rewrite_mono_calls`; `workspace_has_typeclasses``workspace_has_specialisable_targets`; fixpoint loop matches on target variants.
- `crates/ailang-check/src/lift.rs`, `crates/ailang-check/src/builtins.rs` — synth call sites pass new `&mut free_fn_calls` Vec.
- `crates/ailang-codegen/src/lib.rs` — deletions per Task 8 (Emitter fields, `lower_polymorphic_call`, `emit_specialised_fn`, mono drain loop, Pass-1 poly population, `is_static_callee` poly arm). `use` import shrunk.
- `crates/ailang-codegen/src/subst.rs``apply_subst_to_term` and `descriptor_for_subst` removed (dead post-Task-8).
- `crates/ailang-codegen/src/synth.rs``type_descriptor` removed (dead post-Task-8).
- `crates/ail/src/main.rs``emit-ir` command gains `lift_letrecs` + `monomorphise_workspace` pre-passes (matches `build` shape).
- `crates/ail/tests/typeclass_22b3.rs` — updated three test sites to `MonoTarget::ClassMethod` enum form.
- `crates/ail/tests/mono_hash_stability.rs` (new) — regression pin for six primitive Eq/Ord mono-symbol body hashes.
- `crates/ail/tests/mono_unification.rs` (new) — six tests covering variant key disjointness, free-fn target emission, free-fn synthesis with rigid substitution, rewrite walker on free fns, identity for poly-free-fn-only workspaces, and end-to-end `cmp_max_smoke` runtime correctness.
- `examples/cmp_max_smoke.ail.json` (new) — `cmp_max : forall a. Ord a => (a, a) -> a` composition fixture.
- `examples/mono_hash_pin_smoke.ail.json` (new) — fixture exercising the six primitive Eq/Ord mono symbols for hash-stability pinning.
- `docs/DESIGN.md` — §"Monomorphisation (post-typecheck, pre-codegen)" amended.
## Stats
bench/orchestrator-stats/2026-05-11-iter-23.4.json
-104
View File
@@ -1,104 +0,0 @@
# iter cadence — audit/fieldtest/docwriter cadence restructure
**Date:** 2026-05-11
**Branch:** main
**Status:** DONE
**Tasks completed:** 5 of 5
## Summary
This iter sorts the milestone-close stretch of the pipeline into
three cadence buckets, packaged by stability-window rather than by
milestone-grain:
1. **Per-milestone-mandatory — `audit`.** Runs every milestone close.
Architect drift review against DESIGN.md plus the three regression
scripts (`bench/check.py`, `bench/compile_check.py`,
`bench/cross_lang.py`). Step 3 ("Optional rustdoc audit") and the
`ailang-docwriter` cross-reference are removed; the rustdoc
warning line is gone from the Handoff Contract and the Common
Rationalisations table. Audit is now purely drift + bench.
2. **Boss-judgment post-audit — `fieldtest`.** Fires only when the
orchestrator decides the iteration is correct and wants a field
test, after audit closes clean (or with `ratify`-d drift only).
Trigger is reframed from "audit closes" to "Boss judgment call";
the expected finding shape (simple bugs route via `debug`,
catastrophic architecture problems route via next `brainstorm`)
is named explicitly. Cross-references add the ordering rule:
fieldtest runs *before* docwriter.
3. **Boss-judgment post-fieldtest — `docwriter`.** New skill at
`skills/docwriter/SKILL.md`. Boss-dispatched only, never
triggered by audit closing. Pre-condition is clean audit plus any
pending fieldtest. The agent (`ailang-docwriter`) carries the
substantive rustdoc rules; the SKILL.md only governs trigger and
dispatch. Symlinks added at `.claude/skills/docwriter` and
`.claude/agents/docwriter`.
`skills/README.md` is the one place that visibly mirrors the new
three-bucket taxonomy: the skill table grew from six rows to seven,
the pipeline diagram extends past `audit --(clean)-->` into
fieldtest and docwriter as two distinct Boss-decision points, and
the agent roster moves `ailang-docwriter` into its own dispatch
group.
## Per-task subjects
- iter cadence.1: skills/docwriter — new Boss-dispatched skill (split from audit)
- iter cadence.2: skills/audit — drop docwriter step (now own skill)
- iter cadence.3: skills/fieldtest — Boss-dispatched framing + ordering note
- iter cadence.3-fix: address quality-review minors (frontmatter desc + cross-ref path)
- iter cadence.4: skills/README — three-bucket cadence taxonomy
- iter cadence.4-fix: drop stale audit-dispatches-docwriter parenthetical (README Conventions)
- iter cadence.4-fix: address quality-review importants + German-loanword sweep (English-only in-tree)
- iter cadence.5: per-iter journal + INDEX
## Lesson
**Packaging cadence by stability-window outperforms packaging by
milestone-grain.** The previous arrangement bundled rustdoc cleanup
into `audit` as an "optional Step 3", which papered over the real
mismatch: rustdoc rot is a *post-stability* concern (documenting
something that gets rewritten three iterations later is waste),
while drift review and bench regression are per-iteration concerns
(letting them slip even one milestone compounds). Bundling those
two cadences inside one skill made the optional step easy to skip
on every milestone, which is exactly the failure mode the skill
discipline exists to prevent. Splitting docwriter out into its own
Boss-dispatched skill makes the cadence honest: audit is
non-negotiable per milestone, docwriter is non-negotiable per
*stability window*. The fieldtest framing flip — from "fires when
audit closes" to "Boss judgment after audit closes" — is the same
refactor at a smaller scale: the milestone clock was not the right
trigger; the orchestrator's judgement that the iteration is
*complete* is.
## Concerns
None worth carrying forward. Three quality-review-fix follow-ups on
cadence.3 and cadence.4 (frontmatter description drift, stale
audit-dispatches-docwriter parenthetical, one German-loanword in
in-tree text) — all caught by the standard implement-skill
two-stage review and resolved inline.
## Known debt
- The agent file `skills/docwriter/agents/ailang-docwriter.md` was
moved from `skills/audit/agents/` by the user before this iter
began. Its standing reading list still names `skills/audit` as
its home in one or two phrasings; a sweep of the agent's
cross-references is a candidate tidy iteration but did not
block this iter's narrower SKILL.md reshuffle.
- `docs/specs/2026-05-11-audit-fieldtest-docwriter-cadence.md` is a
dated artefact — the spec describes the design at time of
writing and is not retroactively updated as the implementation
shipped. Per the or.1 convention, the per-iter journal is the
running annotation.
## Blocked detail
N/A — DONE.
## Commits
`e351969^..HEAD` (8 commits including 3 quality-review-fix
follow-ups across cadence.3 and cadence.4).
-190
View File
@@ -1,190 +0,0 @@
# iter disc.1 — boss-only commits and main-as-quarantine
**Date:** 2026-05-11
**Started from:** 841d65d
**Status:** DONE
**Tasks completed:** 10 of 10 (Boss-direct implementation; no orchestrator dispatch)
## Summary
Skill-system-wide discipline update. Two project-wide rules are now
explicit:
1. **Only the Boss commits.** No skill agent — implementer,
brainstormer, planner, debugger, fieldtester, docwriter,
architect, bencher — performs `git commit`. Agents write all
their artefacts into the working tree as unstaged changes; the
Boss inspects, decides commit shape (typically one cohesive iter
commit, occasionally a few logical commits), and commits.
2. **main HEAD is sacrosanct.** No actor — Boss or agent — runs
`git reset` or `git revert` on main. Bad work stays in the
working tree where it is still discardable via
`git checkout -- <paths>`; main moves forward only via Boss
commits.
The two rules together force a working-tree-as-quarantine
discipline: nothing half-baked enters main, because nothing can
be taken back off main. The implement skill loses its
`iter/<iter_id>` branch-per-iter mechanic entirely; per-task agent
commits are also gone — Phase 4 of the orchestrator-agent now
writes the per-iter journal and the stats file into the working
tree (Write tool) but does not commit them.
## Per-task notes
- disc.1.1: `skills/implement/SKILL.md` — Iron Law rewritten
("IMPLEMENTER NEVER COMMITS" replaces "ONE BRANCH PER ITER";
"MAIN HEAD IS SACROSANCT" added). Step 3 "Boss merge step"
renamed and rewritten as "Boss inspect + commit step" (no
fast-forward; Boss reads `git status` / `git diff`, decides
commit shape, commits). Step 4 (BLOCKED) rewritten: Working-Tree
inspect, Repair re-dispatch or Discard via
`git checkout -- .`, no branch deletion. Description, Overview,
Rationalisations, Red Flags updated.
- disc.1.2: `skills/implement/agents/ailang-implement-orchestrator.md`
rewritten — Phase 0 is now a clean-tree check
(`git status --porcelain` must be empty), records
`start_sha` informationally (not as reset target), refuses to
proceed on a dirty tree. All `git commit` invocations removed.
Phase 4 (journal) and Phase 5 (stats) write to the working tree
via the Write tool, no commit. End-report Branch-field replaced
by `Working tree: dirty (N files)` and `(uncommitted)` markers
on file paths.
- disc.1.3: `skills/implement/agents/ailang-implementer.md`,
`ailang-tester.md` — commit steps in The Process removed.
Red-Flag "commit on red tests" replaced by the broader "never
commits, ever". Self-review now operates on `git diff HEAD`
with `git checkout -- <path>` for undo.
- disc.1.4: `skills/implement/agents/ailang-spec-reviewer.md`,
`ailang-quality-reviewer.md` — Carrier fields `pre_task_sha` /
`head_sha` replaced by `diff_command` (default `git diff HEAD`).
The full working-tree diff accumulates across tasks; reviewers
focus on the current task's footprint via the task block's file
list (or an optional `task_footprint_hint`).
- disc.1.5: `skills/brainstorm/SKILL.md` — Step 8 spec-commit
block removed; brainstorm writes the spec, user reviews, Boss
commits. The Step-7.5 retire-on-BLOCK procedure was rewritten
too: it now deletes the working-tree spec file (Bash `rm`, not
`git rm`, since the file was never committed) and appends a
roadmap entry; Boss commits the roadmap edit.
- disc.1.6: `skills/planner/SKILL.md` — Step 6 plan-commit
block removed; planner writes the plan, hand-off carries the
filepath, Boss commits. Plan-template tasks lose their
per-task "Step 5: Commit" — tasks are now 4 steps (write-failing-
test, verify-fail, write-impl, verify-pass) and leave work in
the working tree for the iter-level Boss commit. The "Bundle
Tasks 3 and 4" rationalisation reframed: tasks are units of
review, not units of commit.
- disc.1.7: `skills/debug/SKILL.md`, `skills/debug/agents/ailang-debugger.md`
— debugger writes the RED test to the working tree (uncommitted)
and hands off. The Boss chooses between two flows for
implement-mini: commit the RED separately for an audit-trail
before dispatching mini-mode (clean-tree mini gets the RED via
HEAD diff), or commit RED+GREEN together at the end (Boss
decides before dispatching).
- disc.1.8: `skills/fieldtest/SKILL.md`,
`skills/fieldtest/agents/ailang-fieldtester.md` — Phase 5
commit block removed; fieldtester writes `.ailx` files,
`.ail.json` files, and the spec to the working tree; Boss
commits with the suggested subject
`fieldtest: <milestone> — <N> examples, <K> findings`.
- disc.1.9: `skills/docwriter/agents/ailang-docwriter.md`
Iron Law extended with "YOU NEVER COMMIT"; Red-Flag added.
`skills/audit/SKILL.md` — Step 4 reframed to name the Boss as
the actor who commits tidy fixes / ratify baselines / clean
closes. Other audit-agent files (architect, bencher) had no
commit invocations and are unchanged in this dispatch.
- disc.1.10: `skills/README.md` — Conventions section gained
two new bullets ("Only the Boss commits", "main HEAD is
sacrosanct"). Skill-table Output column updated for implement,
docwriter, fieldtest, debug to say "uncommitted in the working
tree". `CLAUDE.md` Direction-freedom subsection rewritten
(recovery is no longer about reverting commits; it is about
discarding bad working-tree state). New
"Commit discipline and main-branch sanctity" subsection added,
stating both rules in CLAUDE.md form.
## Concerns
(empty)
## Known debt
- Three historical files describe the obsolete branch-per-iter
discipline: `docs/specs/2026-05-11-implement-orchestrator-agent.md`,
`docs/journals/2026-05-11-iter-pr.1.md`,
`docs/plans/2026-05-11-orchestrator-refactor.md`. They are
zeitliche records and are left intact; this journal entry is the
forward-pointer that supersedes them. A future tidy iter could
add "Superseded by" banners, but the journal-archive convention is
append-only history.
- The implement orchestrator's Phase 0 refuses to run on a dirty
tree. The first real dispatch under the new discipline may
surface friction around this (e.g. Boss prepared scratch files
before dispatch). Capture in that iter's journal.
## Blocked detail
(none — DONE)
## Files touched
- `CLAUDE.md`
- `skills/README.md`
- `skills/audit/SKILL.md`
- `skills/brainstorm/SKILL.md`
- `skills/debug/SKILL.md`
- `skills/debug/agents/ailang-debugger.md`
- `skills/docwriter/agents/ailang-docwriter.md`
- `skills/fieldtest/SKILL.md`
- `skills/fieldtest/agents/ailang-fieldtester.md`
- `skills/implement/SKILL.md`
- `skills/implement/agents/ailang-implement-orchestrator.md`
- `skills/implement/agents/ailang-implementer.md`
- `skills/implement/agents/ailang-quality-reviewer.md`
- `skills/implement/agents/ailang-spec-reviewer.md`
- `skills/implement/agents/ailang-tester.md`
- `skills/planner/SKILL.md`
- `docs/journals/2026-05-11-iter-disc.1.md` (this file)
- `docs/journals/INDEX.md`
## Motivation
On 2026-05-11 iter 23.4 (eq/ord prelude — Free-Fns) BLOCKED three
times. A retrospective brainstorm produced a corrected spec; the
implement-orchestrator was dispatched on the revised plan and
immediately BLOCKED at Phase 0 with a branch-state mismatch: an
existing `iter/23.4` branch carried prep2 + prep3 commits that
the spec falsely claimed lived on main. They had never been
fast-forwarded; they were stranded on the iter-branch when the
iter was abandoned. The grounding-check agent had passed the
spec because it verified main-behaviour-via-green-tests, not
git-reachability of named commit SHAs.
The user identified the structural root cause: branch-per-iter
plus manual Boss-merge plus iter-stacking allows work to strand
on a branch that nobody integrates. The discipline correction is
this iter:
- Drop the branch mechanic in implement entirely.
- Centralise commit authority on the Boss across every skill.
- Make main forward-only by policy: no reset, no revert, by
anyone.
Per-task agent commits were eh too many (user phrasing: "es wird
eh viel zu viel commitet") — the new policy eliminates them as
a side-effect.
## Forward pointer
Step 5 of the 2026-05-11 recovery plan: re-attempt the eq/ord
prelude mono-pass unification (iter 23.4 in the corrected spec
`docs/specs/2026-05-11-23-eq-ord-prelude.md`), now under the
discipline this iter installs. A fresh plan is written first;
the implement-orchestrator is dispatched against it; the Boss
inspects + commits at the end. No branches, no agent commits.
## Stats
None — this iter was Boss-direct implementation (no orchestrator
dispatch; no stats file written).
-126
View File
@@ -1,126 +0,0 @@
# iter gc.1 — Grounding-Check Agent, brainstorm Step 7.5 hard-gate
**Date:** 2026-05-11
**Branch:** iter/gc.1
**Status:** DONE
**Tasks completed:** 5 of 5
## Summary
This iter ships `ailang-grounding-check`, a read-only spec reviewer
dispatched between brainstorm Step 7 (self-review) and Step 8
(user-approval). The agent reads a freshly-written spec with no
sunk-cost bias, extracts the spec's load-bearing assumptions about
current compiler / checker / codegen / schema behaviour, and for
each one searches the workspace for a currently-green test that
ratifies it. PASS or BLOCK; no PARTIAL credit. The output is the
hybrid format the agent template prescribes: a ratified-assumptions
table for fast Boss scanning plus a per-assumption prose block for
the unratified set.
Wiring is markdown-only. `skills/brainstorm/SKILL.md` gets a new
Step 7.5 hard-gate section, two new Handoff-Contract rows
(brainstorm → grounding-check, brainstorm → roadmap on no-override
BLOCK), and a Cross-references rewrite that splits the old "No
private agents" bullet into "Private agent" (the new grounding-check
dispatch) plus "Ad-hoc dispatch" (the existing plan-recon
opt-in). `skills/README.md` gets one new agent-roster row and one
new symlink-discovery line. `.claude/agents/brainstorm` is the
matching symlink. Spec-23 (Eq/Ord prelude) is retired because the
session that produced gc.1's spec is the same session that
discovered Spec-23's polymorphic-free-fn-monomorphisation assumption
was unratified — a fact gc.1's whole machinery exists to catch from
now on. The roadmap P1 Post-22 Prelude entry is amended to record
the retirement, name the unratified mechanism, and point forward to
the re-brainstorm.
The bootstrap irony stands: the spec for `ailang-grounding-check`
did not itself run through `ailang-grounding-check`. The agent did
not exist when its own spec was written, so per Spec §Architecture's
bootstrap-exclusion the gc.1 plan was implemented without the
gating mechanism it introduces. The first real run of the new
pipeline is the re-brainstormed Spec-23 in a separate session, not
this iter.
## Per-task subjects
- iter gc.1.1: agent — ailang-grounding-check definition
- iter gc.1.2: brainstorm SKILL — insert Step 7.5 (grounding-check hard-gate)
- iter gc.1.3: skills README — roster + symlink for ailang-grounding-check
- iter gc.1.4: symlink .claude/agents/brainstorm -> skills/brainstorm/agents
- iter gc.1.5: retire Spec-23, roadmap forward-pointer to re-brainstorm
## Concerns
- Plan §Step 2.4 and §F.2 expect `grep -c "^### Step " == 9` after
the 7.5 splice, but the brainstorm SKILL has nine *original* step
headings (Step 1 through Step 9), so adding 7.5 lands at 10. The
plan's parenthetical comment in §F.2 enumerates ten labels (1, 2,
3, 4, 5, 6, 7, 7.5, 8, 9) while asserting `wc -l = 9` — internally
inconsistent. Substantive intent (Step 7.5 between 7 and 8, leave
Step 9 in place) is unambiguous and honored; the off-by-one is a
plan-template miscount, not an implementation bug. Surfaced as a
cadence-flow signal to the Boss in the end-report.
## Cadence-flow notes (for skillsystem postmortem)
Two observations from the orchestrator's end-report, surfaced here
for the Boss's future skillsystem-postmortem entry — NOT in scope
for gc.1 itself:
1. **Planner verify-step counts derived from offsets become stale
at plan-freeze time.** The §Step 2.4 and §F.2 miscount above is
one instance. The orchestrator suggested two fixes: (a) planner
re-runs grep counts against the current file state at
plan-freeze time, or (b) planner expresses such counts as
`≥ N` inequalities rather than exact-match equalities. The
latter is cheaper and survives subsequent edits to the same
file.
2. **Markdown-only verbatim-content iters bottom out cleanly in the
spec-check + quality-check phases.** Zero review-loop fixups
landed this iter; all five tasks went DONE on first pass through
both phases. The spec-check phase effectively reduced to "is
the verbatim text from the plan in the diff?" and the
quality-check phase had little to flag beyond whitespace
hygiene. Both passes still ran (discipline preserved), but the
value they added was low. This is expected for verbatim-content
tasks and not a concern about the discipline itself — captured
here so it is not misread, in retrospect, as evidence that the
phases are too lenient.
3. **First real run of grounding-check (2026-05-11 same-day, on
the revised Spec-23): PASS, 15 assumptions ratified.** The
agent's output format leaked an internal-thinking preamble
before the structured `GROUNDING-CHECK REPORT` block — the
Iron Law should be tightened to forbid any preamble before the
structured report. Substantively unproblematic (the judgement
and test pins were clear) but a real prompt-discipline gap.
4. **Behaviour change requested for impending-BLOCK paths.** On a
miss (assumption with no green test), the agent should attempt
to write the missing GREEN test itself — strictly test code,
never `src/`-side app code — and only BLOCK if test creation
fails. This is two changes: (a) tools — add `Write` (or `Edit`)
to the agent frontmatter, today `Read, Glob, Grep, Bash` only;
(b) discipline — Iron Law gains "you may write tests under
`crates/*/tests/` or `#[cfg(test)] mod tests`; you may NOT touch
any file outside test scope." The first real PASS run did not
exercise this path, so the constraint surfaces here as a future
tightening rather than as a tested behaviour.
## Known debt
None.
## Blocked detail
N/A — DONE.
## Commits
`606a2eb..HEAD` (5 task commits, no review-loop fixups).
## Stats
bench/orchestrator-stats/2026-05-11-iter-gc.1.json
-151
View File
@@ -1,151 +0,0 @@
# iter or.1 — orchestrator-refactor
**Date:** 2026-05-11
**Branch:** iter/or.1
**Status:** DONE
**Tasks completed:** 7 of 7
## Summary
This iter lands the three coupled changes from
`docs/specs/2026-05-11-implement-orchestrator-agent.md`:
1. **Boss-context offload.** A new agent
`skills/implement/agents/ailang-implement-orchestrator.md` owns
one full `/implement` iter end-to-end — per-task implementer →
spec-reviewer → quality-reviewer loop, with the canonical
re-loop limits — running in its own context. The Boss now
dispatches one subagent and receives a ≤500-token end-report.
2. **Iter isolation via branches.** Each `/implement` run owns a
dedicated `iter/<iter_id>` branch, created from `origin/main`
in Phase 0 of the orchestrator-agent. Integration is a Boss-side
`git switch main && git merge --ff-only` after reading the
end-report.
3. **Per-iter journals.** `docs/JOURNAL.md` (14k lines) is renamed
to `docs/journal-archive.md` with an archived-status header.
Going forward, each iter writes its own file under
`docs/journals/<YYYY-MM-DD>-iter-<id>.md`; the Boss appends a
one-line pointer to `docs/journals/INDEX.md` at merge time.
This file is the first such per-iter journal.
The orchestrator-agent goes live on the *next* `/implement` run after
this iter lands. This iter itself was executed in the pre-refactor
mode (Boss dispatched workers directly) — a bootstrap necessity since
the new agent did not yet exist while creating it.
Model selection was unchanged: every dispatch this iter used Opus 4.7.
The token savings come from token-volume relocation, not from
intelligence downgrade.
## Per-task subjects
- iter or.1.1: migrate JOURNAL.md to journal-archive.md + docs/journals/
- iter or.1.2: repoint top-level docs to docs/journals/ + journal-archive.md
- iter or.1.2-fix: address quality-review minor + nit
- iter or.1.3: repoint skill + agent reading lists to docs/journals/
- iter or.1.3-fix: address quality-review minors (architect / audit phrasing)
- iter or.1.4: carrier switch task_text -> task_text_path
- iter or.1.5: new agent ailang-implement-orchestrator
- iter or.1.6: rewrite skills/implement/SKILL.md to delegate to orchestrator-agent
- iter or.1.6-fix: correct git rebase syntax + README cross-ref wording
- iter or.1.6-fix2: SKILL.md description drift + self-contained exception ref
- iter or.1.7: document ailang-implement-orchestrator as named exception
## Concerns
Three `DONE_WITH_CONCERNS` observations across the iter, all
plan-template issues rather than scope deviations:
- Task 1: the plan's Step 6 listed `git add docs/JOURNAL.md` after
a `git mv`; this errors because the path is already gone from the
working tree. The implementer dropped the dead path. Plan defect.
- Task 4: the plan's per-step instructions enumerated only the
carrier-contract row, but the plan's Step 6 verification clause
(`grep -n "task_text\b" → empty`) required visiting every body-prose
mention of `task_text` too. The implementer correctly expanded
scope to satisfy the verification; rewrites preserved semantics.
- Task 6: the plan's Step 1 verbatim content is 194 lines; Step 2's
sanity check expected 110160. Step 1 is canonical; the sanity
range was my optimistic estimate when writing the plan. The
implementer correctly followed Step 1.
- Task 7: the plan's Step 3 grep expected 1 match for
`ailang-implement-orchestrator`, but the new exception paragraph
itself names the agent, so the actual count is 2. Same shape of
plan-template error.
None of these affect correctness; all are notes for the next plan
template revision.
## Known debt
- Two bare `JOURNAL` uppercase mentions remain in `CLAUDE.md` at
lines 285/287 (the "answer three questions" closing paragraph).
They refer conceptually to the historical log, but the surface
reading is slightly stale now that JOURNAL is no longer a file.
Out of scope for Task 2's enumerated sites; carry-on observation.
- "Boss" / "Boss-Orchestrator" vocabulary is introduced in the new
`skills/implement/SKILL.md` and `ailang-implement-orchestrator.md`
to distinguish from "the orchestrator-agent". `CLAUDE.md` uses
"orchestrator" throughout. The distinction is intentional, but a
one-line glossary entry in `CLAUDE.md` would prevent term drift
in future docs.
- Verification ladder steps 15 (real-orchestrator-agent dispatch
exercise) are deferred to the first real `/implement` run after
this iter merges. Steps 68 (intentional-BLOCKED, intentional
re-loop, manual Boss-merge replay) get checked off when those
failure modes arise organically.
## Blocked detail
N/A — DONE.
## Commits
`1891030..92e39bb` (11 commits including 4 quality-review-fix
follow-ups; the per-task pattern was DONE/DONE-WITH-CONCERNS →
spec-reviewer compliant → quality-reviewer approved-or-fixed-then-
approved, per the implement skill's two-stage review).
## Stats
Not produced this iter — `bench/orchestrator-stats/` is a new
artefact introduced by the orchestrator-agent (Phase 5 of its
process). This iter was executed in pre-refactor mode by the Boss
directly, so no stats file exists. The first stats file will be
written by the orchestrator-agent on the next real `/implement` run.
## Audit / milestone close
Audit ran post-iter (skill-system mandatory at milestone close).
- **Architect drift review:** `drift_found`. Live docs and a handful
of concept-noun sites still mentioned `JOURNAL.md` as if it were a
current path. Highest-impact misses were `docs/WhatsNew.md` (live
changelog framing-text — wrongly carved out of the Task 3 plan as
"dated artifact"), `docs/roadmap.md` lines 25-27 (live convention
text), and four bare `JOURNAL` mentions in `CLAUDE.md` (lines 108,
267, 285, 287). Five concept-noun spots in
`skills/audit/agents/ailang-architect.md` (Iron Law line 100 was
already correct; the others were process descriptions). All
resolved inline via `iter or.1.tidy` (single commit, four files).
Spec-vs-shipped divergences (the spec still prescribes the invalid
`git rebase ... onto main` and lists 3 blocked reasons where the
agent ships 5) were intentionally left as-is — specs are dated
artefacts capturing design at time of writing; the per-iter journal
is the running annotation.
- **Bench-regression check:** `bench/check.py` 63 metrics stable
(exit 0); `bench/compile_check.py` 24 metrics stable (exit 0);
`bench/cross_lang.py` 25 metrics stable (exit 0). No language code
changed; the green result was expected and confirms it.
- **Rustdoc:** 18 pre-existing warnings (2 in `ailang-core`, 16 in
`ailang-check`) about private-item links and unresolved cross-refs.
None caused by this iter. Deferred to a future docwriter sweep.
Milestone close: clean after `or.1.tidy`. Three roadmap follow-ups
worth noting:
- WhatsNew.md should NOT be carved out as "dated artifact" in any
future doc-sweep plan; it is a live changelog.
- A separate docwriter sweep for the 18 pre-existing rustdoc warnings.
- Once a real `/implement` run goes through the new orchestrator-agent,
ladders 15 of the verification ladder in `docs/specs/
2026-05-11-implement-orchestrator-agent.md` are checked off.
-189
View File
@@ -1,189 +0,0 @@
# iter or.2 — orchestrator-agent design correction
**Date:** 2026-05-11
**Branch:** none (trivial-mechanical carve-out — doc-only revision, single commit on main)
**Status:** DONE
**Tasks completed:** N/A (doc revision, not a plan-driven iter)
## Summary
This entry corrects a factual error in the `or.1` architecture that
surfaced during the first real dispatch (pr.1, same day). The
revision lands as one direct-on-main commit because the change is
doc-only: no code, no tests, no behaviour shift in any compiler
crate. The Iron-Law mini-fix from earlier today (`origin/main`
`main`) is preserved unchanged — that fix remains correct.
### What was wrong
`or.1` and `pr.1` shipped the `ailang-implement-orchestrator` agent
with frontmatter `tools: Read, Edit, Write, Bash, Glob, Grep, Agent`
and an architecture that depended on the orchestrator-agent
spawning further subagents per task (implementer → spec-reviewer →
quality-reviewer → tester, each in fresh context). The architecture
was framed in `skills/README.md` as "agents do not call other
agents — one named exception" (the orchestrator-agent being the
exception).
The exception never existed in Claude Code. The platform forbids
**all** nested-subagent dispatch, categorically:
> "Subagents cannot spawn other subagents, so `Agent(agent_type)`
> has no effect in subagent definitions."
> — https://code.claude.com/docs/en/sub-agents
> "Subagents cannot spawn their own subagents. Don't include
> `Agent` in a subagent's `tools` array."
> — https://code.claude.com/docs/en/agent-sdk/subagents
The `Agent` tool was silently dropped from the orchestrator-agent's
tool set at dispatch time (no error, no warning). The pr.1 run
caught this empirically; the diagnosis closed the question today.
### Why the failure took the form it did
The Boss-Orchestrator (me) designed `or.1` on the false premise
that the project's house rule "agents do not call other agents"
was a local convention with a documented exception. In reality the
house rule was a *restatement of an external constraint* — and the
"named exception" was incompatible with the constraint itself.
This is a verification-of-capability failure: the architecture was
locked in before checking the platform docs for what subagents can
do. The right order is the other way around. (Memorised as a
feedback memory for future architecture decisions.)
### The corrected architecture
Same skill body, same skill triggers, same branch-and-merge
discipline, same Boss-side end-report contract. What changes is
*inside* the orchestrator-agent's run:
- Per-task phases (implementer → spec-compliance check → quality
check) run sequentially as **role-switches in the orchestrator-
agent's own context**, not as nested subagents.
- The `ailang-implementer.md` / `ailang-spec-reviewer.md` /
`ailang-quality-reviewer.md` / `ailang-tester.md` files now serve
as **phase reference files** — the orchestrator-agent consults
them at each role-switch to inhale the discipline of that role,
but does not dispatch them as separate subagents.
- The Boss-context offload property is preserved (the Boss still
sees only the ≤500-token end-report; per-task chatter stays
inside the orchestrator-agent's context).
- The fresh-per-phase context property — which was the design goal
that drove `or.1` to attempt nested dispatch — is **given up**.
Pure role-switching inside one context is a weaker discipline
than fresh-context-per-phase. The trade-off is named openly: the
cost is the loss of fresh-eyes review per phase; the benefit is
that the architecture is buildable in Claude Code at all.
- The pr.1 "degraded mode" was not actually degraded — it was the
only mode this agent can run in. The pr.1 journal's degraded-
mode caveat is preserved as-is (historical record of how the
constraint was discovered).
### Why option (A), not (B) or (C)
Three options were on the table:
- **(A)** Single-context inline phases (what landed) — preserves
Boss-context offload, gives up fresh-per-phase context.
- **(B)** Roll back to pre-or.1: Boss dispatches each per-task
subagent itself. Preserves fresh-per-phase context, gives up
Boss-context offload.
- **(C)** Hybrid: orchestrator-agent for setup / branch / journal,
Boss for per-task dispatches in between. Preserves both
theoretically but multiplies moving parts.
(A) was chosen because the Boss-context offload is the scarce
resource (user chat budget). Empirically, the pr.1 run with the
plan template's verbatim per-task content blocks made fresh-per-
phase context contribute little additional signal beyond a careful
single-context phase loop. The decision was made by the user
2026-05-11 after the diagnosis was presented; the rationale belongs
in the user's hands, not the agent's.
### Files changed
- `skills/implement/agents/ailang-implement-orchestrator.md`
frontmatter (`Agent` removed from tools, description rewritten),
Iron Law (sequential role-switches instead of fresh subagents),
Phase 2.1 / 2.2 / 2.3 rewritten as inline phases, Phase 3 likewise,
common rationalisations and red flags updated.
- `skills/implement/SKILL.md` — frontmatter description, Iron Law,
per-task sub-status mechanics note (vocabulary is preserved but
now describes inline role-phase status), one new
Common-Rationalisation row, Cross-references rewritten (role-files
recast as phase references, with a "Why inline phases" subsection
pointing here).
- `skills/README.md` — Conventions: "Agents do not call other
agents — one named exception" → "Agents do not call other agents"
(the exception is gone, with an explanation of why). Agent roster
rows for implementer / spec-reviewer / quality-reviewer / tester
recast as phase references for the orchestrator-agent.
- `docs/roadmap.md` — P1 entry "`Agent` tool wiring for the
dispatched orchestrator-agent" removed (resolved as
categorically-not-fixable).
- `docs/journals/INDEX.md` — this entry appended.
- `docs/WhatsNew.md` — user-facing correction entry appended.
### Files NOT changed (and why)
- `CLAUDE.md` — no stale references; the file does not encode the
named-exception story.
- `skills/implement/agents/ailang-implementer.md`,
`ailang-spec-reviewer.md`, `ailang-quality-reviewer.md`,
`ailang-tester.md` — these remain unchanged. Their content
(discipline, reading list, output format) is correct in the new
architecture; their *role* in the system has shifted from
"dispatched subagent" to "phase reference for the orchestrator-
agent". The shift is documented in the README roster row and the
SKILL.md Cross-references; no in-file edits are required for
correctness. A future cleanup iter may rewrite their frontmatter
to drop the "subagent" framing if it begins to mislead — for now
the cost of touching four files outweighs the marginal clarity
gain.
- `docs/specs/2026-05-11-implement-orchestrator-agent.md` and
`docs/specs/2026-05-11-plan-recon-subagent.md` — specs are
historical records of the design as conceived; they remain
accurate to that moment. Drift between spec and the corrected
architecture is acknowledged here, not by editing the specs.
## Concerns
None. The revision is deliberate and self-contained.
## Known debt
- **Phase reference files frame themselves as dispatched subagents.**
`ailang-implementer.md` and its three siblings still read like
files describing a subagent receiving a carrier from a dispatcher.
In the corrected architecture, the orchestrator-agent IS the
dispatcher and the worker for those phases. If the next
`/implement` run shows the orchestrator-agent confused by this
framing during phase reads, a follow-up cleanup iter rewrites
the four files. Not pre-emptive — wait for an empirical signal.
- **No verification ladder for the corrected design.** `or.1`'s
verification ladder (items 15 in
`docs/specs/2026-05-11-implement-orchestrator-agent.md`) was
written for the nested-dispatch architecture. Items 13
(Boss-side dispatch, branch creation, journal write) carry over
unchanged. Items 45 (intentional BLOCKED, intentional re-loop)
are weaker in the inline-phase world — re-loop is a same-context
retry, not a fresh-context re-dispatch — and a new ladder entry
for the inline phase-switch should be added the next time
`/implement` runs against a non-trivial plan. Tracked
informally; no ticket.
## Blocked detail
N/A — DONE.
## Commits
This entry's revision lands as a single trivial-mechanical commit
on `main` (no iter branch, no plan template). The commit subject is
`or.2: orchestrator-agent design correction — no nested subagent dispatch`.
## Stats
N/A — no orchestrator-agent dispatch (this is a doc-only revision
made directly by the Boss-Orchestrator).
-171
View File
@@ -1,171 +0,0 @@
# iter pr.1 — plan-recon subagent
**Date:** 2026-05-11
**Branch:** iter/pr.1
**Status:** DONE
**Tasks completed:** 5 of 5
## Summary
This iter lands the changes from
`docs/specs/2026-05-11-plan-recon-subagent.md`: a new read-only
subagent `ailang-plan-recon` that the `planner` skill dispatches at
Step 2 (file-structure mapping), moving the read-heavy code-recon out
of Boss context. `brainstorm` gets a one-paragraph note in its
Cross-references allowing ad-hoc dispatch of the same agent when
entering unfamiliar code territory; it does NOT receive a dedicated
agent. Five sites changed: one new agent file, one new symlink, three
SKILL/README edits. No Rust, no benchmark impact.
The recon agent goes live the next time `planner` runs after this
iter merges. This plan itself was written in pre-recon mode (Boss did
the file-mapping in-context) — a bootstrap necessity since the recon
agent did not yet exist while creating it.
**Degraded-mode dispatch note (orchestrator-agent).** This was the
first real dispatch of `ailang-implement-orchestrator` after milestone
`or.1` (verification ladder steps 13). The dispatch was launched
without the `Agent` tool wired through to the orchestrator-agent's
tool set, so the canonical per-task implementer → spec-reviewer →
quality-reviewer sub-loop could not run. Two paths were available:
return `BLOCKED` with `reason: infra`, or execute the tasks inline
within the orchestrator's context as a degraded mode. The latter was
chosen because (a) every task in this plan carries verbatim content
blocks pre-written, leaving no judgement work for an implementer; (b)
the Iron-Law goal of *Boss-context offload* is still preserved (the
Boss sees one end-report regardless of how the per-task work was
executed inside the orchestrator); (c) Auto Mode is active and the
user explicitly named this run as a verification exercise, so
bailing on infra forces a re-dispatch for a tool-wiring oversight at
the dispatch layer, not at the agent-design layer. The per-task
discipline (RED check → write → GREEN check → commit) was preserved;
the fresh-subagent-per-task isolation was not. This degraded path is
*one-time*, not a precedent: the orchestrator-agent's standing tool
set must include `Agent`, and the next dispatch should restore the
canonical loop. Recorded for the verification ladder.
## Per-task subjects
- iter pr.1.1: new agent ailang-plan-recon
- iter pr.1.2: symlink .claude/agents/planner
- iter pr.1.3: planner Step 2 dispatches ailang-plan-recon
- iter pr.1.4: brainstorm allows ad-hoc plan-recon dispatch
- iter pr.1.5: roster + discovery entries for ailang-plan-recon
## Concerns
One `DONE_WITH_CONCERNS` observation, a plan-template issue rather
than a scope deviation:
- Task 4's Step 1 RED-check regex is malformed:
`grep -q "No private agents.\*\*\* This skill is dialogue-driven\."`
expects three literal stars (`\*\*\*`) after the period, but the
file content has only two (markdown bold `**`). The check exits
non-zero against a correct pre-state. The implementer (the
orchestrator-agent in this degraded mode) verified RED by intent —
the line exists, "ad-hoc dispatch" does not — and proceeded. Plan
defect; note for the next plan template revision.
## Known debt
- Verification ladder items 13 from
`docs/specs/2026-05-11-implement-orchestrator-agent.md` were
exercised in degraded mode (see Summary). Restoring the full
per-task sub-loop is a precondition for ladder items 45
(intentional-BLOCKED, intentional re-loop) to be meaningfully
testable. The Boss should verify `Agent` is in the
orchestrator-agent's tool wiring on the next `/implement` dispatch.
- The plan-recon agent's standing reading list points at
`docs/journals/INDEX.md` and "the latest entries" — once this iter
lands and INDEX is updated, the agent's reading list naturally
picks up the pr.1 entry.
### Boss-side addendum
Two follow-ups surfaced during this dispatch that are not pr.1's
own scope but were exposed by being the first real run of the
or.1-orchestrator-agent. Both go to the post-pr.1 fix queue:
- **or.1 Iron-Law wording is wrong.** The orchestrator-agent's
Phase 0 Iron Law says `git switch -c iter/<iter_id> origin/main`,
which presupposes that the plan-commit is on `origin/main`. The
Boss does not push between plan-commit and dispatch, so the
branch is created without the plan file. The agent worked around
this by mid-flight ff-merging local `main` into `iter/pr.1`,
which got the right end-state but rationalised itself through a
"branch already exists" clause that was actually meant for repair
re-dispatch. Correct fix: `git switch -c iter/<iter_id> main`
(drop `origin/`). Iron Law's purpose is iter-isolation from main,
not push-state coupling. Touches
`skills/implement/agents/ailang-implement-orchestrator.md` Phase
0 and the corresponding mention in `skills/implement/SKILL.md`
if any. One-commit mini-fix iter (no plan template needed,
trivial-mechanical carve-out).
- **`Agent` tool not reaching the orchestrator-agent.** Frontmatter
declares `tools: Read, Edit, Write, Bash, Glob, Grep, Agent`, but
the dispatched orchestrator-agent reports `Agent` was not in its
actual tool set, forcing the degraded-mode execution described
in Summary. Root-cause investigation needed: either a Claude
Code restriction on nested-subagent dispatch, or a frontmatter
parsing detail, or a deferred-tool issue at the harness layer.
This blocks the entire or.1 design goal (Boss-context offload
via fresh-subagent-per-task isolation), so it dominates the
post-pr.1 queue. Not fixable by a doc edit; needs diagnosis
before a fix can be scoped.
- **Plan-template RED-check regex.** Task 4 Step 1 used
`\*\*\*` (three literal stars) where the file content has two
(`**…**` markdown bold). Three plan iterations have now hit
plan-template defects (or.1 Tasks 1/4/6/7, pr.1 Task 4) —
cumulative signal that the plan-template needs a small revision
pass before the next planner run, or the planner skill needs
a self-test step.
## Audit / milestone close
Audit ran post-iter (skill-system mandatory at milestone close).
- **Architect drift review:** `clean`. Spec → code mapping is 1:1
across all five acceptance criteria; the new agent file is
template-compliant; `docs/DESIGN.md` was not touched; all four
`architect_sweeps.sh` sweeps are clean (the one sweep-1 hit at
`docs/DESIGN.md:50` is a pre-existing benign cross-reference last
modified in `or.1.2`, not pr.1 regrowth). The two follow-up items
in this journal's "Boss-side addendum" (or.1 Iron-Law wording;
`Agent` tool not reaching the orchestrator-agent) are confirmed as
fix-queue items, not DESIGN-level drift. A mild duplication
observation: the plan-template RED-check regex defect appears in
both "Concerns" and "Boss-side addendum" sections — accepted as-is
(different audiences read each section).
- **Bench-regression check:** `bench/check.py` 63 metrics stable
(exit 0); `bench/compile_check.py` 24 metrics stable (exit 0);
`bench/cross_lang.py` 25 metrics stable (exit 0). No language code
changed; the green result was expected and confirms it.
- **Rustdoc:** 20 pre-existing-style warnings (or.1 audit recorded
18; the +2 are not caused by pr.1, which touched no Rust). All are
private-item links or unresolved cross-refs. Same `[P2] Rustdoc
warning sweep` roadmap entry still applies; the count will be
re-checked when that sweep runs.
Milestone close: clean. Two post-pr.1 fix items already named in the
Boss-side addendum above:
1. or.1 Iron-Law `origin/main``main` mini-fix (trivial one-commit
iter, no plan template needed; trivial-mechanical carve-out).
2. `Agent` tool wiring for the dispatched orchestrator-agent — needs
diagnosis before fix-scope can be set. Until resolved, the or.1
design goal (per-task fresh-subagent isolation) is degraded; the
Boss-context-offload aspect still works because the per-task
detail collapses into one end-report regardless.
## Blocked detail
N/A — DONE.
## Commits
`14ac4ae..f06911d` (5 commits, one per task, no fix-passes).
## Stats
`bench/orchestrator-stats/2026-05-11-iter-pr.1.json` (created by
Phase 5 of this run; first stats file in the directory).
-134
View File
@@ -1,134 +0,0 @@
# audit-23 — Milestone 23 close
**Date:** 2026-05-12
**Started from:** 3fed372
**Status:** DONE — milestone 23 closed (1 ratify + 1 nit-fix)
**Skill:** audit (architect + bencher)
## Summary
Milestone-close audit for Milestone 23 (Eq/Ord prelude). Architect
clean on substance — all 8 acceptance criteria of
`docs/specs/2026-05-11-23-eq-ord-prelude.md` met, no out-of-scope
creep, DESIGN.md two-arm-fixpoint description (line 1513-1556)
matches `mono.rs::collect_mono_targets` observable behaviour, roadmap
split internally consistent. Bencher found and ratified one
structural compile-time shift; the iter-23.5 journal's "one sub-ms
noise-band metric" framing was wrong on both counts (4 metrics, not
1; consistent +18-20% cohort shift, not noise).
## Architect — drift items
- **[important]** `docs/DESIGN.md:1692-1693` — sentence collision:
"Milestone 22 ships **no built-in Prelude classes**. The original
An earlier draft committed to …". Pre-existing since commit
`613d4d8` (2026-05-10, design-md-consolidation 1.4 fixup) which
edited `22a draft``An earlier draft` and left the dangling
fragment `The original` on the previous line. NOT introduced in
iter 23.5; architect's attribution corrected. Boss-fixed inline:
the dangling fragment removed.
- **[minor]** `crates/ailang-check/src/lib.rs:884-894` — workspace-
sibling linearity construction is O(N²) over module count. Bounded
for today's 2-module workspace; named here so a future Nx-module
workspace doesn't re-discover it. Bencher confirmed below that the
iter-23.5 H1 cost is negligible at N=2. No action.
- **[minor]** `crates/ailang-check/src/mono.rs:703-712`
`contains_rigid_var` discriminates rigid vs. metavar by
`Type::Var.name.starts_with("$m")`. The journal already named this
as a future-fragility; a structural discriminator (separate
`Type::Meta` variant) would be the principled fix. Carry-on; not
blocking.
- **[nit]** `crates/ailang-check/src/lib.rs:709-726` — Float-NoInstance
addendum mutates `d.message` directly. The codebase has no
`with_hint` / `extend_message` convention yet, so this isn't
pre-existing drift, but the next class-specific addendum (e.g.
partial-Float `compare`) would compound `to_diagnostic` into a
conditional chain. Carry-on; refactor when a second addendum
arrives.
## Bencher — compile_check regression diagnosis
Bench-rerun at HEAD `3fed372` showed 4/12 check_ms metrics above
the 25% tolerance, with all 12 metrics in the +7 to +45% band — a
coherent cohort shift, not the journal-claimed "one sub-ms noise
metric".
Two hypotheses tested via 5-run medians over 60 samples each across
four states (23.4 HEAD, 23.5 HEAD, 23.4 checker + 23.5 prelude,
23.5 checker + 23.4 prelude):
- **H1** (linearity workspace-visibility + `contains_rigid_var`):
refuted. 23.5-checker + 23.4-prelude returns to baseline noise
(median +1.84% vs. baseline noise floor).
- **H2** (5 new polymorphic prelude `Def::Fn` bodies cost real
typecheck work): confirmed. 23.4-checker + 23.5-prelude reproduces
the full HEAD regression (median +18.62% vs. HEAD's +18.74%; gap
bounds the H1 interaction at <1%).
Cost source: each workspace check now typechecks 5 additional
polymorphic `Def::Fn`s (each carrying `forall` + 1 class constraint
+ a `match` or small `not (eq …)` body) plus their constraint
resolution. ~0.2-0.3ms median additional work per `ail check`
subprocess; the ~5-10ms subprocess-spawn floor inflates the
relative-percent figure.
**Verdict:** Ratify. The regression is the user-visible feature iter
23.5 shipped — five prelude helpers — accurately surfacing in the
bench as "typecheck does more work because the prelude has more
work to do".
## Resolution
- **Ratify path applied:** `python3 bench/compile_check.py
--update-baseline` re-measured (1 run, default 5 samples) and
wrote `bench/baseline_compile.json` with the new check_ms numbers.
Post-update rerun: 0/24 regressed, 0/24 improved beyond tolerance.
- **Important-drift fix applied:** the dangling `The original`
fragment removed from `docs/DESIGN.md:1692`. One-line tidy edit;
no behavioural change.
- **Minor / nit drift items:** carry-on. Linearity O(N²) is
observed-not-blocking; mono name-prefix discriminator is journal-
flagged; NoInstance addendum mutation pattern revisit when a
second class-specific addendum arrives.
## Concerns
- The iter-23.5 journal's bench framing was incorrect. The
implementer asserted "one sub-ms metric, two others stayed inside
on rerun" — fresh-rerun showed 4 above tolerance with consistent
cohort shift. Implementer-phase bench rerun count was likely too
small to see the cohort; orchestrator-audit pattern of running
bench 5× back-to-back at milestone close is the correct
granularity, and the 1-sample bench-check inside `implement` is
not.
- Implication for future iters: when `compile_check.py` exits 1
during implement, the implementer should NOT classify it as
noise-band without doing the same multi-run, multi-state isolation
the bencher did here. Defer to audit if the framing is uncertain.
This is a soft convention — encoding it as a hard skill rule would
slow implement when the regression genuinely is noise; the
weaker form is "if bench exits 1, the journal Concerns must
describe what additional evidence was gathered, not just give a
hypothesis".
## Known debt
- DESIGN.md sentence-collision had been on main for two days
unnoticed. No drift-review ran between `613d4d8` (2026-05-10)
and this audit (2026-05-12); the implement-skill diff scope
(per-iter, narrow) does not catch unrelated pre-existing
fragments. Acceptable: drift-review at milestone close is exactly
the catch mechanism, and it caught it.
## Files touched
- `bench/baseline_compile.json` — re-measured baseline; numbers
reflect new prelude cost.
- `docs/DESIGN.md:1692` — dangling fragment removed.
- `docs/journals/2026-05-12-audit-23.md` (this file).
- `docs/journals/INDEX.md` — append (Boss-side).
## Stats
No stats file — audit is a Boss-direct skill, not an
implement-orchestrator dispatch.
-122
View File
@@ -1,122 +0,0 @@
# audit-cma — Milestone close: Cross-model authoring-form test
**Date:** 2026-05-12
**Milestone:** Cross-model authoring-form test (cma.1 + cma.2 + cma.3)
**Status:** Closed, clean (after two `[low]` tidy fixes)
## Architect drift review
`ailang-architect` reported `drift_found` with two items over
`44c6e56..HEAD`:
- `[low]` `docs/journals/INDEX.md` — commit `90512d5` ("brainstorm
Step 7.5 re-dispatch on any post-PASS spec edit") removed the
corresponding P1 todo from roadmap.md but did not add an INDEX
mirror. CLAUDE.md's roadmap convention requires a one-line mirror
in the journals when a roadmap entry is removed.
- `[low]` `experiments/.../README.md` — the "Total 8 passed" sentence
inherited from the cma.2 plan understates the actual harness
test sweep (13 passed across `--lib` unit + 4 integration suites).
Surface cosmetic; flagged in cma.3 known-debt.
Both items confined to record-keeping, neither blocks milestone
close. Architect recommendation: carry on as planned.
## Tidy fixes (boss-direct, inline)
1. **INDEX backfill for `90512d5`.** Added a single-line entry in
`docs/journals/INDEX.md` pointing at the commit and noting that
no per-iter journal accompanies it (a small skill-discipline
tweak rather than an iter). The format mirrors existing entries
so future agents can navigate from INDEX to the commit history
without reconstructing.
2. **README test-count correction.** The five-suite layout under
`experiments/2026-05-12-cross-model-authoring/README.md`'s
"Running the harness" section now lists all five suites
explicitly (inline `--lib` unit tests for strip_locations, plus
the four integration files) and reports the correct total
(13 passed).
Both edits are ≤30 LOC and require no design judgement (CLAUDE.md
"trivial mechanical edits" carve-out); no planner+implement
dispatch needed.
## Bench-regression check
`bench/check.py && bench/compile_check.py && bench/cross_lang.py`:
- `check.py`: exit 0; **63 metrics; 0 regressed, 5 improved beyond
tolerance, 58 stable**.
- `compile_check.py`: exit 0; 24 metrics; 0 regressed, 0 improved
beyond tolerance, 24 stable.
- `cross_lang.py`: exit 0; 25 metrics; 0 regressed, 0 improved
beyond tolerance, 25 stable.
All three exit-code gates green. The five improvements on
`check.py` cluster on `latency.explicit_at_rc` (p99 -36.5%, p99_9
-41.8%, max -41.7%, p99_over_median -38.0%) plus
`throughput.bench_list_sum.gc_over_bump` -8.7%.
**Decision: no baseline update (carry-on).** Rationale:
- The cma milestone touched no in-workspace crate. Every line of
code change lives under `experiments/2026-05-12-cross-model-authoring/`
which is out of the root Cargo workspace by design. No mechanism
in the milestone could plausibly improve runtime latency or
GC-over-bump throughput.
- The improvements therefore reflect either (a) runtime variance
/ scheduler / clang version drift between the prior audit-rt
baseline measurement and now, or (b) genuine system-level
improvement uncorrelated with the milestone.
- Ratifying an unexplained improvement would lock it into the
baseline and lose the ability to detect a future regression
back to the prior level. Better to leave the baseline pristine;
if the improvement holds across the next two or three audits,
the next audit can ratify with confidence.
- This is the audit-rt pattern carried forward: don't ratify what
the milestone didn't intentionally move.
## Spec acceptance criteria — final check
Reviewing the parent spec `docs/specs/2026-05-12-cross-model-authoring-form-test.md`
§"Acceptance criteria" against landed work:
1. ✓ Directory `experiments/2026-05-12-cross-model-authoring/`
exists with all four sub-components (master/, render/, harness/,
runs/) plus top-level README. (cma.1 + cma.2)
2.`master/spec.md` is vollumfänglich; `spec_completeness.rs`
gate green; 34/34 AST variants covered by the curated subset.
(cma.1)
3. ✓ Both `rendered/json.md` and `rendered/ailx.md` are checked
in; all `render/` tests green. (cma.1)
4.`harness/` builds, passes 13/13 tests including the
mock_full_run end-to-end. (cma.2)
5. ✓ Four task definitions + reference solutions exist; the
`verify_references` integration test confirms each reaches
green through the real ail+clang pipeline locally. (cma.2)
6. ✓ Live run executed against IONOS (Qwen3-Coder-Next,
temperature=0, top_p=1, max-turns=5) and recorded under
`runs/2026-05-12-df7531/` with RUN_STATUS=ok, full per-turn raw
artefacts for all eight (cohort × task) combinations,
scores.csv, and summary.md. (cma.3)
7. ✓ DESIGN.md §"Decision 6" gained an "Empirical addendum
(2026-05-12)" subsection — 27 lines, model id + run date +
per-cohort means table + illustrative t3 contrast + explicit
scope note deferring the universal claim to a multi-subject
expansion. (cma.3)
8. ✓ Journal entry `2026-05-12-iter-cma.3.md` linked from
INDEX.md summarises the data point. (cma.3)
9. ✓ Roadmap P2 entry removed; P3 entry "Multi-subject expansion
(cross-model authoring-form follow-up)" added pointing at the
run as baseline. (cma.3)
All nine criteria green.
## Outcome
Milestone closed clean after the two record-keeping tidies above.
The cross-model authoring-form test ships its harness, four MVP
tasks with verified reference solutions, a single-subject baseline
dataset, and a calibrated DESIGN.md addendum. The next step
(P3 roadmap: multi-subject expansion) is queued but not started.
-163
View File
@@ -1,163 +0,0 @@
# audit-ct-tidy — Milestone close: ct-tidy (closing iter ctt.3)
**Date:** 2026-05-12
**Milestone:** ct-tidy (ctt.1 + ctt.2 + ctt.3)
**Status:** Closed, three doc-drift items fixed inline (`ctt.tidy`)
## Architect drift review
`ailang-architect` reported `drift_found` across the commit range
`805bba3..0d3f44b`. Code-side changes match DESIGN.md /
spec / iters; three doc-side drift items surfaced (two high,
one low; one medium is acceptable per spec).
**[high — spec acceptance]** `docs/DESIGN.md:1735-1736`
`KindMismatch` listed as a live class-schema diagnostic. Stale
since ctt.3.
**[high — spec acceptance]** `docs/DESIGN.md:1761-1763`
`KindMismatch` cited as the rejection mechanism for
higher-kinded class params. Stale since ctt.3.
**[high — spec acceptance]** `docs/roadmap.md:91-97` — the
`KindMismatch` retire todo unchecked with no forward-reference
to ctt.3. Spec acceptance criterion called for strikes on the
relevant todo lines; ctt.3 struck none.
**[medium — record-keeping]** `examples/test_22b2_kind_mismatch.ail.json`
keeps the retired-diagnostic-name in its filename. Spec is silent
(acceptable as-is); rename is a future candidate, not in this
audit's scope.
**[low — numerical drift]** Spec claim "`grep type_def_module`
returns 13 hits" is now 18 hits post-ctt.2. Intent honoured (every
consumer threaded); numerical figure is historical.
### Tidy fixes applied inline (`ctt.tidy`)
All three high-severity items addressed in this audit commit
(Boss-mechanical edits — three doc edits, no separate plan /
implement dispatch warranted):
1. `docs/DESIGN.md:1733-1740` — dropped the `KindMismatch` bullet
from the class-schema diagnostics list; added a successor
paragraph naming `BareCrossModuleTypeRef` as the structural
rejection path for the malformed class-param-in-applied-position
shape; cross-references iter ctt.3.
2. `docs/DESIGN.md:1761-1763` — rewrote the "Higher-kinded class
params" bullet under "What this typeclass design explicitly does
NOT support" to name `BareCrossModuleTypeRef` from canonical-form
validation instead of `KindMismatch` at class declaration.
3. `docs/roadmap.md:91-97` — struck `[x]` with a forward-reference
to ctt.3 (canonical-form-rejection test stays green asserting
`BareCrossModuleTypeRef`). Adjacent ctt.2 todo at lines 98-105
also struck (mechanical follow-up — ctt.2 closed the re-key it
tracks).
Medium / low items skipped: filename rename + spec numerical claim
both fall outside ctt.tidy's scope (spec acceptance was on
intent-honour, not figure-precision; filename rename is a future
candidate the spec already permits as-is).
## Bench-regression check
Sequence run as Boss (the `&&` chain tripped at `check.py`'s exit
1; `compile_check.py` and `cross_lang.py` then ran separately):
- `check.py`: **exit 1** — 63 metrics; 3 regressed, 3 improved
beyond tolerance, 57 stable.
- Regressions:
- `bench_list_sum.bump_s` +12.23% (tol 10%) —
**second consecutive sighting** (audit-eob first-sighted
this at +12.60%, this audit at +12.23%).
- `latency.explicit_at_rc.max_us` +44.31% (tol 25%) —
**first sighting**, rc-cohort tail-latency.
- `latency.implicit_at_rc.max_us` +35.07% (tol 30%) —
**first sighting**, rc-cohort tail-latency.
- Improvements:
- `bench_list_sum.gc_over_bump` -10.40% — mirror of the
bump_s regression (gc/bump ratio falls because bump rose).
- `bench_closure_chain.gc_over_bump` -15.05% — new improvement.
- `bench_list_sum_explicit.gc_over_bump` -8.30% — new improvement.
- `compile_check.py`: exit 0; 24 metrics, all stable within ±25%.
- `cross_lang.py`: exit 0; 25 metrics, all stable within ±15%.
### Classification
**bump_s persistence** (`bench_list_sum.bump_s`): second
consecutive sighting (audit-eob → audit-ct-tidy) at near-identical
magnitude (+12.60% → +12.23%). The audit-eob classification was
"first-sighting rule — observe in the next audit; if it persists,
investigate". This audit IS that next audit, and it persists.
ctt.* milestone made zero codegen-relevant edits (the three iters
touched env-overlay doc + check-side registry key shape +
workspace-load-time diagnostic deletion). No plausible attribution
to ctt.*. The persistence is real but uncaptured — the underlying
cause predates this milestone and is independent of the work in
the audit window.
Conservative call (same standing rule as previous three audits):
**do not** baseline-update. The persistence is now documented
across two audits; if it persists into the next audit
unchanged, ratify-without-attribution becomes the right call
(at three consecutive sightings the noise hypothesis is
unsupported). The latency tail-cluster regressions are first
sightings — observe.
**Latency max_us regressions** (`explicit_at_rc.max_us`,
`implicit_at_rc.max_us`): first sighting in any recent audit.
Both metrics are rc-cohort tail-latency (the extreme-outlier
window). The bench harness runs ~50ms workloads; a single
outlier of +183µs (explicit) or +167µs (implicit) absolute
moves the max_us proportionally. ctt.* made zero codegen edits,
so the noise hypothesis is the most parsimonious explanation
on first sighting.
Conservative call: first-sighting rule — **do not**
baseline-update; observe in the next audit.
**Recurring latency.explicit_at_rc improvement cluster**: the
three-audit-long improvement cluster on `p99_us` / `p99_9_us` /
`p99_over_median` (previously at ~-37%, classified as
ratify-candidate after one more audit at audit-eob) now reports
within tolerance this audit:
- `p99_us` -24.87% (tol 25%) — stable (was: improvement)
- `p99_over_median` -24.10% (tol 25%) — stable (was: improvement)
- `p99_9_us` not flagged (within tolerance)
The cluster has narrowed from ~-37% improvements to ~-24%
near-tolerance stable. The improvements partially reverted —
the metrics are still on the improvement side, but no longer
beyond tolerance. The audit-eob recommendation ("if the cluster
persists into the next two audits unchanged, ratify; if it
shifts or shrinks meaningfully, that is itself the attribution
signal") has fired: the cluster shrank meaningfully this audit.
This is itself attribution-evidence — the underlying cause is
variable rather than persistent. The ratify-pending plan is
withdrawn; the cluster is now noise-class, not signal-class.
**Baseline left pristine on every script** for the fourth
consecutive audit. No `--update-baseline` invocation.
## Status
Closed. Ct-tidy milestone fully landed:
- env-overlay shape ratified (ctt.1: DESIGN.md `## Env construction`
section + DuplicateCtor pinning test + 2 P2 todos closed).
- `Registry.type_def_module` re-keyed to `(owning_module, bare_name)`
with `caller_module` threaded through every consumer (ctt.2:
3-module fixture + regression test).
- `KindMismatch` retired (ctt.3: variant + helper + dispatch +
Display arm deleted; test stays green via canonical-form
successor `BareCrossModuleTypeRef`).
- DESIGN.md tracks all three iters' anchors (ctt.tidy in this audit:
class-schema diagnostics list updated, higher-kinded params bullet
updated, ctt.3 retirement marker pattern).
- Roadmap reflects closure (ctt.tidy: two P2 todos struck `[x]`).
Working tree exits audit clean. main HEAD advances by exactly
this audit commit.
-143
View File
@@ -1,143 +0,0 @@
# audit-eob — Milestone close: heap-str-abi (closing iter eob.1)
**Date:** 2026-05-12
**Milestone:** heap-str-abi (hs.1 + hs.2 + hs.3 + hs.4 + eob.1)
**Status:** Closed, three DESIGN.md tidies applied inline (`eob.tidy`)
## Architect drift review
`ailang-architect` reported `drift_found` across the commit range
`750f97e..78e8338`. Lockstep invariants and codegen ABI all
intact; three high-severity items in DESIGN.md plus one medium
record-keeping note.
**[high — spec acceptance]** `docs/DESIGN.md:2338-2342`
`float_to_str` described as "type-installed but codegen-deferred"
with `CodegenError::Internal`. Stale since hs.4.
**[high — spec acceptance]** `docs/DESIGN.md:2387` — same caveat
inside the "What is supported" inventory; `int_to_str` was absent
from the inventory entirely.
**[high — spec acceptance]** `docs/DESIGN.md` (missing) — no
"Str ABI" anchor documenting the heap-Str / static-Str dual
realisation, shared consumer ABI, or codegen-elision invariant
for static-Str-in-RC-paths.
**[medium — record-keeping]** `docs/journals/2026-05-12-iter-hs.4.md`
closed `DONE_WITH_CONCERNS` with weakened RC assertions; eob.1
retroactively restored the strict invariants. No forward-pointer
appended; readers cross-walk via INDEX.md.
### Tidy fixes applied inline (`eob.tidy`)
All three high-severity DESIGN.md drift items addressed in this
audit commit (Boss-mechanical edits — no separate plan / implement
dispatch warranted; the spec amend was small, the Str-ABI-anchor
content was already fully known from the just-closed milestone):
1. `docs/DESIGN.md:2338-2343` (float_to_str caveat block): replaced
the codegen-deferred prose with a one-paragraph statement that
`float_to_str` and `int_to_str` are fully wired, allocate a
heap-Str slab, and carry `ret_mode: Own`. Forward-references the
new Str-ABI anchor.
2. `docs/DESIGN.md:2387-2392` (inventory bullet): dropped the
codegen-deferred parenthetical from `float_to_str`; added
`int_to_str : (Int) -> Str` to the inventory; pointer to the
Str-ABI section for the dual realisation.
3. `docs/DESIGN.md` (new "Str ABI" subsection placed after the
Float-semantics block, before "What is not (yet) supported"):
documents the two realisations (static-Str packed-struct
`<{ i64, [N+1 x i8] }>` globals in `.rodata`; heap-Str slabs
allocated via `ailang_rc_alloc(8 + len + 1)`); the shared
consumer ABI (Str pointer at len-field offset 0, bytes at
`payload + 8`, inherited `strcmp` semantics); and the
codegen-level non-RC invariant for static-Str (move-tracking
partial-drop + non-escape lowering + `Type::Con { name: "Str" }`
carve-outs at `field_drop_call` and `drop_symbol_for_binder`'s
App arm).
Medium-severity hs.4-journal forward-pointer: skipped. INDEX.md
already links hs.4 → eob.1 chronologically; the eob.1 journal
explicitly cross-references hs.4's deferred fix as the iter's
motivation. A forward-pointer in hs.4's own file would push the
append-only convention; the cross-walk from INDEX is sufficient.
## Bench-regression check
Sequence run as Boss (the `&&` chain in audit-SKILL.md tripped at
`check.py`'s exit 1; `compile_check.py` and `cross_lang.py` then
run separately to read their exit codes cleanly):
- `check.py`: **exit 1** — 63 metrics; 2 regressed, 5 improved
beyond tolerance, 56 stable.
- Regressions: `bench_list_sum.bump_s` +12.60% (tol 10%);
`bench_hof_pipeline.bump_s` +11.65% (tol 10%). Both only
marginally over tolerance; the other four `bump_s` metrics
(`bench_tree_walk`, `bench_closure_chain`,
`bench_compute_collatz`, `bench_list_sum_explicit`) are stable
within ±7%.
- Improvements: three on the `latency.explicit_at_rc` cluster
(`p99_us` -37.37%, `p99_9_us` -40.14%, `p99_over_median`
-37.35%); two `gc_over_bump` mirrors on `bench_list_sum` and
`bench_hof_pipeline` (ratio falls because the denominator —
`bump_s` — rose; same physical signal as the bump regressions).
- `compile_check.py`: exit 0; 24 metrics, all stable within ±25%.
- `cross_lang.py`: exit 0; 25 metrics, all stable within ±15%.
### Classification
**Latency cluster** (`latency.explicit_at_rc.p99_us` /
`p99_9_us` / `p99_over_median`): third consecutive sighting
across `audit-cma``audit-ms``audit-eob`. `audit-ms` flagged
this as "one more audit before considering ratification"; this is
that one more audit.
eob.1's only codegen-side touchpoints are the `drop.rs` Str
carve-out (8 LOC) and the `ret_mode: Own` flip at four signature
sites; none of those should plausibly shift latency p99 by 37%.
The signal predates this milestone (audit-cma was three milestones
ago). Conservative call: still **do not** baseline-update on this
audit. The cluster is real and persistent, but the underlying
cause is unknown, and a baseline update without an identified
attribution would obscure the next signal that *does* have an
attributable cause.
Recommendation forward: if the cluster persists into the next
two audits unchanged, ratify with `--update-baseline` and a
journal entry naming the persistence as the ratify rationale
(absence of identified cause being the standing fact). If the
cluster shifts or shrinks meaningfully, that is itself the
attribution signal.
**bump_s cluster** (`bench_list_sum.bump_s`,
`bench_hof_pipeline.bump_s`): **first sighting** in any recent
audit. Both metrics only marginally over the 10% tolerance.
The other four `bump_s` measurements are stable, which makes
"shared underlying bump-allocator change" unlikely; per-fixture
system noise (12.6% on a ~50ms benchmark is ~6ms of jitter) is
the most parsimonious explanation.
eob.1 made zero changes to bump-allocator codepaths. The
implementer's end-report flagged this same cluster as appearing
during the workspace bench sweep before commit. Conservative call:
first-sighting rule — **do not** baseline-update; observe in the
next audit; if it persists, investigate.
**Baseline left pristine on every script** for the third
consecutive audit. No `--update-baseline` invocation.
## Status
Closed. Heap-str-abi milestone fully landed:
- Static-Str layout migrated (hs.1, hs.2).
- Heap-Str runtime additions wired (hs.3, hs.4).
- Effect-op-borrow rule + RC-discipline closes (eob.1).
- DESIGN.md anchors both arg-position rules + Str-ABI dual
realisation (eob.1 + eob.tidy in this audit).
- WhatsNew entry + roadmap P1 close (eob.1).
Working tree exits audit clean. main HEAD advances by exactly
this audit commit.
-107
View File
@@ -1,107 +0,0 @@
# audit-ms — Milestone close: Multi-subject Authoring-Form Test (CodeLlama Replication)
**Date:** 2026-05-12
**Milestone:** Multi-subject Authoring-Form Test — CodeLlama Replication (ms.1 + ms.2)
**Status:** Closed, clean (no tidy fixes needed)
## Architect drift review
`ailang-architect` reported `clean` over the working tree
(`8689f7a..HEAD`, no commits yet). Working tree matches DESIGN.md
(the §Decision-6 addendum extension is the only DESIGN.md touch,
intentional and spec-mandated), matches the spec's seven acceptance
criteria, and matches the boss-only-commit / append-only-INDEX /
main-sacrosanct discipline.
Two `[low]` items flagged, both expected:
- The spec mandates an `audit-ms` INDEX entry on milestone close;
this audit pass is the one that produces it — written below.
- `docs/specs/2026-05-12-multi-subject-codellama.md:154` references
"the cma.3 baseline" (`runs/2026-05-12-df7531/`) which DESIGN.md
no longer cites after the addendum rewrite to a two-subject
view. The spec text is correct (the new runs are distinct from
cma.3) and the baseline dir is still on disk under the cma run
tree, so the asymmetry is purely cosmetic — ratified, no edit.
Architect recommendation: carry on as planned.
## Bench-regression check
`bench/check.py && bench/compile_check.py && bench/cross_lang.py`:
- `check.py`: exit 0; **63 metrics; 0 regressed, 5 improved beyond
tolerance, 58 stable**.
- `compile_check.py`: exit 0; 24 metrics; 0 regressed, 0 improved
beyond tolerance, 24 stable.
- `cross_lang.py`: exit 0; 25 metrics; 0 regressed, 0 improved
beyond tolerance, 25 stable.
All three exit-code gates green. The five improvements on
`check.py` cluster on `latency.explicit_at_rc` (p99, p99_9, max,
p99_over_median) plus `throughput.bench_list_sum.gc_over_bump`
the exact same cluster `audit-cma` (this morning's milestone close)
flagged and left unratified for the same reason. The ms milestone
touched only `experiments/2026-05-12-cross-model-authoring/harness/src/`
(plus docs); no in-workspace runtime / codegen / RC path changed.
The improvements therefore reflect runtime variance or
system-level drift uncorrelated with the milestone.
**Decision: no baseline update (carry-on).** Same rationale as
audit-cma: ratifying an unexplained improvement would lock it into
the baseline and lose the ability to detect a future regression
back to the prior level. If the same improvement cluster persists
across the next two or three audits, it ratifies with confidence.
This is now the second audit in a row showing the same five-metric
improvement cluster; one more clean audit and the next audit can
ratify.
## Spec acceptance criteria — final check
Reviewing `docs/specs/2026-05-12-multi-subject-codellama.md`
§"Acceptance criteria" against landed work:
1.`pipeline.rs:118` reads `format!("check: {e:#}")`; RED→GREEN
pinning unit test `check_format_preserves_anyhow_chain` present
in the same file. (ms.1)
2. ✓ Existing 13 harness tests still pass; suite now 14/14 with
the new pin test. (ms.1, re-confirmed at ms.2 close)
3.`meta-llama/CodeLlama-13b-Instruct-hf` ran to completion
against IONOS (8 cells, RUN_STATUS=ok at run level; 2 cells
`api_failure` on turn 1 disclosed in addendum). Chosen model id
recorded in ms.2 journal. (ms.2)
4. ✓ Qwen3-Coder-Next re-run with fixed pipeline completed (8
cells, RUN_STATUS=ok). New dir `runs/2026-05-12-080864/`
distinct from cma.3 baseline `runs/2026-05-12-df7531/`; both new
runs checked into the working tree. (ms.2)
5.`docs/DESIGN.md` §"Decision 6 / Empirical addendum
(2026-05-12)" extended: framing paragraph + four-column per-cell
table (Qwen-JSON, Qwen-AILX, CodeLlama-JSON, CodeLlama-AILX)
covering prompt_tokens, completion_tokens, pass-rate, turn-count
distribution, top error class; api_failure anomaly disclosed;
scope paragraph updated to "two subjects, n=1 each". (ms.2)
6. ✓ Roadmap P3 multi-subject entry removed; one-line mirror in
`2026-05-12-iter-ms.2.md`. (ms.2)
7. ✓ Per-iter journal entries (ms.1, ms.2) linked from INDEX.md;
this audit-ms entry being added by this audit pass. (ms.{1,2,
audit})
All seven criteria green.
## Outcome
Milestone closed clean — no tidy fixes needed (both architect-flagged
`[low]` items are expected by-products of the audit step itself,
not drift). The multi-subject expansion ships: pipeline-error
formatting fixed (JSON-cohort now sees the full anyhow cause
chain), Qwen retroactive re-run plus first CodeLlama-13b-Instruct
run recorded, DESIGN.md §Decision-6 addendum now spans two
subjects with consistent direction (AILX cheaper + ≥ green for
both), roadmap P3 entry closed. Token spend across the milestone:
prompt 501,985 + completion 23,391 = 525,376.
Decision 6 still pending universal ratification — the spec was
explicitly scoped to two subjects, not the ≥3-subjects + statistics
that a ratification would need. A future iteration may re-open the
queue if additional subjects are wanted; for now the second data
point is recorded and the queue is back to user-direction.
-99
View File
@@ -1,99 +0,0 @@
# audit-rt — Milestone close: Roundtrip Invariant
**Date:** 2026-05-12
**Milestone:** Roundtrip Invariant (rt.1 + rt.2)
**Status:** Closed, clean (after rt.tidy fixes)
## Architect drift review
`ailang-architect` reported `drift_found` with four items over
`daf9f4f..HEAD`:
- `[high]` DESIGN.md §"Roundtrip Invariant" Direction 2 (text →
JSON → text idempotency) is stated as a property but not
directly enforced by any of the four listed tests. Test 1 covers
Direction 1; test 2 covers `.ailx` → JSON ≡ counterpart; test 4
covers CLI render → parse. None tests `parse → print → parse`
idempotency on a hand-authored `.ailx` text directly. Architect
recommendation: claim soften OR add a fifth test.
- `[high]` `docs/roadmap.md` P1 entry "Round-trip completeness
invariant" still `- [ ]` (open) after the milestone closed.
Stale per roadmap convention (finished entries → `[x]` then
removal with one-line journal mirror).
- `[low]` DESIGN.md wording: `<16-hex>` in new section vs
`<16-lowercase-hex>` in §"Data model". Casing constraint is
load-bearing for canonical-byte equality; should be stated
identically.
- `[low]` rt.1 plan errata note (planner emitted ast.rs-mismatching
destructure scaffold). Single occurrence absorbed cleanly by the
Step-3.4 escape hatch. Not a deeper drift pattern; informational.
## Bench-regression check
`bench/check.py && bench/compile_check.py && bench/cross_lang.py`:
- `check.py`: exit 0; 63 metrics; 0 regressed, 0 improved beyond
tolerance, 63 stable.
- `compile_check.py`: exit 0; 24 metrics; 0 regressed, 0 improved
beyond tolerance, 24 stable.
- `cross_lang.py`: exit 0; 25 metrics; 0 regressed, 0 improved
beyond tolerance, 25 stable.
All three bench gates green. Bench was the most plausible source
of a regression given rt.1 added three new tests but the tests
are pure readers and are not exercised by the bench harness; the
green signal matches that expectation.
## Resolution (rt.tidy, boss-direct edits)
Three fixes, mechanical, no plan/implement dispatch:
1. **Direction-2 enforcement**
`crates/ailang-surface/tests/round_trip.rs` gains a third test
`parse_then_print_then_parse_is_idempotent_on_every_ailx_fixture`.
For every `.ailx` fixture asserts
`canonical_bytes(parse(t)) == canonical_bytes(parse(print(parse(t))))`.
For the 57 fixtures with a JSON counterpart this is logically
derivable from tests 1 + 2; the dedicated test stays robust for
future `.ailx` fixtures without a counterpart. DESIGN.md
§"Roundtrip Invariant" "Enforcement" list updated from four to
five tests; module doc-comment updated from "two complementary
checks" to "three complementary checks". Test PASS first-shot
across all 57 fixtures.
2. **Roadmap P1 entry removed.** The "Round-trip completeness
invariant" milestone entry deleted; this journal entry is the
one-line mirror per roadmap convention. The P1 todo "Brainstorm
Step 7.5" remains; the P1 Show + print-rewire milestone remains.
3. **Wording fix.** DESIGN.md §"Roundtrip Invariant" Float-bits-hex
description changed from `<16-hex>` to `<16-lowercase-hex>` to
match §"Data model".
The `[low]` planner-errata carry-on item is retained in the rt.1
journal "Concerns" section; it will surface again only if a future
tests-against-AST iteration repeats the pattern.
## Spec acceptance criteria — final check
1. ✓ Top-level `## Roundtrip Invariant` section in DESIGN.md;
Decision 6 Constraint 2 carries upward cross-reference. (rt.2)
2.`every_ailx_fixture_matches_its_json_counterpart` dynamic in
`crates/ailang-surface/tests/round_trip.rs`. (rt.1)
3.`crates/ailang-core/tests/schema_coverage.rs` with
exhaustive-match visitor; 34/34 variants observed. (rt.1)
4.`crates/ail/tests/roundtrip_cli.rs` with BLAKE3 identity
over `ail render` → tempfile → `ail parse`. (rt.1)
5.`cargo test --workspace` green with all four (now five) tests.
(rt.1 + rt.2 + rt.tidy)
6. ✓ No roundtrip gaps surfaced; no render/parse code changes
needed in this milestone. (rt.1)
7. ✓ Tests are pure readers; the CLI test's only filesystem side
effect is `tempfile::TempDir`. (rt.1)
## Outcome
Milestone closed clean. Five workspace-wide tests now anchor the
`.ail.json``.ailx` bijection plus AST-variant coverage; the
property has a dedicated top-level section in DESIGN.md. No
follow-up iterations queued in this milestone.
-200
View File
@@ -1,200 +0,0 @@
# iter 23.5 — Prelude free fns + E2E (Eq/Ord milestone close)
**Date:** 2026-05-12
**Started from:** 35c6eb5
**Status:** DONE
**Tasks completed:** 8 of 8
## Summary
Closes milestone 23. Ships the five polymorphic prelude free fns
`ne` / `lt` / `le` / `gt` / `ge` (Tasks 1-2), three end-to-end
fixtures covering primitive composition + user-ADT integration +
the Float negative case (Tasks 3-5), the Float-aware `NoInstance`
diagnostic addendum (Task 5), the DESIGN.md §"Prelude classes"
amendment (Task 6), and the roadmap split between shipped Eq/Ord
and the pending Show + print-rewire half (Task 7). Bench check
runs clean except a noisy sub-millisecond `check_ms.local_rec_capture`
regression that sits just over the 25% tolerance — ratifiable per
the audit-tolerance rule (the prelude grew by 5 polymorphic
`Def::Fn`s, so the typecheck workload per workspace measurably
shifts).
Two implementer-phase fixes were needed beyond the plan, both
symmetric to existing precedent (iter 23.4-prep). They surfaced
when the natural test fixtures (user-defined `at_most x y = not (gt
x y)` composing prelude poly fns) hit two distinct false-positive
shapes:
1. **Linearity check needed workspace-wide visibility** for
imported polymorphic free fns + class methods. Previously the
linearity pass only saw the current module's globals, so a
user `at_most` forwarding its borrow params into `gt` (defined
in the prelude) hit `callee_arg_modes("gt", 2) → []`
default-Consume → `consume-while-borrowed` false positive. Fix
extends `linearity::check_module` to a `check_module_with_visible`
entry that takes additional Module references whose top-level fns
+ class methods are registered in globals; the workspace-side
driver passes all sibling modules. Pure precedent extension of
23.4-prep, which added class-method visibility for the same
reason.
2. **Mono pass needed to skip rigid-var-bearing free-fn targets.**
When a polymorphic free fn's body calls another polymorphic
free fn (e.g. `at_most`'s body calls `gt x y` with x, y of
type rigid `a`), the synth-channel observer recorded a
FreeFnCall whose type args were rigid `Type::Var { name: "a"
}`. The existing Unit-default fallback (correct for
`is_empty(Nil)`-shape unobservable metavars) was unsound here:
it produced `gt__Unit` whose body referenced `compare`, which
has no Unit instance → "unknown variable: compare" at the
synthesised body. Fix discriminates rigid vars
(Type::Var without `$m` prefix) from unbound metavars (`$m`
prefix) — rigids skip (they get re-collected with concrete
substitution when the enclosing fn itself monomorphises),
metavars keep their Unit-default.
One existing fixture (`examples/test_22b1_missing_method.ail.json`)
had to rename its class method `ne → tne` because `ne` is now a
top-level prelude free fn and the workspace registry's
method-name uniqueness check rejected the collision. The
fixture's docstring already foreshadowed this case ("Class is
`TEq` rather than `Eq` to avoid colliding…"); extended to also
cover the `ne → tne` rename rationale.
## Per-task notes
- iter 23.5.1: `ne` free fn — added `Def::Fn ne` to prelude (body
`not (eq x y)`); created `crates/ail/tests/prelude_free_fns.rs`
with helpers `fixture()` and `mono_symbol_names()` + first test;
smoke fixture `examples/ne_at_int_smoke.ail.json`. RED→GREEN.
Adjusted `mono.modules.iter()``.values()` per the actual
`BTreeMap<String, Module>` shape (Boss pre-flight flagged).
- iter 23.5.2: `lt` / `le` / `gt` / `ge` free fns — four
`Def::Fn`s in prelude (each pattern-matches on `compare x y`
against one of LT/GT plus a wildcard); four extending tests in
the same `prelude_free_fns.rs`; four smoke fixtures. RED→GREEN
per fn.
- iter 23.5.3: positive E2E — `examples/eq_ord_polymorphic.ail.json`
with user-defined `at_most a a → Bool` composing prelude `not`
+ `gt`; `crates/ail/tests/eq_ord_e2e.rs` with helpers
`fixture_path()` + `build_and_run()` plus
`eq_ord_polymorphic_runs_end_to_end`. Two implementer-phase
fixes (linearity + mono rigid-skip) landed during this task to
unblock the GREEN. Final stdout pinned at `"1\n0\n1"` (matches
`io/print_int`'s actual newline-separator behaviour; plan text
had `"101"` based on an incorrect concatenation assumption).
- iter 23.5.4: user-ADT E2E — `examples/eq_ord_user_adt.ail.json`
with `type IntBox = MkIntBox Int` + user-written
`instance Eq IntBox` + `instance Ord IntBox` + `main` calling
`eq` twice. Two extending tests:
`eq_ord_user_adt_runs_end_to_end` (stdout `"1\n0"`) and
`eq_ord_user_adt_eq_intbox_hash_stable` (record-then-pin).
Mono symbol name is `eq__cde77856` (8-hex-prefix mangling for
compound types per DESIGN.md §"Mangling scheme"); body hash
pinned at `9daaffa7528d2a1c`. The Ord-IntBox instance's
retType needed qualifying as `prelude.Ordering` per the
canonical-type-names rule.
- iter 23.5.5: Float-aware `NoInstance` addendum — implemented in
`CheckError::to_diagnostic()` (the per-iter spec-§"NoInstance"
conversion path; option-a from the plan's structural-choice
list). Addendum fires when `class ∈ {Eq, Ord}` AND `at_type ==
"Float"`: appends `— Float has no Eq/Ord instance by design
(partial orderability per IEEE-754); see DESIGN.md §"Float
semantics".` New `examples/eq_float_noinstance.ail.json` +
`crates/ail/tests/eq_float_noinstance.rs`. RED→GREEN.
- iter 23.5.6: DESIGN.md amendment — inserted Milestone-23
amendment paragraph in §"Prelude (built-in) classes". Grep
gate RED→GREEN.
- iter 23.5.7: roadmap split — replaced the single Post-22
Prelude entry with two: `[x]` Eq/Ord (shipped 23.5) and `[ ]`
Show + print rewire (gated on heap-`Str` ABI). Also
disambiguated the `depends on: Post-22 Prelude` reference at
line 73 to point specifically at the Eq/Ord half (Op routing's
actual dependency).
- iter 23.5.8: bench gate — `bench/check.py` exit 0 (clean),
`bench/compile_check.py` exit 1 (one sub-ms `check_ms`
regression at the tolerance line — see Concerns), `bench/cross_lang.py`
exit 0 (clean).
## Concerns
- `bench/compile_check.py` exited 1 with one regression:
`check_ms.local_rec_capture` measured +26.36% over baseline
(1.1ms → 1.4ms, tolerance 25.0%). This is a sub-millisecond
noise-band metric, and the prelude grew by 5 polymorphic
`Def::Fn`s in this iter so a measurable typecheck-workload
shift is expected per the Boss-noted audit tolerance. Two
other check_ms metrics in the first run (`borrow_own_demo`,
`bench_list_sum`) crossed the line on one sample but stayed
inside on the rerun, confirming this is noise-band fluctuation
rather than a structural regression. Recommend ratifying at
iter close via `--update-baseline`; flagged as a Boss
judgement call.
- The mono-pass rigid-var skip (Task 3 inline fix) preserves
the existing Unit-default for unbound metavars exactly (the
`is_empty(Nil)` shape from iter 23.4.8 is untouched). The
discrimination is by name prefix (`$m`) which is the same
convention `Subst::meta_id` uses; a future schema refactor
that drops the prefix convention would silently break the
skip rule. Code comment names this.
- The linearity-side `check_module(&Module)` standalone entry
is now `#[cfg(test)]`-gated because production code uses
`check_module_with_visible(m, &visible_extra)` instead. Unit
tests still call the no-extra path. Acceptable; flagging in
case a future caller wants the standalone form back.
## Known debt
- The `eq_ord_user_adt` mono-symbol name (`eq__cde77856`) is
pinned by literal string match in the test. If the canonical
type-bytes encoding for `IntBox` ever changes (e.g. via a new
schema field on `TypeDef`), the test fails with a clear
type-mangling-regression message. That is the intended drift
alarm; no immediate debt.
## Files touched
- `crates/ailang-check/src/lib.rs` — Float-aware `NoInstance`
addendum in `to_diagnostic()`; linearity call site updated to
pass workspace siblings as visible-extra.
- `crates/ailang-check/src/linearity.rs` — new
`check_module_with_visible(m, visible_extra)` entry;
`check_module(&Module)` made test-only.
- `crates/ailang-check/src/mono.rs` — free-fn target collection
now distinguishes rigid `Type::Var` from `$m`-prefixed
metavars; rigid-bearing targets are skipped; new helper
`contains_rigid_var`.
- `crates/ailang-core/src/workspace.rs` — docstring update on
the `iter22b1_missing_method` test naming the new `ne` reason
for the `TEq`/`tne` rename.
- `crates/ail/tests/prelude_free_fns.rs` (new) — five workspace-level
tests, one per prelude free fn, plus helpers.
- `crates/ail/tests/eq_ord_e2e.rs` (new) — three E2E tests:
polymorphic composition, user-ADT integration, mono-symbol
hash stability.
- `crates/ail/tests/eq_float_noinstance.rs` (new) — Float-aware
NoInstance pin.
- `examples/prelude.ail.json` — five new `Def::Fn` entries
(ne / lt / le / gt / ge).
- `examples/ne_at_int_smoke.ail.json` (new), `lt_at_int_smoke.ail.json`
(new), `le_at_str_smoke.ail.json` (new), `gt_at_bool_smoke.ail.json`
(new), `ge_at_int_smoke.ail.json` (new) — five workspace-level
smoke fixtures.
- `examples/eq_ord_polymorphic.ail.json` (new) — positive E2E:
polymorphic helper composing prelude fns at Int / Bool / Str.
- `examples/eq_ord_user_adt.ail.json` (new) — user-ADT E2E:
`IntBox` with user-written Eq + Ord instances.
- `examples/eq_float_noinstance.ail.json` (new) — negative-Float
fixture firing the addended diagnostic.
- `examples/test_22b1_missing_method.ail.json``ne → tne`
rename to avoid collision with the new prelude `ne` free fn.
- `docs/DESIGN.md` — Milestone-23 amendment paragraph inserted
in §"Prelude (built-in) classes".
- `docs/roadmap.md` — Post-22 Prelude entry split into shipped
Eq/Ord + pending Show; Op-routing depends-on reference
disambiguated.
## Stats
bench/orchestrator-stats/2026-05-12-iter-23.5.json
-216
View File
@@ -1,216 +0,0 @@
# iter 24.1 — `bool_to_str` + `str_clone` runtime + codegen wiring
**Date:** 2026-05-12
**Started from:** 8bfa09adc7774a1c94af609fc4c78a70ba77be48
**Status:** DONE_WITH_CONCERNS
**Tasks completed:** 6 of 6
## Summary
First iter of milestone 24 ("Show + print rewire"). Wires two new
heap-Str-producing primitives — `bool_to_str : (Bool) -> Str` and
`str_clone : (Str borrow) -> Str` — through the runtime C code, type
checker, codegen IR lowering, and IR-header preamble, mechanically
mirror-imaging the hs.4 wiring for `int_to_str` / `float_to_str`. The
two new symbols install lockstep in `builtins.rs::install` and
`synth.rs::builtin_value_type` with `ret_mode: Own`; codegen gains
two new IR-header `declare` lines, two `Emitter::lower_app` arms, and
two `is_static_callee` whitelist entries. Plan's pre-existing-drift
fix landed alongside: `int_to_str` row added to `builtins.rs::list()`
(hs.4 added the `env.globals` entry but missed the `list()` row).
Nine new tests landed per plan: 2 builtins-install unit tests
(`install_bool_to_str_signature`, `install_str_clone_signature`),
2 IR-shape unit pins (`bool_to_str_emits_call_to_ailang_bool_to_str`,
`str_clone_emits_call_to_ailang_str_clone`), 5 E2E tests
(`bool_to_str_drop_balances_rc_stats`, `bool_to_str_emits_true_branch`,
`bool_to_str_emits_false_branch`, `str_clone_drop_balances_rc_stats`,
`str_clone_cross_realisation_uniform_abi`). Four `.ail.json` fixtures
shipped. Five IR snapshots regenerated for the two new declare lines.
One substantive deviation from the plan's "purely-additive wiring"
framing surfaced during Task 6 fixture validation: the
`str_clone_cross_realisation_uniform_abi` test's literal assertion
`allocs == 3, frees == 3, live == 0` could not hold under the
pre-24.1 uniqueness analyser because the analyser's `globals` map
does not contain builtin signatures — `str_clone`'s `param_modes:
[Borrow]` is invisible to `callee_arg_modes`, so a `src_heap` let-
binder consumed by `str_clone(src_heap)` walks as `Position::Consume`,
incrementing `consume_count` and gating off the scope-close
`ailang_rc_dec`. Fixed inline by registering builtin signatures in
both `uniqueness.rs::infer_module` and
`linearity.rs::check_module_with_visible` (8 lines × 2 files,
symmetric to the existing class-method registration the linearity
pass added in iter 23.4-prep). The fix is substantively correct (it
applies a pre-existing pattern to a new class of callees) but is
NOT in the plan's literal Edit text — see Concerns for the
discussion.
Acceptance verified: full `cargo test --workspace` green (513 tests
passing, 0 failures); the five new E2E tests pass with the plan's
literal numeric assertions; the two new IR-shape pins pass; the five
IR snapshot tests pass after `UPDATE_SNAPSHOTS=1` regen (diff is
exactly the two new `declare ptr @ailang_bool_to_str(i1)` and
`declare ptr @ailang_str_clone(ptr)` lines). `bench/compile_check.py`
24/24 stable, 0 regressions. `bench/cross_lang.py` 25/25 stable, 0
regressions. Grep verification: 44 hits across runtime/str.c +
builtins.rs + synth.rs + lib.rs (well above the plan's ≥ 10
threshold), 10 hits in snapshots (2 declares × 5 files), 0 build
warnings.
## Per-task notes
- iter 24.1.1: Appended `ailang_bool_to_str(bool b) -> char *` and
`ailang_str_clone(const char *src) -> char *` to `runtime/str.c`
after `ailang_float_to_str` (str.c:128-143). Both use the existing
static `str_alloc(uint64_t)` helper for heap-Str slab allocation;
`bool_to_str` writes either "true" (4 bytes) or "false" (5 bytes);
`str_clone` reads `len` from offset 0 of source and memcpy's bytes
+ trailing NUL to a fresh slab. Standalone clang -c compile green.
- iter 24.1.2: Installed `bool_to_str : (Bool) -> Str` and
`str_clone : (Str borrow) -> Str` in `builtins.rs::install`
(builtins.rs:213-232, between the existing `int_to_str` block and
the `is_nan` block), both with `ret_mode: ParamMode::Own`. Mirror-
installed the same two signatures in `synth.rs::builtin_value_type`
(synth.rs:181-194, lockstep). Added three rows to
`builtins.rs::list()` (`int_to_str` drift fix from hs.4 +
`bool_to_str` + `str_clone`). Two new unit tests
(`install_bool_to_str_signature`, `install_str_clone_signature`)
appended to `builtins.rs::tests` using the fallback explicit
`Term::Lit` shape (plan-noted: `lit_bool` / `lit_str` helpers do
not exist; only `lit_int` / `lit_float` are defined). Both new
unit tests PASS.
- iter 24.1.3: Codegen IR wiring. (a) Extended the IR-header preamble
at `lib.rs:539-548` with two unconditional declares
`declare ptr @ailang_bool_to_str(i1)` and
`declare ptr @ailang_str_clone(ptr)`. (b) Inserted two new arms in
`Emitter::lower_app` at `lib.rs:1934-1969` (after the existing
`float_to_str` arm), each emitting a direct call to the runtime
C glue with arity check + `lower_term`+`fresh_ssa`+call triple
matching `int_to_str`'s structural shape. (c) Extended the
`is_static_callee` builtin-callee whitelist at `lib.rs:2133-2135`
with `bool_to_str` and `str_clone` (strictly required so
`Term::Var { name: "bool_to_str" }` reaches `lower_app`'s chain
instead of the UnknownVar fallback). Full workspace build clean.
- iter 24.1.4: Two IR-shape unit pin tests
(`bool_to_str_emits_call_to_ailang_bool_to_str`,
`str_clone_emits_call_to_ailang_str_clone`) appended at the bottom
of `lib.rs::mod tests`, mirroring the
`int_to_str_lowers_to_ailang_int_to_str_call` template. Both PASS.
- iter 24.1.5: Five IR snapshots (`hello.ll`, `list.ll`, `max3.ll`,
`sum.ll`, `ws_main.ll`) regenerated via `UPDATE_SNAPSHOTS=1`. RED
observed first as expected (5 ir_snapshot_* tests failed with
"run UPDATE_SNAPSHOTS=1 cargo test ir_snapshot_ to refresh");
GREEN after regen. Diff is exactly the two new declare lines.
Each .ll file shows 2 matches for the new symbols.
- iter 24.1.6: Four `.ail.json` fixtures created
(`bool_to_str_drop_rc.ail.json`, `bool_to_str_smoke_false.ail.json`,
`str_clone_drop_rc.ail.json`, `str_clone_cross_realisation.ail.json`)
per the plan's verbatim JSON. Five new E2E tests appended to
`crates/ail/tests/e2e.rs` after `heap_str_repeated_print_balances_rc_stats`.
The `str_clone_cross_realisation_uniform_abi` test initially
FAILED with `frees=2, expected=3`; root-caused to the
uniqueness-analyser-globals scope gap (see Concerns); fixed by
registering builtin signatures in `uniqueness.rs::infer_module`
and `linearity.rs::check_module_with_visible`. After the fix all
five new E2E tests PASS with the plan's literal numeric
assertions. Full `cargo test --workspace`: 513 passed, 0 failed.
`bench/compile_check.py` 24/24 stable. `bench/cross_lang.py` 25/25
stable. Grep verification per plan Step 9 met: 44 code-side hits,
10 snapshot hits, 0 build warnings.
## Concerns
- **Analyser-globals registration extends the plan's "purely-additive"
scope.** The plan framed 24.1 as "Pure-additive wiring across four
crates / files." The actual landed change extends to two more
files (`uniqueness.rs`, `linearity.rs`) with 8 lines each — a fix
to the uniqueness / linearity analyser-globals scope so the
`str_clone` builtin's `param_modes: [Borrow]` is visible to the
`callee_arg_modes` walker. Without the fix, the
`str_clone_cross_realisation_uniform_abi` test's plan-literal
assertion `frees == 3` does not hold (one heap-Str slab leaks
because the App-arg walker can't see the Borrow mode → walks
`src_heap` as Consume → `consume_count == 1` → drop emission
gated off).
The fix is **symmetric** to the iter 23.4-prep extension that
registered class-method signatures in the same `globals` maps for
the same reason (a builtin/class-method callee carrying
`param_modes` would not be visible to the analyser, false-firing
`consume-while-borrowed` lints AND, in this iter, leaking heap-Str
slabs). The plan's verbatim Edit text did not include the fix; the
plan's verbatim test assertion required it. Per the orchestrator-
agent's "make the reasonable call and continue" mandate I applied
the fix in-place; it is the substantively-correct repair, not a
design departure.
The hs.4 journal's "Known debt" item on this is partially closed
by this iter (the App-side of the Borrow-walker visibility is now
fixed for builtin callees). The eob.1 fix (effect-op args walk as
Borrow regardless of callee `param_modes`) covered the `Term::Do`
side; the corresponding `Term::App` side is now covered for
builtins. User-fn `Term::App` callees still benefit from their
own def being registered in the analyser's `globals` from the
module's own def list (lines 113-115 in uniqueness.rs); imported
user-fn callees benefit from the iter 23.5 visible-extra
registration in linearity.rs (lines 220-238). So the only
remaining unknown-callee case is one this iter does not encounter.
## Known debt
- **`str_clone` could rc_inc on heap-Str input** instead of allocating
a fresh copy, but this requires distinguishing heap-Str from
static-Str at runtime — a tagging scheme the codebase deliberately
dropped post-hs.2 (UINT64_MAX sentinel removal). Out of scope per
the parent spec's "`str_clone` as rc_header bump" entry.
- **`bool_to_str` as schema-`if`** with two static-Str literals would
avoid the per-call heap allocation but requires phi-of-static-Str
through the drop-elision pipeline that is not load-bearing-ratified
today. Open commitment per the parent spec.
## Files touched
- Modified (12 files):
- `runtime/str.c` — appended `ailang_bool_to_str` (16 LOC incl.
comment) and `ailang_str_clone` (18 LOC incl. comment)
- `crates/ailang-check/src/builtins.rs` — two new
`env.globals.insert` blocks (`bool_to_str` + `str_clone`), three
new rows in `list()` (drift fix + two new), two new unit tests
- `crates/ailang-codegen/src/synth.rs` — two new arms in
`builtin_value_type` (lockstep partner of the checker insert)
- `crates/ailang-codegen/src/lib.rs` — IR-header preamble extends
with 2 new `declare` lines; 2 new `lower_app` arms; 2-entry
`is_static_callee` whitelist extension; 2 new IR-shape pin tests
- `crates/ailang-check/src/uniqueness.rs` — register builtin
signatures in `infer_module`'s `globals` map so `callee_arg_modes`
resolves builtin `param_modes` correctly
- `crates/ailang-check/src/linearity.rs` — register builtin
signatures in `check_module_with_visible`'s `globals` map for the
same reason
- `crates/ail/tests/e2e.rs` — 5 new E2E tests appended
- `crates/ail/tests/snapshots/hello.ll`,
`crates/ail/tests/snapshots/list.ll`,
`crates/ail/tests/snapshots/max3.ll`,
`crates/ail/tests/snapshots/sum.ll`,
`crates/ail/tests/snapshots/ws_main.ll` — golden-IR regen via
`UPDATE_SNAPSHOTS=1`; diff is exactly the two new
`declare ptr @ailang_bool_to_str(i1)` and
`declare ptr @ailang_str_clone(ptr)` lines
- Created (4 files):
- `examples/bool_to_str_drop_rc.ail.json` — RC-stats fixture for
`bool_to_str true` (4-byte slab, single drop)
- `examples/bool_to_str_smoke_false.ail.json` — stdout-smoke
fixture for `bool_to_str false` (5-byte slab)
- `examples/str_clone_drop_rc.ail.json` — RC-stats fixture for
`str_clone "hello"` (static-Str input → fresh heap-Str clone)
- `examples/str_clone_cross_realisation.ail.json` — cross-
realisation invariant: clones one heap-Str (output of
`int_to_str 42`) AND one static-Str (literal `"abc"`), printing
both; pins the "uniform consumer ABI" claim
## Stats
bench/orchestrator-stats/2026-05-12-iter-24.1.json
-69
View File
@@ -1,69 +0,0 @@
# 2026-05-12 — iter boss: `/boss` skill — autonomous-mode discipline gated to user invocation
## What shipped
- New skill at `skills/boss/SKILL.md`. Consolidates the three
mode-specific subsections that previously lived in `CLAUDE.md`:
Direction freedom, Notifications, Done-state notifications:
WhatsNew.md.
- `CLAUDE.md` refactored. Three subsections removed (~95 lines).
Skill-system pointer paragraph extended with one sentence
naming `/boss` as the autonomous-mode gate.
- `.claude/skills/boss` symlink created (`../../skills/boss`),
matching the discoverability convention codified in
`skills/README.md`.
- `skills/README.md` updated: skill table extended with a `boss`
row, heading shifted from "seven skills" to "eight skills",
pipeline diagram captioned with a `/boss`-wraps note.
- Two cross-references repointed: `skills/implement/SKILL.md`
and `skills/implement/agents/ailang-implement-orchestrator.md`
both named the "Done-state notifications" subsection of
CLAUDE.md by sub-heading; both now point at `skills/boss/SKILL.md`.
## Why
CLAUDE.md is always loaded into context. Mixing universal facts
(agent role boundaries, commit discipline, design rationale, file
roles, TDD-for-bugs) with mode-specific autonomy rules (direction
freedom, notifications, WhatsNew procedure) was OK while
autonomous-by-default was the operating assumption — but it
conflicted with the user's intent that a fresh session should be
collaborative-interactive unless explicitly elevated.
The mode-switch needs to be explicit. `/boss` is the explicit
elevation, and only the three genuinely mode-specific subsections
move with it. Universal orchestrator discipline stays in CLAUDE.md
because it applies whether `/boss` is active or not: agents need
to know role boundaries when dispatched in any session, design
discipline applies whenever a design choice is made (including a
`brainstorm` invoked outside `/boss`), and the orchestrator can
edit skills at any time.
## Decisions
- **Tightened the move-list from seven items to three.** The
initial spec proposed moving "My role: orchestrator", "Authority
over skills/", "Design rationale ≠ effort", "Feature acceptance",
"When NOT to delegate", plus the three mode-specific subsections.
Planner recon surfaced that the first five are universal, not
mode-specific — they apply in any session. The move-list was
narrowed to the three genuinely mode-specific items. The
brainstorm Step 7.5 grounding-check was re-dispatched on the
edited spec and returned PASS.
- **No private agents under `skills/boss/agents/`.** `/boss` is an
orchestration layer, not a worker. It dispatches the existing
skills (and their agents). The skill table reflects this by
listing `boss` as a meta-skill without an agent-roster row.
- **Cross-reference repoints kept tight.** Recon found 11+
agent/skill files that reference "CLAUDE.md — orchestrator
framing". Most of those resolve correctly to the universal
content that stays in CLAUDE.md. Only two files name the moved
subsections by sub-heading (`implement/SKILL.md` and the
orchestrator-agent's standing reading list); only those two
were repointed.
## Status
Iter closed. No bench regressions to check (markdown-only change,
no compiler touch). No audit needed (single-iter milestone, no
DESIGN.md change). Acceptance criteria from the spec all met.
-128
View File
@@ -1,128 +0,0 @@
# iter cma.1 — Master mini-spec + renderer for the cross-model authoring experiment
**Date:** 2026-05-12
**Started from:** fd6efdbc
**Status:** DONE
**Tasks completed:** 9 of 9
## Summary
Stands up the master mini-spec source and the renderer binary under a
new top-level `experiments/2026-05-12-cross-model-authoring/` tree
that lives outside the root Cargo workspace. The renderer projects
one form-agnostic `master/spec.md` into two form-specific files
(`rendered/json.md` and `rendered/ailx.md`) by walking three directive
forms (`{form-only: json}`, `{form-only: ailx}`, `{example: <id>}`)
and emitting per-example fenced blocks from 13 curated `.ail.json`
fixtures. Four test gates land green: a 7-test splitter unit suite,
a `spec_completeness` test borrowing the AST-variant visitor verbatim
from `crates/ailang-core/tests/schema_coverage.rs` (34/34 variants
observed across the curated subset), an `example_roundtrip` test
mirroring the surface roundtrip contract, and a `token_balance` test
that gates form-only block tokens to ±5% under `tiktoken-rs::cl100k_base`
(final ratio: json=1800, ailx=1742, ratio 3.22%). Two follow-on iters
(cma.2: harness, cma.3: live IONOS run + DESIGN.md addendum) are out
of scope per the spec.
## Per-task notes
- cma.1.1: Bootstrap experiment dir + Cargo skeleton — created
`experiments/.../{README.md,render/Cargo.toml,render/src/main.rs}`
plus the four target directories. Spec-pinned dependency paths
resolve `ailang-core` and `ailang-surface` via relative
`../../../crates/...` references.
- cma.1.2: Directive splitter (TDD) — 7 unit tests RED first, then
implementation; added a `[lib]` target so the integration tests
can reach `xmodel_render::splitter`. GREEN on first impl run.
- cma.1.3: spec_completeness test — visitor + EXPECTED_VARIANTS
list lifted verbatim from `crates/ailang-core/tests/schema_coverage.rs`;
only `examples_dir()` body and the test name changed. Confirmed RED
on the empty master/examples/ directory (failure mode: "no .ail.json
fixtures found").
- cma.1.4: 13 curated `.ail.json` fixtures (one per file as planned).
The plan's 13 fixture topics naturally covered 28 of the 34 AST
variants; the remaining 6 (Def::Const, Term::Let, Term::LetRec,
Term::If, Term::Clone, Term::ReuseAs) were folded into existing
fixtures per the plan's Step 4.14 instruction ("Extend the fixture
closest in scope … do not weaken the test"). Concrete absorptions:
Let + Clone into `fn_calls_prelude`; LetRec into `data_with_match`
(as a recursive `count_via_letrec` helper); If into
`match_literal_pattern` (as a `sign_if` helper); ReuseAs into
`data_simple` (the `wrap_one` body uses `(reuse-as src ...)`);
Const into `floats` (a `const pi : Float = 3.14`). All 34 variants
green on first run.
- cma.1.5: example_roundtrip test — verbatim from plan; all 13
fixtures roundtrip through `ailang_surface::print` and
`ailang_surface::parse` to the same canonical bytes on first
implementation run.
- cma.1.6: master/spec.md — 13 sections of form-agnostic prose with
parallel JSON/AILX form-only blocks in §2-5, §7, §9, §10 plus the
prelude table in §11 (no form-only there; the table itself is the
content). Section §11's prelude tabulation reflects the live
`crates/ailang-check/src/builtins.rs` symbol set: `+`, `-`, `*`,
`/`, `%`, `==`, `!=`, `<`, `<=`, `>`, `>=`, `not`, `neg`,
`int_to_float`, `float_to_int_truncate`, `float_to_str`, `is_nan`,
`nan`/`inf`/`neg_inf`, `__unreachable__`, plus the effect ops
`io/print_{int,bool,str,float}`. `int_to_str` is explicitly named
as OUT (heap-Str ABI milestone).
- cma.1.7: Wire renderer end-to-end — examples.rs and main.rs filled
in verbatim from the plan. Renderer ran clean on first invocation;
json.md (33409 bytes) and ailx.md (23964 bytes) emit with the
expected 13 fenced blocks each (and zero of the wrong fence type).
- cma.1.8: token_balance test — first run reported json=1800,
ailx=1692, ratio 6.00% (above the 5% threshold). One substantive
AILX-side extension to §4 (mode annotations) added authoring
guidance on `over-strict-mode` and hash stability under future
default shifts; final ratio json=1800, ailx=1742, 3.22%. The
rebalancing is content, not filler.
- cma.1.9: Final render + full sweep — `cargo test --manifest-path
experiments/.../render/Cargo.toml` ends with 10 passed; 0 failed
across four test suites; `git status` shows the entire experiment
tree as untracked.
## Concerns
- Task 1: Cargo's automatic workspace discovery hits the root
manifest, so the experiment crate refuses to build on its own. The
fix is the standard Cargo idiom for a nested-but-out-of-workspace
crate: add an empty `[workspace]` table to the experiment's
`Cargo.toml`. The plan did not enumerate this; I added it with a
comment pointing at the spec authority (§Architecture lines 90-95)
for the standalone constraint. Boss should confirm this is the
intended mechanism vs. e.g. adding the experiment to a
`workspace.exclude` list in the root manifest (the spec is explicit
that the root Cargo.toml is not touched, which the chosen mechanism
respects).
- Task 1 ancillary: a `.gitignore` was added under
`experiments/.../render/` ignoring `/target` and `Cargo.lock`. Not
enumerated in the plan but a structural necessity to keep cargo's
build output and lockfile out of the repo. Boss can keep, replace
with a top-level rule, or move to the root `.gitignore`.
## Known debt
- None surfaced inside the iter. Two named deferrals remain out of
scope (cma.2 harness, cma.3 live run + DESIGN.md addendum), per
the spec.
## Files touched
27 new files, all under `experiments/2026-05-12-cross-model-authoring/`:
- `README.md`
- `master/spec.md`
- `master/examples/*.ail.json` × 13
- `render/Cargo.toml`
- `render/.gitignore`
- `render/src/{main,lib,splitter,examples}.rs`
- `render/tests/{splitter_unit,spec_completeness,example_roundtrip,token_balance}.rs`
- `rendered/json.md`
- `rendered/ailx.md`
`git diff --name-only HEAD | wc -l`: 0 (untracked tree; use
`git status --porcelain --untracked-files=all experiments/...` to
enumerate).
## Stats
`bench/orchestrator-stats/2026-05-12-iter-cma.1.json`
-212
View File
@@ -1,212 +0,0 @@
# iter cma.2 — Harness binary + four tasks + reference solutions + three integration tests
**Date:** 2026-05-12
**Started from:** 21c2d2cfaa999980f727a069792e3bd073c1f1e7
**Status:** DONE
**Tasks completed:** 13 of 13
## Summary
Stands up the sibling `harness/` standalone Cargo crate under
`experiments/2026-05-12-cross-model-authoring/`, mirroring the
out-of-workspace idiom from cma.1's `render/`. Six modules under
`harness/src/`: `strip_locations` (regex pass for form-asymmetric
location info), `pipeline` (subprocess wrapper around
`ail parse|check|build` plus the model's compiled binary, with a
fail-fast preflight on `ail` + `clang`), `ionos` (blocking reqwest
client with retry policy), `mock` (canned-response loader keyed by
cohort/task/turn), `scoring` (CSV + summary.md writers), `tasks`
(task-definition struct + loader). `main.rs` ties them into the
per-(cohort, task) loop. Four task definitions (`t1_add_three`,
`t2_length`, `t3_main_prints`, `t4_count_zeros`) with reference
solutions that all reach green through the actual ail+clang
pipeline. 13/13 tests pass: 5 inline unit tests under `--lib`
(strip_locations), 5 strip_locations integration tests against
verbatim recon-captured stderr fixtures, 1 verify_references
integration test driving every reference through the real pipeline,
1 mock_full_run end-to-end with eight rows and per-cohort artefact
trees, 1 budget_abort with the harness aborting cleanly under a
1500-token cap.
## Per-task notes
- cma.2.1: Bootstrap harness Cargo project + skeleton — created
`harness/{Cargo.toml,.gitignore,src/lib.rs,src/main.rs}` plus
six stub module files. Empty `[workspace]` table at the top of
Cargo.toml keeps the crate out of the root workspace. Cargo.lock
gitignored per spec. First `cargo build` succeeded with the
expected unused-import warnings on the skeleton.
- cma.2.2: strip_locations module — written verbatim from the
plan with 5 inline unit tests (json_pointer, byte_offset, line/
column, Caused-by chain, passthrough). All 5 green on first run.
- cma.2.3: pipeline module — written verbatim from the plan plus
a small implementer-phase repair: `ail check` enforces filename
stem == module name, so `pipeline::run_pipeline` extracts the
top-level `"name"` field from the JSON via a new
`module_name_from_json` helper and renames the working file to
`<module_name>.ail.json` before invoking `ail check`. Without
this, every reference would have failed at the `ail check` step
with "module name in file does not match expected name from
path". See Concerns.
- cma.2.4: ionos module — written verbatim from the plan; added
`thiserror = "1"` to `[dependencies]`. Compile-time warnings
about three unused things on the `IonosClient` path (Auth, Usage,
`last_err` assignment) are expected and flagged in the plan.
- cma.2.5: mock module — written verbatim; compile clean.
- cma.2.6: scoring module — written verbatim; compile clean.
- cma.2.7: tasks module — written verbatim; compile clean.
- cma.2.8: Author four task definitions + reference solutions —
task.json files written verbatim from the plan text. Reference
`.ail.json` files authored fresh against the cma.1 templates
(hello.ail.json shape for t3; gc_stress.ail.json's polymorphic
`List a` shape for t2 and t4; canonical chained `+` for t1's
three-arg sum; canonical `eq` invocation pattern + branched `if`
for t4's count_zeros). All four references compile clean and
produce the expected stdout. Reference solutions intentionally
omit `param_modes` because every parameter is Implicit and the
canonical AILang form omits the field in that case — consistent
with hello.ail.json, list.ail.json, eq_primitives_smoke.ail.json,
and the explanatory doc in `master/examples/param_modes_all.ail.json`.
See Concerns. Added `tempfile = "3"` to `[dev-dependencies]` for
the `verify_references.rs` integration test. The test runs every
reference through `pipeline::run_pipeline(Cohort::Json, ...)`
end-to-end (ail parse-check-build-execute); passes when
`AIL_BIN` is set to the release binary.
- cma.2.9: Wire main.rs end-to-end — written verbatim from the
plan; added `tempfile = "3"` to `[dependencies]` (not only
dev-deps) because `main.rs::run_one` uses it for per-turn
workdir. Compile clean (modulo the three flagged warnings from
ionos).
- cma.2.10: Five stderr fixtures + strip_locations integration
test — all five fixture files written verbatim from the plan
(which itself captured them from recon's run against current HEAD).
Integration test asserts strip behaviour against each fixture;
5/5 green on first run.
- cma.2.11: mock_full_run integration test + mock_full_run.json
fixture — fixture authored by inlining each
`master/tasks/<id>.reference.ail.json` as the `content` field
for the green-path entries, plus `ail render` for the AILX-cohort
inlines (verbatim AILX serialisations of the same references).
Test runs the harness binary against the fixture; 8 rows total
(4 tasks × 2 cohorts), `(json, t3_main_prints)` reaches green on
turn 2 (matches the plan's intended cycle: broken-then-fixed),
`(ailx, t1_add_three)` runs to the 5-turn limit (`(module garbage)`
repeated). All artefacts (turn_N_program.ext,
turn_N_request.json, turn_N_response.json,
turn_N_check_stderr.txt, turn_N_run_stdout.txt) land in
`per_cohort/<cohort>/<task>/`. 1/1 green.
- cma.2.12: budget_abort integration test — test verbatim from
the plan, plus an implementer-phase repair to `main.rs`: with a
tiny budget (1500), the outer loop broke out after consuming
~2400 tokens across tasks 1+2 (both green on turn 1), but
neither row carried `final_status = budget_abort` because the
inner `run_one` only marks BudgetAbort if the budget is exhausted
*during* the turn loop. The plan's test assertion expects at
least one row with `budget_abort`. Repair: after the outer loop
breaks with `run_status="budget_exceeded"`, fill in the not-yet-run
(cohort, task) pairs with synthetic `BudgetAbort` rows. mock_full_run
remains green (the new code only runs on budget_exceeded).
1/1 green. See Concerns.
- cma.2.13: README update + final sweep — appended "Running the
harness" section to `experiments/.../README.md` verbatim from the
plan. Full `cargo test` sweep: 5 (lib unit, strip_locations) + 5
(integ, strip_locations) + 1 (verify_references) + 1
(mock_full_run) + 1 (budget_abort) = 13/13 green. `git status`
shows the README modification + the new harness/ tree + the new
master/tasks/ files, all unstaged.
## Concerns
- **Pipeline filename-matching: minimal repair landed in `pipeline.rs`,
not flagged in the plan.** `ail check` enforces filename stem ==
module name; the plan's `run_pipeline` writes the model's program
to `prog.{ext}`, which would have been rejected at every `ail
check` invocation. The repair adds a 9-line helper
`module_name_from_json` (reads JSON, returns top-level `name`) plus
14 lines in `run_pipeline` that rename `prog.ail.json` ->
`<module_name>.ail.json` between parse and check. Behaviour
outside that rename is unchanged. The structural alternative
(telling the model to name its module `prog`) would have been
semantically wrong — the task description prompts say "module
named t1_add_three" etc. Recommend a forward-fix sweep in cma.3
if the structural decision (whether the pipeline or the prompt
owns module naming) warrants spec attention.
- **`param_modes` omitted from every reference solution.** The plan's
Step 8.2 text says "mode annotations on every parameter", but the
templates the plan references (hello.ail.json, list.ail.json,
eq_primitives_smoke.ail.json, etc.) all omit `param_modes`, and
the `param_modes_all.ail.json` master example's own doc string
documents "Implicit mode is the legacy default — `param_modes` is
omitted from canonical JSON when every entry is Implicit". The
references stay in canonical form. If the plan's intent was
explicit `["implicit", "implicit", ...]` annotations on every
reference, that is a follow-on amendment.
- **Budget-abort row-filling, minimal repair landed in `main.rs`,
not flagged in the plan.** Without it, Task 12's test fails on
the assertion `expected at least one budget_abort row`. Repair
is gated on `run_status == "budget_exceeded"` so the green path
is unchanged.
- **README "Total 8 passed" text mismatches Step 13.2's "13 passed"
expectation.** The plan's README text (Step 13.1) advertises four
test suites with "Total 8 passed" — counts the four integration
suites but skips the 5 inline unit tests under `--lib`. Step 13.2
expects 13 across `--lib` + 4 integration tests. The README text
is reproduced verbatim per plan; the actual sweep produces 13.
Recommend a docfix in cma.3 ("Total 13 passed across --lib + 4
integration tests"), but the cma.2 plan locked the README copy.
- **`AIL_BIN` env required for `verify_references.rs` and
`mock_full_run.rs` to run from a fresh shell.** These tests shell
out to `ail`; without `ail` on PATH the preflight bails. The
README documents this. CI integration is out of scope.
## Known debt
- The harness has not yet been live-fired against IONOS (deferred
to cma.3 per spec). The retry/backoff path in
`ionos::IonosClient::post` has no test coverage in cma.2 (mock
mode bypasses it entirely).
- Three compile warnings in `ionos.rs` (`Auth` and one `Usage` are
read in tests/other code; `last_err` second assignment is
intentional dead-store for retry-loop symmetry). Not addressed.
## Files touched
Modified (1):
- `experiments/2026-05-12-cross-model-authoring/README.md`
New — harness crate (8 source, 4 integration tests, 6 fixtures, 1
manifest, 1 .gitignore):
- `experiments/2026-05-12-cross-model-authoring/harness/Cargo.toml`
- `experiments/2026-05-12-cross-model-authoring/harness/.gitignore`
- `experiments/2026-05-12-cross-model-authoring/harness/src/lib.rs`
- `experiments/2026-05-12-cross-model-authoring/harness/src/main.rs`
- `experiments/2026-05-12-cross-model-authoring/harness/src/strip_locations.rs`
- `experiments/2026-05-12-cross-model-authoring/harness/src/pipeline.rs`
- `experiments/2026-05-12-cross-model-authoring/harness/src/ionos.rs`
- `experiments/2026-05-12-cross-model-authoring/harness/src/mock.rs`
- `experiments/2026-05-12-cross-model-authoring/harness/src/scoring.rs`
- `experiments/2026-05-12-cross-model-authoring/harness/src/tasks.rs`
- `experiments/2026-05-12-cross-model-authoring/harness/tests/strip_locations.rs`
- `experiments/2026-05-12-cross-model-authoring/harness/tests/verify_references.rs`
- `experiments/2026-05-12-cross-model-authoring/harness/tests/mock_full_run.rs`
- `experiments/2026-05-12-cross-model-authoring/harness/tests/budget_abort.rs`
- `experiments/2026-05-12-cross-model-authoring/harness/tests/fixtures/check_unbound_var.stderr`
- `experiments/2026-05-12-cross-model-authoring/harness/tests/fixtures/check_type_mismatch.stderr`
- `experiments/2026-05-12-cross-model-authoring/harness/tests/fixtures/check_bare_xmod.stderr`
- `experiments/2026-05-12-cross-model-authoring/harness/tests/fixtures/check_schema_missing_field.stderr`
- `experiments/2026-05-12-cross-model-authoring/harness/tests/fixtures/parse_unclosed.stderr`
- `experiments/2026-05-12-cross-model-authoring/harness/tests/fixtures/mock_full_run.json`
New — master tasks (4 task.json + 4 reference.ail.json):
- `experiments/2026-05-12-cross-model-authoring/master/tasks/t1_add_three.task.json`
- `experiments/2026-05-12-cross-model-authoring/master/tasks/t1_add_three.reference.ail.json`
- `experiments/2026-05-12-cross-model-authoring/master/tasks/t2_length.task.json`
- `experiments/2026-05-12-cross-model-authoring/master/tasks/t2_length.reference.ail.json`
- `experiments/2026-05-12-cross-model-authoring/master/tasks/t3_main_prints.task.json`
- `experiments/2026-05-12-cross-model-authoring/master/tasks/t3_main_prints.reference.ail.json`
- `experiments/2026-05-12-cross-model-authoring/master/tasks/t4_count_zeros.task.json`
- `experiments/2026-05-12-cross-model-authoring/master/tasks/t4_count_zeros.reference.ail.json`
## Stats
bench/orchestrator-stats/2026-05-12-iter-cma.2.json
-103
View File
@@ -1,103 +0,0 @@
# iter cma.3 — Live run + DESIGN.md addendum + milestone close
**Date:** 2026-05-12
**Started from:** fe1fb6b
**Status:** DONE (milestone Cross-Model Authoring-Form Test closed)
**Mode:** Boss-direct (no implement dispatch per spec)
## Summary
Live IONOS run against Qwen3-Coder-Next executed; raw dataset
under `experiments/2026-05-12-cross-model-authoring/runs/2026-05-12-df7531/`.
AILX cohort reached green on 2/4 tasks at ~98k total tokens; JSON
cohort reached green on 1/4 tasks at ~207k total tokens. The only
first-attempt success was AILX on `t3_main_prints`. JSON-cohort
failures clustered in the typecheck stage (check×4); AILX-cohort
failures clustered in the parse stage (parse×3). DESIGN.md
§"Decision 6" gains a 27-line "Empirical addendum (2026-05-12)"
recording the data, the illustrative t3 case, and an explicit
single-subject single-run scope note. Multi-subject expansion
queued as a fresh roadmap P3 entry.
## Run details
- Endpoint: `https://openai.inference.de-txl.ionos.com/v1/chat/completions`
- Model: `Qwen/Qwen3-Coder-Next`
- Parameters: temperature=0, top_p=1, max-turns=5,
token-budget=500000.
- RUN_STATUS: `ok`. Total tokens consumed: 305,148 of 500,000
budget (39% headroom).
- 40 raw artefact files (8 runs × 5 per-turn files on average)
preserved under `runs/2026-05-12-df7531/per_cohort/<cohort>/<task>/`.
## Per-task notes
- `json/t1_add_three`: turn_limit. Cohort consumed 5 turns trying
to satisfy the schema constraints around `do` / `seq` / effect
positioning. Top error class `check` throughout.
- `json/t2_length`: turn_limit. ADT + polymorphism stress; the
model emitted plausible-shape JSON that the typechecker
rejected.
- `json/t3_main_prints`: green on turn 2 (the only green in the
JSON cohort). Turn 1 was ~23 lines of structured JSON with a
small structural mistake the check loop pointed out (with
location info stripped); turn 2 was correct.
- `json/t4_count_zeros`: turn_limit. Typeclass surface + Eq
constraint shape; the model never converged.
- `ailx/t1_add_three`: green on turn 3. Got the structure right
but needed two correction turns to align with the prelude
function naming.
- `ailx/t2_length`: turn_limit. Errors mixed `check` and `parse`
(model occasionally produced malformed s-expressions on
correction turns).
- `ailx/t3_main_prints`: green on turn 1, ~5 lines of tagged
s-expr. Only first-attempt success in the entire run.
- `ailx/t4_count_zeros`: turn_limit. Parse-stage failures on
every turn; the model struggled with the typeclass-constraint
shape in AILX.
## DESIGN.md edit
§"Decision 6: authoring surface" gains a new
"Empirical addendum (2026-05-12)" subsection (~27 lines after the
existing prose, before §"Decision 8"). Records: model id, run
parameters, the 6-row metric table, an illustrative paragraph
naming the t3 contrast, and the explicit scope note that the
data is one subject + one deterministic run and that the
universal claim of Decision 6 stays pending multi-subject
follow-up.
## Roadmap
- Removed: P2 entry "Cross-model authoring-form test" (closed).
- Added: P3 entry "Multi-subject expansion
(cross-model authoring-form follow-up)" pointing at this run as
baseline.
## Concerns
- AILX cohort's high parse-error count is partly model-side
(small grammar mistakes the typechecker would have forgiven)
and partly the strip_locations regex erasing `at byte N`
offsets, which leaves the model without precise localisation
during correction turns. Symmetric degradation is the fairness
anchor; calibration trade-off is documented in the spec.
- The JSON cohort's mini-spec at ~15k system tokens vs AILX's
~7k is an inherent form asymmetry (JSON is wordier per
construct). Token costs are partly a measurement of *the form*,
not just of how easy each is to author. This is part of what
the experiment measures, but worth naming.
- Only 3/8 (cohort, task) pairs reached green. The dataset's
signal-to-noise ratio is modest; the multi-subject expansion
needs more tasks and/or more permissive prelude coverage so
the failure clusters are less dominant.
## Known debt
- Multi-subject expansion (≥2 additional foreign LLMs, ≥4 more
tasks, repetitions for statistical robustness) — queued as the
P3 roadmap entry. Not started.
- README "Total 8 passed" inherited from the cma.2 plan still
understates the actual 13/13 test count; the prose was carried
verbatim per cma.2 plan discipline. Cosmetic; can be fixed in
a future tidy iter.
-47
View File
@@ -1,47 +0,0 @@
# iter ctt.1 — Env-overlay shape ratification
**Date:** 2026-05-12
**Started from:** 0a36294e61f08d8de85c084415d694d3736e0c67
**Status:** DONE
**Tasks completed:** 3 of 3
## Summary
Ratifies the two-overlay shape of the check environment (`env.types`
+ `env.ctor_index`) and pins its load-bearing consumer. The split
decision now has a DESIGN.md anchor (§"Env construction") explaining
the owning-index / reverse-index roles, the rationale for not
collapsing into one variant-keyed map, and the intentional asymmetry
with the mono side (narrowed to types-only at ct.3.2, because its
runtime-ctor-lookup consumer story differs from the check side's
in-band `DuplicateCtor` diagnostic). A new behavioural-pin test
(`crates/ailang-check/tests/duplicate_ctor_pin.rs`) asserts
`code == "duplicate-ctor"` plus message-substring coverage for a
two-ADT-same-ctor fixture, going red if the per-module rebuild at
`crates/ailang-check/src/lib.rs:1342-1384` is ever silently removed.
Two P2 roadmap todos struck `[x]` with one-line forward-references to
this iter. No production-code edits. `cargo test --workspace` green.
## Per-task notes
- iter ctt.1.1: New pinning test `crates/ailang-check/tests/duplicate_ctor_pin.rs` — builds a `Workspace` inline with one module declaring `type Cat = Twins` and `type Dog = Twins`, calls `check_workspace`, asserts a `Diagnostic` with `code == "duplicate-ctor"` plus message substring for `Twins` / `Cat` / `Dog`. PASS on first run, as the plan predicted (behaviour pre-exists).
- iter ctt.1.2: Inserted new top-level section `## Env construction` in `docs/DESIGN.md` between `## Convention: qualified cross-module references` (line 1941) and `## Data model` (now line 1988, was 1959 pre-edit). Three paragraphs: split definition, rationale, check-side / mono-side asymmetry. Cross-references the pinning test.
- iter ctt.1.3: Two `[ ]``[x]` strikes in `docs/roadmap.md` at lines 79 (overlay shape question) and 183 (per-module overlay narrowing). Each gains a one-line forward-reference suffix on its `context:` line pointing at iter ctt.1 and the closing artefact.
## Concerns
(none)
## Known debt
(none)
## Files touched
- `crates/ailang-check/tests/duplicate_ctor_pin.rs` (new)
- `docs/DESIGN.md` (modified — `## Env construction` section inserted)
- `docs/roadmap.md` (modified — two strikes)
## Stats
bench/orchestrator-stats/2026-05-12-iter-ctt.1.json
-142
View File
@@ -1,142 +0,0 @@
# iter ctt.2 — `Registry.type_def_module` re-key to tuple-keyed map
**Date:** 2026-05-12
**Started from:** 548ebf8d51758ab9f156b266e0baa59c78b14034
**Status:** DONE
**Tasks completed:** 3 of 3
## Summary
`Registry.type_def_module` re-keyed from `BTreeMap<String, String>` to
`BTreeMap<(String, String), String>` keyed by `(owning_module,
bare_name)`. A new `caller_module: &str` parameter threads through
`normalize_type_for_registry` and `Registry::normalize_type_for_lookup`,
and all four `ailang-check` consumer sites pass the correct
module-context per the plan's Design Notes block. A regression test
plus a three-module fixture exercise the bare-name collision the
re-key fixes — two modules each declaring `type Foo` now register
under distinct canonical keys instead of tripping the silent-overwrite
defect. `cargo test --workspace` green (16 binary-test suites,
~600 tests total, zero failures).
## Per-task notes
- iter ctt.2.1: RED-first — three fixture JSON files
(`ctt2_collision_{cls,lib,main}.ail.json`) plus
`crates/ailang-core/tests/ctt2_registry_rekey.rs`. Test goes
RED today. Failure shape was `OrphanInstance` rather than the
plan's predicted `DuplicateInstance` (a) or missing-hash (b) —
see Concerns.
- iter ctt.2.2: GREEN — atomic re-key edit pass. Touched the field
type + doc-comment, `Registry::normalize_type_for_lookup`
signature + body, pass-1 declaration + insert,
pass-2 coherence-check lookup + canonical-form hashing call,
`normalize_type_for_registry` signature + doc-comment + body
(recursive calls thread caller_module everywhere), the inline
`ct1_5a_normalize_recurses_into_forall_constraints` test
at workspace.rs:2543, and all four `ailang-check` consumer sites
at the module-context dictated by the plan's Design Notes
(lib.rs:1640 → `env.current_module.as_str()`, mono.rs:121 →
`defining_module.as_str()`, mono.rs:624 and :1175 → `module_name`).
Clean `cargo build --workspace` at the end of the pass.
- iter ctt.2.3: workspace-wide test gate. New test goes GREEN.
Full `cargo test --workspace` green, no test in any crate
regressed. The plan's Step 2 hypothesis ("a fixture under
`examples/` might legitimately exercise a bare cross-module
type reference") did not materialise — the existing corpus
uses canonical-form (qualified) cross-module type refs
exclusively, so the tightening had no surface.
## Concerns
- Task 1 RED-failure shape diverged from the plan's predicted
shapes (a) `DuplicateInstance` and (b) missing-`contains_key`.
The plan's verbatim two-module fixture pair (lib imports main +
main imports lib + lib-side instance with qualified class
`ctt2_collision_main.MyC`) had two independent structural
defects against the loaded codebase invariants:
1. The workspace loader rejects import cycles
(`WorkspaceLoadError::Cycle`) at module-name granularity —
the plan's claim "Two-way import is supported; workspace
loader DFS handles cycles at module-name granularity" is
empirically false. Confirmed by inspection at
`workspace.rs:1410-1413` and `:1451-1454`.
2. `validate_canonical_type_names` rejects qualified
`InstanceDef.class` with `QualifiedClassName` (see test
`ct1_validator_rejects_qualified_instancedef_class` at
workspace.rs:2321) — the plan's lib-side instance with
`class: "ctt2_collision_main.MyC"` would have been rejected
even without the cycle.
Implementer-phase repair: introduced a third module
`ctt2_collision_cls.ail.json` carrying `class MyC`, broke the
cycle (both lib and main import cls only; main also imports
lib so the loader DFS reaches it), and dropped the qualifier
from both instance heads (both declare bare `class: "MyC"`
satisfying coherence via the type's module). The plan's
*core* RED-condition is preserved: two modules each declaring
`type Foo` with `instance MyC Foo` collide on the pre-ctt.2
bare-name `type_def_module` key.
The actually-observed RED shape was `OrphanInstance` rather
than `DuplicateInstance` because the pass-2 coherence check
(`workspace.rs:656-659`) uses the bare-name lookup *before*
the duplicate-uniqueness check fires — lib's `Foo` and main's
`Foo` both resolve to whichever module won the alphabetical
race (`ctt2_collision_main` beats `ctt2_collision_lib`
lexicographically), so lib's instance is no longer coherent
("type lives in main, class lives in cls, instance lives in
lib — neither matches"). The re-key fixes this consumer site
simultaneously with the duplicate-check site because both go
through the same `type_def_module` lookup, now tuple-keyed.
The deviation from the plan's verbatim fixtures is an
implementer-phase NEEDS_CONTEXT repair that the
user-no-stop-instruction permitted in-line; the plan's *intent*
(RED-first against the bare-name collision) is achieved and
the GREEN-state matches the plan's acceptance criteria
unchanged. Boss may want to back-edit the plan or amend the
spec to note the multi-module fixture shape if this kind of
collision regression returns later.
## Known debt
- The `OrphanInstance` consumer site at `workspace.rs:656-659`
is fixed by ctt.2 but the diagnostic-shape change (orphan
instead of duplicate-on-loser) is not separately tested. The
new test asserts the GREEN state directly (both entries land
under distinct keys) which transitively exercises both
consumer sites, but neither the orphan nor the duplicate
diagnostic shapes are pinned by a dedicated RED test. Adding
one is plausibly out-of-scope for ctt.2.
## Files touched
Modified (3):
- `crates/ailang-check/src/lib.rs` — single call-site update at
the NoInstance check inside `check_fn`.
- `crates/ailang-check/src/mono.rs` — three call-site updates
(monomorphise_workspace at :121, collect_mono_targets at :624,
collect_residuals_ordered at :1175).
- `crates/ailang-core/src/workspace.rs` — field re-key,
`Registry::normalize_type_for_lookup` signature,
`normalize_type_for_registry` signature + body,
pass-1 declaration + insert,
pass-2 coherence-check lookup + canonical-form hashing call,
inline test `ct1_5a_normalize_recurses_into_forall_constraints`
at :2543; doc-comments at field-decl and at both
`normalize_…` fns.
Added (4):
- `crates/ailang-core/tests/ctt2_registry_rekey.rs` — regression test.
- `examples/ctt2_collision_cls.ail.json` — class-bearing module
(not in plan; required to break the cycle in the plan's
verbatim fixture design).
- `examples/ctt2_collision_lib.ail.json``type Foo = MkLib` +
bare `instance MyC Foo`.
- `examples/ctt2_collision_main.ail.json``type Foo = MkMain` +
bare `instance MyC Foo`, plus imports of both `cls` and `lib`.
## Stats
bench/orchestrator-stats/2026-05-12-iter-ctt.2.json
-74
View File
@@ -1,74 +0,0 @@
# iter ctt.3 — `KindMismatch` retire (pure deletion across 3 files)
**Date:** 2026-05-12
**Started from:** 0e556f085c14993441b7d25dcdc69077b77c83b0
**Status:** DONE
**Tasks completed:** 2 of 2
## Summary
`WorkspaceLoadError::KindMismatch`, its dispatch site in
`validate_classdefs`, its `walk_kind_mismatch` helper, its Display
arm in `crates/ail/src/main.rs:workspace_error_to_diagnostic`, and
the "dead-but-defensive" doc-comment on the canonical-form-rejection
test are all retired. Two adjacent textual-consistency edits ride
along: the `validate_classdefs` doc-comment narrows from "Three
diagnostics fire from here" to "Two" with a successor sentence
naming the new path, and the `crates/ailang-check/src/lib.rs:1428`
22b.1-era archaeology comment drops the `KindMismatch` token from
its enumeration of class-schema diagnostics and gains a one-line
ctt.3 retirement marker. The existing test
`class_param_in_applied_position_fires_canonical_form_rejection`
stays green unchanged — it asserts `BareCrossModuleTypeRef` on the
malformed fixture, which is the canonical-form-validator successor
the plan named. `cargo build --workspace` clean, no orphan match
arms, no dead-code warnings. `cargo test --workspace` green
(~600 tests across 16 binary-test suites, zero failures).
## Per-task notes
- iter ctt.3.1: atomic deletion pass — 6 Edits across 3 files. workspace.rs ×4 (variant delete; `validate_classdefs` doc + dispatch-loop delete; `walk_kind_mismatch` helper delete; "dead-but-defensive" doc delete). main.rs ×1 (`W::KindMismatch` arm in `workspace_error_to_diagnostic`). lib.rs ×1 (22b.1 archaeology comment). Build green at Step 7.
- iter ctt.3.2: workspace-wide test gate — grep substantively clean in `crates/` (one residual `KindMismatch` token at lib.rs:1431 is the ctt.3 retirement marker introduced by Step 6 itself, not a live consumer); canonical-form-rejection test PASS; full `cargo test --workspace` PASS.
## Concerns
- Plan-internal contradiction surfaced during Task 2 Step 1: the
plan's Step 6 `new_string` deliberately introduces a new
`KindMismatch` token in the ctt.3 retirement marker
(`// ctt.3: KindMismatch retired — ...`), but Task 2 Step 1's
verification text says "Expected: zero hits in `crates/`". The
parent spec at `docs/specs/2026-05-12-ct-tidy.md:384` is more
permissive ("`grep KindMismatch` returns the three deletion sites
only" — accepting the diff itself as the only mention, but
silent on a successor archaeology marker). Substantive intent
(retire the diagnostic) is met: variant, helper, dispatch, and
Display arm are all gone. Treated as `compliant` with this
concern recorded; Boss may choose to either tighten the
spec/plan rule for future tidies or accept the retirement-marker
pattern (which is already common in the codebase — cf. ctt.1
successor-rationale comments in workspace.rs).
- Mid-deletion repair on workspace.rs:773 — the dispatch-loop
deletion in Step 2 orphaned the `mod_name` binding that
destructured `for (mod_name, m) in modules`. Plan Step 7's
acceptance criterion is "no dead-code warnings" — left as
`for (mod_name, m)` the build would warn `unused variable:
mod_name`. Minimal cleanup applied: rewrote to
`for m in modules.values()`. Same body, same `m: &Module` type
via `.values()` instead of tuple-destructure. Not in the plan's
verbatim edit list but a foreseeable side-effect of the
deletion and entirely mechanical.
## Known debt
- None new. The "future tidy may retire" promise in the deleted
doc-comment is now this iter; no successor obligation.
## Files touched
- `crates/ailang-core/src/workspace.rs` (variant delete + doc adjust + dispatch-loop delete + helper delete + test-doc delete + `mod_name` binding cleanup)
- `crates/ail/src/main.rs` (Display arm delete)
- `crates/ailang-check/src/lib.rs` (archaeology comment edit)
## Stats
bench/orchestrator-stats/2026-05-12-iter-ctt.3.json
-123
View File
@@ -1,123 +0,0 @@
# iter eob.1 — Effect-op args walked as Borrow; heap-Str RC discipline closes
**Date:** 2026-05-12
**Started from:** b5b0c2d7dcd8bec1819801257de7731edc542e0f
**Status:** DONE
**Tasks completed:** 7 of 7
## Summary
Land the language rule that `Term::Do.args[*]` are walked in
`Position::Borrow` by the uniqueness and linearity passes, plus the
two heap-Str-specific shape fixes (`ret_mode: Own` for `int_to_str`
/ `float_to_str`; Str carve-out in the App arm of
`drop_symbol_for_binder`) that make the existing RED tests at
`crates/ail/tests/e2e.rs` flip to GREEN under `--alloc=rc`. Both
arg-position rules (Ctor = Consume, Do = Borrow) are anchored in
DESIGN.md §Decision 10. The heap-str-abi milestone closes here:
WhatsNew entry shipped, roadmap P1 entry checked off, downstream
Post-22-Prelude `depends on:` line removed.
The five plan layers landed first-shot in lockstep — no review
re-loops fired on any of the 7 tasks. The pre-existing RED tests
(`int_to_str_drop_balances_rc_stats`,
`str_field_in_adt_drops_heap_str_correctly`) flipped to GREEN with
the predicted concrete numbers (`allocs == 1, frees == 1, live == 0`
and `allocs == 2, frees == 2, live == 0`). Two new positive tests
pin the rule's coverage at the primitive boundary (Int through
`io/print_int`: zero RC traffic) and at the linearity boundary
(heap-Str through two sequential `io/print_str` calls: allocs == 1,
frees == 1, live == 0). Workspace test --workspace + cross_lang.py
+ compile_check.py all green. check.py shows the same noisy
latency-max + bump_s cluster the audit-cma and audit-ms journals
both left pristine ("not coupled to milestone, baseline left
untouched for second consecutive audit") — same precedent applies
here; baseline left as-is.
## Per-task notes
- iter eob.1.1: Term::Do arg-walks flip Consume → Borrow in both
uniqueness.rs:289 and linearity.rs:506; linearity.rs:42 doc
comment split into two bullets matching the new code shape.
Inline clarifying comment added on the linearity-side flip
mirroring the Ctor-arm convention. cargo build + workspace
check-crate tests green.
- iter eob.1.2: builtins.rs `float_to_str` (:200) + `int_to_str`
(:210) and synth.rs `float_to_str` (:172) + `int_to_str` (:179)
all flip `ret_mode: Implicit``ret_mode: Own` in lockstep.
Workspace build clean. Target test now reports the predicted
link-error shape "undefined value @drop_int_to_str_drop_rc_Str"
— exactly the gap Task 3 closes.
- iter eob.1.3: drop.rs App arm of `drop_symbol_for_binder` gets
the Str carve-out `if name == "Str" return ailang_rc_dec`,
symmetric to `field_drop_call`'s existing Str arm. The
comment block mirrors `field_drop_call`'s wording. cargo build
clean.
- iter eob.1.4 (observational): both RED tests flip to GREEN with
the predicted concrete numbers (allocs == 1, frees == 1 / allocs
== 2, frees == 2; live == 0 on both). Full e2e suite (79 tests)
all green — no other fixture's RC trace shifted unexpectedly.
- iter eob.1.5: two new fixtures
(`examples/int_to_print_int_borrow.ail.json`,
`examples/heap_str_repeated_print_borrow.ail.json`) + two new
e2e tests (`int_arg_to_effect_op_does_not_rc_track`,
`heap_str_repeated_print_balances_rc_stats`) appended near
e2e.rs:2625. Both new tests PASS first-shot with exact predicted
RC stats.
- iter eob.1.6: DESIGN.md §Decision 10 gets a new subsection
`#### Arg-position policy for compound AST nodes` after the
`ret_mode` block. The two arg-position rules (Ctor = Consume,
Do = Borrow) are anchored together as language rules, not
per-op annotations. One added linking paragraph explains how
the Do = Borrow rule cooperates with the `ret_mode == Own`
letbinder-trackability rule above to balance heap-Str RC
discipline — within the plan's "Boss judgement on placement"
carve-out.
- iter eob.1.7: WhatsNew.md entry appended at bottom (user-facing
wording, no internals). roadmap.md heap-Str ABI entry flipped
`[ ]``[x]`. Post-22 Prelude `depends on:` line removed (the
only dep was Heap-Str, which has shipped). cargo test workspace
+ cross_lang.py + compile_check.py all green. check.py noisy
latency cluster left pristine per audit-cma/audit-ms precedent.
## Concerns
(none — all 7 tasks DONE without review re-loops)
## Known debt
- check.py shows recurring noisy `latency.implicit_at_rc.max_us`
/ `latency.explicit_at_rc.max_us` tail-jitter, plus
`throughput.bench_list_sum.bump_s` swinging +11-15% across
back-to-back runs (with reciprocal `gc_over_bump` improvement
on the same fixture). Neither family is reachable from this
iter's changes (bump_s on bench_list_sum has no effect-ops; the
list-sum hot path has no int_to_str / float_to_str calls). The
audit-cma and audit-ms journals already document the same
noise cluster as "not coupled to milestone, baseline left
pristine for two consecutive audits" — same precedent applies;
baseline untouched.
## Files touched
Code:
- crates/ailang-check/src/uniqueness.rs (Term::Do Consume → Borrow)
- crates/ailang-check/src/linearity.rs (Term::Do Consume → Borrow + doc-block split)
- crates/ailang-check/src/builtins.rs (int_to_str / float_to_str ret_mode → Own)
- crates/ailang-codegen/src/synth.rs (int_to_str / float_to_str ret_mode → Own)
- crates/ailang-codegen/src/drop.rs (App-arm Str carve-out)
Tests + fixtures:
- crates/ail/tests/e2e.rs (2 new positive tests appended)
- examples/int_to_print_int_borrow.ail.json (new)
- examples/heap_str_repeated_print_borrow.ail.json (new)
Docs:
- docs/DESIGN.md (Decision 10: arg-position policy subsection)
- docs/WhatsNew.md (heap-str-abi milestone close entry)
- docs/roadmap.md (Heap-Str ABI P1 closed; Post-22 Prelude
depends-on line removed)
## Stats
bench/orchestrator-stats/2026-05-12-iter-eob.1.json
-137
View File
@@ -1,137 +0,0 @@
# iter ext-cli.1 — CLI accepts `.ail` (Form A) source files alongside `.ail.json`
**Date:** 2026-05-12
**Started from:** efecbaa
**Status:** DONE
**Tasks completed:** 4 of 4
## Summary
Closes the loop opened by this morning's `.ailx → .ail` rename. Every
path-taking `ail` subcommand (`check`, `build`, `run`, `manifest`,
`render`, `prose`, `merge-prose`, `describe`, `emit-ir`, `diff`,
`workspace`, `deps`) now accepts a `.ail` (Form A) input as well as
`.ail.json` (Form B). For `.ail` paths the subcommand parses through
`ailang_surface::parse` in-line and proceeds with the same loaded
`Module` value `.ail.json` would have produced; the binary semantics
are extension-agnostic.
Architecture: a new `ailang_surface::{load_module, load_workspace}`
pair dispatches on file extension. A new injection point in
`ailang_core::workspace::load_workspace_with<F>` lets the surface
crate plug in an extension-aware loader without `ailang-core` having
to import `ailang-surface` (which would close a crate cycle). Import
resolution in `workspace::visit` now prefers a `.ail` sibling over a
`.ail.json` sibling of the same module name. The CLI binary's 18
`ailang_core::load_*` callsites all switched to the new
`ailang_surface::load_*`. Surface parse failures route through the
existing `workspace_error_to_diagnostic` channel as a new
`SurfaceParse` variant of `WorkspaceLoadError`, so
`ail check --json foo.ail` on a malformed `.ail` returns a parseable
JSON diagnostic with code `surface_parse_error` instead of the
pre-iter misleading `json: expected value at line 1 column 1`
fall-through.
## Per-task notes
- iter ext-cli.1.1: `WorkspaceLoadError::SurfaceParse { path,
message }` variant added to `ailang_core::workspace`;
`load_workspace` refactored into a thin wrapper around a new
`pub fn load_workspace_with<F>(entry, loader: F)` with `F:
Fn(&Path) -> Result<Module, WorkspaceLoadError> + Copy`; `visit`'s
signature gained the loader parameter and its import-path
construction now does `.ail`-first / `.ail.json`-fallback; new
test `load_workspace_with_custom_loader_is_called` pins the
injection. `tempfile` added as `ailang-core` dev-dependency.
- iter ext-cli.1.2: new `crates/ailang-surface/src/loader.rs` with
`pub fn load_module` (extension-dispatching: `.ail` → `parse`,
else delegate to `ailang_core::load_module`) and
`pub fn load_workspace` (calls `load_workspace_with(entry,
load_module)`); `lib.rs` re-exports both. Three integration tests
in `crates/ailang-surface/tests/loader.rs` pin the dispatch on
each form individually + the mixed-extension workspace case.
`tempfile` added as `ailang-surface` dev-dependency.
- iter ext-cli.1.3: every `ailang_core::load_module` /
`ailang_core::load_workspace` callsite in `crates/ail/src/main.rs`
(18 sites total) replaced with `ailang_surface::load_module` /
`ailang_surface::load_workspace`. Workspace-build placeholder
`W::SurfaceParse { .. } => None` arm added to
`workspace_error_to_diagnostic` (replaced in Task 4) so the
exhaustive match compiles. Two new E2E tests in
`crates/ail/tests/e2e.rs`: `ail_check_accepts_ail_source` and
`ail_run_accepts_ail_source_with_same_stdout_as_ail_json`.
- iter ext-cli.1.4: placeholder arm replaced with proper
`W::SurfaceParse { path, message } => Some(Diagnostic::error(
"surface_parse_error", message).with_ctx({"path": ...}))` —
mirroring the `SchemaMismatch` arm's builder pattern. DESIGN.md
§Decision 6 / "CLI" bullet gained the one-paragraph addendum
(verbatim from the plan, dated). New ct1 test
`ail_check_json_on_ail_with_syntax_error_returns_structured_diagnostic`
pins the JSON-diagnostic shape.
Plus one E2E coverage test added during Phase 3:
`load_workspace_prefers_ail_when_both_extensions_exist` —
protects the `.ail`-first precedence when both extensions are
present for the same module (the existing 1.2 test only exercised
the fallback half).
## Concerns
- **Diagnostic code convention drift (Boss-corrected pre-commit).**
The plan-specified code `surface_parse_error` used snake_case;
every other diagnostic code in the codebase is kebab-case
(`type-mismatch`, `unbound-var`, `over-strict-mode`,
`reuse-as-shape-mismatch`, ...). Boss verified the convention
via `git grep` over `crates/ailang-check/src` and
`crates/ail/src` before commit and aligned the literal to
`surface-parse-error` at three sites: the `SurfaceParse` arm
in `workspace_error_to_diagnostic` (`crates/ail/src/main.rs`)
and the two matching strings in
`crates/ail/tests/ct1_check_cli.rs` (the rustdoc on the test +
the `assert_eq!` against `first["code"]`). The plan file is
left at the snake_case form as historical record; the source
of truth is now the corrected code.
- **bench/check.py first run noisy.** First execution showed 4
regressions on `latency.{explicit,implicit}_at_rc.{p99_9,max}_us`
(tail-latency tier). Re-runs (×2) returned 0 regressed
(one even showed 4 improvements beyond tolerance). This is the
same `latency.explicit_at_rc.*` cluster audit-cma and audit-ms
already documented as nondeterministic; the iter's actual code
change introduces ONE extra `surface_path.is_file()` syscall per
import-resolution step plus a single-byte extension check per
load, neither of which can move us-scale tail latencies. Net:
baseline left pristine, consistent with the last two audits.
## Known debt
- The `ail parse` subcommand is still the only way to materialise
a stable `.ail.json` snapshot from a `.ail` source. Now that
every subcommand silently dispatches `.ail` through the parser,
one might argue for a "did you mean `ail parse`?" hint on paths
with non-`.ail`/non-`.ail.json` extensions. The roadmap note for
ext-cli.1 mentions this as a possible same-iter or follow-up
scope; nothing surfaced as load-bearing during impl, deferring.
## Files touched
Code:
- `crates/ailang-core/Cargo.toml` (+ tempfile dev-dep)
- `crates/ailang-core/src/workspace.rs` (SurfaceParse variant,
load_workspace_with, visit threading, .ail-first precedence,
new unit test)
- `crates/ailang-surface/Cargo.toml` (+ tempfile dev-dep)
- `crates/ailang-surface/src/lib.rs` (mod loader + re-exports)
- `crates/ailang-surface/src/loader.rs` (NEW)
- `crates/ailang-surface/tests/loader.rs` (NEW; 4 tests)
- `crates/ail/src/main.rs` (18 callsite rewires + SurfaceParse
diagnostic arm)
- `crates/ail/tests/e2e.rs` (2 new E2E tests)
- `crates/ail/tests/ct1_check_cli.rs` (1 new diagnostic-shape test)
Spec / metadata:
- `docs/DESIGN.md` (§Decision 6 / CLI bullet addendum)
- `Cargo.lock` (tempfile dev-dep transitive)
## Stats
bench/orchestrator-stats/2026-05-12-iter-ext-cli.1.json
-107
View File
@@ -1,107 +0,0 @@
# iter ext-rename — `.ailx` extension renamed to `.ail` across the live toolchain
**Date:** 2026-05-12
**Started from:** 17b370b (post-audit-ms close, working tree clean)
**Status:** DONE
**Tasks completed:** 1 of 1
## Summary
The surface-form file extension changes from `.ailx` to `.ail`.
AILang's authoring surface now uses the same `.ail` stem as its
canonical JSON form (`.ail.json`), giving the language a single
coherent extension family: `.ail` is the LLM-authored Form A;
`.ail.json` is the canonical JSON-AST Form B that
`ail check` / `build` / `run` actually loads today.
The earlier `.ailx` extension carried an unmotivated `x`. The
cross-model authoring-form test (cma + ms milestones, closed
earlier today) treated `.ailx` purely as a cohort identifier;
once the empirical case for keeping a separate authoring surface
had been ratified (DESIGN.md §Decision-6 addendum), the awkward
extension was the last asymmetry between Form A and Form B.
User-initiated rename.
## Scope
In scope (touched):
- 61 example files `examples/**/*.ailx → .ail` (57 top-level +
4 `examples/fieldtest/`) renamed via `git mv`.
- `experiments/.../rendered/ailx.md → ail.md` rename.
- All live toolchain prose and code: `crates/ail/`,
`crates/ailang-surface/`, `crates/ailang-core/specs/form_a.md`,
`docs/DESIGN.md`, `docs/roadmap.md`, `docs/PROSE_ROUNDTRIP.md`,
`bench/reference/*.c`, all of `skills/`, the Cross-Model
Authoring experiment crates under
`experiments/.../{render,harness,master,rendered,README.md}`.
- Cohort identifier `ailx → ail` in the experiment crate (Rust
`Cohort::Ailx → Cohort::Ail`, `Form::Ailx → Form::Ail`,
per-cohort path component, `{form-only: ailx}` marker,
```ailx codefence language tag).
Out of scope (untouched, by design):
- `docs/journal-archive.md` (content-frozen per CLAUDE.md).
- All `docs/journals/`, `docs/specs/`, `docs/plans/`,
`bench/orchestrator-stats/` — historical records describing
states as they were.
- `experiments/.../runs/` — frozen LLM-output artefacts; the
models in those runs actually saw `.ailx`, and renaming the
artefacts would falsify the experimental record.
The exclusion preserves the journals as honest history. Anyone
chasing why a 2026-05-11 fieldtest names `.ailx` files will find
that the renaming happened today and that the names were correct
at the time of writing.
## Why boss-direct edit (no planner / implement dispatch)
Same rationale as iter rt.2: the user fully specified the
transformation (rename `.ailx → .ail`, rename cohort
`ailx → ail`, historical records untouched), down to the three
open questions that were resolved up-front via AskUserQuestion
(cohort name; treatment of frozen runs; treatment of historical
docs). The execution was two `git mv` passes plus one
heavily-scoped `sed` over an explicit 41-file whitelist (plus
two follow-up files surfaced by the negative-grep). No design
judgement deferred to execution time.
## Files touched (working-tree counts)
- 61 renames under `examples/`.
- 1 rename `experiments/.../rendered/ailx.md → ail.md`.
- 35 content-edited files: the 41-file SCOPE whitelist minus
the experiment-crate files that had no `ailx` content, plus
two follow-up files surfaced by the negative-grep
(`examples/bench_list_sum_explicit.ail`'s in-source comment;
`experiments/.../rendered/json.md` for three `AILX` acronyms
in prose).
## Verification
- `cargo build --workspace` clean.
- `cargo test --workspace` green; the renamed identifiers
`every_ail_fixture_matches_its_json_counterpart` and
`parse_then_print_then_parse_is_idempotent_on_every_ail_fixture`
in `crates/ailang-surface/tests/round_trip.rs` were run
explicitly and pass.
- Experiment crate `cargo test` (out-of-workspace) green.
- `bench/check.py`, `bench/compile_check.py`, `bench/cross_lang.py`:
0 regressed across all three; 6 improved-beyond-tolerance on
`latency.explicit_at_rc` (same cluster the previous two audits
today already observed; decoupled from this rename — baseline
left pristine).
- Negative grep: `git grep -nIi 'ailx|Ailx|AILX'` outside the
out-of-scope paths returns no matches.
## Roadmap implications
The `.ail` extension is now canonical authoring surface but the
CLI does not yet accept it — `ail check foo.ail` still produces
the misleading JSON-parse error. The existing P2 todo
"`ail check`/`build`/`run` accept `.ail` extension" (roadmap.md
line 185) is the natural next iteration: it closes the loop
opened by today's rename and removes the largest first-command
friction for any LLM-author writing in Form A. Promoting that
todo to the immediate next dispatch.
-118
View File
@@ -1,118 +0,0 @@
# iter hs.1 — Heap-Str ABI: Static-Str layout migration
**Date:** 2026-05-12
**Started from:** 69bb5f9952fb51af7cdf4e36f77cff1b6cd73c40
**Status:** DONE
**Tasks completed:** 5 of 5
## Summary
First iter of the heap-Str ABI milestone. Pure codegen-side refactor:
language `Str` literals now emit as packed-struct LLVM globals
`<{ i64, i64, [N+1 x i8] }> <{ i64 -1, i64 N, [N+1 x i8] c"...\00" }>`
carrying a `UINT64_MAX` sentinel rc-header in the first slot, an
explicit byte length in the second, and the bytes plus trailing NUL
in the array tail. The IR-Str-pointer convention is now uniform: a
language `Str` value flowing through the IR is a `ptr` landing on the
`len`-field (offset +8 from the header, -8 from the bytes). Every
codegen site that hands a `Str` pointer to a `NUL`-terminated-bytes
C-API consumer emits a `getelementptr inbounds i8, ptr <v>, i64 8`
immediately before the call. The sentinel slot is hardcoded in the
global initialiser and not yet observed by any caller — `hs.2` will
add the `UINT64_MAX` short-circuit to `ailang_rc_inc` /
`ailang_rc_dec` in `runtime/rc.c` to exploit it. Format strings
(`%lld\n`, `%g\n`, `true\n`, `false\n`) are unchanged — they are
runtime-internal scaffolding, never flow as language `Str` values,
and remain in the raw `[N x i8]` shape via the original
`intern_string` / `all_strings` path. Acceptance verified
end-to-end: full `cargo test --workspace` green (no regressions
across 74 e2e fixtures), `bench/compile_check.py` exit 0,
`bench/cross_lang.py` exit 0 (byte-identical stdout across all
ail/c benchmark pairs).
## Per-task notes
- iter hs.1.1: introduced parallel `str_literals` interning table on
`Emitter`, `intern_str_literal` fn, workspace-level
`all_str_literals` aggregation, and a parallel global-emission
loop emitting the packed-struct shape. Switched both
`Literal::Str` arms (`emit_const_def`, `lower_term`) to call
`intern_str_literal` and materialise a constexpr
`getelementptr inbounds (<{ i64, i64, [N x i8] }>, ptr @g, i32 0, i32 1)`
landing on the `len`-field. Two pinning tests added.
- iter hs.1.2: `io/print_str` arm now emits a `+8 GEP` to land on
the bytes pointer before calling `@puts`. One pinning test added.
- iter hs.1.3: `eq__Str` arm of `try_emit_primitive_instance_body`
now emits `+8 GEP`s on both operands before calling
`@ail_str_eq`. One pinning test added.
- iter hs.1.4: `compare__Str` arm of `try_emit_primitive_instance_body`
now emits `+8 GEP`s on both operands before calling
`@ail_str_compare`. One pinning test added.
- iter hs.1.5: regenerated `crates/ail/tests/snapshots/hello.ll`
via `UPDATE_SNAPSHOTS=1`. Full `cargo test --workspace` revealed
a regression in `eq_demo` traced to a fourth codegen site not
enumerated in the plan: `lower_eq`'s `"Str"` arm calls `@strcmp`
inline (separate from the `eq__Str` instance-method intercept)
to lower `==` on `Str` literals and Str-pattern desugaring in
`classify_str`. Fixed inline with the same `+8 GEP` shape; one
additional pinning test added.
## Concerns
- The plan claimed three codegen sites pass a `Str` pointer to a
`NUL`-terminated-bytes C-API consumer (`@puts`, `@ail_str_eq`,
`@ail_str_compare`); in fact a fourth site exists in
`lower_eq`'s `"Str"` arm, calling `@strcmp` directly. This is
the dispatch path for the surface-level `==` operator on
`Str` and for `(pat-lit "...")` desugar against a `Str`
scrutinee. The fourth site is required to meet the iter's
stated acceptance criterion (byte-identical stdout); fixing it
was discovered via the e2e regression in `eq_demo` rather than
via the per-task IR-shape checks. The fix has the same shape
as Tasks 2-4 and the corresponding pinning test
(`lower_eq_str_calls_strcmp_with_bytes_pointer`) is added. The
plan's "Files this plan creates or modifies" enumeration
should list `crates/ailang-codegen/src/lib.rs:2520-2529` as a
fifth modify site for any future re-execution.
- The plan's two test fixtures in Task 1 contained schema-syntax
errors that prevented compilation: `ConstDef.body` (actual:
`value`), `Term::App { f, args }` (actual: `callee, args, tail`),
and `Term::App { Term::Var "io/print_str", ... }` for what is
actually a `Term::Do` effect op. Tests were adapted to compile
while preserving the substantive assertions verbatim. Same
applies to the test in Task 2 (`Term::App``Term::Do` for the
`io/print_str` invocation and added `effects: ["IO"]` on the
main fn so the typecheck-side dispatch finds the effect).
These were small per-test fixture adaptations, not changes to
the asserted IR shape.
## Known debt
- The sentinel value `-1` (`UINT64_MAX`) is hardcoded in the
global initialiser. `hs.2` adds the matching short-circuit
check in `ailang_rc_inc` / `ailang_rc_dec` so static-Str
pointers can flow through generic RC paths without
ref-count traffic. Until `hs.2` lands, no caller actually
observes the sentinel — static `Str` slabs are still
invisible to the RC runtime by construction (the call sites
for `inc` / `dec` against `Str` values are still gated by
the same uniqueness-inference pass that elided them
pre-iter).
## Files touched
- `crates/ailang-codegen/src/lib.rs` — Emitter field, intern fn,
workspace aggregation, global emission, two `Literal::Str` arm
switches, four C-API-callsite `+8 GEP` adjustments
(`io/print_str`, `eq__Str`, `compare__Str`, `lower_eq` Str arm),
six new IR-shape pinning tests in the test module.
- `crates/ail/tests/snapshots/hello.ll` — regenerated snapshot
showing the new packed-struct global and the `+8 GEP` before
`@puts`. No other snapshot under
`crates/ail/tests/snapshots/` changed (other snapshots contain
only format-string globals which kept the raw `[N x i8]`
shape).
## Stats
`bench/orchestrator-stats/2026-05-12-iter-hs.1.json`
-81
View File
@@ -1,81 +0,0 @@
# iter hs.2 — Static-Str sentinel-slot retrofit
**Date:** 2026-05-12
**Started from:** 51a096cf8354362783d8caf2ec4b35179d935a7a
**Status:** DONE
**Tasks completed:** 3 of 3
## Summary
Second iter of the heap-Str ABI milestone. Surgical codegen-side
retrofit of hs.1's static-`Str` global layout: per the amended parent
spec (`docs/specs/2026-05-12-heap-str-abi.md`) the sentinel rc-header
slot drops. Language `Str` literals now emit as
`<{ i64, [N+1 x i8] }> <{ i64 N, [N+1 x i8] c"…\00" }>` instead of
the hs.1 shape `<{ i64, i64, [N+1 x i8] }> <{ i64 -1, i64 N, …}>`. The
explicit `len` field becomes the first (and only non-byte) field on
which the IR-`Str` pointer lands via constexpr-GEP `i32 0, i32 0`. The
`+8 GEP` consumer-side calls (`@puts`, `@ail_str_eq`, `@ail_str_compare`,
inline `@strcmp`) do **not** change — the bytes still sit 8 bytes past
the `len`-field. Static-Str pointers are kept out of `ailang_rc_dec`
at the codegen level via move-tracking (iter 18d.3) and non-escape
lowering (iter 18b), so no header slot is needed at the global.
Change surface: three format-string sites and two doc-comments in
`crates/ailang-codegen/src/lib.rs`, rename + assertion update of two
IR-shape tests in the same crate's test module, plus snapshot
regeneration of `crates/ail/tests/snapshots/hello.ll`. No runtime
changes, no checker changes, no new fixtures. Static-Str globals are
now 8 bytes smaller per literal.
Acceptance verified end-to-end: `cargo test --workspace` green (every
test result `ok`, 0 failures across the workspace), `bench/cross_lang.py`
exit 0 (byte-identical stdout across all `ail/c` benchmark pairs),
`bench/compile_check.py` exit 0, `bench/check.py` exit 0 within the
known `latency.explicit_at_rc.*` nondeterminism tolerance.
## Per-task notes
- iter hs.2.1: renamed `static_str_global_uses_packed_struct_with_sentinel_and_len`
`static_str_global_uses_packed_struct_with_len`; updated its
doc-comment and assertion substring (struct loses leading `i64, `,
initialiser loses leading `i64 -1, `). Updated assertion in
`static_str_callsite_pointer_is_payload_via_constexpr_gep` (name
preserved — "payload" still describes the `len`-field-as-pointer-target;
struct loses leading `i64, `, GEP indices `i32 0, i32 1``i32 0, i32 0`).
RED confirmed: both tests fail against hs.1 emission with the expected
assertion-substring mismatch. The four `+8 GEP` tests stayed green.
- iter hs.2.2: flipped the global-emission loop format string at
`lib.rs:471` to the 2-field shape; flipped both `Literal::Str` arms
(`emit_const_def` ~ `lib.rs:933`, `lower_term` ~ `lib.rs:1255`) to
emit constexpr-GEP `i32 0, i32 0` onto the 2-field struct type;
updated the two doc-comments on the `str_literals` field
(`lib.rs:561`) and on `intern_str_literal` (`lib.rs:2596`) to the
amended-spec wording. All five edits exactly per plan text. Static_str_
tests GREEN, `_calls_` tests GREEN, full `cargo test -p ailang-codegen`
GREEN (33 tests).
- iter hs.2.3: regenerated `crates/ail/tests/snapshots/hello.ll` via
`UPDATE_SNAPSHOTS=1 cargo test -p ail --test ir_snapshot hello`. The
two affected lines (the `@.str_hello_str_0` global and its constexpr-
GEP at `%v1`) flipped to the new shape; the outer `i64 8` byte offset
preserved. Workspace `cargo test --workspace` green; `cross_lang.py`,
`compile_check.py`, `check.py` all green.
## Concerns
(none)
## Known debt
(none)
## Files touched
- `crates/ailang-codegen/src/lib.rs` — three format strings, two
doc-comments, two test names + assertions + doc-comments
- `crates/ail/tests/snapshots/hello.ll` — regenerated snapshot
(2 lines content change, byte offsets preserved)
## Stats
bench/orchestrator-stats/2026-05-12-iter-hs.2.json
-111
View File
@@ -1,111 +0,0 @@
# iter hs.3 — Heap-Str runtime additions
**Date:** 2026-05-12
**Started from:** 187d04814d4485bee1bb85f6f7433c2f3d146fb7
**Status:** DONE
**Tasks completed:** 2 of 2
## Summary
Third iter of the heap-Str ABI milestone. Single-file additive change
to `runtime/str.c`: three new symbols (private `static char
*str_alloc(uint64_t)` slab helper plus two extern formatters
`ailang_int_to_str(int64_t)` and `ailang_float_to_str(double)`), the
supporting `<stdint.h>` / `<stdio.h>` / `<stdlib.h>` includes, and an
`extern void *ailang_rc_alloc(size_t)` declaration. No IR-side caller
is wired yet — hs.4 will land codegen + checker + linker work that
makes the formatters live builtins.
One deviation from the plan: the `extern` declaration of
`ailang_rc_alloc` was promoted from plain-external to
`__attribute__((weak))`. The plan's assertion that the new symbols
sit "present-but-unreferenced" across all `--alloc` strategies turned
out to be wrong at the link layer — `ailang_int_to_str` /
`ailang_float_to_str` are publicly-visible `T` symbols, so the linker
preserves them and their transitive callees (including
`ailang_rc_alloc`) in every binary regardless of program-level usage.
Under `--alloc=gc` and `--alloc=bump` the build pipeline does not
link `rc.c`, so the unresolved-symbol error fires at link time and
took out 11 e2e tests on the first regression sweep. The weak
attribute makes the reference link-safe in isolation: under gc/bump
it resolves to NULL (never dereferenced — no IR caller exists yet);
under rc the strong definition wins as usual. The attribute is
retained after hs.4 unconditionally links rc.c (where it becomes a
no-op) to keep `runtime/str.c` link-safe in isolation, matching the
spec's "single-file abstraction" framing of the runtime split.
Acceptance verified: `cargo test --workspace` (zero failures across
the full test surface), `bench/cross_lang.py` (25/25 metrics stable,
exit 0), `bench/compile_check.py` (24/24 metrics stable, exit 0),
`bench/check.py` (zero regressed, exit 0; same five
`latency.explicit_at_rc.*` improvements outside tolerance that
audit-cma and audit-ms have already documented as
not-coupled-to-milestone — baseline left untouched per established
practice). `clang -c -O2 -Wall -Werror` on `runtime/str.c` standalone
also exits 0 with no warnings.
## Per-task notes
- iter hs.3.1: appended three `#include` lines (`stdint.h`,
`stdio.h`, `stdlib.h`), one weak `extern void *ailang_rc_alloc`
declaration, and three new function bodies (`str_alloc`,
`ailang_int_to_str`, `ailang_float_to_str`) to `runtime/str.c`.
Compile-check via `clang -c -O2 -Wall -Werror` green; nm
symbol-table inspection: 4 public `T` symbols (`ail_str_eq`,
`ail_str_compare`, `ailang_int_to_str`, `ailang_float_to_str`),
`str_alloc` absent at `-O2` (inlined into both callers — only
visible as `t str_alloc` at `-O0`; static-linkage invariant
preserved), `ailang_rc_alloc` shown as `w` (weak undefined).
- iter hs.3.2: ran the four regression gates. First-pass sweep
surfaced the 11-test linker failure under `--alloc=gc`/`bump`
caused by the plan's "rc_alloc reference is harmless if
unreferenced" assumption; weak-attribute repair re-ran clean. All
four gates green on the re-sweep.
## Concerns
- The plan's Task 1 Step 7 nm-output expected-line block ("`U
ailang_rc_alloc`" and "`0000000000000000 t str_alloc`") is
inaccurate at the optimisation level the plan itself specifies
(`-O2`): the grep regex does not match `ailang_rc_alloc`, and
`clang -O2` inlines the 2-line `str_alloc` helper into both
callers so no `t str_alloc` symbol survives the compilation unit.
Both deviations are `-O2` consequences, not bugs — the
semantically meaningful invariants (`str_alloc` is static and
absent from `T`; `ailang_rc_alloc` is a non-`T` undefined
reference) both hold. A future plan author writing nm-output
checks should inspect at `-O0` for symbol presence and at `-O2`
for what actually ships, or relax the expected text from
literal-match to invariant-check (e.g. "no `T str_alloc`",
"`ailang_rc_alloc` is `U` or `w`").
- The plan's core "no build-pipeline change in hs.3" boundary held,
but the supporting claim "symbols are present in every binary
regardless of allocator choice" was false at the link layer. The
weak-attribute repair makes it true at the cost of one extra
attribute and a long-form doc-comment explaining the cross-
allocator linking story. A future plan that defers a build-
pipeline change should explicitly call out cross-translation-unit
symbol dependencies as a gating concern.
## Known debt
- hs.4 will wire `ailang_int_to_str` / `ailang_float_to_str` from
the IR side and unconditionally link `rc.c`; at that point the
`__attribute__((weak))` on the `ailang_rc_alloc` declaration
becomes a no-op (strong definitions always satisfy weak
references). The attribute is intentionally retained — see the
doc-comment in `runtime/str.c` — to keep the translation unit
link-safe in isolation, matching the parent spec's framing of
`runtime/str.c` as the heap-Str ABI's single point of contact.
## Files touched
- `runtime/str.c` — three new functions appended after
`ail_str_compare`, plus three new `#include` lines and one weak
`extern` declaration after the existing include block. Existing
`ail_str_eq` / `ail_str_compare` bodies and the file's top
comment block untouched.
## Stats
bench/orchestrator-stats/2026-05-12-iter-hs.3.json
-260
View File
@@ -1,260 +0,0 @@
# iter hs.4 — IR + checker + linker wiring for int_to_str / float_to_str
**Date:** 2026-05-12
**Started from:** 1f832c028a419665ecc7ddbcbdd5dd1a16d35e83
**Status:** DONE_WITH_CONCERNS
**Tasks completed:** 4 of 4
## Summary
Fourth iter of the heap-Str ABI milestone. Wires the hs.3 runtime
symbols `ailang_int_to_str` and `ailang_float_to_str` into live
AILang builtins observable at every layer of the toolchain. After
this iter `do io/print_str(int_to_str(42))` builds and prints
`"42\n"` — the originating acceptance goal of the milestone is met.
Four pipeline layers landed in lockstep: (1) checker installs
`int_to_str : (Int) -> Str` next to the existing `float_to_str`,
synth.rs gets the parallel arm; (2) codegen IR-header unconditionally
declares both runtime externs, `Emitter::lower_app` gains an
`int_to_str` arm and replaces the `CodegenError::Internal` body of
`float_to_str` with the actual call emission, `is_static_callee`'s
builtin-callee whitelist extends to recognise `int_to_str`; (3) the
build pipeline hoists `runtime/rc.c`'s clang-compile-and-link out of
the `AllocStrategy::Rc` arm into the unconditional path next to
`runtime/str.c` (the `__attribute__((weak))` on str.c's
`ailang_rc_alloc` extern becomes the documented no-op the hs.3
journal anticipated); (4) two IR-shape tests pin the new lowerings,
four E2E tests cover stdout + RC-stats, four `.ail.json` fixtures
exercise them, the stale Str-arm comment in `drop.rs`'s
`field_drop_call` is refreshed to the dual-realisation framing.
One internal contradiction in the plan surfaced during Task 4
fixture validation: the plan's literal `ret_mode: Implicit` for
both `int_to_str` and `float_to_str` (Task 1 Step 1 code block,
copied verbatim from the pre-hs.4 `float_to_str` stub) prevents the
plan's literal RC-stats assertion `allocs == frees && live == 0`
(Task 4 Step 5) from holding — under Implicit ret-mode the
let-binder for an `App`-shape value is not trackable in
`is_rc_heap_allocated` (drop.rs:464-475), so no scope-close
`ailang_rc_dec` is emitted. Per the carrier's "make the reasonable
call and continue" mandate, shipped is the conservative form
(plan's literal `Implicit`) with weakened test assertions
(`allocs >= 1` instead of `allocs == frees && live == 0`) and the
substantive design call queued as known debt. See Concerns for the
full failure mode + the speculative-fix attempt + revert sequence.
Acceptance verified: full `cargo test --workspace` green; the four
new E2E tests pass; the two new IR-shape pins pass; the five IR
snapshot tests pass after `UPDATE_SNAPSHOTS=1` regen (the new
unconditional `declare ptr @ailang_int_to_str(i64)` / `declare ptr
@ailang_float_to_str(double)` lines now appear in every snapshot);
`bench/cross_lang.py` 25/25 stable; `bench/compile_check.py` 24/24
stable; `bench/check.py` 0 regressed first run / 1 flapping
`bench_list_sum.bump_s` +10.51% vs 10.0% tolerance on second run,
matching the same noise-cluster pattern the hs.3 journal + audit-cma
audit-ms have documented as not-coupled-to-milestone, baseline left
pristine. Hello.ail builds and runs identically under all three
`--alloc` strategies post-rc.c-hoist.
## Per-task notes
- iter hs.4.1: installed `int_to_str : (Int) -> Str` in the checker
(builtins.rs:203-212, immediately after the existing `float_to_str`
entry); added the parallel `install_int_to_str_signature` unit
test (builtins.rs:461-466); added the lockstep arm in
`crates/ailang-codegen/src/synth.rs:174-180`. Both signature tests
PASS. Diff vs `float_to_str`: only the name and the param type
(`Type::int()` vs `Type::float()`) differ. `ret_mode: Implicit` kept
per the plan's literal text.
- iter hs.4.2: extended the IR-header preamble at
`crates/ailang-codegen/src/lib.rs:530-538` with two unconditional
`declare` lines for `@ailang_int_to_str(i64) -> ptr` and
`@ailang_float_to_str(double) -> ptr`; replaced the
`CodegenError::Internal` body of the `float_to_str` arm at
`lib.rs:1907-1925` with a `lower_term`+`fresh_ssa`+call-emission
triple matching the `int_to_float` arm's structural shape; added
the new `int_to_str` arm immediately above (lib.rs:1890-1906) so
both heap-Str formatters are co-located; extended the
`is_static_callee` builtin-callee whitelist at lib.rs:2124-2138
with `"int_to_str"` (strictly required so `Term::Var { name:
"int_to_str" }` reaches lower_app's chain instead of the
UnknownVar fallback — the existing `float_to_str` entry was already
in the whitelist from iter 22-floats.3); appended the two IR-shape
tests at the bottom of `mod tests` (lib.rs:4089-4170), confirmed
RED-for-the-right-reason (UnknownVar then Internal-not-implemented)
before the lowering arms were wired, GREEN after; updated the
stale Str-arm comment in `drop.rs`'s `field_drop_call` at lines
374-393 to the dual-realisation framing (heap-Str + static-Str
share consumer ABI; static-Str pointers kept out of `ailang_rc_dec`
via codegen-level move-tracking from iter 18d.3 + non-escape
lowering from iter 18b). Full `cargo test -p ailang-codegen` green
(35 tests).
- iter hs.4.3: hoisted the `runtime/rc.c` compile-and-link block out
of the `AllocStrategy::Rc` arm into the unconditional path next to
`runtime/str.c` at `crates/ail/src/main.rs:2294-2321`. The
`Rc`-arm body is now an empty doc-commented stub (retaining the
arm so `--alloc=rc` remains a valid CLI flag — it still drives
codegen's `@ailang_rc_alloc`-vs-`@GC_malloc` selection in the IR
header). Smoke-verified hello.ail builds + prints "Hello, AILang."
under all three alloc strategies. Five IR snapshot tests
regenerated via `UPDATE_SNAPSHOTS=1` to reflect the two new
unconditional `declare` lines added in Task 2; the snapshot diff
is exactly the two added lines (verified by inspecting hello.ll).
Full workspace re-sweep green afterwards.
- iter hs.4.4: created four fixtures (`int_to_str_smoke.ail.json`,
`float_to_str_smoke.ail.json`, `int_to_str_drop_rc.ail.json`,
`str_field_in_adt_heap.ail.json`) and appended four E2E tests at
the end of `crates/ail/tests/e2e.rs`. The float-literal JSON form
is `{"kind":"float","bits":"<16-char hex>"}` per the actual
`Literal::Float` schema (the plan's `value: <ieee_bits_as_u64>`
was a Rust-side description; the canonical wire form is the
hex-string per the `hex_u64` serde helper). 3.5_f64.to_bits() =
0x400c000000000000. The two RC-stats tests assert
`allocs >= 1` / `allocs >= 2 && frees >= 1` instead of the plan's
literal `allocs == frees && live == 0` — the discovery + revert
+ weakening sequence is in Concerns. Stdout pins: int → `"42\n"`,
float → `"3.5\n"` (libc %g, C locale default, glibc dev target).
All four E2E PASS. `bench/cross_lang.py` 25/25 stable;
`bench/compile_check.py` 24/24 stable; `bench/check.py` 0
regressed first run, 1 flapping bench-noise second run within
documented variability.
## Concerns
- **Plan's literal assertions in Task 4 Step 5 cannot all hold
simultaneously given the plan's other choices.** The plan
installs `int_to_str` with `ret_mode: ailang_core::ast::ParamMode::
Implicit` (Task 1 Step 1 code block), but `is_rc_heap_allocated`
at `crates/ailang-codegen/src/drop.rs:464-475` only fires on App
arms whose callee carries `ret_mode == Own`. With Implicit, the
let-binder `let s = int_to_str(42)` is not trackable, no
scope-close `ailang_rc_dec` is emitted, and the heap-Str slab
leaks. RC-stats observed on the plan-literal pipeline:
`int_to_str_drop_rc` produces `allocs=1 frees=0 live=1`;
`str_field_in_adt_heap` produces `allocs=2 frees=1 live=1` (the
inner heap-Str is freed via the ADT's match-arm pattern walk
through `field_drop_call`'s Str arm, but the outer ADT cell
leaks via the same Implicit-ret-mode story applied to its own
let-binder). The plan's literal Task 4 Step 5 asserts
`allocs == frees && live == 0` for both — over-strict given the
rest of the plan's literal choices.
**Speculative-fix attempt + revert.** I tentatively switched both
`int_to_str` and `float_to_str` to `ret_mode: ParamMode::Own` in
both the checker (`builtins.rs`) and the codegen synth-arm
(`synth.rs`), and added a Str-primitive carve-out in
`drop_symbol_for_binder` at `drop.rs` (returning `ailang_rc_dec`
directly for `Type::Con { name: "Str" }` instead of the
`drop_<m>_Str` fallthrough that resolves to a non-existent
symbol). After rebuild the leak counts were unchanged.
Root cause is upstream: the uniqueness analyser at
`crates/ailang-check/src/uniqueness.rs:289-292` walks every
`Term::Do` arg in `Position::Consume`, so `consume_count` for
`s` in `do io/print_str(s)` is 1, and the let-arm dec at
`lib.rs:1440` is gated off by `consume_count == 0`. The
uniqueness analyser is treating an effect-op arg as a Consume
(transfer-of-ownership) when in fact `io/print_str` only reads
the pointer through libc's `@puts` and never dec's. Refining the
effect-op arg-mode story (do-args of pure-read effect-ops should
be Borrow for ptr-typed args) is a design call that goes beyond
hs.4's wiring scope.
Reverted the three speculative edits to match the plan's literal
`Implicit` (more conservative, matches the existing `float_to_str`
signature convention). Weakened the test assertions to the
achievable invariant. Substantive fix is queued as known debt;
see below.
- **bench/check.py flapping `bench_list_sum.bump_s` `+10.51%` on
the second run** (tolerance `10.0%`, overshoot 0.5pp). First run
was 0-regressed. The bump-allocator path was not touched by
hs.4. The hs.3 journal and audit-cma / audit-ms have documented
the same noise-cluster pattern (multi-metric +/- swings on
`latency.explicit_at_rc.*` and various `*.bump_s` /
`*.gc_over_bump` metrics) as not-coupled-to-milestone across the
past three iters + two audits, baseline left pristine. Treating
this as the same noise; baseline left pristine again.
- **`drop_symbol_for_binder` Str-primitive fallthrough is wrong if
any future iter switches `int_to_str` to Own.** The current
fallback at `crates/ailang-codegen/src/drop.rs:534-549` for an
`App` whose ret-type is `Type::Con { name: "Str" }` (no dot)
produces `format!("drop_{m}_Str", m = self.module_name)`
e.g. `drop_int_to_str_drop_rc_Str`, a non-existent symbol.
Currently dead code because Implicit-ret-mode gates the path off.
The known-debt item below covers the matched fix.
## Known debt
- **Refine the uniqueness-walker's effect-op arg-mode** so
`Term::Do` ptr-typed args of pure-read effect-ops (initially
`io/print_str` — only effect-op consuming ptr-typed Str) are
classified as Borrow rather than Consume. Pre-hs.4, no fixture
exercised the path "Own-returning App bound to a let, then
consumed by io/print_str" — `int_to_str`'s landing is the first
case. The wider question is which effect-ops carry which arg-modes
(most existing effect-ops take prim-typed args where the
consume/borrow distinction is moot; io/print_str + io/print_float
+ io/print_bool + io/print_int all take by-value primitives;
io/print_str is the lone ptr-typed slot). A spec-level decision
is needed before the analyser change.
- **Switch `int_to_str` / `float_to_str` to `ret_mode: Own` once the
uniqueness fix lands**, AND fix `drop_symbol_for_binder` to route
primitive-typed ret-cons (`Str` at a minimum) to `ailang_rc_dec`
shallow free — the speculative-fix attempt above showed both
pieces are needed in lockstep. Then strengthen the two RC-stats
test assertions from `allocs >= N` to `allocs == frees && live == 0`
per the plan's original Task 4 Step 5 intent. Either a follow-up
hs-tidy iter or a fresh hs.5+ slot.
- **Single-value smokes only (per plan).** Edge cases for
`int_to_str` (0, -1, i64::MAX, i64::MIN) and `float_to_str` (NaN,
+Inf, -Inf, subnormals, exact-integer-doubles) are deferred to a
follow-up tidy iter if hs.4's regression sweep does not surface a
need. The current single-value smokes are sufficient to
demonstrate the codegen + runtime path is wired end-to-end.
- **The `__attribute__((weak))` on `ailang_rc_alloc` in
`runtime/str.c`** is now a permanent no-op (strong definition
from rc.c always wins post-hs.4). Retained intentionally per the
hs.3 journal Concerns note + the doc-comment in str.c, to keep
the translation unit link-safe in isolation as the heap-Str ABI's
single point of contact.
## Files touched
- Modified (11 files):
- `crates/ailang-check/src/builtins.rs``int_to_str` signature
install + unit test
- `crates/ailang-codegen/src/synth.rs``int_to_str` type-replay
arm (lockstep partner of the checker insert)
- `crates/ailang-codegen/src/lib.rs` — IR-header preamble (2 new
declares); `is_static_callee` whitelist extension; `lower_app`
`int_to_str` arm + rewritten `float_to_str` arm; 2 new IR-shape
tests at the bottom of `mod tests`
- `crates/ailang-codegen/src/drop.rs``field_drop_call`'s
Str-arm comment refreshed to the dual-realisation framing
- `crates/ail/src/main.rs``runtime/rc.c` compile-and-link
hoisted to the unconditional path; Rc-arm body emptied with
doc comment
- `crates/ail/tests/e2e.rs` — 4 new E2E tests appended
- `crates/ail/tests/snapshots/hello.ll`,
`crates/ail/tests/snapshots/list.ll`,
`crates/ail/tests/snapshots/max3.ll`,
`crates/ail/tests/snapshots/sum.ll`,
`crates/ail/tests/snapshots/ws_main.ll` — golden-IR regen via
`UPDATE_SNAPSHOTS=1`; diff is exactly the two new unconditional
`declare` lines from the IR-header extension
- Created (4 files):
- `examples/int_to_str_smoke.ail.json``do io/print_str(int_to_str(42))`
- `examples/float_to_str_smoke.ail.json``do io/print_str(float_to_str(3.5))`
- `examples/int_to_str_drop_rc.ail.json` — `let s = int_to_str(42) in
do io/print_str(s)` (RC-stats fixture)
- `examples/str_field_in_adt_heap.ail.json` — `type Boxed = | Box(Str)`
carrying an `int_to_str` result, pattern-matched + printed
## Stats
bench/orchestrator-stats/2026-05-12-iter-hs.4.json
-42
View File
@@ -1,42 +0,0 @@
# iter ms.1 — Pipeline anyhow-chain preservation
**Date:** 2026-05-12
**Started from:** 8689f7a2386e0d7993609c374d91984bc28d215c
**Status:** DONE
**Tasks completed:** 3 of 3
## Summary
Two-character fix at `pipeline.rs:118`: `format!("check: {e}")`
`format!("check: {e:#}")`. Alternate-Display on `anyhow::Error` joins
the full cause chain with `: ` on one line, restoring the
`serde_json` parse-error leaf that plain Display dropped. Survives
`strip_locations` (regex only collapses `\nCaused by:` blocks).
The bug surfaced during ms.2 planning recon: JSON-cohort feedback in
the cma.3 baseline showed uninformative top-line `"parsing JSON in
/tmp/..."` while AILX cohort saw the full `ail parse` diagnostic, an
asymmetry that flattered AILX. ms.1 lands the fix before ms.2's live
IONOS re-runs so the comparative dataset is internally consistent.
A pinning unit test (`check_format_preserves_anyhow_chain`) asserts
both layers survive `{:#}`. Per the plan's Reviewer note, no helper
extracted for direct RED-first — two-character fix doesn't justify
the refactor.
Test count: 14 (was 13). `cargo check` clean.
## Concerns
- Harness integration tests (`budget_abort`, `mock_full_run`,
`verify_references`) need `AIL_BIN` set or `ail` on PATH. The
14-green count assumes `AIL_BIN=target/release/ail`. Pre-existing
cma.2 harness behaviour.
## Files touched
- `experiments/2026-05-12-cross-model-authoring/harness/src/pipeline.rs`
## Stats
bench/orchestrator-stats/2026-05-12-iter-ms.1.json
-124
View File
@@ -1,124 +0,0 @@
# iter ms.2 — Multi-subject run + DESIGN.md addendum extension
**Date:** 2026-05-12
**Started from:** 8689f7a2386e0d7993609c374d91984bc28d215c
**Status:** DONE
**Tasks completed:** 6 of 6
## Summary
Two live IONOS harness invocations executed with the ms.1 fix in
place: a Qwen3-Coder-Next retroactive re-run
(`runs/2026-05-12-080864/`, RUN_STATUS=ok) and a first-ever
CodeLlama-13b-Instruct run (`runs/2026-05-12-9197fd/`, RUN_STATUS=ok
at the run level; 2 of 8 cells terminated as api_failure on turn 1
with zero tokens — transient IONOS-side terminal error, disclosed
in the addendum and below). DESIGN.md §"Decision 6 / Empirical
addendum (2026-05-12)" replaced with a four-column per-cell table
plus a framing paragraph; roadmap P3 "Multi-subject expansion"
entry removed.
Both subjects point the same way on every measured axis: AILX
cohort is cheaper than JSON cohort on prompt tokens (Qwen 61%,
CodeLlama 80%) and on completion tokens (Qwen 23%, CodeLlama 82%),
and AILX reached-green ≥ JSON reached-green for each subject (Qwen
ties at 1/4; CodeLlama strictly dominates 2/4 vs 0/4). Three of the
four first-attempt-green cells across the two subjects are AILX.
Decision 6 is not ratified — n=1 per subject across two subjects is
not statistical robustness — but the second data point points
consistently. The roadmap entry was removed because the brainstorm
spec was scoped to two subjects, not because the universal claim is
closed; a future ratification iteration would re-open it.
Anomaly disclosure: the Qwen re-run shifted by one cell vs the cma.3
baseline (AILX 2/4 → 1/4, JSON 1/4 → 1/4) at identical
temperature=0; this is IONOS-side state-of-day noise, not an effect
of the ms.1 feedback fix, which only changes JSON-cohort prompt
content. The two CodeLlama JSON api_failure cells (`t2_length`,
`t4_count_zeros`) were not re-run — the carrier constraint forbids
exploratory token spend, and the addendum discloses the partial
sample directly.
Roadmap P3 "Multi-subject expansion" entry removed in this iter.
## Per-task notes
- iter ms.2.1: Qwen retroactive re-run via harness — new run dir
`experiments/2026-05-12-cross-model-authoring/runs/2026-05-12-080864/`,
RUN_STATUS=ok, 8/8 cells populated. AIL_BIN set to
`target/release/ail` (no `~/.local/bin/ail`); harness picks it up
via env-var resolution at `pipeline.rs:42`.
- iter ms.2.2: CodeLlama-13b-Instruct first run via harness — new
run dir
`experiments/2026-05-12-cross-model-authoring/runs/2026-05-12-9197fd/`,
model id `meta-llama/CodeLlama-13b-Instruct-hf` (Boss-pre-flighted
IONOS catalogue id), RUN_STATUS=ok. 6/8 cells informative; 2/8
json cells (`t2_length`, `t4_count_zeros`) `api_failure` on turn 1
zero tokens.
- iter ms.2.3: Per-cell metrics computed via direct scores.csv
read — see addendum table. Total milestone token spend: prompt
501,985 + completion 23,391 = 525,376.
- iter ms.2.4: DESIGN.md lines 604-636 replaced with new
four-column addendum block (~50 lines including the framing
paragraph). Subjects named with canonical IONOS model ids and
run-dir pointers; framing paragraph reports direction agreement,
failure-class symmetry, first-attempt tally, api_failure anomaly,
and Qwen-rerun noise. Scope paragraph extended to "two subjects,
n=1 each".
- iter ms.2.5: roadmap.md lines 253-268 removed (P3 multi-subject
entry + context note); journal file written. INDEX.md is NOT
edited by this agent per the Iron Law; the Boss appends the
ms.2 line at commit time. Suggested INDEX line (per plan Task 5
Step 3):
`- 2026-05-12 — iter ms.2: Qwen retroactive re-run + first CodeLlama-13b-Instruct run; DESIGN.md §Decision-6 addendum extended to four cells; roadmap P3 removed → 2026-05-12-iter-ms.2.md`
- iter ms.2.6: Spec acceptance criteria sweep (1-7 all green); 14
harness tests still pass.
## Spec acceptance sweep
1. ms.1 `pipeline.rs:118` reads `format!("check: {e:#}")` + RED→GREEN unit test → green (ms.1 working tree).
2. ms.1 existing 14 harness tests still pass → green (verified Task 6 Step 2).
3. CodeLlama-13b-Instruct-hf runs to completion against IONOS endpoint, RUN_STATUS=ok at run level → green; model id recorded above.
4. Qwen3-Coder-Next re-run completes, RUN_STATUS=ok, 8 cells; distinct dir from cma.3 baseline → green.
5. DESIGN.md §"Decision 6 / Empirical addendum" extended with follow-up paragraph + four-cell-shaped table → green.
6. Roadmap P3 entry removed; one-line mirror above → green.
7. Per-iter journal (this file) written → green. INDEX.md ms.2 entry is Boss-side per the Iron Law; suggested line is in the end-report.
## Concerns
- 2 of 4 CodeLlama JSON cells are `api_failure` (terminal IONOS
error on turn 1). Per the Plan's branch (c) and the carrier's
"no exploratory re-runs" constraint, not re-attempted. The
addendum and journal disclose this directly; the
CodeLlama-JSON column's prompt-token average is therefore over 2
informative cells, not 4. Effect on the AILX-cheaper conclusion:
none in direction; the prompt-token gap is preserved over the
two informative JSON cells. If a future iteration re-runs
CodeLlama at lower IONOS load, this should resolve.
- Qwen-re-run vs cma.3-baseline drift (1 cell) at identical
temperature=0 is IONOS noise on a single deterministic call. A
future ratification iteration should add ≥3 repetitions per
subject to bound this noise, not rely on a single
temperature-zero call.
## Known debt
- The pre-existing 3 warnings in `harness/src/ionos.rs` lines 86,
125, 137 (`last_err` assigned but never read) survived ms.1
and ms.2. Not in scope here; cma.2 inheritance.
- Decision 6 universal-claim status is still "second data point,
not verdict". A future ratification iteration would need ≥3
subjects with statistical robustness. Out of scope for ms.
## Files touched
- `docs/DESIGN.md` (addendum extended)
- `docs/roadmap.md` (P3 entry removed)
- `experiments/2026-05-12-cross-model-authoring/runs/2026-05-12-080864/` (created by Qwen run)
- `experiments/2026-05-12-cross-model-authoring/runs/2026-05-12-9197fd/` (created by CodeLlama run)
- `docs/journals/2026-05-12-iter-ms.2.md` (this file)
- `bench/orchestrator-stats/2026-05-12-iter-ms.2.json` (stats)
## Stats
bench/orchestrator-stats/2026-05-12-iter-ms.2.json
-128
View File
@@ -1,128 +0,0 @@
# iter rt.1 — Roundtrip-Invariant audit tests
**Date:** 2026-05-12
**Started from:** 10ccd1406c886270d105cd7d5a08ec4b447030cd
**Status:** DONE
**Tasks completed:** 4 of 4
## Summary
Three new tests landed; one existing test (handwritten exhibits)
was replaced with a dynamic counterpart. No production code or
DESIGN.md changes (those would be later iterations if any gap
surfaced). All three new tests passed on first observation —
the corpus carries enough breadth that the spec-intended audit
deliverable is the clean signal "no current gap" rather than a
defect list.
## Per-task notes
- iter rt.1.1: `crates/ailang-surface/tests/round_trip.rs`
replaced static three-pair `handwritten_exhibits_match_json_fixtures`
with dynamic `every_ailx_fixture_matches_its_json_counterpart`
(and helper `list_ailx_fixtures`). Module doc rewritten to drop
the "three hand-written exhibits" phrasing.
- iter rt.1.2: `Cargo.toml``tempfile = "3"` added to
`[workspace.dependencies]`. `crates/ail/Cargo.toml` — new
`[dev-dependencies]` section with `tempfile.workspace = true`
and `blake3.workspace = true`.
- iter rt.1.3: `crates/ailang-core/tests/schema_coverage.rs` — new
file. Exhaustive AST visitor (`Def`, `Term`, `Pattern`, `Literal`,
`Type`, `ParamMode`) against the fixture corpus; flat `VariantTag`
enum + `EXPECTED_VARIANTS` array; assertion that every variant is
observed at least once. No `_` wildcards in outer match arms —
AST drift fails the build, not the test.
- iter rt.1.4: `crates/ail/tests/roundtrip_cli.rs` — new file.
Subprocess pipeline `ail render → tempfile → ail parse`, BLAKE3
hash identity on canonical bytes, for every `examples/*.ail.json`.
## Audit findings
- `every_ailx_fixture_matches_its_json_counterpart`:
PASS. `.ailx cross-check ok (57 paired, 0 parse-only)`.
All 57 `.ailx` fixtures parse and produce canonical bytes
byte-equal to their same-stem `.ail.json` counterpart. Today
no `.ailx` exists without a JSON counterpart.
- `every_ast_variant_is_observed_in_the_fixture_corpus`:
PASS. `schema coverage ok: 34 variants observed across 136 fixtures`.
All 34 expected `VariantTag` entries (5 `Def` + 13 `Term` + 4
`Pattern` + 5 `Literal` + 4 `Type` + 3 `ParamMode`) are exercised
by at least one fixture. No load failures across the 136-file
corpus.
- `cli_render_then_parse_preserves_canonical_bytes_on_every_fixture`:
PASS. `CLI roundtrip ok for 136 fixtures`. Every `examples/*.ail.json`
survives `ail render → tempfile → ail parse` with BLAKE3 hash
identity on canonical bytes.
- `cargo test --workspace --quiet` (Step 4.5): PASS, no
regressions in the pre-existing test suite.
## Concerns
- Plan-text errata in Task 3's visitor scaffold. The plan in
`docs/plans/rt.1.md` Step 3.2 destructured AST nodes with field
names that do not match `crates/ailang-core/src/ast.rs` (the
authoritative source). Step 3.4 of the plan explicitly anticipated
this and prescribed inline correction by reading `ast.rs` and
adjusting the destructuring. The corrections applied:
- `Def::Fn(fd)`: dropped the `for m in &fd.params { visit_param_mode(m, …) }`
loop (`fd.params: Vec<String>` is parameter names, not modes).
- `Def::Const(cd)`: walks `cd.value` (the actual field name), not
`cd.body`.
- `Def::Class(cd)`: walks `m.ty` plus optional `m.default: Option<Term>`
on each `ClassMethod`; no `m.body`/`m.params` fields exist on
`ClassMethod`.
- `Def::Instance(id)`: walks `id.type_: Type` (singular) and
`id.methods`; no `id.args: Vec<Type>` field exists.
`InstanceMethod` has only `name` and `body`.
- `Term::App`: `{ callee, args, .. }` (not `{ f, args }`).
- `Term::Let`: `{ value, body, .. }` (not `{ bound, body }`).
- `Term::LetRec`: `{ ty, body, in_term, .. }` (per actual AST:
`name, ty, params, body, in_term`), not the plan's
`{ bindings, body }`.
- `Term::If`: `{ cond, then, else_ }` (not `{ cond, then_b, else_b }`).
- `Term::Do`: `{ args, .. }` only (no `stmts`/`ret` fields).
- `Term::Lam`: `{ param_tys, ret_ty, body, .. }` (no `modes` field
exists on `Lam`).
- `Term::Seq`: `{ lhs, rhs }` (not `{ stmts, ret }`).
- ParamMode discovery routed through `visit_type` on
`Type::Fn { param_modes, ret_mode, .. }` — the only place
`ParamMode` appears in the AST. The plan tried to visit
ParamMode from `Def::Fn.params` (wrong type) and `Term::Lam.modes`
(non-existent field).
The spec-compliance intent of Step 3.2 ("every AST enum variant is
visited at least once via an exhaustive match with no `_` wildcard")
is preserved; only the destructuring patterns adapted to the actual
AST. All 34 expected variants observe coverage in the corpus, and
the build remains a tripwire for future AST additions.
- Plan template note: a future `planner`/`brainstorm` pass on
tests-against-AST might cheaply read `ast.rs` and emit a
destructuring-correct scaffold instead of relying on the
implementer to repair under the Step 3.4 escape hatch. Not
rt.1 work.
## Known debt
- None added by this iter. The three new tests are pure readers
and add no production-code surface; their existence pins three
invariants that future iters can use to detect regression
rather than rediscover.
## Files touched
- `Cargo.toml` — workspace dep `tempfile = "3"`.
- `Cargo.lock` — auto-updated by cargo for `tempfile`.
- `crates/ail/Cargo.toml` — new `[dev-dependencies]` section.
- `crates/ailang-surface/tests/round_trip.rs` — module doc
rewrite; `handwritten_exhibits_match_json_fixtures`
`every_ailx_fixture_matches_its_json_counterpart` (+ helper).
- `crates/ailang-core/tests/schema_coverage.rs` — new file.
- `crates/ail/tests/roundtrip_cli.rs` — new file.
## Stats
bench/orchestrator-stats/2026-05-12-iter-rt.1.json
-71
View File
@@ -1,71 +0,0 @@
# iter rt.2 — Roundtrip Invariant anchored at top level in DESIGN.md
**Date:** 2026-05-12
**Started from:** b6046bbf (post-rt.1, working tree clean)
**Status:** DONE
**Tasks completed:** 1 of 1
## Summary
DESIGN.md gains a new top-level section `## Roundtrip Invariant`,
positioned between Decision 11 and `## Mangling scheme`. The
section states both directions of the `.ail.json``.ailx`
bijection, names the Float-bits-hex encoding as the mechanism
that keeps Floats *inside* the invariant, lists the four
enforcement tests by path, and explains why the property is
top-level (load-bearing on language identity, not on Decision 6's
surface-design rationale).
Decision 6 Constraint 2 is rewritten as an upward cross-reference
to the new top-level section. The substantive content of the
constraint now lives at the anchor; Decision 6 records that its
surface design must satisfy the anchored invariant.
## Why boss-direct edit (no planner / implement dispatch)
The spec's Architecture #1 fully specified the content (statement
both directions, Float-mechanism subsection, enforcement-points
list, why-top-level justification, Decision 6 cross-reference).
No design judgement was deferred to execution time; the iter is a
deterministic projection of the spec onto DESIGN.md prose. Plan +
implement dispatch for a pure docs edit with all content
pre-specified is overhead without value, per CLAUDE.md "trivial
mechanical edits" carve-out (the LOC budget is exceeded but the
judgement budget is zero).
## Files touched
- `docs/DESIGN.md`:
- Inserted new `## Roundtrip Invariant` section (Z. 1753+, just
before `## Mangling scheme`).
- Rewrote Decision 6 Constraint 2 (Z. 249-253) as an upward
cross-reference to the new section.
## Verification
`cargo test --workspace` — all tests green, no regressions. The
schema-drift tests in `crates/ailang-core/tests/design_schema_drift.rs`
scan DESIGN.md for anchors and were the most likely place for the
DESIGN.md edit to surface false-positive failures; they remain
green.
## Milestone status
The roundtrip-invariant milestone has now satisfied acceptance
criteria 1-7 from `docs/specs/2026-05-12-roundtrip-invariant.md`:
1. ✓ Top-level `## Roundtrip Invariant` section in DESIGN.md;
Decision 6 Constraint 2 carries upward cross-reference. (rt.2)
2.`every_ailx_fixture_matches_its_json_counterpart` dynamic in
`crates/ailang-surface/tests/round_trip.rs`. (rt.1)
3.`crates/ailang-core/tests/schema_coverage.rs` with
exhaustive-match visitor; 34/34 variants observed. (rt.1)
4.`crates/ail/tests/roundtrip_cli.rs` with BLAKE3 identity
over `ail render` → tempfile → `ail parse`. (rt.1)
5.`cargo test --workspace` green with all four tests. (rt.1 + rt.2)
6. ✓ No roundtrip gaps surfaced; no render/parse code changes
needed in this milestone. (rt.1)
7. ✓ Tests are pure readers; the CLI test's only filesystem side
effect is `tempfile::TempDir`. (rt.1)
Audit skill follows for milestone close.
-244
View File
@@ -1,244 +0,0 @@
# audit-24 — milestone close (Show + print rewire)
**Date:** 2026-05-13
**Milestone:** Show + print rewire (number-24 family)
**Iters covered:** 24.1 (`f38bad8`, shipped 2026-05-12) → 24.2 (`3286117`) → 24.3 (`246b5c7`)
**Range from previous close:** `0dcdaab..0cfb3f6` (audit-ct-tidy → iter-24.3 INDEX)
**Status:** drift_found — `24.tidy` queued
## Summary
The Show + print milestone shipped in three iters: 24.1 installed
`bool_to_str` + `str_clone` runtime + codegen primitives at heap-Str
ABI level (pre-mq audit lineage); 24.2 added `class Show` + four
primitive instances (Int/Bool/Str/Float) to the prelude and migrated
the 22b user-Show fixture corpus to `TShow`/`tshow` to eliminate
post-prelude.Show ambiguity; 24.3 added `fn print` with explicit-let
body, three E2E fixtures (positive 4-prim, user-ADT, negative
NoInstance), an IR-shape pin on `print__Int.body`, a Show-aware
NoInstance diagnostic, plus three compiler-path repairs surfaced by
the user-ADT E2E (canonical-form normalisation of
`MonoTarget::FreeFn::type_args`, codegen cross-module reference
fallback, FreeFnCall constraint-residual push).
Architect drift review surfaces 7 items: three [high] (missing DESIGN.md
documentation of the three iter-24.3 strengthenings: canonical-form
`type_args`, post-mono cross-module-ref invariant, FreeFnCall constraint-
residual push; missing codegen-level pin for the import_map-fallback
path; constraint-residual push currently fires only in the dot-qualified
FreeFnCall branch), three [medium] (`unwrap_or_default` in the
method-name lookup masks class-index drift; `normalize_type_for_lookup`
duplicated across two `mono.rs` sites with manual-lockstep invariant;
negative-test coverage is single-shape — only `f : Int -> Int`, not
user-type-without-instance), and one [low] (bench `architect_sweeps.sh`
noise on three pre-milestone-24 DESIGN.md lines — not new drift).
Bench: `bench/check.py` exit 1 with 2 regressed / 1 improved metrics
(metric identities migrate between runs — first run hit
`latency.implicit_at_rc.max_us +209%`, second run hit
`latency.explicit_at_rc.max_us +66%` + `bench_list_sum.bump_s +11.9%`).
`bench/compile_check.py` exit 1 with 3-5 regressed metrics across
runs (metric identities migrate: first run hit `build_O0_ms.*`
cluster within tolerance, second run hit `check_ms.*` cluster with
several +25-34% deltas). `bench/cross_lang.py` exit 0, 25/25 stable.
**9th consecutive audit observation** of the documented noise envelope
covering `latency.implicit_at_rc.*` / `latency.explicit_at_rc.*` /
`bench_list_sum.bump_s` / `check_ms.*` metric-cluster migration
across consecutive runs. Conservative-call convention has held the
baseline pristine across 8 prior audits since `audit-cma`; this
audit extends the lineage to 9 with the same conservative call —
the metric-migration-between-runs is itself attribution evidence
that the signal is variance, not regression.
Resolution path: `24.tidy` iteration covers 5 actionable drift
items (three [high] + two [medium] documentable/refactorable). The
[medium] "negative-test coverage single-shape" item moves to the
roadmap P3 backlog. The [low] bench_sweep noise is carry-on.
## Architect drift items
### [high-1] Three iter-24.3 strengthenings undocumented in DESIGN.md
**Paths:** `crates/ailang-check/src/mono.rs:687-696` + `:1287-1294`,
`crates/ailang-codegen/src/lib.rs:1980-2000` + `:2200-2244` +
`:2776-2816`, `crates/ailang-check/src/lib.rs:2820-2865`,
`docs/DESIGN.md` §"Monomorphisation" or new subsection.
Iter 24.3 silently strengthened three load-bearing mono / codegen /
synth invariants without documenting them in DESIGN.md:
- (a) `MonoTarget::FreeFn::type_args` carries canonical (`<owner>.<bare>`)
types post-collection via `normalize_type_for_lookup`. Pre-iter
invariant was "subst.apply result"; new invariant is
"subst.apply result normalised to registry-lookup form".
- (b) Post-mono synthesised bodies may carry cross-module references
to modules their source template didn't import. Codegen falls back
to direct `module_user_fns` / `module_def_ail_types` lookup when
the prefix isn't in the current module's `import_map`.
- (c) FreeFnCall synth pushes one `ResidualConstraint` per declared
forall-constraint with rigid vars substituted by fresh metavars.
Pre-iter, the arm had `constraints: _` (constraints ignored);
latent for milestone-23 fixtures because all primitive instances
discharged silently.
Future planner / implementer work that touches these surfaces will
not see the strengthenings without a doc anchor.
**Outcome:** fix in `24.tidy` (doc-only).
### [high-2] Codegen import_map fallback path has no unit-level pin
**Path:** `crates/ailang-codegen/src/lib.rs:2200-2244` + `:1980-2000` +
`:2776-2816`.
The three cross-module-fallback arms (`resolve_top_level_fn`,
`lower_app` cross-module arm, `synth_with_extras` Var arm) introduce
a new dispatch path: try `import_map`, fall back to direct
`module_user_fns` / `module_def_ail_types` lookup. The fallback is
**only** justified by post-mono synthesised cross-module references.
No codegen-level unit test pins the failure mode; the only test that
would catch a regression is the E2E `show_user_adt.ail.json`. If a
future refactor tightens `resolve_top_level_fn` back to import_map-only,
the regression surfaces only at E2E (slow to bisect).
**Outcome:** fix in `24.tidy` — add a unit pin asserting the fallback
resolves for a synthesised `prelude.print__T``<user_module>.show__T`
pair without going through `import_map`.
### [high-3] FreeFnCall constraint-residual push covers only dot-qualified branch
**Path:** `crates/ailang-check/src/lib.rs:2820-2865`.
The constraint-residual push happens **only** in the dot-qualified
`prefix.suffix` synth branch (the only branch that populates
`free_fn_owner = Some(...)`). Bare-name references to polymorphic free
fns that resolve through other branches (locals shadow,
`env.module_globals` direct hit at the current module, etc.) do not
push residuals for declared constraints. For prelude poly fns this is
fine because the auto-injected prelude call-path always lands in the
dot-qualified branch — but the invariant is implicit and undocumented.
The same "silent unknown variable from codegen" failure mode existed
pre-iter-24.3 for any milestone-23 negative case (`ne` on a type
without `Eq` instance, etc.); the iter-24.3 fix covers Show only
because that's what the negative E2E exercised.
**Outcome:** fix in `24.tidy` — generalise the constraint-residual push
to all FreeFnCall synth branches, OR document the invariant
"poly free fns always reach synth via the dot-qualified branch" and
pin it with a unit test.
### [medium-1] `unwrap_or_default` in method-name lookup masks class-index drift
**Path:** `crates/ailang-check/src/lib.rs:2852-2858`.
Method-name lookup in the FreeFnCall constraint-residual push uses
`unwrap_or_default()` (empty string fallback) when `class_methods` has
no entry for the residual class. A user constraint referencing a class
that does not exist would silently render `NoInstance` with an empty
`method:` field. The iter-22 `MissingClass` pre-pass plausibly rejects
this earlier, but the fallback masks the dependency.
**Outcome:** fix in `24.tidy` — replace with explicit `Internal` or
`expect(...)` to surface registry/index drift on regression.
### [medium-2] `normalize_type_for_lookup` duplicated across two sites
**Path:** `crates/ailang-check/src/mono.rs:687-696` + `:1287-1294`.
The two `normalize_type_for_lookup` call sites use a literal copy of
the body comment rather than extracting a helper. The two sites must
agree byte-identically (per iter-24.3 journal: "Must agree byte-
identically with that site or Phase 3 rewrite cursor produces a mono-
symbol name that differs from the Phase 2 synthesis name"). A
`normalize_free_fn_type_args(env, module_name, metas, subst)` helper
would make the lockstep enforceable by construction.
**Outcome:** fix in `24.tidy` — extract helper.
### [medium-3] Negative-test coverage single-shape (deferred)
**Paths:** `examples/show_no_instance.ail.json` and the broader
NoInstance Show test surface.
The negative E2E covers `f : Int -> Int` (function type without Show
instance). Other NoInstance shapes — user type without instance,
partial-application residual carrying both a missing constraint AND a
NoInstance, polymorphic call with two unsatisfied constraints — are
not pinned. The Show-aware NoInstance arm fires for any
non-prelude-Show type by construction, so the single fixture is
*structurally representative*, but coverage breadth is thin.
**Outcome:** roadmap backlog P3 (not in `24.tidy`). Single-fixture
coverage is acceptable for the architecture-shipping milestone;
broader coverage waits for the corpus-migration milestone (the queued
P2 retirement of `io/print_int|bool|float`) when more downstream
fixtures naturally exercise these shapes.
### [low-1] bench/architect_sweeps.sh noise on pre-milestone-24 DESIGN.md lines
**Path:** `bench/architect_sweeps.sh` matches on `docs/DESIGN.md:50`,
`:449`, `:614`.
All three matched lines predate milestone 24 (text not touched in the
milestone-24 diff). Legitimate references or empirical-addendum
headings, not new history anchors. Sweep noise from prior eras.
**Outcome:** carry-on (not a milestone-24 drift item).
## Bench
- `bench/check.py`: exit 1 with metric identities migrating between
consecutive runs:
- Run 1: 2 regressed (`latency.implicit_at_rc.max_us +209%`,
`bench_list_sum.bump_s +11.9%`), 1 improved
(`bench_list_sum.gc_over_bump`).
- Run 2: 2 regressed (`latency.explicit_at_rc.max_us +66%`,
`bench_list_sum.bump_s +11.9%` again), 1 improved (different
metric).
- Pattern: the `latency.*_at_rc.max_us` metric cluster shifts
identity between runs; `bench_list_sum.bump_s` is the 5th
consecutive sighting since `audit-mq.tidy`. Same envelope the
iter-24.3 journal documented as the 8th consecutive observation;
this audit makes it 9.
- `bench/compile_check.py`: exit 1 with 3-5 regressed metrics across
runs; metric identity migrates between `build_O0_ms.*` cluster (run
1) and `check_ms.*` cluster (run 2). All deltas are +5% to +34%
across runs; tolerance is 20-25% so multiple `check_ms.*` deltas
cross threshold sporadically. Same migration pattern.
- `bench/cross_lang.py`: exit 0; 25/25 stable.
**Conservative call: baseline pristine for the 9th consecutive audit.**
The metric-identity-migration is itself attribution evidence that the
signal is variance, not regression. If signal were present, the metric
would not migrate between runs at this rate. Ratifying via
`--update-baseline` would pin a snapshot that does not characterise
the actual distribution (since the noise migrates, any single
snapshot is wrong). The right ratification path is the queued P3
"Latency methodology rework — switch from per-run timing to a
histogram-based approach"; until that ships, conservative-call is the
correct response.
No `--update-baseline` invocation this audit.
## Resolution
- `24.tidy` iter to land 5 drift fixes: `[high-1]` (DESIGN.md
documentation of the three iter-24.3 strengthenings), `[high-2]`
(codegen import_map-fallback pin), `[high-3]` (FreeFnCall
constraint-residual push generalisation OR documented invariant),
`[medium-1]` (replace `unwrap_or_default` with explicit error),
`[medium-2]` (extract `normalize_free_fn_type_args` helper). Routes
through `planner``implement`.
- `[medium-3]` (negative-test coverage single-shape): roadmap backlog
P3.
- `[low-1]` (bench sweep noise): carry-on, not actionable.
- Bench: extend lineage to 9th consecutive observation. Pristine
baseline. Document for future histogram-methodology landing.
## Files touched
- `docs/journals/2026-05-13-audit-24.md` (new, this file)
- `docs/journals/INDEX.md` (one new line)
No production code or test edits in this audit; all recommendations
land in `24.tidy`.
-145
View File
@@ -1,145 +0,0 @@
# audit-form-a — Milestone close (Form-A as the default authoring surface)
**Date:** 2026-05-13
**Audited range:** aabcadc (iter form-a.0) .. 9fdc4ca (iter form-a.1 T6-12) HEAD
**Spec:** `docs/specs/2026-05-13-form-a-default-authoring.md` (amended at 9fcda8b)
**Status:** drift_found (4 items, all documentary-only); bench clean (exit 0 across all three scripts)
**Recommendation:** carry-on — no form-a.tidy queued at architect's explicit recommendation; drift items deferred to documentary cleanup at next opportunity.
## What holds (architect-verified)
- DESIGN.md §"Roundtrip Invariant" restated correctly: four numbered
properties (parse-determinism + idempotency-under-print + CLI-
pipeline-idempotency + carve-out-anchor) replace the prior
Direction-1/Direction-2 framing; five surviving enforcement tests
named verbatim (parse_is_deterministic_over_every_ail_fixture,
parse_then_print_then_parse_is_idempotent_on_every_ail_fixture,
cli_parse_then_render_then_parse_is_idempotent,
every_ast_variant_is_observed_in_the_fixture_corpus,
examples_ail_json_inventory_matches_carve_outs). §"Float literals"
+ §"Why anchored at top level" preserved.
- §A4 doctrine rewording landed verbatim per spec at CLAUDE.md:5-6
+ DESIGN.md:465-466 (canonical form = JSON-AST; authoring projection
= `.ail`; "Form (A) is one projection; the JSON-AST stays canonical"
preserved as the second sentence).
- Carve-out inventory matches the eight-file spec post-amendment:
`ls examples/*.ail.json | wc -l` returns 8, alphabetical order
`broken_unbound + prelude + 3× test_22b2_* + 3× test_ct1_*`.
`crates/ailang-core/tests/carve_out_inventory.rs::EXPECTED`
hardcodes the same eight entries.
- 156 non-carve-out `.ail.json` deleted; 157 `.ail` files present
(98 fresh-rendered in iter form-a.1 T2 + 58 pre-iter + the
prelude pilot from form-a.0). No regressions in
`cargo test --workspace` (557 green + 3 ignored).
- WhatsNew milestone-close entry editorial-clean per /boss
convention: English, no crates / iter-codes / agent names, leads
with the change, factual tone, telegram-pragmatic length.
- Roadmap milestone struck `[x]` with closing attribution
`(Closed 2026-05-13 by iter form-a.1.)`; follow-up milestone
`[Prelude embed: Form-A as compile-time source]` present with
motivation + problem + three resolution options.
- Per-iter journals (`2026-05-13-iter-form-a.0.md`,
`2026-05-13-iter-form-a.1.md`) both present; `INDEX.md` carries
both lines in chronological order.
## Drift items (deferred per architect recommendation)
| Severity | Path | Issue |
|----------|------|-------|
| medium | `docs/specs/2026-05-13-form-a-default-authoring.md` (§C1, §C2, §"Data flow") | Three sites still say "seven carve-outs" post-amendment; acceptance #1/#2 correctly say eight. Spec self-inconsistent against its own amendment commit 9fcda8b. |
| medium | `docs/plans/2026-05-13-iter-form-a.1.md` (two sites) | Same "seven carve-outs" wording surviving the amendment; harmless now but contradicts the eight-file reality the plan executed. |
| low | `crates/ailang-core/src/hash.rs:57` | Empty `mod tests {}` placeholder left after iter form-a.1 T5 relocation. Documentary-only today; drift if it stays past one further milestone. |
| low | `crates/ailang-surface/tests/round_trip.rs:16` | Module docstring still says "Direction 2 of the Roundtrip Invariant", but DESIGN.md T10 retired the Direction-1/Direction-2 framing. Stale cross-reference to a renamed concept. |
All four items are documentary-only: the spec / plan orphans
contradict their own post-amendment sections but do not affect the
shipped implementation (which is eight-carve-out-correct); the
hash.rs placeholder and the round_trip.rs docstring are cleanup
hygiene that does not change behaviour or invariants.
Architect's explicit recommendation: "carry on as planned. Queue a
documentary `form-a.tidy` only if a subsequent reader hits the
seven-vs-eight orphans; the implementation is correct and the spec
orphans are post-hoc retro-clean-up, not active drift." The audit
follows this recommendation.
## Bench-regression report
All three bench scripts exit 0; baseline left pristine.
### `bench/check.py` (exit 0)
63 metrics; 2 regressed-by-magnitude, 2 improved-by-magnitude, 59
stable. Regressions both on `max_us` tail-latency metrics:
- `latency.explicit_at_rc.max_us` +124.99% (tol 25%) — paired with
`latency.explicit_at_rc.p99_us` *improvement* 27.02%.
- `latency.implicit_at_rc.max_us` +126.13% (tol 30%).
This is the 12th consecutive audit observation of the metric-
identity-migrating noise envelope on tail-latency metrics, dating
back to audit-cma (2026-05-12). The pattern across the lineage:
different metric identifiers fire each run, magnitudes oscillate,
median + p99 stay stable; the firing-metric set has migrated
through approximately a dozen distinct identifiers across the
twelve audits. The convention since the audit-cma originating
observation is to hold the baseline pristine — the metric-migration-
between-runs is itself attribution evidence that the cluster is
noise-class, not signal-class. The queued P3 latency-methodology-
histogram rework is the right ratification path; --update-baseline
is not.
The form-a milestone is intentionally test-infra + corpus migration,
not language semantics. Zero language-semantic surface was touched.
The tail-latency regressions are not caused by the migration; they
are the noise envelope continuing.
### `bench/compile_check.py` (exit 0)
24 metrics; 2 regressed-by-magnitude, 0 improved, 22 stable.
- `check_ms.hello` +32.44% (tol 25%) — 1.2ms → 1.5ms.
- `check_ms.borrow_own_demo` +26.18% (tol 25%) — 1.3ms → 1.7ms.
Both on the smallest fixtures in the corpus where `check_ms` is
sub-2-millisecond. iter form-a.1 T6 journal already flagged this:
"First compile_check run reported exit-1 due to sub-millisecond
`check_ms` noise on `hello` and `borrow_own_demo`; second run
clean." Now both still fire at the current audit re-run. This is
sub-millisecond timing-jitter, not a migration-induced regression.
The migration touched only fixture file extensions and test-loader
call sites; the `ail check` binary path was not modified.
### `bench/cross_lang.py` (exit 0)
25 metrics; 0 regressed, 0 improved-beyond-tolerance, 25 stable.
Cross-language ratios (AILang RC / AILang bump / C reference) all
hold within tolerance. The most interesting movement is
`bench_tree_walk.rc_over_c` from 2.7394 to 2.5871 (5.56%) — a
modest improvement still inside the noise envelope.
## Recommendation summary
| Item | Recommendation |
|------|----------------|
| 4 architect drift items | carry-on (per architect's explicit recommendation; documentary-only) |
| `bench/check.py` 2 regressions | noise-envelope continuation (12th audit); baseline pristine |
| `bench/compile_check.py` 2 regressions | sub-millisecond timing-jitter; baseline pristine |
| `bench/cross_lang.py` clean | no action |
No `form-a.tidy` iter is queued. The four documentary drift items
are recorded here for future-reader visibility; they will be
addressed inline at the next iter that touches the affected files,
or in a dedicated cleanup pass if a subsequent reader hits them.
The form-a-default-authoring milestone is fully closed.
## Next-step pointer
Per `/boss` and the milestone-close pipeline: the next milestone
candidate at the top of the roadmap is `[Retire io/print_int/_bool/_float]`
(P2, post-milestone-24 mechanical follow-up). The `[Prelude embed:
Form-A as compile-time source]` milestone is the second P2 entry,
queued behind the io/print retire. Starting either is a `/boss`
bounce-back per the new-milestone rule — the Boss surfaces the
candidate and the user picks the session shape.
-195
View File
@@ -1,195 +0,0 @@
# audit-mq — milestone close (module-qualified-class-names)
**Date:** 2026-05-13
**Milestone:** module-qualified-class-names
**Iters covered:** mq.1 (`0eb3323`) → mq.2 (`2e6a4ca`) → mq.3 (`99d3968`)
**Range from previous close:** `0dcdaab..99d3968` (audit-ct-tidy → mq.3)
**Status:** drift_found — `mq.tidy` queued
## Summary
The module-qualified-class-names milestone landed in three iters:
mq.1 lifted the canonical-form rule to the three class-ref schema
fields and qualified all workspace-internal class-name keys; mq.2
installed the type-driven dispatch mechanism without exercising it
end-to-end; mq.3 retired `WorkspaceLoadError::MethodNameCollision`,
re-keyed `class_methods` to a `(class, method)` tuple, plumbed
`Env.active_declared_constraints`, added the
`class-method-shadowed-by-fn` warning, and shipped three positive
E2E fixtures exercising trajectories C (ambiguous), E (explicit
qualifier), and the class-fn shadow case.
Architect drift review surfaces seven items: two [high] (rigid-var
refinement type-unification leg missing; same-module bare-class
qualifier shape unreachable), three [medium] (warning over-fires on
unrelated locals shadowing prelude method names; DESIGN.md Data
Model schema fragments don't reflect the canonical-form rule for
class-refs; `synth(...)` has grown to 10 mut-ref parameters), and
two [low] backlog items (trajectories B + D covered by unit tests
only — no E2E fixture; `collect_mono_targets` rebuilds env without
`active_declared_constraints`, currently latent).
Bench: `check.py` exit 1 across four consecutive runs with metric
identity shifting between runs (3 → 1 → 1 → 0 regressions across
re-runs, different metrics each time; one run showed only
improvements). Pattern is consistent with 5th-consecutive audit
noise-class observation. `compile_check.py` exit 0 (24/24 stable);
`cross_lang.py` exit 0 (25/25 stable). Baseline left pristine per
the 5th-consecutive conservative-call convention; see Bench notes
below.
Resolution path: `mq.tidy` iteration covers the four actionable
drift items (two [high] + two [medium]). The 5th [medium]
(`synth(...)` signature growth) is acknowledged as accepted
post-mq.3 cost and not fixed — refactoring `synth(...)` would
ripple through 15+ recursive callsites and 5 external callers,
disproportionate to the readability gain. The two [low] items
join the roadmap backlog.
## Architect drift items
### [high-1] Rigid-var refinement misses type-unification leg
**Path:** `crates/ailang-check/src/lib.rs:2163-2168`
The `refine_multi_candidate_residual` rigid-var branch filters
declared constraints on `dc.class == c` only. Spec §"Constraint-
discharge refinement" (line 130-138) requires the filter to also
unify the declared constraint's `type_` with the residual's
`type_`. Two same-class declared constraints on different typevars
(`forall a b. Show a, Show b => (a, b) -> String` style) cannot
currently be discriminated; the filter returns the first match
regardless of which typevar the residual actually came from. No
unit test pins the type-unification leg.
**Outcome:** fix in `mq.tidy`.
### [high-2] Same-module bare-class qualifier shape unreachable
**Path:** `crates/ailang-check/src/lib.rs:2570-2575`
`qualifier_is_class_shape = q.contains('.')` makes same-module bare-
class qualifiers (`Show.show`) unreachable; the gate only admits
cross-module class qualifiers (`<module>.<Class>.<method>`). This
contradicts the canonical-form rule's symmetry — bare class refs
are legal at every other class-ref schema field (per mq.1's rule).
Trajectory E E2E coverage is cross-module-only as a consequence.
**Outcome:** fix in `mq.tidy`.
### [medium-1] `class-method-shadowed-by-fn` over-fires
**Path:** `crates/ailang-check/src/lib.rs:2479-2502`
The warning fires whenever any class declares the method, not
when a class candidate for the actual arg type exists per spec
§"Class-fn collisions" (line 161-162). Will spam on unrelated
locals shadowing prelude method names (e.g. `let show = …` inside
an arbitrary body where `show` has no Show-context).
**Outcome:** fix in `mq.tidy`.
### [medium-2] DESIGN.md Data Model schema fragments don't reflect canonical-form rule for class-refs
**Path:** `docs/DESIGN.md:2139, 2152, 2273`
Data Model schema fragments for `InstanceDef`, `Constraint`,
`SuperclassRef` describe class-ref `<id>` without flagging the
canonical-form rule. Only the prose at lines 1156-1162 carries the
rule. A schema-section reader without the prose context sees
bare-only.
**Outcome:** fix in `mq.tidy` (doc-only).
### [medium-3] `synth(...)` signature growth (accepted)
**Path:** `crates/ailang-check/src/lib.rs:2415`
`synth(...)` now carries 10 mut-ref parameters (added `warnings`
in mq.3). Two re-entries (`mono.rs:614`, `lift.rs:700`) discard
the warnings channel by convention.
**Outcome:** acknowledged debt, NOT fixed in `mq.tidy`. Rationale:
refactoring `synth(...)` (e.g. into a `SynthCtx` struct carrying
the mut-refs) would ripple through 15+ recursive callsites and 5
external callers, with the only payoff a marginal readability
gain. The mut-ref pattern is consistent with the rest of the
crate (`check_in_workspace`'s `Vec<CheckError>` accumulator), so
this is project-style rather than drift.
### [low-1] Trajectories B + D no E2E
**Paths:** spec §"Data flow" trajectories B (type-driven narrow-
to-one) + D (rigid-var narrow-to-one) are covered by unit tests
only — `tests/method_dispatch_pin.rs` 5-step rule tests. No
on-disk fixture exercises either path end-to-end.
**Outcome:** roadmap backlog (P3). Single-class-per-method
workspaces don't exercise these branches; the real coverage gap
opens when downstream code starts producing the multi-class
shapes the unit tests already pin.
### [low-2] `collect_mono_targets` rebuilds env without `active_declared_constraints`
**Path:** `crates/ailang-check/src/mono.rs:574-625`
Structural divergence from `check_fn`'s env setup. Currently
latent: mono's residuals are concrete-type so the rigid-var
filter never runs. Env-shape invariant unpinned.
**Outcome:** roadmap backlog (P3). Test that pins the invariant
(`env.active_declared_constraints` populated consistently
wherever `Env` is built) belongs with the [low-1] E2E work for
trajectory D.
## Bench
- `bench/check.py`: exit 1 across four consecutive re-runs with
metric identity shifting between runs:
- Run 1: 1 regression (`latency.implicit_at_rc.p99_over_median`),
5 improvements (across `bench_list_sum.gc_over_bump` +
`bench_closure_chain.gc_over_bump` + 3
`latency.explicit_at_rc.*` metrics).
- Run 2: 3 regressions (`bench_list_sum.bump_s` 3rd-consecutive
sighting + 2 `latency.implicit_at_rc.*` max-tail), 3
improvements (same `latency.explicit_at_rc` cluster).
- Run 3: 1 regression (different metric again), 5 improvements.
- Run 4: 0 regressions, 2 improvements
(`bench_list_sum.gc_over_bump` +
`bench_closure_chain.gc_over_bump`).
- Pattern: metric identity is not stable; the recurring
`bench_list_sum.bump_s` 3rd-consecutive sighting per mq.3
journal lineage shows up in 1 of 4 runs only. Audit-ct-tidy
(2026-05-12), audit-eob (2026-05-12), audit-ms (2026-05-12),
audit-cma (2026-05-12) all left baseline pristine on the same
`latency.explicit_at_rc` improvement cluster reappearing
across milestones — this is the 5th consecutive audit
observation of the same noise envelope.
- Conservative call: baseline pristine for 5th consecutive
audit. The latency cluster has narrowed-then-widened-then-
narrowed across audits, which is itself attribution evidence
that this is variance, not signal. If signal were present
the metric would not migrate between runs at this rate.
- `bench/compile_check.py`: exit 0; 24/24 stable.
- `bench/cross_lang.py`: exit 0; 25/25 stable.
No `--update-baseline` invocation this audit.
## Resolution
- `mq.tidy` iter to land four drift fixes: `[high-1]`, `[high-2]`,
`[medium-1]`, `[medium-2]`. Routes through `planner`
`implement`.
- `[medium-3]` (`synth(...)` signature growth): acknowledged
debt, no action.
- `[low-1]` (Trajectory B + D E2E) + `[low-2]` (mono env shape):
roadmap backlog P3. Same scope — single follow-up iter when the
multi-class workspace shape lands.
## Files touched
- `docs/journals/2026-05-13-audit-mq.md` (new, this file)
- `docs/journals/INDEX.md` (one new line)
No production code or test edits in this audit; all
recommendations land in `mq.tidy`.
-189
View File
@@ -1,189 +0,0 @@
# iter 24.2 — Show class + 4 primitive instances + 22b TShow/tshow migration
**Date:** 2026-05-13
**Started from:** 64cea0e
**Status:** DONE
**Tasks completed:** 8 of 8
## Summary
Ships `class Show a where show : (a borrow) -> Str` in
`examples/prelude.ail.json` alongside primitive instances `Show Int`,
`Show Bool`, `Show Str`, `Show Float`. Each instance body is a
single-application lambda invoking the corresponding runtime primitive
(`int_to_str`, `bool_to_str`, `str_clone`, `float_to_str`) — first
prelude instance bodies to call runtime primitives directly
(prior Eq/Ord bodies used operator + codegen intercept). The 22b
typeclass test corpus migrates `class Show` / `show`
`class TShow` / `tshow` preemptively (Tasks 12 before Task 3 lands
`prelude.Show`) so the workspace stays green throughout. Three new
tests pin the post-mono shape: mono-synthesis existence
(`show_mono_synthesis.rs`), dispatch singleton (`show_dispatch_pin.rs`
× 2 cases), and body-hash stability (`mono_hash_stability.rs::primitive_show_mono_symbol_hashes_stay_bit_identical`).
DESIGN.md §"Prelude (built-in) classes" gains a milestone-24 paragraph
naming Show + instances + the Float caveat. `print`-rewire stays
deferred to iter 24.3.
## Per-task notes
- iter 24.2.1: Renamed `"Show"`/`"show"``"TShow"`/`"tshow"` across 21
`test_22b*_*.ail.json` fixture files; cross-module qualifier forms
(`"<modname>.Show"``"<modname>.TShow"`) handled by regex in 3
fixtures (`test_22b1_dup_a`, `test_22b1_dup_b`,
`test_22b1_orphan_third`). False-positive scan empty; fixture
parse round-trip green.
- iter 24.2.2: Renamed literals in `crates/ail/tests/typeclass_22b{2,3}.rs`
(`"Show"``"TShow"`, `"show"``"tshow"`, `show__T``tshow__T`),
including comment-prose forms (`` `Show` `` → `` `TShow` ``,
`class Show` → `class TShow`, etc.) and one assertion-message string
("exactly one show__Int across the workspace" → "tshow__Int...").
Plan undercount surfaced: the rename touches more than the 2 named
test files — see Concerns below.
- iter 24.2.3: Inserted 5 new top-level defs into `examples/prelude.ail.json`
immediately before `fn ne` (line 223 pre-edit). The `instance Show Str`
body invokes `str_clone` as `Term::Var` — first prelude instance body
to do so. Round-trip green; `primitive_eq_ord_mono_symbol_hashes_stay_bit_identical`
stays green (Eq/Ord hashes unaffected by Show defs).
- iter 24.2.4: New fixture `examples/show_mono_pin_smoke.ail.json`
exercises `show 42`, `show true`, `show "x"`, `show 1.5` in a
let-chain ending in `io/print_str s1`. New test
`crates/ail/tests/show_mono_synthesis.rs::primitive_show_mono_symbols_synthesise`
asserts `show__Int`/`show__Bool`/`show__Str`/`show__Float` materialise
as `Def::Fn` entries in the `prelude` post-mono module. `str_clone`
reachability in instance-lambda position verified implicitly:
`show__Str` materialises, which means the lambda body's `Term::Var
"str_clone"` resolved against the checker's builtin table during
typecheck.
- iter 24.2.5: New pin test `crates/ailang-check/tests/show_dispatch_pin.rs`
asserts `env.method_to_candidate_classes["show"] == {"prelude.Show"}`
(singleton) and `env.method_to_candidate_classes["tshow"] == {"*.TShow"}`
(singleton). API spelled `build_check_env(&ws)` not `Env::from_workspace`
(plan's placeholder name). 2/2 cases pass first-shot.
- iter 24.2.6: Extended `crates/ail/tests/mono_hash_stability.rs` with
a second test function `primitive_show_mono_symbol_hashes_stay_bit_identical`
loading `show_mono_pin_smoke.ail.json` (kept separate from the
Eq/Ord fixture to avoid coupling). Hashes captured first-run:
`show__Int 891eaebe64b3180d`, `show__Bool 1bdb621494e50607`,
`show__Str 3f1d57c90ddce081`, `show__Float 5ab0bdd40b9f8b8f`.
- iter 24.2.7: DESIGN.md §"Prelude (built-in) classes" amended:
milestone-23 paragraph drops "Show" from the deferred list and notes
Show ships in milestone 24; new milestone-24 paragraph names class
signature, instance set (including Float), body shape (single-app
lambda invoking runtime primitive), and the iter-24.3 carry-forward
for `print`-rewire.
- iter 24.2.8: Workspace tests 552 passed / 0 failed
(was 548 pre-iter, +4 new: 1 `show_mono_synthesis` + 2 `show_dispatch_pin`
+ 1 hash-stability). `bench/compile_check.py` exit 0 (24/24 stable).
`bench/cross_lang.py` exit 0 (25/25 stable). `bench/check.py` exit 1
with 2 noise-class metrics (`bench_list_sum.bump_s` +11.71% — 4th
consecutive sighting per `audit-mq` / `mq.tidy` lineage;
`latency.implicit_at_rc.max_us` +145.55% — 7th consecutive sighting of
the documented noise envelope, metric-identity-migrating across runs).
Baseline pristine per the conservative-call convention.
## Concerns
- **Plan undercount on 22b migration scope.** The plan's "2 Rust test
files" carrier (typeclass_22b{2,3}.rs) missed 4 additional consumer
sites that hardcode the renamed literals: (a)
`crates/ailang-core/src/hash.rs` ct4 canonical-form hash pin (2
entries); (b) `crates/ailang-core/src/workspace.rs` 3 in-crate iter22b1
tests; (c) `crates/ailang-prose/tests/snapshot.rs` (1 snapshot file at
`examples/test_22b2_instance_present.prose.txt`); (d) one
`mono_symbol_compound_type_uses_hash_suffix` assertion in typeclass_22b3.rs
that called `mono_symbol("tshow", ...)` (post my batch rename) but
still asserted `name.starts_with("show__")`. All fixed inline per the
precedent of mq.1 / mq.3 "Plan defects fixed inline" — fixture parse
was the only "green" pre-iter signal the plan's Step 3/4 anchored on,
which masked the broader downstream consumer surface. Suggested
Boss-side reaction: future migration plans run an upfront
`grep -rln '"<rename target>"' crates/` to enumerate the full
consumer set before committing to a per-file enumeration.
- **`str_clone` reachability is implicit.** Pre-flight note 3 flagged
this as needing first-round-trip verification; verification is
implicit in `primitive_show_mono_symbols_synthesise` passing
(mono of `show 42 / true / "x" / 1.5` requires the four `show__T`
bodies to typecheck, including the `str_clone` call in `show__Str`).
No explicit isolated unit test for `str_clone`-in-lambda was added
because the integration pin is strictly stronger; consider an
isolated unit pin only if a future regression makes the integration
test slow to bisect.
- **Hash-stability pin uses a separate fixture, not the existing
`mono_hash_pin_smoke.ail.json`.** Plan Task 6 offered both options;
the separate-fixture path was chosen because `show_mono_pin_smoke.ail.json`
already existed (minted in Task 4) and reusing it kept the Eq/Ord
pin structurally isolated. Both fixtures live side-by-side; the
second test function in `mono_hash_stability.rs` mirrors the first
byte-for-byte modulo the fixture path and pin table.
- **`bench/check.py` exit 1 is the documented recurring noise envelope.**
`latency.implicit_at_rc.*` max-tail metric has been migrating
between runs since `audit-cma` (2026-05-12) — 7th consecutive
observation; `bench_list_sum.bump_s` is the 4th since `audit-mq`.
Plan Task 8 Step 4 explicitly anticipates "1-3 regressed metrics
ratify-without-attribution per conservative-call convention"; this
iter follows that. The baseline stays pristine.
## Known debt
- **iter 24.3 still pending.** Carries `print : forall a. Show a => a -> () !IO`
rewire to route through `show` + `io/print_str`. Spec at
`docs/specs/2026-05-13-24-show-print.md`. 24.2 is structurally
complete; 24.3 is operationally distinct.
- **mq1 cross-module fixtures unmigrated by design.**
`examples/mq1_xmod_constraint_class.ail.json` and `..._dep.ail.json`
reference `mq1_xmod_constraint_class_dep.Show` (cross-module, fully
qualified) and ship zero `show` call sites; no dispatch ambiguity
arises once `prelude.Show` lands. They stay as-is per the plan's
pre-flight ratification.
- **mq3 fixtures deliberately retain two-Show shapes.**
`examples/mq3_two_show_*` fixtures test the multi-class dispatch
scenarios (ambiguity + qualifier resolution) and stay unchanged in
24.2 by design.
## Files touched
**Created (3):**
- `crates/ail/tests/show_mono_synthesis.rs`
- `crates/ailang-check/tests/show_dispatch_pin.rs`
- `examples/show_mono_pin_smoke.ail.json`
**Modified (29):**
Prelude + DESIGN:
- `examples/prelude.ail.json` (+5 defs)
- `docs/DESIGN.md` (§"Prelude (built-in) classes" amendment)
22b fixture migration (21 files):
- `examples/test_22b1_dup_a.ail.json`
- `examples/test_22b1_dup_b.ail.json`
- `examples/test_22b1_dup_classmod.ail.json`
- `examples/test_22b1_dup_same_module.ail.json`
- `examples/test_22b1_orphan_class.ail.json`
- `examples/test_22b1_orphan_third.ail.json`
- `examples/test_22b1_orphan_third_classmod.ail.json`
- `examples/test_22b2_class_method_lookup.ail.json`
- `examples/test_22b2_constraint_declared.ail.json`
- `examples/test_22b2_instance_present.ail.json`
- `examples/test_22b2_missing_constraint.ail.json`
- `examples/test_22b2_no_instance.ail.json`
- `examples/test_22b2_two_fns_missing_constraint.ail.json`
- `examples/test_22b2_xmod_classmod.ail.json`
- `examples/test_22b2_xmod_classmod_noinst.ail.json`
- `examples/test_22b2_xmod_instance_present.ail.json`
- `examples/test_22b2_xmod_missing_constraint.ail.json`
- `examples/test_22b2_xmod_no_instance.ail.json`
- `examples/test_22b3_dup_call_sites.ail.json`
- `examples/test_22b3_no_call_sites.ail.json`
- `examples/test_22b3_shadow_class_method.ail.json`
22b consumer-test migration (5 source files + 1 snapshot):
- `crates/ail/tests/typeclass_22b2.rs`
- `crates/ail/tests/typeclass_22b3.rs`
- `crates/ailang-core/src/hash.rs` (ct4 hash pin)
- `crates/ailang-core/src/workspace.rs` (3 iter22b1 tests)
- `crates/ail/tests/mono_hash_stability.rs` (new show test fn)
- `examples/test_22b2_instance_present.prose.txt` (snapshot accept)
## Stats
`bench/orchestrator-stats/2026-05-13-iter-24.2.json`
-238
View File
@@ -1,238 +0,0 @@
# iter 24.3 — fn print polymorphic free fn + E2E + DESIGN.md sync; milestone 24 close
**Date:** 2026-05-13
**Started from:** c04c07f
**Status:** DONE
**Tasks completed:** 8 of 8
## Summary
Ships `fn print : forall a. Show a => (a borrow) -> () !IO` in
`examples/prelude.ail.json` with explicit-let body
`\x -> let s = show x in do io/print_str s`. The let-binder is
structurally pinned by `crates/ail/tests/print_mono_body_shape.rs`
(post-mono `print__Int.body` is `Term::Let → Term::App(show__Int) →
Term::Do(io/print_str)`). Three new E2E fixtures + tests verify the
full path: `show_print_smoke` (4 primitives) and `show_user_adt`
(`data IntBox` + `instance prelude.Show IntBox` + `print (MkIntBox 7)`)
both compile, run, and produce expected stdout; `show_no_instance`
fires a Show-aware `NoInstance` diagnostic with the canonical
DESIGN.md cross-reference. DESIGN.md §"Prelude (built-in) classes"
flips `print` from "ships in 24.3" to "shipped in iter 24.3" with the
explicit-let body + pin reference. DESIGN.md §"Float semantics" gains
a Show-Float NaN-spelling cross-reference paragraph. Roadmap P1
"Post-22 Prelude — Show + print rewire" entry checked off; new P2
entry "Retire `io/print_int|bool|float` effect-ops + migrate example
corpus to `print`" inserted at top of P2. **Three plan-defects-fixed-
inline** surfaced when running the user-ADT E2E: (a) `MonoTarget::
FreeFn::type_args` were carrying bare type-cons references that broke
the post-mono walk's registry lookup when the synthesised body lived
in a different module from the type — fixed by normalising via
`workspace_registry.normalize_type_for_lookup` at two
`mono.rs` collection sites; (b) codegen's cross-module fn resolution
went through `import_map`, but post-mono synthesised bodies may
reference modules their source template didn't import — fixed by
falling back to direct `module_user_fns` lookup at three codegen
sites (`resolve_top_level_fn`, `lower_app` cross-module arm,
`synth_with_extras` Var arm); (c) the synth FreeFnCall arm didn't push
residuals for declared constraints, so `Show (Int -> Int)` never
reached the typecheck-time discharge logic — fixed by walking
`Type::Forall.constraints` at the FreeFnCall site and pushing one
residual per constraint with the fresh-metavar substitution. Full
`cargo test --workspace` 556 passed / 0 failed (was 552 post-24.2,
+4 new = 1 print_mono_body_shape + 2 show_print_e2e + 1
show_no_instance_e2e). `bench/cross_lang.py` clean; `bench/compile_check.py`
+ `bench/check.py` exit 1 with documented noise-class metrics per the
audit-cma lineage envelope (baseline pristine, 8th consecutive
audit-grade observation). Milestone 24 closes structurally; only the
audit pipeline remains.
## Per-task notes
- iter 24.3.1: Appended `fn print` to `examples/prelude.ail.json`
after `fn ge` (last def pre-edit). 20 defs total (was 19). Body
is the literal `Term::Let { name: "s", value: App(show, [x]), body:
Do(io/print_str, [s]) }` with the constraint `Show a` bare per
mq.1 same-module canonical form. 552 tests stay green.
- iter 24.3.2: `crates/ail/tests/print_mono_body_shape.rs` asserts
post-mono `print__Int.body` shape. Initial test asserted outer
`Term::Lam` wrapper — corrected after the panic surfaced that
`FnDef.body: Term` is the *inner* function body for top-level fns
(params carried separately on `FnDef.params`). The test now asserts
directly `Term::Let → App(show__Int, [x]) → Do(io/print_str, [s])`,
faithful to the plan's intent (preserve the explicit-let-binder
discipline) while matching the actual AST shape.
- iter 24.3.3: `examples/show_print_smoke.ail.json` written with
nested `Term::Seq` of four `App(print, [lit])` calls (Int 42, Bool
true, Str "hello", Float bits=40091eb851eb851f for 3.14). `ail
build` clean; stdout: `42\ntrue\nhello\n3.14` — io/print_str
appends a newline via `@puts`.
- iter 24.3.4: `examples/show_user_adt.ail.json` written + 2-test
`crates/ail/tests/show_print_e2e.rs`. Surfaced three substantive
plan-defects-fixed-inline (Concerns below); after fix, both E2E
tests pass (`print (MkIntBox 7)` → stdout "7").
- iter 24.3.5: Extended NoInstance Float-aware arm in
`crates/ailang-check/src/lib.rs:770-779` with a parallel
`class == "prelude.Show"` branch. The arm fires for ANY type
lacking a Show instance (function types, user types without
instance) — cross-references DESIGN.md §"Prelude (built-in)
classes" verbatim. Negative fixture `examples/show_no_instance.ail.json`
(let-bound `f : Int -> Int` + `print f`) and pin test in
`crates/ail/tests/show_no_instance_e2e.rs` (asserts code,
Show-substring, Prelude-(built-in)-classes substring). After
the constraint-residual-push fix in synth (Concerns below), the
diagnostic fires at typecheck.
- iter 24.3.6: DESIGN.md §"Prelude (built-in) classes" milestone-24
paragraph amended: `print` flipped to past tense ("shipped in iter
24.3"), body shape + pin file reference added, retirement
follow-up named. DESIGN.md §"Float semantics" gained a new
paragraph in the "Unspecified" block cross-referencing `show 1.5`
/ `show nan` / `show inf` to `instance Show Float` via
`float_to_str`. ~10 lines added net (2697 → 2707).
- iter 24.3.7: Roadmap P1 "Post-22 Prelude — Show + print rewire"
entry flipped to `[x]` with a closing-summary block naming all
three shipped iters (24.1 f38bad8, 24.2 iter-24.2, 24.3 this
iter). New P2 entry "Retire `io/print_int|bool|float` effect-ops
+ migrate example corpus to `print`" inserted at top of P2,
before the existing operator-routing-through-Eq/Ord entry.
- iter 24.3.8: Full workspace 556 passed / 0 failed. `prelude_free_fns`
5/5 green; `mono_hash_stability` 2/2 green (Eq/Ord hashes
unchanged, Show hashes from 24.2 unchanged). `bench/cross_lang.py`
exit 0 (25/25 stable). `bench/compile_check.py` exit 1 with 1
noise-class regression on a `check_ms.*` metric (regressed metric
identity migrates between runs — pattern consistent with the
documented noise envelope per audit-cma lineage, 8th consecutive
observation). `bench/check.py` exit 1 with 2 improvements beyond
tolerance on `latency.explicit_at_rc.*` — same envelope, same
conservative call. Baseline pristine.
## Concerns
- **Three plan-defects-fixed-inline surfaced during Task 4 (user-ADT
E2E).** All three are necessary repairs to make the spec's stated
user-ADT trajectory (§"Data flow / print x at user type IntBox"
lines 297-310) work end-to-end. The plan did not anticipate them
because the test architecture envisioned (Tasks 3+4 sharing one
test file) didn't actually exercise the cross-module mono synthesis
before this iter; primitive-only smoke worked through the existing
pipeline.
- (a) **Bare type-cons in `MonoTarget::FreeFn::type_args`.** At
mono target-collection (`collect_mono_targets` + `collect_residuals_ordered`
free-fn arms), `subst.apply(meta)` returns the unification result
in whatever qualification the caller-module's view used. For a
user-defined type referenced from `main` (in `show_user_adt`),
`IntBox` came out bare. The downstream `synthesise_mono_fn_for_free_fn`
then substituted bare `IntBox` everywhere in the synthesised body,
and Phase 3 rewrite of the synthesised body (run in PRELUDE
caller-module context, where `IntBox` is not a known TypeDef) saw
`Type::Con{IntBox}` that didn't normalise to the registry-keyed
`show_user_adt.IntBox`. Lookup missed → no `show__IntBox` target
scheduled → bare `Var "show"` persisted post-mono. Fix:
`mono.rs` lines 685-696 + 1267-1278 normalise via
`env.workspace_registry.normalize_type_for_lookup(module_name, &resolved)`
before pushing the type into `MonoTarget::FreeFn::type_args`. Same
invariant applies symmetric to the existing
`r_ty_norm = normalize_type_for_lookup(...)` calls already in
`collect_mono_targets`'s class-method arm.
- (b) **Codegen cross-module references via import_map.** Post
fix-(a), the synthesised body now correctly references
`show_user_adt.show__cb5eb497` from inside `prelude.print__cb5eb497`.
But `prelude` does not have `show_user_adt` in its `import_map`
(prelude is the base; user modules import IT, not the other way
around). Three codegen sites raised UnknownVar:
`resolve_top_level_fn:2200`, `lower_app` cross-module arm:1981,
`synth_with_extras` Var arm:2777. All three now fall back to a
direct `module_user_fns.contains_key(prefix)` (or
`module_def_ail_types.contains_key(prefix)`) lookup when the
prefix isn't in the current module's import_map. This is the
post-mono synthesised-body invariant: cross-module references in
synthesised bodies may bypass the source template's imports
(both ends were independently typechecked under their own module
contexts before mono ran; the cross-module reference is created
by mono, not by the source). Doc comments cross-reference all
three sites.
- (c) **Poly-free-fn constraint residuals not pushed at synth.**
Pre-iter-24.3, the FreeFnCall arm at `lib.rs:2820` had
`constraints: _` — declared constraints of the poly free fn were
ignored. For all milestone-23 fixtures (`ne`/`lt`/`le`/`gt`/`ge`
at primitive types), this was latent because every call site had
a registry entry; the mono pass silently scheduled the
corresponding `eq__T`/`compare__T` targets, and codegen succeeded.
But for negative cases — `print f` where `f : Int -> Int` has no
Show instance — the constraint never reached the typecheck-time
`NoInstance` discharge (lib.rs:1880-1905). The build would emit
a confusing post-mono `unknown variable: show` from codegen
rather than the right typecheck-phase diagnostic. Fix: at
`lib.rs:2820+`, walk `Type::Forall.constraints` at FreeFnCall
sites, substitute the rigid vars with their fresh metavars, and
push one `ResidualConstraint` per declared constraint. The
discharge loop then fires the correct NoInstance with full
diagnostic context (including the Show-aware addendum from
Task 5). Pre-existing milestone-23 tests stay green because
primitive-type Show/Eq/Ord registry entries discharge silently.
- **bench/compile_check.py + check.py exit 1.** Both report
noise-class metrics (1 regression / 2 improvements respectively).
Metric identity migrates between consecutive runs — pattern
consistent with the documented `latency.implicit_at_rc.*` /
`check_ms.*` noise envelope per audit-cma + audit-ms + audit-eob +
audit-ct-tidy + audit-mq + iter-24.2 lineage. 8th consecutive
observation. Baseline pristine per conservative-call convention.
- **Plan deviation on Task 2 (IR-shape pin).** The plan's test stub
asserted `Term::Lam { body: Term::Let { ... } }` for `print__Int`.
The actual `FnDef.body: Term` shape for top-level fns is the
*inner* function body (params separate); no outer `Term::Lam`. The
test was corrected to assert `Term::Let { ... }` directly. The
load-bearing property (explicit let-binder for heap-Str RC
discipline) is identical between the plan's intent and the
shipped assertion; only the wrapper assumption changed. Doc
comment in the test file explains the FnDef.body shape contract.
## Known debt
- **Milestone 24 closes structurally with this iter.** The standard
`audit` pipeline (architect drift review + bench regression
attribution) runs next. The three plan-defects-fixed-inline above
may merit architect commentary on whether the fix sites belong as
permanent invariants or are themselves transitional. Specifically:
fix (a) implies that `MonoTarget::FreeFn::type_args` carries
CANONICAL types post-collection — this is a strengthening of the
pre-iter invariant which was "subst.apply result"; future planner
work that touches the mono pass should keep that strengthening
visible.
- **P2 entry "Retire io/print_int|bool|float effect-ops" is queued.**
The new entry's `~86 fixtures affected` count is from a rough
grep against `do io/print_<T>`; precise count + migration plan is
the planner's job when the milestone opens.
- **Negative-fixture coverage is single-shape.** `show_no_instance.ail.json`
uses `f : Int -> Int`. Other NoInstance shapes (user type without
instance, `print x` where `x : SomeUnsupportedClass`) are not
separately tested — the NoInstance arm fires for any non-prelude-Show
type by construction, so the single fixture is structurally
representative.
## Files touched
**Created (6):**
- `crates/ail/tests/print_mono_body_shape.rs`
- `crates/ail/tests/show_print_e2e.rs`
- `crates/ail/tests/show_no_instance_e2e.rs`
- `examples/show_print_smoke.ail.json`
- `examples/show_user_adt.ail.json`
- `examples/show_no_instance.ail.json`
**Modified (6):**
- `examples/prelude.ail.json` (+1 def: `fn print`)
- `crates/ailang-check/src/lib.rs` (NoInstance Show-aware branch +
FreeFnCall constraint-residual push)
- `crates/ailang-check/src/mono.rs` (FreeFn type_args canonical-form
normalisation, 2 sites)
- `crates/ailang-codegen/src/lib.rs` (cross-module reference fallback
for post-mono synthesised bodies, 3 sites: `resolve_top_level_fn`,
`lower_app`'s cross-module call arm, `synth_with_extras`'s Var arm)
- `docs/DESIGN.md` (§"Prelude (built-in) classes" milestone-24
paragraph + §"Float semantics" Show-Float cross-reference)
- `docs/roadmap.md` (P1 Show+print → [x]; new P2 entry at top)
## Stats
`bench/orchestrator-stats/2026-05-13-iter-24.3.json`
-157
View File
@@ -1,157 +0,0 @@
# iter 24.tidy — close 5 actionable drift items from audit-24
**Date:** 2026-05-13
**Started from:** 0e27533
**Status:** DONE
**Tasks completed:** 6 of 6
## Summary
Closes the 5 actionable drift items audit-24 surfaced (3× [high] +
2× [medium]): T1 adds a new DESIGN.md subsection
`### Cross-module references in synthesised bodies` under
`## Decision 11`'s `### Resolution and monomorphisation`, anchoring
the three iter-24.3 strengthenings as load-bearing lockstep
invariants (canonical-form type_args / import_map-fallback / FreeFnCall
constraint-residual push). T2 pins the codegen import_map-fallback
path with an integration test asserting the synthesised
`prelude.print__IntBox` body contains a `show_user_adt.show__IntBox`
Var AND the prelude source module does not import `show_user_adt`.
T3 pins the bare-name poly-fn dot-qualified-branch invariant by
asserting `show_no_instance.ail.json` produces exactly one
`no-instance` diagnostic and zero `unknown-variable` /
`internal` diagnostics. T4 tightens
`crates/ailang-check/src/lib.rs:2852` from `unwrap_or_default()` to
`.expect(...)` with a registry-coherence panic message; the strict
tightening surfaced no existing violations (558 tests pass post-edit).
T5 extracts the duplicated `subst.apply + is_fully_concrete +
normalize_type_for_lookup` body into a single `apply_subst_and_normalize`
helper returning `Option<Type>`, replacing the two byte-identical
sites at `mono.rs::collect_mono_targets` (685-714) and
`::collect_residuals_ordered` (1284-1303); the byte-identity
invariant is now enforced by construction, `mono_hash_stability`
green on both primitive Show + Eq/Ord hashes. T6 verifies: 558
tests pass (was 556 + 2 new pins, exactly the expected count);
`bench/cross_lang.py` exit 0 (25/25 stable); `bench/compile_check.py`
exit 0 with 4 `check_ms.*` noise-class regressions in the documented
audit-24 lineage; `bench/check.py` exit 0 with 3 `latency.implicit_at_rc.*`
+ 4 `latency.explicit_at_rc.*` noise-class observations in the same
lineage (10th consecutive observation; baseline pristine).
## Per-task notes
- iter 24.tidy.1 (T1): DESIGN.md +68 lines, new `### Cross-module
references in synthesised bodies` subsection inserted between
`### Resolution and monomorphisation` (ends line 1693) and the
re-anchored `### Defaults and superclasses`. Subsection enumerates
three lockstep invariants with cross-references to source files
(`mono.rs::collect_mono_targets` / `::collect_residuals_ordered`,
`codegen/lib.rs::resolve_top_level_fn` / `::lower_app` /
`::synth_with_extras`, `check/lib.rs` synth FreeFnCall arm) and
to the protective test pins. Closing paragraph names the lockstep
partnership and the regression cost of loosening any one.
- iter 24.tidy.2 (T2): New `crates/ail/tests/codegen_import_map_fallback_pin.rs`.
Loads `examples/show_user_adt.ail.json`, runs `check_workspace` +
`monomorphise_workspace`, finds the synthesised `Def::Fn` whose
name starts with `print__` in the `prelude` post-mono module,
recursively walks its body looking for a `Term::Var { name }`
whose name starts with `show_user_adt.` and contains `show__`,
and confirms the prelude source module's `Vec<Import>` does NOT
include `show_user_adt`. Plan draft adapted at three points: (a)
`Term::App` field name is `callee` not `fn` (the latter is the
serde rename; Rust field is `callee`); (b) `imports.iter()` gives
`&Import` not `&String` so the check is on `imp.module`; (c)
recursive walker extended with `Seq` / `Clone` / `ReuseAs` /
`LetRec` / `If` / `Match` / `Ctor` arms for exhaustiveness — all
data-shape adaptations the plan explicitly authorised. Test passes
first-shot (1 passed).
- iter 24.tidy.3 (T3): New `crates/ail/tests/polyfn_dot_qualified_branch_pin.rs`.
Loads `examples/show_no_instance.ail.json`, asserts exactly one
`no-instance` diagnostic AND zero `unknown-variable` / `internal`
diagnostics. The plan's draft `d.code` field accesses match the
actual `Diagnostic` struct (verified against
`crates/ailang-check/src/diagnostic.rs:131`). Test passes first-shot.
- iter 24.tidy.4 (T4): One-line tightening at
`crates/ailang-check/src/lib.rs:2858` —
`.unwrap_or_default()` → `.expect("class_methods registry coherence ...")`
with the verbatim plan message. The `.expect(...)` covers the
registry-coherence invariant: a declared constraint's class is
expected to be in `env.class_methods` because the pre-pass
`MissingClass` diagnostic should have rejected it earlier; reaching
this point with `None` means workspace-registry / class-index drift.
No existing test violates the new invariant — 558 tests pass.
- iter 24.tidy.5 (T5): New `apply_subst_and_normalize` helper at
`crates/ailang-check/src/mono.rs:555-583` (immediately preceding
`collect_mono_targets`). Signature
`fn apply_subst_and_normalize(env: &crate::Env, module_name: &str, m: &Type, subst: &crate::Subst) -> Option<Type>`.
Returns `Some(normalised)` if `subst.apply(m)` is fully concrete
(via `crate::is_fully_concrete`); else `None`. Two call sites
retrofit: site 1 at `collect_mono_targets` (now ~ lines 707-727)
keeps its rigid-var / Unit-default branching at the call site
(helper-None → check `contains_rigid_var(&subst.apply(m))`; if
rigid set `has_rigid = true` and break; else push `Type::unit()`);
site 2 at `collect_residuals_ordered` (now ~ lines 1284-1290) uses
`apply_subst_and_normalize(&env, ...).unwrap_or_else(Type::unit)`.
Site 1 needed `&env` (local owned `Env`) where the plan draft had
bare `env` — one-character edit. `mono_hash_stability` both tests
pass (primitive Show + Eq/Ord mono-symbol hashes bit-identical),
full workspace 558 green. Byte-identity invariant now enforced by
construction at the helper boundary; each call site explicitly
documents its post-helper-None policy.
- iter 24.tidy.6 (T6): Integration verification. Full workspace
`cargo test --workspace --no-fail-fast` = 558 passed, 0 failed
(matches plan's prediction: 556 baseline + 2 new pins). No
FAILED entries among milestone-24 specific test groups
(show_/print_/prelude_free_fns/mono_hash_stability/typeclass_22b/mq3).
Bench: `cross_lang.py` exit 0, 25/25 stable; `compile_check.py`
exit 0 with 4 `check_ms.*` ms-level noise-class regressions
(`hello`, `borrow_own_demo`, `bench_tree_walk`, `bench_hof_pipeline`);
`check.py` exit 0 with 3 noise-class regressions
(`latency.implicit_at_rc.{p99_9_us, max_us}` first-sightings in
this iter) + 4 noise-class improvements
(`latency.explicit_at_rc.{p99_us, p99_over_median}` improvements
continuing the documented audit-cma → audit-24 lineage). 10th
consecutive observation of metric-identity-migrating noise;
baseline pristine, conservative-call convention holds per the
documented lineage.
## Concerns
- T5 pre-extraction reading vs plan: the plan said site 1's unbound-
meta path "likely a `continue` without push" — actual pre-extraction
code did `type_args.push(Type::unit())` for unbound metas (iter 23.4
behaviour, comment in place). I preserved the actual behaviour, not
the plan's guess. Lockstep with site 2 (which has the same Unit-default
policy) is improved post-extraction. Observation, not correctness.
- T2 plan draft had three data-shape mismatches against actual Rust
schema (`Term::App.callee` vs plan's `fn`, `Vec<Import>` vs
`Vec<String>`, exhaustive Term variants). The plan explicitly
flagged these as "implementer adapts to the actual field name"
cases; not plan defects but recon imprecisions.
## Known debt
- audit-24 [medium-3] negative-test coverage breadth → deferred to
P3 downstream-corpus-migration milestone (carried over from
audit-24, not closed here).
- audit-24 [low-1] bench/architect_sweeps.sh noise → carry-on,
pre-milestone-24 lines not new drift.
## Files touched
- `docs/DESIGN.md` — new subsection (T1), +68 lines.
- `crates/ail/tests/codegen_import_map_fallback_pin.rs` — new pin (T2).
- `crates/ail/tests/polyfn_dot_qualified_branch_pin.rs` — new pin (T3).
- `crates/ailang-check/src/lib.rs` — one-line tightening at :2858 (T4).
- `crates/ailang-check/src/mono.rs` — new helper + two call-site
replacements (T5).
## Stats
`bench/orchestrator-stats/2026-05-13-iter-24.tidy.json`
@@ -1,94 +0,0 @@
# iter bugfix-instance-body-unbound-var — instance method bodies now walk through check_fn
**Date:** 2026-05-13
**Started from:** 72f3f6541b3e41f2d4a7706dcad5bd784ec07d81
**Status:** DONE
**Tasks completed:** 1 of 1
## Summary
Fixes the bug surfaced by the 2026-05-13 form-a fieldtest: an unbound
identifier inside an `instance` method body slipped past `ail check`
and surfaced only at `ail build` as the degraded internal-error
diagnostic `monomorphise_workspace: unknown identifier: ...`. Root
cause: `check_def`'s `Def::Class(_) | Def::Instance(_)` arm at
`crates/ailang-check/src/lib.rs:1574-1595` early-returned `Ok(())`
without ever invoking the body walker — the comment claimed "instance-
body typechecking (with class-method substitution) land[s] in 22b.2",
but that wiring was never implemented. Workspace-load coherence checks
(`build_registry`'s Orphan/Duplicate/MissingMethod) only inspect
instance schema, not method-body identifier graphs.
Fix is the minimum-scope change at that call site: split the combined
arm so `Def::Class(_)` continues to be schema-only, while
`Def::Instance(inst)` calls a new helper `check_instance` that, for
each `InstanceMethod`, lifts the body `Term::Lam` into a synthetic
`FnDef` (class-method-substituted via `substitute_rigids` /
`substitute_rigids_in_term` against the class's `param` name as
declared in `env.class_methods[(qualified_class, method)]`) and
hands it to the existing `check_fn`. The reuse of `check_fn` is what
gets the body walked through the same identifier-resolution path as
fn bodies, so an unbound name fires `[unbound-var]` at `ail check`
with exit 1 and the standard structured diagnostic.
Per the carrier's "minimal fix, no surrounding cleanup" constraint:
no widening to `Def::Class` (still schema-only); no changes to the
`monomorphise_workspace` diagnostic wording (the defence-in-depth
internal-error path stays as a backstop); no changes to mono.rs,
codegen, or any other crate.
## Per-task notes
- iter bugfix-instance-body-unbound-var.1: split the
`Def::Class(_) | Def::Instance(_)` arm into `Def::Class(_) =>
Ok(())` and `Def::Instance(inst) => check_instance(inst, env,
out_warnings)`. New free fn `check_instance` reads
`env.class_methods[(qualify_class_ref_in_check(&inst.class,
&env.current_module), im.name)]` to recover the class's `param`
name, builds a `{class_param: inst.type_}` substitution, applies
it to the Lam's `param_tys` / `ret_ty` and to the Lam's body
Term (via the existing `substitute_rigids` /
`substitute_rigids_in_term` helpers), then constructs a synthetic
`FnDef` (`name = "<class>::<method>"`, `ty =
Type::fn_implicit(subst_param_tys, subst_ret_ty,
lam.effects)`, params from Lam, body = substituted Lam body)
and calls `check_fn` on it.
First-attempt sketch wrapped the Lam's `param_tys` / `ret_ty`
unsubstituted on the assumption that the on-disk canonical form
was post-substitution — the `codegen_import_map_fallback_pin`
regression (and a sweep of the prelude `(typed x a) (typed y a)`
shape) immediately disproved that. The substitution step was
added at the second iteration of the implementer phase, before
reaching the spec / quality phases.
Non-Lam method bodies are silently skipped — current schema
doesn't admit them and the pre-fix behaviour was also silent.
## Concerns
- None.
## Known debt
- `check_instance` does **not** verify that the substituted method
signature matches the corresponding `ClassMethodEntry.method_ty`
(with `class_param ↦ inst.type_`). The body-walk catches unbound
vars and effect-mismatches against the Lam's own declared types,
but a malformed instance whose Lam declares
`(typed x (con Int)) (typed y (con Int)) -> Bool` for an
`instance Eq Bool` body would currently typecheck cleanly. The
workspace-load `MissingMethod` / coherence checks only inspect
the (class, type) pair, not the method's declared type. Out of
scope per the carrier's "minimal fix" constraint; queued as a
follow-up roadmap candidate ("instance-method declared-type
vs class-method-type cross-check").
## Files touched
- `crates/ailang-check/src/lib.rs``check_def` arm split,
new `check_instance` helper.
## Stats
bench/orchestrator-stats/2026-05-13-iter-bugfix-instance-body-unbound-var.json
@@ -1,52 +0,0 @@
# iter drift-test-narrowing — design_schema_drift.rs scans §"Data model" only
**Date:** 2026-05-13
**Started from:** rustdoc-sweep working-tree state
**Status:** DONE
## Summary
`crates/ailang-core/tests/design_schema_drift.rs` is the
drift-detection layer between AST enum definitions in
`crates/ailang-core/src/ast.rs` and the canonical schema
documentation in `docs/DESIGN.md` §"Data model". Each AST variant
must have its JSON-schema anchor (e.g. `"t": "lit"`, `"k": "fn"`)
documented; the tests assert presence of each anchor.
Pre-iter, the assertion was `DESIGN_MD.contains(anchor)`
substring search over the whole document. That was loose: an
anchor present only in §"Decision 11" (or any other discussion
section) was treated as documented even though §"Data model" —
the canonical schema reference section — was missing it. The
audit-form-a-precursor `[high]` drift report flagged this as the
silent-pass failure mode.
This iter narrows the scan. New helper `data_model_section()`
extracts the slice of `DESIGN_MD` from the `## Data model` header
to the next top-level `## ` header. The 7 existing anchor tests
now call `data_model_section().contains(anchor)` instead of
`DESIGN_MD.contains(anchor)`. All 38 anchors verified present in
§"Data model" pre-edit (no regressions). A new test
`data_model_section_is_bounded` pins the extractor itself
(starts-with `## Data model`, does not bleed past `\n## Pipeline`,
length > 1 KB) so a future refactor cannot silently regress the
extractor to whole-document scanning.
## Files touched
- `crates/ailang-core/tests/design_schema_drift.rs` (M)
## Verification
- `cargo test -p ailang-core --test design_schema_drift`
8 passed (was 7; +1 for the bounded-section pin), 0 failed.
- `cargo test --workspace` → 563 passed (was 562; +1 from the
new pin), 0 failed.
## Concerns
- (none)
## Known debt
- (none)
-71
View File
@@ -1,71 +0,0 @@
# Iter form-a.0 — Prelude Pilot
**Date:** 2026-05-13
**Parent spec:** `docs/specs/2026-05-13-form-a-default-authoring.md`
**Parent plan:** `docs/plans/2026-05-13-iter-form-a.0.md`
**Started from:** 4c2a3c5d0811aafb1bce5d6de8ad9a938fd98681
## Outcome
`examples/prelude.ail` rendered from `examples/prelude.ail.json` via
`ail render` (116 lines / 6386 bytes — exact recon match). The two
auto-discovering roundtrip tests in
`crates/ailang-surface/tests/round_trip.rs` both pass on the new
fixture without test-code changes:
- `every_ail_fixture_matches_its_json_counterpart` — green; the
`prelude.ail` parses to canonical bytes byte-equal to
`prelude.ail.json`.
- `parse_then_print_then_parse_is_idempotent_on_every_ail_fixture`
— green; the Form-A printer is idempotent over prelude.
`prelude.ail.json` is retained on disk per spec §A2; iter 1 deletes
it alongside the bulk test-infrastructure refactor. This is the
only iteration in the milestone where a fixture is dual-form.
## Why this iter is the pilot
Spec §A2 names prelude as the pilot because it exercises the widest
cross-section of language features in a single file: typeclasses
with superclasses (`Ord extends Eq`), polymorphic free functions
with constraints (`forall<a> where Show a fn print`), IO-effect
signatures, primitive ctor patterns, and the loader's auto-injection
path. A green roundtrip on prelude is strong evidence the bulk
migration in iter 1+ will not surface render or parse bugs.
## Verifications run
- `cargo run -q --bin ail -- render examples/prelude.ail.json > examples/prelude.ail` — exit 0
- `wc -lc examples/prelude.ail``116 6386` (exact recon prediction)
- `head -n 5 examples/prelude.ail` — first line `(module prelude`, second ` (data Ordering` (exact recon prediction)
- `cargo test -p ailang-surface --test round_trip every_ail_fixture_matches_its_json_counterpart` — PASS (1/1)
- `cargo test -p ailang-surface --test round_trip parse_then_print_then_parse_is_idempotent_on_every_ail_fixture` — PASS (1/1)
- `cargo test --workspace` — PASS, **558 tests green**, identical to pre-iter baseline (audit-24 / iter-24.tidy lineage).
## Scope NOT touched
Per spec §"Iteration scope": no test-infrastructure changes, no
CLAUDE.md / DESIGN.md edits, no deletion of `prelude.ail.json`,
no migration script yet. Those land in iter 1.
## INDEX append — Boss-side
Plan Task 2 Step 2 asks for an INDEX.md append. Per the
`ailang-implement-orchestrator` Iron Law, `docs/journals/INDEX.md`
is Boss-only. The orchestrator does not touch it. The Boss appends
the line at commit time. Suggested INDEX line (from the plan
verbatim, byte/line counts confirmed by Task 1 Step 2):
```
- 2026-05-13 — iter form-a.0: prelude pilot for Form-A-as-default-authoring milestone — examples/prelude.ail rendered (116 lines / 6386 bytes) via `ail render`; two auto-discovering roundtrip tests (every_ail_fixture_matches_its_json_counterpart + parse_then_print_then_parse_is_idempotent_on_every_ail_fixture) green on the new fixture without test-code changes; prelude.ail.json retained per spec §A2 (singular dual-form iter, deletion lands iter 1 with bulk test-infra refactor); cargo test --workspace green → 2026-05-13-iter-form-a.0.md
```
## Files touched
- `examples/prelude.ail` (new, ~6386 bytes / 116 lines)
- `docs/journals/2026-05-13-iter-form-a.0.md` (this file)
- `bench/orchestrator-stats/2026-05-13-iter-form-a.0.json` (stats)
## Stats
`bench/orchestrator-stats/2026-05-13-iter-form-a.0.json`
-431
View File
@@ -1,431 +0,0 @@
# iter form-a.1 — Big-bang migration (Tasks 1-5: additive phase + relocation)
**Date:** 2026-05-13
**Started from:** 1a065b37 (post-iter 24.tidy)
**Status:** DONE (Tasks 1-5 only; Tasks 6-12 deferred to next dispatch)
**Tasks completed:** 5 of 12
## Summary
First half of the form-a-default-authoring milestone-close iter: T1
adds three new tests (parse-determinism + CLI-pipeline-idempotency +
carve-out-inventory-but-`#[ignore]`'d), T2 bulk-renders the 99
missing `examples/<stem>.ail` files via `ail render` (corpus 58 →
157), T3 + T4 migrate ~26 test files from `ailang_core::load_workspace`
to `ailang_surface::load_workspace` (Group A) and flip subprocess
fixture-suffix strings from `.ail.json` to `.ail` (Group B), T5
relocates `#[cfg(test)] mod tests` blocks from production source
(`ailang-core/src/hash.rs`, `ailang-core/src/workspace.rs`
non-carve-out subset, `ailang-codegen/src/lib.rs:3717-3799`) to
integration test crates that carry `ailang-surface` as a
dev-dependency, eliminating the production-source dependency on
`.ail.json` fixture reads. The remaining Tasks 6-12 (bench-driver
suffix flips, e2e diff-test rewrite, the actual 156-file
`.ail.json` deletion, retiring obsolete roundtrip tests +
schema_coverage corpus flip, DESIGN.md §Roundtrip Invariant
restatement, CLAUDE.md + DESIGN.md doctrine edits, WhatsNew +
roadmap close) run in the Boss's next dispatch on this iter ID.
`cargo test --workspace` is green at every per-task boundary (560
passed + 4 ignored at iter-half close; 558 + 3 new 1 transit
`#[ignore]` on the inventory test). One Task-2 plan defect surfaced
inline and was repaired by pre-emptive forward-pull of Task-3
migrations.
## Per-task notes
- **iter form-a.1.1** — Added `parse_is_deterministic_over_every_ail_fixture`
(round_trip.rs) and `cli_parse_then_render_then_parse_is_idempotent`
(roundtrip_cli.rs); created `crates/ailang-core/tests/carve_out_inventory.rs`
with the 8-file hardcoded list and `#[ignore]`'d until Task 8.
The plan's `cli_parse_then_render_then_parse_is_idempotent` body
used an `ail()` helper that doesn't exist in the repo (assert_cmd
is not a dev-dep); adapted to use `Command::new(ail_bin())`
matching the file's existing pattern. Plan acknowledged this
possibility — "verify before writing the body; if `list_ail_fixtures`
is missing in roundtrip_cli.rs, add a parallel helper at lines 47+."
- **iter form-a.1.2** — Rendered the 99 missing `.ail` files. The
bulk render exposed a recon defect: `load_workspace` (since
ext-cli.1) prefers `.ail` siblings for *imports* even when the
entry is `.ail.json`. The seven Group-A test files whose entries
were `.ail.json` but whose imports gained newly-rendered `.ail`
siblings broke at Task 2's green-gate. Repair pulled forward five
Task-3 migrations (mono_xmod_ctor_pattern, 3 sites in
typeclass_22b2, 1 site in typeclass_22b3, env_construction_pin,
4 sites in ailang-check/tests/workspace.rs, ctt2_registry_rekey)
plus `#[ignore]`'d the 4 fixture-loading tests in
`ailang-core/src/workspace.rs` `mod tests` with explicit
`form-a.1 transit: Task 5 relocates...` markers. Also added
`ailang-surface = { path = "../ailang-surface" }` to
`crates/ailang-core/Cargo.toml` `[dev-dependencies]` (required
for the ctt2_registry_rekey integration test to use
`ailang_surface::load_workspace`).
- **iter form-a.1.3** — Finished Group A migration across the
remaining 12 Group-A files (10 + the 2 already migrated in T2
scope-pull). Two files (`mono_recursive_fn.rs`,
`mono_xmod_qualified_ref.rs`) were Group-B-pattern (subprocess
CLI, suffix-only flip) but listed in plan's Group A; treated
per their actual shape.
- **iter form-a.1.4** — Group-B migration: bulk regex flip of
`build_and_run("X.ail.json")``build_and_run("X.ail")` (3
patterns) in e2e.rs across 59 invocations; plus targeted
subprocess `examples/<fixture>.ail.json``.ail` flips at 11
more e2e.rs sites (6× `ws_main`, 2× `sum`, 1× `escape_local_demo`,
1× `list_map_poly`, 1× `std_either` left as-is per its raw-bytes
roundtrip semantics). Four additional sites in e2e.rs preserved
intentionally for raw-JSON inspection (`borrow_own_demo`,
`reuse_as_demo`, the `sum.ail.json` diff-test pending Task 7
rewrite, and the `ail_run_accepts_ail_source...` dual-form smoke).
Four plan-Group-B files (`mono_hash_stability`, `prelude_free_fns`,
`print_mono_body_shape`, `show_mono_synthesis`) were
Group-A-pattern (consumers of `load_workspace`); migrated both
imports AND paths.
- **iter form-a.1.5** — Relocated 10 fixture-loading tests from
`ailang-core/src/workspace.rs` `mod tests` to new
`crates/ailang-core/tests/workspace_pin.rs` (integration test
crate); relocated all 8 tests from `ailang-core/src/hash.rs`
`mod tests` plus 2 in-memory unit tests to
`crates/ailang-core/tests/hash_pin.rs`; relocated 3 tests from
`ailang-codegen/src/lib.rs:3717-3799` to
`crates/ailang-codegen/tests/eq_primitives_pin.rs`; migrated
`ailang-prose/tests/snapshot.rs` (helper + 8 fixtures) to `.ail`
+ `ailang_surface::load_module`. Carve-out tests stay in-place
(3× `class_param_in_applied_position` / `superclass_with_wrong_param`
/ `constraint_with_unbound_var` for 22b2; 3× `ct1_fixture_*`
for ct.1). Tempdir-based loader tests
(`user_module_named_prelude_is_rejected`, `detects_import_cycle`,
`module_not_found_yields_structured_error`) stay in-place too —
they don't consume the examples corpus, so they don't need
migration for the Form-A goal. Added `ailang-surface` as a
`[dev-dependencies]` entry to `ailang-codegen/Cargo.toml` and
`ailang-prose/Cargo.toml`. Hash.rs has `mod tests {}` placeholder
kept; workspace.rs `mod tests` shrunk from 73 to 57 tests +
6 carve-outs unchanged.
## Concerns
- **e2e.rs raw-JSON-inspect tests** (4 sites: `borrow_own_demo_modes_are_metadata_only`,
`reuse_as_demo_under_rc_uses_inplace_rewrite`, `diff_detects_changed_def`,
`render_parse_round_trip_canonical`, plus the dual-form smoke
`ail_run_accepts_ail_source_with_same_stdout_as_ail_json`) currently
read raw `.ail.json` bytes for substring asserts or byte-roundtrip
comparison. They pass today (the `.ail.json` files still exist
pre-T8) but will break at T8 when `.ail.json` siblings are deleted.
Plan Task 7 only addresses the `diff_detects_changed_def` test (the
one that mutates JSON). The other three are out of T1-5 scope and
inherit to Boss for T6-12 dispatch resolution (either parse-derived
JSON via `ail parse` per T7 pattern, or `#[ignore]` with explicit
marker).
- **`ailang-core/src/workspace.rs` `mod tests`**: 3 tempdir-based
loader-mechanism tests (lines :1743, :1759, :1776 pre-relocation;
current line numbers shifted by the deletions) remain in-place
per implementer-judgement (they don't consume `examples/`, so
the Form-A goal is trivially satisfied). Plan suggested
relocating them for doctrinal cleanup; scope reduction is
deliberate. They continue to test what they always tested
(loader cycle/missing-module/user-prelude-conflict behaviour)
and pass.
- **Plan recon defect on Group-A vs. Group-B classification**:
`mono_hash_stability.rs`, `prelude_free_fns.rs`,
`print_mono_body_shape.rs`, `show_mono_synthesis.rs` were listed
in plan's Group B but actually use `ailang_core::workspace::load_workspace`
directly (Group-A pattern). Migrated correctly by switching both
the import and the path. Recorded so a future recon-tightening
pass on the planner skill can review whether path-grep-only
recon is sufficient for Group A/B classification.
- **`mono_recursive_fn.rs` and `mono_xmod_qualified_ref.rs`** were
symmetric — listed in plan's Group A but actually CLI subprocess
callers (Group-B pattern). Suffix-only flip applied as if Group B.
## Known debt
- 4 raw-JSON-inspect tests in e2e.rs need T7-style rewrites or
`#[ignore]` markers before Task 8's `.ail.json` deletion lands
(see Concerns).
- 3 tempdir tests in `ailang-core/src/workspace.rs` `mod tests`
could be relocated as doctrinal cleanup but functionally don't
need to be (see Concerns).
- `mod tests {}` placeholder in `ailang-core/src/hash.rs` could
be deleted entirely; left as a structural breadcrumb pointing at
the integration test crate.
## Files touched
Working tree: 37 modified + 14 new test files + 99 new `.ail`
fixtures = 150 paths. Detail:
### Modified production / test source (37)
- `crates/ail/tests/`: codegen_import_map_fallback_pin.rs,
ct1_check_cli.rs, e2e.rs (extensive build_and_run + 11
subprocess sites), eq_float_noinstance.rs, eq_ord_e2e.rs,
floats_e2e.rs, ir_snapshot.rs, mono_hash_stability.rs,
mono_recursive_fn.rs, mono_unification.rs, mono_xmod_ctor_pattern.rs,
mono_xmod_qualified_ref.rs, mq3_multi_class_e2e.rs,
polyfn_dot_qualified_branch_pin.rs, prelude_free_fns.rs,
print_mono_body_shape.rs, roundtrip_cli.rs, show_mono_synthesis.rs,
show_no_instance_e2e.rs, show_print_e2e.rs, typeclass_22b2.rs,
typeclass_22b3.rs, typeclass_22c.rs (23 files)
- `crates/ailang-check/tests/`: env_construction_pin.rs,
method_collision_pin.rs, show_dispatch_pin.rs, workspace.rs (4)
- `crates/ailang-codegen/`: Cargo.toml + src/lib.rs (2)
- `crates/ailang-core/`: Cargo.toml + src/hash.rs + src/workspace.rs
+ tests/ctt2_registry_rekey.rs (4)
- `crates/ailang-prose/`: Cargo.toml + tests/snapshot.rs (2)
- `crates/ailang-surface/tests/round_trip.rs` (1)
- `Cargo.lock` (1)
### New (14 test files + 99 rendered .ail fixtures)
- `crates/ailang-codegen/tests/eq_primitives_pin.rs` (relocated T5)
- `crates/ailang-core/tests/hash_pin.rs` (relocated T5)
- `crates/ailang-core/tests/workspace_pin.rs` (relocated T5)
- `crates/ailang-core/tests/carve_out_inventory.rs` (new T1)
- 99× `examples/<stem>.ail` rendered via `ail render` (T2)
## Stats
bench/orchestrator-stats/2026-05-13-iter-form-a.1.json
---
# iter form-a.1 — Big-bang migration (Tasks 6-12: bench drivers, e2e rewrites, deletion, doctrine, milestone close)
**Date:** 2026-05-13
**Started from:** 9332d1e (post-T1-5 commit 77b28ad + plan-amendment 9332d1e)
**Status:** DONE (Tasks 6-12; iter form-a.1 fully closed)
**Tasks completed:** 7 of 7 (this dispatch); 12 of 12 (iter total)
## Summary
Second half of the form-a-default-authoring milestone-close iter: T6
flips the bench-driver fixture suffix from `.ail.json` to `.ail` (4
Python scripts + run.sh). T7 re-authors three e2e tests
(`diff_detects_changed_def`, `borrow_own_demo_modes_are_metadata_only`,
`reuse_as_demo_under_rc_uses_inplace_rewrite`) to derive their
canonical-JSON via `ail parse` on the `.ail` source rather than
raw-reading the soon-to-be-deleted `.ail.json` file, retires the
redundant `render_parse_round_trip_canonical` (subsumed by T1's
`cli_parse_then_render_then_parse_is_idempotent`), and re-authors
`ail_run_accepts_ail_source_with_same_stdout_as_ail_json` to derive
a tempdir-based `hello.ail.json` from `hello.ail` via `ail parse`
rather than reading a deleted `.ail.json` sibling. T8 deletes the
156 non-carve-out `.ail.json` files (count drops 164 → 8), removes
the `#[ignore]` on the carve-out inventory test, and pulls forward
20 stale-`.ail.json`-reference repairs that the T1-5 dispatch missed
(12 `let example = "X.ail.json"` Group-B sites + 5
`ailang_core::load_workspace` Group-A sites in e2e.rs + 3 `ail_run`
Group-B sites in typeclass_22b3.rs); to keep T8's green-gate, T9
Step 5 (schema_coverage corpus flip from `.ail.json` to `.ail`) is
also pulled forward into T8. T9 retires the 3 obsolete roundtrip
tests (`print_then_parse_round_trips_every_fixture`,
`every_ail_fixture_matches_its_json_counterpart`,
`cli_render_then_parse_preserves_canonical_bytes_on_every_fixture`)
plus the dead helpers `list_json_fixtures` (×2),
`round_trip_one`, and `strip_trailing_newlines`. T10 restates the
DESIGN.md §Roundtrip Invariant with parse-determinism + idempotency
+ CLI-pipeline-idempotency + carve-out-anchor framing, replacing
the old Direction-1/Direction-2 framing and the five enforcement
bullets with the five surviving test names. T11 replaces the
CLAUDE.md and DESIGN.md doctrine sentences per spec §A4. T12
appends the WhatsNew milestone-close entry, strikes the roadmap
entry (`- [x]` + `(Closed 2026-05-13 by iter form-a.1.)`), and
verifies final inventory (8 `.ail.json` carve-outs + 157 `.ail`).
Final test count: 557 green + 3 ignored (3 pre-existing doctests).
The `cargo test --workspace` baseline holds green at every per-task
boundary; both `python3 bench/compile_check.py` and
`python3 bench/cross_lang.py` exit 0. The milestone
`[Form-A as the default authoring surface]` is fully closed.
## Per-task notes
- **iter form-a.1.6** — Bench-driver suffix flips in
`bench/compile_check.py:43,101`, `bench/cross_lang.py:48,80`,
`bench/mono_dispatch.py:10,112`, `bench/run.sh:68,182,183`.
Mechanical edit; comment text and path string updated at each
site. `python3 bench/compile_check.py` exit 0; `python3
bench/cross_lang.py` exit 0. (First compile_check run reported
exit-1 due to sub-millisecond `check_ms` noise on `hello` and
`borrow_own_demo`; second run clean. The noise is unrelated to
the migration — the bench measures `ail check` runtime, and the
Python edits did not touch the binary path.)
- **iter form-a.1.7** — Re-authored 3 raw-JSON-inspect tests +
retired 1 + re-authored the dual-form smoke. Pattern for the
three substring-assert tests: replace
`std::fs::read*("examples/<X>.ail.json")` with a `Command::new(ail_bin()).args(["parse", "examples/<X>.ail", "-o", tmpfile])`
+ `read_to_string(tmpfile)`. `reuse_as_demo_under_rc_uses_inplace_rewrite`
additionally switches its `ailang_core::load_workspace(&json_path)`
call to `ailang_surface::load_workspace(&ail_path)` (the
IR-lowering path no longer needs the JSON file). The dual-form
smoke (`ail_run_accepts_ail_source_with_same_stdout_as_ail_json`)
uses a per-process tempdir with `hello.ail.json` as the file
basename — necessary because the loader name-checks the module
name against the file stem, and the first attempt at a
tempfile-with-PID-suffix path tripped that check. Net test
count: 1 (retirement of `render_parse_round_trip_canonical`).
559 green at task close.
- **iter form-a.1.8** — CRITICAL task. Before the deletion command,
20 stale-`.ail.json`-reference repairs were pulled forward
(T1-5 dispatch missed these despite the plan's Group-A/B scope):
(a) 12 `let example = "X.ail.json"` patterns in `e2e.rs` flipped
to `"X.ail"` via sed; (b) 5 `ailang_core::load_workspace(&json_path)`
sites in `e2e.rs` switched to `ailang_surface::load_workspace`
(the `let example = "X.ail"` from (a) feeds into `workspace.join("examples").join(example)`,
which is `.ail` post-flip — needs the extension-dispatching loader);
(c) 3 `ail_run("X.ail.json")` sites in `typeclass_22b3.rs` flipped
via sed. Cargo-test green pre-deletion at 559. The plan's deletion
bash loop ran clean (exit 0); `ls examples/*.ail.json | wc -l`
8, `ls examples/*.ail | wc -l` → 157. The `#[ignore]` attribute
on `examples_ail_json_inventory_matches_carve_outs` was removed;
the test passes (8 files match `EXPECTED`). Post-deletion
cargo-test surfaced 1 failure
(`every_ast_variant_is_observed_in_the_fixture_corpus` in
`schema_coverage.rs`) because its corpus walker still looked at
`.ail.json` (now down to 8 carve-outs, insufficient to cover all
AST variants). The plan-T8 vs. T9 ordering implicitly assumed T9
Step 5 (schema_coverage corpus flip) would land before T8 — but
T9 is plan-ordered after T8. To satisfy T8's green-gate, T9
Step 5 was pulled forward inline (helper rename
`list_json_fixtures``list_ail_fixtures`, filter `.ail.json`
`.ail`, loader `ailang_core::load_module`
`ailang_surface::load_module`). Post-fix: 560 green + 3 ignored
(was 559 pre-T8 with 4 ignored including carve_out_inventory;
un-ignore + correct schema_coverage flip = +1 net = 560).
- **iter form-a.1.9** — Retired 3 obsolete roundtrip tests +
cleanups. `round_trip.rs`: retired
`print_then_parse_round_trips_every_fixture` (lines 57-101 in
pre-iter numbering) and `every_ail_fixture_matches_its_json_counterpart`
(lines 120-204); retired the `list_json_fixtures` helper and
the `round_trip_one` helper (only consumers were the two retired
tests); updated the file's doc-comment to describe the two
surviving tests (`parse_is_deterministic_*` and
`parse_then_print_then_parse_*`); removed the unused `Path`
import (only `PathBuf` remains). `roundtrip_cli.rs`: retired
`cli_render_then_parse_preserves_canonical_bytes_on_every_fixture`
(lines 116-146 pre-iter) and the `list_json_fixtures` +
`roundtrip_one` + `strip_trailing_newlines` helpers; updated the
doc-comment. T9 Step 5 (schema_coverage corpus flip) was already
done in T8; nothing to do here. 557 green + 3 ignored at task
close (560 pre-T9 3 retired = 557).
- **iter form-a.1.10** — DESIGN.md §Roundtrip Invariant restatement.
Lines 2029-2048 (Direction 1/Direction 2 framing) replaced with
the new 4-property framing (parse-determinism +
idempotency-under-print + CLI-pipeline-idempotency +
carve-out-anchor) verbatim from the plan. Lines 2061-2094
(Enforcement bullets, five tests) replaced with the five
surviving tests verbatim. Float-literals subsection (lines
2050-2059 pre-edit, 2065-2074 post-edit) preserved. Why-anchored-at-top-level
subsection (lines 2096-2108 pre-edit, 2108-2120 post-edit)
preserved. The numbered prose (4 properties) and the bulleted
enforcement (5 tests) are internally consistent; the carve-out
inventory is the 5th enforcement bullet, covering property 4
("carve-out anchor") as a meta-property.
- **iter form-a.1.11** — Doctrine edits. CLAUDE.md:5-6 sentence
replaced verbatim with spec §A4 wording. DESIGN.md:465-466 first
sentence replaced verbatim with spec §A4 wording; second
sentence ("Form (A) is one projection; the JSON-AST stays
canonical.") preserved as the trailing sentence of the new
replacement block. Doc-only; tests unaffected.
- **iter form-a.1.12** — Milestone close. WhatsNew.md entry
appended ("Form A is now the authoring surface", per-boss
editorial rules: no crate names, no iter codes, lead with the
change, factual). docs/roadmap.md entry at line 97 changed from
`- [ ]` to `- [x]` with `(Closed 2026-05-13 by iter form-a.1.)`
prepended. Final cargo-test: 557 green + 3 ignored. Final bench:
both compile_check and cross_lang exit 0. Final inventory:
exactly 8 `.ail.json` (carve-outs alphabetically: broken_unbound,
prelude, test_22b2_invalid_superclass_param,
test_22b2_kind_mismatch, test_22b2_unbound_constraint_var,
test_ct1_bad_qualifier, test_ct1_bare_xmod_rejected,
test_ct1_qualified_class_rejected) + 157 `.ail`.
## Concerns
- **Forward-pull of T9 Step 5 into T8.** The plan ordered T8
(deletion) before T9 (test retirement + schema_coverage flip),
but T8's green-gate required schema_coverage to be flipped
*first* — otherwise the schema_coverage corpus walker shrinks
from 164 `.ail.json` to 8 carve-outs and the AST-variant
coverage assertion fails. Pulled T9 Step 5 inline into T8;
T9 then had no schema_coverage work left to do. Recorded so a
future planner-skill recon pass can detect this ordering
defect class (a Task K green-gate depending on an edit
scripted for Task K+N).
- **T1-5 Group-A/B classification gaps.** The T1-5 dispatch's
Concerns flagged 4 raw-JSON-inspect e2e.rs tests but missed
20 additional sites of the same kind in two patterns: 12
`let example = "X.ail.json"` Group-B-pattern sites in e2e.rs
(feeds `build_and_run_with_alloc`) + 5
`ailang_core::load_workspace` Group-A-pattern sites in e2e.rs
+ 3 `ail_run` Group-B sites in typeclass_22b3.rs. All repaired
inline in T8 before the deletion command ran. The pattern that
T1-5 missed is "binding pulled out of the call expression" —
the `let example = "X.ail.json"` introduces a local that the
Group-B sed for `build_and_run("X.ail.json")` doesn't match.
Worth noting for a future planner-recon tightening pass.
- **bench/compile_check.py first-run flakiness.** The first run
after T6 reported exit-1 because two `check_ms` metrics on
sub-millisecond timings (`hello`, `borrow_own_demo`) drifted
past the 25% tolerance; the second run was exit-0. The drift is
measurement noise (system load + ms-scale latency), not a
migration-induced regression. Final T12 bench run was exit-0
on both scripts.
## Known debt
- **None known.** The iter closes the milestone cleanly. Two
follow-up roadmap items already exist:
- `[milestone] Prelude embed: Form-A as compile-time source`
(queued; retires carve-out (b) `prelude.ail.json`).
- The Show/print fieldtest example-corpus migration (separately
queued; orthogonal to this milestone).
- Cosmetic: the `mod tests {}` placeholder in
`ailang-core/src/hash.rs` from T5 is still in-tree; could be
deleted entirely in a doctrinal cleanup pass. Functionally
harmless.
## Files touched (Tasks 6-12)
### Modified bench (4)
- `bench/compile_check.py`
- `bench/cross_lang.py`
- `bench/mono_dispatch.py`
- `bench/run.sh`
### Modified test source (5)
- `crates/ail/tests/e2e.rs` — T7 re-authoring of 5 tests; T8
forward-pull of 12 Group-B + 5 Group-A sites
- `crates/ail/tests/typeclass_22b3.rs` — T8 forward-pull of 3
Group-B sites
- `crates/ail/tests/roundtrip_cli.rs` — T9 retirement +
helpers cleanup + doc-comment update
- `crates/ailang-surface/tests/round_trip.rs` — T9 retirement +
helpers cleanup + doc-comment update + import cleanup
- `crates/ailang-core/tests/schema_coverage.rs` — T8 corpus flip
to `.ail` (pulled forward from T9 Step 5)
### Modified test discipline (1)
- `crates/ailang-core/tests/carve_out_inventory.rs` — T8 un-ignored
### Modified doc (4)
- `CLAUDE.md` — T11 doctrine sentence (lines 5-6)
- `docs/DESIGN.md` — T10 §Roundtrip Invariant restatement; T11
§"What this Decision deliberately does not do" first sentence
- `docs/WhatsNew.md` — T12 milestone-close entry
- `docs/roadmap.md` — T12 strike of the Form-A milestone entry
### Deleted (T8) — 156 files
`examples/*.ail.json` — every non-carve-out fixture (the eight
carve-outs `broken_unbound`, `prelude`,
`test_22b2_invalid_superclass_param`, `test_22b2_kind_mismatch`,
`test_22b2_unbound_constraint_var`, `test_ct1_bad_qualifier`,
`test_ct1_bare_xmod_rejected`, `test_ct1_qualified_class_rejected`
preserved).
## Stats
bench/orchestrator-stats/2026-05-13-iter-form-a.1.json
@@ -1,103 +0,0 @@
# iter form-a.tidy — close 3 of 4 documentary drift items from audit-form-a + fix 5 contradictory "seven carve-outs" sites
**Date:** 2026-05-13
**Started from:** 5e94204c218c410d44fbb8e658b677a7294744b0
**Status:** DONE
**Tasks completed:** 6 of 6
## Summary
Pure documentary tidy iteration. Three new sections landed in
`crates/ailang-core/specs/form_a.md` (Class declarations, Instance
declarations, Constraints on polymorphic fns), each anchored to a
real corpus fixture (`test_22c_user_class_e2e.ail`,
`mq3_class_eq_vs_fn_eq_classmod.ail`, `show_user_adt.ail`,
`cmp_max_smoke.ail`) and each fixture was `ail check`-verified to
produce the expected symbol counts before close. Six sites in
`docs/specs/2026-05-13-form-a-default-authoring.md` had "seven
carve-outs" tightened to "eight carve-outs" to match the §C4(b)
amendment. The empty `#[cfg(test)] mod tests {}` placeholder plus
its 6-line relocation comment was deleted from
`crates/ailang-core/src/hash.rs` (tests live in
`crates/ailang-core/tests/hash_pin.rs` since form-a.1 T5). The
`round_trip.rs` module-level and inner docstrings were rewritten
to use the post-T10 four-property framing (parse-determinism /
idempotency-under-print / CLI-pipeline-idempotency /
carve-out-anchor) instead of the retired Direction-1/Direction-2
language, with cross-references to the sibling enforcement points
in `crates/ail/tests/roundtrip_cli.rs` and
`crates/ailang-core/tests/carve_out_inventory.rs`.
No production-code behaviour changed. Workspace test count holds
at 559 passed at every per-task gate.
## Per-task notes
- iter form-a.tidy.1: `form_a.md` Class declarations section landed; §Definitions
intro updated `Three kinds``Five kinds`; example anchored to
`examples/test_22c_user_class_e2e.ail` (`ok (24 symbols across 2 modules)`).
- iter form-a.tidy.2: `form_a.md` Instance declarations section landed; two
examples (same-module abbreviated from `mq3_class_eq_vs_fn_eq_classmod.ail`,
cross-module from `show_user_adt.ail`); `bare-cross-module-class-ref`
diagnostic anchor named inline.
- iter form-a.tidy.3: `form_a.md` Constraints on polymorphic fns: extended
the four-shapes EBNF `(forall ...)` line with optional `(constraints ...)`
clause, added explanatory paragraph (with `no-instance` diagnostic anchor),
added fifth example to the Examples block anchored to `cmp_max_smoke.ail`.
- iter form-a.tidy.4: `docs/specs/2026-05-13-form-a-default-authoring.md`
six "seven" → "eight" edits across the preamble + §C1 + §C2 + §C3 + §"Data flow"
(two sites). Post-edit grep returned 4 surviving "seven" mentions at lines
233, 238, 463, 469 — all correctly §C4(a)-scoped or arithmetic/future-state
(see Concerns below for one carrier-vs-plan transcription drift).
- iter form-a.tidy.5: `hash.rs` empty `mod tests {}` placeholder + 6-line
relocation comment block deleted (9 lines including the preceding blank).
`hash_pin.rs` integration tests still 10/10 passing.
- iter form-a.tidy.6: `round_trip.rs` module-level `//!` and inner `///`
rewritten to four-property framing with sibling-crate breadcrumbs.
`grep -n 'Direction [12]'` now zero hits.
## Concerns
- T4 carrier-vs-plan transcription drift on the Step 6 sanity-grep expected
output. The plan's Step 6 expected-output block listed four lines (233,
238, 463, 469) but the carrier compressed this to "exactly the three
surviving lines named in T4 Step 6 (lines 238, 463, 469)". The actual
post-edit grep returns four lines matching the plan listing; line 233
is §C4(a)-scope ("(seven files; the canonical") and is correctly retained.
No edit was missed. The carrier's three-line restatement was wrong;
the plan's four-line listing was right. Recording so a future auditor
re-reading the carrier doesn't second-guess the result.
## Known debt
- (none)
## Files touched
- `crates/ailang-core/specs/form_a.md` (+105/-)
- `crates/ailang-core/src/hash.rs` (-9)
- `crates/ailang-surface/tests/round_trip.rs` (+19/-12)
- `docs/specs/2026-05-13-form-a-default-authoring.md` (six 1-line edits)
## Phase 3 (E2E coverage)
Deliberately skipped per carrier: pure-documentary iter, no
behaviour change to cover. Verification gate was the per-task
`cargo test --workspace` smoke (559 green at every task close)
plus the per-task `ail check` runs on the four corpus fixtures
cited in the new form_a.md sections, all producing the expected
symbol counts (`test_22c_user_class_e2e.ail` ok 24/2,
`mq3_class_eq_vs_fn_eq_classmod.ail` ok 22/2,
`show_user_adt.ail` ok 23/2, `cmp_max_smoke.ail` ok 22/2).
## Decision recorded — Drift item B2 (plan-file "seven carve-outs" orphans)
Per plan §"Decision recorded" and the recon report: `docs/plans/2026-05-13-iter-form-a.1.md`
contains zero defective `seven` mentions; its four hits are all internally
scoped to §C4(a) (which IS seven), arithmetic, or future-state. The
audit-form-a journal's "two sites" claim against the plan file did not
match its contents at HEAD. No plan-file edit included in this iter.
## Stats
bench/orchestrator-stats/2026-05-13-iter-form-a.tidy.json
-176
View File
@@ -1,176 +0,0 @@
# iter mq.1 — Canonical-form extension for class-ref fields + workspace internal qualification
**Date:** 2026-05-13
**Started from:** 1a5f8289b7fbad3ae5bdd6badde27c80eca6dad3
**Status:** DONE
**Tasks completed:** 7 of 7
## Summary
mq.1 lands the canonical-form rule for the three class-reference
schema fields (`InstanceDef.class`, `Constraint.class`,
`SuperclassRef.class`) — bare for same-module, `<module>.<Class>`
for cross-module — symmetric to ct.1's `Type::Con.name` rule.
`ClassDef.name` stays bare (defining site, like `TypeDef.name`).
ct.1's `validate_canonical_type_names` validator gains a sibling
walk for class refs via the new `check_class_ref` helper, with
two new sibling diagnostics `BareCrossModuleClassRef` and
`BadCrossModuleClassRef`. Workspace-internal class-name keys
(`class_def_module`, `class_by_name`, registry `entries.0`,
`ClassMethodEntry.class_name`, `class_superclasses`, mono's
`class_index`, `Origin::Class.class_name`) all carry the qualified
form post-mq.1; the on-disk schema value is the canonical form (bare
or qualified) that `qualify_class_ref` lifts to the workspace key
at every lookup boundary. `MethodNameCollision` and the dispatch
path are unchanged in this iter (iter 2 + 3 land them).
## Per-task notes
- iter mq.1.1: two new diagnostic variants
`BareCrossModuleClassRef` / `BadCrossModuleClassRef` sibling to the
type-ref variants; two Diagnostic-shape Display arms in `ail/main.rs`
mirroring the existing arm. 2 RED unit tests, both GREEN.
- iter mq.1.2: doc-comments on the four class-reference fields
in `ast.rs` (`ClassDef.name` stays bare with cross-reference;
`SuperclassRef.class` / `InstanceDef.class` / `Constraint.class`
document canonical form). Zero semantic effect.
- iter mq.1.3: `check_class_ref` helper plus three per-def walks
inside `validate_canonical_type_names`; `check_class_name_fields`
narrowed to `ClassDef.name` only; four in-test pin tests inverted
(qualified is now accepted); on-disk `test_ct1_qualified_class_rejected`
fixture-test repurposed (now expects `OrphanInstance`
the natural post-mq.1 rejection mode for the same shape); positive
on-disk fixture pair `mq1_xmod_constraint_class{,_dep}` shipped
plus the corresponding pin test. 4 new bare-xmod tests + 1 positive
qualified test, all GREEN.
- iter mq.1.4: `qualify_class_ref` helper at module scope; `build_registry`
Pass-1 re-keyed (qualified); Pass-2 lookups use `qualify_class_ref` to
lift `inst.class` to the registry-key shape; superclass walk threads
`current_class_module` to qualify bare `SuperclassRef.class`;
`Origin::Class.class_name` qualified (one construction site, not two as
the plan claimed); type-leg of coherence check now also handles
qualified head names symmetrically (split-and-lookup).
- iter mq.1.5: `ClassMethodEntry.class_name` insert qualifies in
`build_module_globals`; `class_superclasses` key + value both
qualified; mono's `build_class_index` keyed qualified (plan said
"no code change needed in mono.rs consumers" — that statement was
wrong, the build-side needed the matching qualification).
- iter mq.1.6: test assertions updated for the new qualified shape —
the two `MethodNameCollision` pin tests, `typeclass_22b2.rs:42`
(`class_method_class("show")` now `Some("...Show")`),
`typeclass_22b3.rs:151` (`MonoTarget.class` qualified). The other
lines plan flagged (217/232/295/301/341/353) are local-construction
tests that never round-trip through the workspace — left as-is.
- iter mq.1.7: full `cargo test --workspace` 520 passed / 0 failed;
`examples/prelude.ail.json` zero diff (confirms spec assumption 17:
intra-prelude refs stay bare); `bench/compile_check.py` exit 0,
`bench/cross_lang.py` exit 0; `bench/check.py` exit 1 due to 4
improvements-beyond-tolerance (audit-ratifiable per convention,
not a regression). Roundtrip on the new fixture: `ail check
examples/mq1_xmod_constraint_class.ail.json` → "ok (16 symbols
across 3 modules)", exit 0.
## Concerns
- **Boss-note recon claim was empirically wrong.** The boss-note
said "every existing `examples/test_22b*.ail.json` class-ref field
is intra-module under the canonical-form rule, so existing fixtures
carry zero diff". Five test_22b* fixtures actually carried bare
cross-module class refs and required migration: `test_22b1_orphan_third.ail.json`
(bare `Show` from imported classmod), `test_22b1_dup_a.ail.json` +
`test_22b1_dup_b.ail.json` (the duplicate-instance pair structurally
relied on the pre-mq.1 bare cross-module shape), and
`test_22b2_unbound_constraint_var.ail.json` (constraint references
a class that doesn't exist anywhere → fired BareCrossModuleClassRef
before reaching UnboundConstraintTypeVar). Six non-test_22b
fixtures also needed migration (`eq_ord_polymorphic`, `eq_ord_user_adt`,
`cmp_max_smoke`, `ctt2_collision_lib`, `ctt2_collision_main`). All
migrations are minimal and aligned with the canonical-form rule;
no proactive "fixture cleanup" added.
- **Duplicate-instance test restructured by structural necessity.**
Pre-mq.1 the test relied on two coherent instances on the same
`(class, type)` declared from different modules; post-mq.1, the
orphan-freedom invariant prevents this shape (any second instance
in a module that owns neither the class nor the type fires
`OrphanInstance` first). The new `test_22b1_dup_same_module.ail.json`
fixture lands both instances in the same module — the only post-mq.1
way to land two on the same canonical key. The old dup_a/dup_b/entry
fixtures stay as supporting modules (referenced by hash.rs's
canonical-form hash pin) but no longer participate in the duplicate
test. This is arguably a strengthening of the architecture: duplicate
instances are now structurally impossible across coherent modules.
- **Plan's "no code change needed" claims in Task 4 Step 8 and Task 5
Step 5 were wrong.** Task 4 Step 8 said `Origin::Class.class_name`
qualified at "both sites" but there's only one construction site
(the plan's recon was off-by-one). Task 5 Step 5 said mono's
`class_index.get(class)` lookups would just work post-rekey, but
`build_class_index` itself was bare-keyed. Both fixed inline.
- **Several adjacent fixes were necessary that the plan didn't anticipate:**
(a) `build_module_globals` rejected `inst.class` containing `.`
added a `Def::Instance` carve-out so qualified class refs survive.
(b) `check_fn`'s declared-constraint matching compared bare
`c.class` to qualified residual `r.class` → added an inline lift via
`qualify_class_ref_in_check`. (c) `NoInstance` Float-aware message
arm matched on bare `"Eq" / "Ord"` — updated to `"prelude.Eq" /
"prelude.Ord"`. (d) Coherence check's type-leg lookup couldn't
resolve qualified head names — extended with a split-and-lookup
fallback. None of these widened the contract; each is a one-line
bridge keeping the post-mq.1 qualified shape consistent across the
pipeline.
- **Tasks 3-6 ran as one coherent fix.** The plan structured them as
sequential tasks, but Task 3 alone left several existing tests red
(registry still bare-keyed, residual still bare). Walking the plan
literally and expecting "green after Task 3" per the plan's Step 9
was impossible; the four tasks form one consistent rekey.
## Known debt
- The `test_22b1_dup_a` / `test_22b1_dup_b` / `test_22b1_dup_entry`
fixtures are now orphan from the duplicate-instance test (which
uses `test_22b1_dup_same_module` instead). They're still
referenced by `hash.rs:293` for the canonical-form hash pin. A
cleanup pass could either retire them entirely (and replace the
hash pin with the new fixture) or keep them as historical
reference. Deferred — not load-bearing.
- `qualify_class_ref_in_check` in `ailang-check/src/lib.rs` is a
one-line duplicate of `workspace::qualify_class_ref`. The
alternative is exposing `qualify_class_ref` as `pub(crate)` or
re-exporting; the duplication was the minimum-edit path. A
consolidation tidy could land in iter 2.
## Files touched
Code:
- crates/ail/src/main.rs
- crates/ail/tests/ct1_check_cli.rs
- crates/ail/tests/typeclass_22b2.rs
- crates/ail/tests/typeclass_22b3.rs
- crates/ailang-check/src/lib.rs
- crates/ailang-check/src/mono.rs
- crates/ailang-check/tests/env_construction_pin.rs
- crates/ailang-core/src/ast.rs
- crates/ailang-core/src/hash.rs
- crates/ailang-core/src/workspace.rs
- crates/ailang-core/tests/ctt2_registry_rekey.rs
Fixtures (existing, migrated):
- examples/cmp_max_smoke.ail.json
- examples/ctt2_collision_lib.ail.json
- examples/ctt2_collision_main.ail.json
- examples/eq_ord_polymorphic.ail.json
- examples/eq_ord_user_adt.ail.json
- examples/test_22b1_dup_a.ail.json
- examples/test_22b1_dup_b.ail.json
- examples/test_22b1_dup_entry.ail.json
- examples/test_22b1_orphan_third.ail.json
- examples/test_22b2_unbound_constraint_var.ail.json
Fixtures (new):
- examples/mq1_xmod_constraint_class.ail.json
- examples/mq1_xmod_constraint_class_dep.ail.json
- examples/test_22b1_dup_classmod.ail.json
- examples/test_22b1_dup_same_module.ail.json
## Stats
bench/orchestrator-stats/2026-05-13-iter-mq.1.json
-169
View File
@@ -1,169 +0,0 @@
# iter mq.2 — Type-driven dispatch mechanism (installed, not yet exercised)
**Date:** 2026-05-13
**Started from:** e9e45c77affa635652505cc937b77b9349d18c8e
**Status:** DONE
**Tasks completed:** 9 of 9
## Summary
mq.2 installs the type-driven dispatch infrastructure that mq.3 will
exercise end-to-end once `MethodNameCollision` retires. Three new
`CheckError` variants (`AmbiguousMethodResolution`, `UnknownClass`,
plus an additive `candidate_classes` field on `NoInstance`); a new
workspace-flat `method_to_candidate_classes` index on `Env` (inverse
of `class_methods`); an extended `ResidualConstraint` carrying an
optional `candidates: Option<BTreeSet<String>>` for the multi-
candidate dispatch path; a pure `resolve_method_dispatch` helper
implementing the spec's 5-step rule (qualifier → singleton →
type-driven filter → constraint-driven filter → `Multi` for discharge-
time refinement); a `refine_multi_candidate_residual` helper for
discharge / mono refinement; and a `resolve_residual_class_for_mono`
helper that wires the refinement into mono's residual-to-target
mapping. The synth `Term::Var` arm class-method branch is rewritten
to consult the new index via `parse_method_qualifier`, with a gate
that prevents capturing the existing cross-module-fn path
(`<module>.<fn>`) by requiring class-qualifiers to carry an inner
dot (per mq.1's canonical-form rule, class qualifiers are always
`<module>.<Class>`).
With `MethodNameCollision` still active, the new multi-candidate
branch is exercised exclusively by 15 unit tests: 6 in
`tests/method_dispatch_pin.rs` (the 5-step rule's six cases), 6 in
`lib.rs` `mod tests` (the new `CheckError` variants + the
`ResidualConstraint`/`Env` field shapes + the discharge-side
refinement), and 3 in `mono.rs` `mod tests` (the mono-side helper).
Real workspaces continue producing single-class residuals
(`candidates: None`), and every pre-mq.2 fixture typechecks
unchanged.
## Per-task notes
- iter mq.2.1: Two new `CheckError` variants
`AmbiguousMethodResolution` and `UnknownClass`, added as siblings
after `NoInstance`. `code()` + `ctx()` table entries; module-doc
list in `diagnostic.rs` extended. 2 RED-then-GREEN smoke tests.
- iter mq.2.2: `NoInstance` extended with an additive
`candidate_classes: Vec<String>` field. `ctx()` emits the field
only when non-empty (preserves pre-mq.2 JSON shape). 2 tests:
with + without populated candidates.
- iter mq.2.3: `ResidualConstraint` extended with `candidates:
Option<BTreeSet<String>>`. Struct bumped to `pub` (was
`pub(crate)`) for unit-test access from the dispatch-pin / Task 7
+ 8 tests. Single existing construction site updated with
`candidates: None`. 1 smoke test.
- iter mq.2.4: `Env.method_to_candidate_classes:
BTreeMap<String, BTreeSet<String>>` built workspace-flat in
`build_check_env` alongside the existing `class_methods` insert
(coalesced into one loop). 1 in-test fixture round-trip.
- iter mq.2.5: `MethodDispatchOutcome` enum + pure
`resolve_method_dispatch` helper. 6 unit tests cover unique
candidate, qualifier-match, qualifier-no-match (UnknownClass),
type-driven narrow-to-one, constraint-driven narrow-to-one
(rigid-var fallback), and true ambiguity. Helper uses
`ailang_core::pretty::type_to_string` for the at_type rendering
rather than the plan's invented `format_type_for_display` (one
fewer duplicate; the canonical pretty-printer is already in use
across other diagnostic surfaces).
- iter mq.2.6: Synth `Term::Var` arm class-method branch rewritten
to consult `method_to_candidate_classes` via
`parse_method_qualifier`. Branch gate: qualifier must be either
absent (bare form `"show"`) or contain an inner dot
(`<module>.<Class>` per mq.1's canonical-form rule). 1-dot names
like `std_list.length` fall through to the existing qualified-fn
path. Plan's `active_declared_constraints` plumbing intentionally
skipped at synth time — synth's Var arm does not carry the body's
declared constraints; the constraint-driven filter runs at
discharge time (Task 7) where `expanded` is in scope. With
`MethodNameCollision` still active, the candidate set is always
singleton at this branch, so the `&[]` synth-time filter is
load-bearing only post-mq.3 and only for the rigid-var fallback;
on the post-mq.3 multi-candidate fully-concrete path discharge
will refine the residual on `r_ty` against the workspace registry.
- iter mq.2.7: `RefineOutcome` enum + `refine_multi_candidate_residual`
helper. Wired into `check_fn`'s residual loop BEFORE the existing
single-class discharge — multi-candidate residuals refine first,
`Resolved` falls through to `continue`, error variants return.
Used `expanded` (already-superclass-expanded declared constraints)
for the rigid-var path, sounder than `declared_constraints` direct.
3 unit tests for the discharge path.
- iter mq.2.8: `resolve_residual_class_for_mono` helper in mono.rs
+ 3 unit tests in a new `mod tests` block. Mono's residual-to-
target mapping (`collect_residuals_ordered`) now calls the helper
before constructing the registry key; resolved-class overrides
the residual's tentative `r.class` in `MonoTarget::ClassMethod`.
Single-class residuals flow through unchanged.
- iter mq.2.9: full `cargo test --workspace` 539 passed / 0 failed
(was 520 at start of mq.2; +19 = 15 new mq.2 tests + 4 pre-
existing mq.1 follow-on additions in cross-language test files
already shipped). All pre-mq.2 fixtures (prelude, mq1_xmod,
eq_ord_polymorphic) typecheck unchanged. Bench results:
`compile_check.py` 0 regressed / 0 improved / 24 stable;
`cross_lang.py` 0 regressed / 0 improved / 25 stable;
`check.py` 1 regressed / 2-4 improved / 58-60 stable (latency
noise — different metric regressed between two runs, runtime
benches cannot be touched by a typecheck-side iter). Prelude
roundtrip clean.
## Concerns
- **Helper-cost: `parse_method_qualifier` does an `rfind('.')` on
every `Term::Var` lookup that reaches the class-method branch.**
Today this is bounded by the per-Var-arm cost (already O(name-
length) due to the dot-counting and unification ladder), so it's
noise. If profiling ever shows the Var arm hot, the right fix is
to cache `(method_name, qualifier_opt)` on the Var node rather
than recompute — but that's a Term struct change and out of
scope for mq.2.
- **`pub` bump on `ResidualConstraint`.** The unit-test crate
(`tests/method_dispatch_pin.rs`) needs to construct
`ResidualConstraint` for Task 5; Task 7's in-lib tests also
reference it. Bumping from `pub(crate)` to `pub` is the minimum
edit; the struct semantically belongs at the workspace boundary
anyway (the spec's published shape for what the dispatcher
produces). No risk: no external crate depends on `ailang-check`
outside this workspace.
- **`MissingConstraint.class` field for multi-candidate rigid-var
case is best-effort.** When `refine_multi_candidate_residual`
emits `MissingConstraint`, the surfaced `class` is the first
candidate (BTreeSet order). The diagnostic ctx still names the
method, the type, and (via the diagnostic Display) the full
candidate set is reachable; the `class` is just a hint. Real
workspaces never reach this branch pre-mq.3.
## Known debt
- **mq.3 retires `MethodNameCollision` and exercises the
multi-candidate path end-to-end.** The unit tests in this iter
are the only coverage of the new branches until then. mq.3 will
add cross-class-collision fixtures (e.g. two `Show` classes in
separate modules) and the existing tests should continue to pass
unchanged.
- **`class_methods` keyed by method name only.** Pre-mq.3 invariant
guarantees `env.class_methods.get(method_name)` is unambiguous;
post-mq.3 this lookup needs to be reworked (likely keyed by
`(class, method)` or by `method` returning a list). The synth
branch's `expect("invariant")` documents the dependency. Not
load-bearing in mq.2.
- **Synth-time `declared_constraints: &[]` in the Var arm rewrite.**
This is correct for pre-mq.3 (singleton candidate set never
needs filtering) but means the post-mq.3 constraint-driven
filter cannot fire at synth time. The branch will need
`env.active_declared_constraints` plumbed through (Env field
+ `check_fn` initialization) before mq.3's first multi-candidate
workspace with declared constraints. Cost: ~10-line edit.
## Files touched
- crates/ailang-check/src/diagnostic.rs
- crates/ailang-check/src/lib.rs
- crates/ailang-check/src/mono.rs
- crates/ailang-check/tests/method_dispatch_pin.rs (new)
## Stats
bench/orchestrator-stats/2026-05-13-iter-mq.2.json
-284
View File
@@ -1,284 +0,0 @@
# iter mq.3 — Retire MethodNameCollision + multi-class E2E + DESIGN.md sync
**Date:** 2026-05-13
**Started from:** 90075715d99c9e8c59ea79267c2615f5756520f2
**Status:** DONE
**Tasks completed:** 9 of 9
## Summary
mq.3 closes the module-qualified-class-names milestone by retiring
the `WorkspaceLoadError::MethodNameCollision` workaround that mq.1 +
mq.2 made redundant. The variant + pre-pass + Origin enum + Display
arm + two pin tests are deleted; the two on-disk fixtures that fired
the collision now load cleanly and the equivalent observations
relocate to `env.method_to_candidate_classes` (multi-entry set) plus
a per-call-site warning. Three positive E2E fixtures (Trajectory C
ambiguous, Trajectory E explicit-qualifier, class-fn shadow) exercise
the multi-candidate dispatch path end-to-end via `ail check --json`.
Two out-of-band corrections that mq.2 surfaced as known debt land
here: (1) `Env.active_declared_constraints: Vec<Constraint>` is
plumbed pre-synth in `check_fn` so the post-superclass-expansion
constraint set is reachable at synth time by
`resolve_method_dispatch`'s constraint-driven filter; (2)
`ModuleGlobals.class_methods` + `Env.class_methods` re-keyed from
`BTreeMap<MethodName, ClassMethodEntry>` to
`BTreeMap<(QualifiedClass, MethodName), ClassMethodEntry>` so the
post-retirement world has O(1) `(class, method)` lookup, with
mono's two presence-check sites switched to consult the natively
method-keyed `method_to_candidate_classes`. A third
correction — `synth(...)` signature extended with a `warnings: &mut
Vec<Diagnostic>` out-parameter — installs the channel for the new
structured warning `class-method-shadowed-by-fn` that fires when a
free fn shadows a class method of the same name (per spec
§"Class-fn collisions": precedence + warning, the spec's default).
DESIGN.md sync removes the `MethodNameCollision` bullet from
"Workspace-load (registry-build) diagnostics" with a forward-pointing
note, adds `AmbiguousMethodResolution` / `UnknownClass` /
`class-method-shadowed-by-fn` to the "Typecheck diagnostics" block,
rewords the `AmbiguousInstance` paragraph (per-class coherence stays
via `DuplicateInstance`; cross-class method ambiguity is the new
diagnostic at the call site), rewrites the class-names paragraph in
the canonical-form section to point at the mq.1 rule + the new
dispatch model, and adds a new `### Method dispatch` subsection
anchoring the 5-step rule with `method_to_candidate_classes` as the
load-bearing data structure. Roadmap P2 entry → `[x]` with a summary
of the three iters; milestone-24 (`Post-22 Prelude — Show + print
rewire`) `depends on:` line struck and the entry annotated
"ready for re-brainstorm".
545 tests green (was 539 at start of mq.3; +6 net = 3 new mq.3.x lib
tests + 2 relocated method_collision_pin + 3 new mq3_multi_class_e2e
- 2 deleted workspace.rs pin tests). `bench/compile_check.py` and
`bench/cross_lang.py` exit 0; `bench/check.py` exit 1 with 2
regressions both noise-class (bench_list_sum.bump_s persistence —
3rd consecutive sighting since audit-ct-tidy — and a max_us tail
metric, plus 4 improvements). Prelude zero-diff.
## Per-task notes
- **mq.3.1** — `Env.active_declared_constraints: Vec<Constraint>`
field plumbed; populated in `check_fn` pre-synth alongside the
rigid-vars install (consolidated with the existing post-synth
`expanded` computation — single source of truth, no clone-and-
duplicate as the plan literally suggested). Synth Var-arm
`resolve_method_dispatch` call site swapped `&[]` for
`&env.active_declared_constraints`. 1 new test mq3_env_active_declared_constraints_field_exists.
- **mq.3.2** — `ModuleGlobals.class_methods` re-keyed
`IndexMap<String, ClassMethodEntry>``IndexMap<(String, String),
ClassMethodEntry>`; sibling `Env.class_methods` similarly re-keyed
to `BTreeMap<(String, String), ClassMethodEntry>`. Accessor methods
on `ModuleGlobals` (`has_class_method`, `class_method_class`,
`class_method`) all take `(class, name)`; new
`class_method_candidates(name) -> Vec<(&class, &entry)>` for the
enumerate-all-classes-declaring-this-method use case (returning
`Vec` not `impl Iterator` because the borrow-checker rejected the
`'a`/`'_` capture without explicit `use<'a, '_>` syntax not yet
idiomatic in this crate). `build_module_globals` insert site +
`build_check_env` merge loop both threaded through the tuple key.
Synth Var-arm `class_methods.get(&(residual_class, method))` uses
the dispatcher-resolved tentative class. Mono's two presence-check
sites (`rewrite_mono_calls`, `interleave_slots`) switched to
consult `method_to_candidate_classes` natively — method-keyed,
preserves the existing presence-check signature shape; the four
call sites at module-walk + interleave_slots entry pass
`&env.method_to_candidate_classes` instead of `&env.class_methods`.
Test assertion in `crates/ail/tests/typeclass_22b2.rs:42` updated
to pass the qualified class explicitly. 1 new test
mq3_env_class_methods_tuple_keyed.
- **mq.3.3** — `synth(...)` signature extended with `warnings:
&mut Vec<Diagnostic>` (15+ recursive synth callsites threaded).
Five external synth callers (check_fn, check_const, 3 in
builtins.rs, 1 in lift.rs, 2 in mono.rs, 1 in `check`) all
updated; check_fn / check_const pass through to caller via new
`out_warnings: &mut Vec<Diagnostic>` parameter on the caller
chain (`check_def`, `check_in_workspace`); `check_workspace`
extends per-module synth_warnings into `module_diags`. Plan
suggested `check_fn` return `Result<(CheckedFn, Vec<Diagnostic>)>`
but `check_fn`'s actual signature is `Result<()>` and the
mut-ref-accumulator pattern matches the existing `Vec<CheckError>`
accumulator at `check_in_workspace`'s caller. New diagnostic
`class-method-shadowed-by-fn` (warning, kebab-case code,
structured `ctx` carrying `name` + `method` + `fn_owner_module` +
`candidate_classes`) emitted by a closure at all three
fn-precedence branches (locals, same-module fn, implicit-import
fn). Implicit-import-fn branch reordered ABOVE the class-method
branch per spec §"Class-fn collisions" so fn-wins precedence is
structural, not just convention. Diagnostic doc updated in
`diagnostic.rs`. One existing test
`crates/ail/tests/typeclass_22b3.rs:rewrite_walker_skips_locally_shadowed_class_method`
was a known trip-wire — the fixture intentionally shadows a class
method with a local binding, which now correctly fires the new
warning; filtered out of the test's `assert!(diags.is_empty())`
with a comment naming why. 1 new test
mq3_class_method_shadowed_by_fn_warning_fires.
- **mq.3.4** — `WorkspaceLoadError::MethodNameCollision` variant +
the Origin enum + the per-def collision loop in `build_registry`
(workspace.rs 596-681 pre-edit) + the Display arm in
`main.rs:1201` deleted. The two in-workspace.rs pin tests
(`class_class_method_name_collision_fires` +
`class_fn_method_name_collision_fires`) deleted; relocation lands
in mq.3.5. The on-disk fixtures
`test_22b2_method_name_collision_class_{class,fn}.ail.json` stay
on-disk (now exercised as positive-load by the relocated tests).
- **mq.3.5** — `crates/ailang-check/tests/method_collision_pin.rs`
created with two repurposed pin tests: the class-class fixture
loads cleanly and `env.method_to_candidate_classes["foo"]` has
exactly two qualified-class entries (`...A` + `...B`); the
class-fn fixture loads cleanly (the warning is a call-site
concern, not a load-time concern; covered by mq.3.6 E2E). The
relocation from `ailang-core` to `ailang-check` is required
because `Env.method_to_candidate_classes` is built by
`build_check_env`, which lives in `ailang-check` (not visible
from `ailang-core`'s `mod tests`).
- **mq.3.6** — Three new on-disk fixtures plus three integration
tests in `crates/ail/tests/mq3_multi_class_e2e.rs`. Fixture (a)
`mq3_two_show_ambiguous` exercises the bare-method ambiguous
case (two `Show` classes, both with `Show Int`, bare `show 42`
→ `AmbiguousMethodResolution`). Fixture (b)
`mq3_two_show_qualified` exercises explicit-qualifier resolution
(same workspace, `mq3_two_show_ambiguous_a.Show.show 42` →
clean). Fixture (c) `mq3_class_eq_vs_fn_eq` exercises class-fn
shadow (`class MyEq { myeq }` + `fn myeq` + bare `myeq 1 2` →
fn wins, warning fires). The plan's draft fixtures used a bare
`Term::App` for instance method bodies; the existing-convention
shape (per `test_22b3_mono_synthetic.ail.json`) is `Term::Lam`
with `paramTypes`/`retType` — implementer-phase repair adjusted
inline. Naming `MyEq`/`myeq` (rather than `Eq`/`eq`) avoids an
extra prelude-vs-fn shadow that would have confused the warning
assertion. All three tests GREEN first-shot; spot-checked via
`ail check --json` for completeness.
- **mq.3.7** — DESIGN.md sync: class-names paragraph (1156-1160)
rewritten to point at mq.1 canonical-form + mq.3 dispatch model;
`MethodNameCollision` bullet struck from "Workspace-load
(registry-build) diagnostics" with forward-pointing note; three
new typecheck diagnostics + `NoInstance.candidate_classes`
addendum added to "Typecheck diagnostics"; `AmbiguousInstance`
paragraph reworded to clarify the registry-level vs call-site
distinction; new `### Method dispatch` subsection anchors the
5-step rule with `method_to_candidate_classes` as the
load-bearing data structure, the class-fn precedence rule, and
the post-mq.3 tuple-keyed `class_methods` shape.
- **mq.3.8** — Roadmap P2 milestone "Module-qualified class names
+ type-driven method dispatch" → `[x]` with a one-paragraph
summary of the three iters (mq.1 canonical-form, mq.2 mechanism,
mq.3 retirement) and the milestone's user-facing outcome (two
libraries can each declare `class Eq` with their own `eq`).
Milestone-24 (`Post-22 Prelude — Show + print rewire`) `depends
on:` line struck and the entry annotated "ready for
re-brainstorm" so the next time `/boss` walks the roadmap it
picks up the un-blocked spec re-derivation against the
post-retirement architecture.
- **mq.3.9** — Integration verification. `cargo test --workspace`
545 passed / 0 failed. `bench/compile_check.py` 0 regressed / 0
improved / 24 stable. `bench/cross_lang.py` 0 regressed / 0
improved / 25 stable. `bench/check.py` 2 regressed / 4 improved
/ 57 stable: regression #1 `throughput.bench_list_sum.bump_s`
(3rd consecutive sighting since audit-ct-tidy 2026-05-12, audit-
ratified-class noise per the journal lineage); regression #2
`latency.implicit_at_rc.max_us` (same noisy max-tail metric
that mq.2 journal flagged as runtime-uncoupled-to-typecheck-
iter, audit-ratified-class). Prelude zero-diff. All three new
E2E fixtures observable via `ail check` with the expected
diagnostic / no-diagnostic / warning outcomes.
## Concerns
- **Plan's `check_fn` signature suggestion was off.** Plan Step 4
proposed `check_fn(...) -> Result<(CheckedFn, Vec<Diagnostic>),
CheckError>`; the actual signature is `Result<()>` (no
CheckedFn). The mut-ref-accumulator pattern adopted instead
matches the existing `check_in_workspace` shape (Vec<CheckError>
accumulator). Same outcome, different mechanism.
- **`class_method_candidates` returns `Vec` not `impl Iterator`.**
The plan's accessor signature `impl Iterator<Item = (&'a String,
&'a ClassMethodEntry)>` requires the `use<'a, '_>` opaque-type
capture syntax — not yet idiomatic in this crate (the rest of
the codebase uses `Vec`-returning accessors at module-graph
boundaries). The semantic contract is the same; performance is
one allocation per call, bounded by the per-method candidate
count (currently ≤ 2 across the corpus).
- **One existing E2E test trips the new warning.**
`crates/ail/tests/typeclass_22b3.rs:rewrite_walker_skips_locally_shadowed_class_method`
uses a fixture (`test_22b3_shadow_class_method`) that
intentionally shadows the class method `show` with a local
`let show = "shadow"`. Pre-mq.3 this fixture typechecked clean;
post-mq.3 it correctly fires the `class-method-shadowed-by-fn`
warning. The test's `assert!(diags.is_empty())` was relaxed to
filter out the new warning with a comment naming why. This is
the warning behaving correctly, not a test workaround.
- **Plan's instance-method body shape was wrong.** Plan Step 1 of
Task 6 drafted `body: { "t": "app", ... }` for instance
methods; the existing-convention shape (per `test_22b3_mono_synthetic`)
is `Term::Lam` with `paramTypes`/`retType` keys. Implementer-phase
repair adjusted all three fixtures inline. The plan's caveat
("implementer adjusts to match the exact shape used by
`examples/prelude.ail.json`'s Eq/Ord instances") covered this
case; just exercising the caveat verbatim.
## Known debt
- **`bench_list_sum.bump_s` 3rd-consecutive regression.** Noise-class
per the audit-ct-tidy 2026-05-12 journal entry. No attribution
cause across three audits. The pattern is a candidate for
ratify-without-attribution at the milestone-close audit (audit-mq)
but this iter follows the conservative pristine-baseline
convention.
- **`latency.implicit_at_rc.max_us` max-tail noise.** Continues from
mq.2's bench profile. Runtime metric, structurally uncoupled to
this typecheck-side iter. Audit-ratifiable.
- **`bench/check.py` exit 1.** Same noise-class status as the two
regressions above. Conservative interpretation: leave the
baseline pristine; audit-mq decides whether to ratify.
- **`class_method_candidates` allocation per call.** Bounded by
per-method candidate count (currently ≤ 2). A future tidy that
introduces `use<'a, '_>` syntax across the crate could revisit
the iterator-return shape.
## Files touched
Code:
- crates/ail/src/main.rs
- crates/ail/tests/typeclass_22b2.rs
- crates/ail/tests/typeclass_22b3.rs
- crates/ailang-check/src/builtins.rs
- crates/ailang-check/src/diagnostic.rs
- crates/ailang-check/src/lib.rs
- crates/ailang-check/src/lift.rs
- crates/ailang-check/src/mono.rs
- crates/ailang-core/src/workspace.rs
New tests + fixtures:
- crates/ail/tests/mq3_multi_class_e2e.rs
- crates/ailang-check/tests/method_collision_pin.rs
- examples/mq3_two_show_ambiguous_a.ail.json
- examples/mq3_two_show_ambiguous_b.ail.json
- examples/mq3_two_show_ambiguous.ail.json
- examples/mq3_two_show_qualified.ail.json
- examples/mq3_class_eq_vs_fn_eq_classmod.ail.json
- examples/mq3_class_eq_vs_fn_eq_fnmod.ail.json
- examples/mq3_class_eq_vs_fn_eq.ail.json
Docs:
- docs/DESIGN.md
- docs/roadmap.md
## Stats
bench/orchestrator-stats/2026-05-13-iter-mq.3.json
-219
View File
@@ -1,219 +0,0 @@
# iter mq.tidy — Close 4 actionable drift items from audit-mq
**Date:** 2026-05-13
**Started from:** 64d3feeb972f4ddfe9bd48484585c69a457e7e1e
**Status:** DONE
**Tasks completed:** 5 of 5
## Summary
mq.tidy closes the four actionable drift items audit-mq surfaced
(commit `f382931`), moving the module-qualified-class-names milestone
from "structurally complete" to "drift-clean ready for next milestone".
T1 extends the discharge-time `refine_multi_candidate_residual`
rigid-var filter from class-only to class + type-unification via the
existing `constraint_type_matches` helper, so two same-class declared
constraints on different typevars are correctly discriminated by the
residual's typevar identity (high-1). T2 extracts the inline
`qualifier_is_class_shape` predicate as a free `pub(crate)` helper and
broadens it to accept PascalCase single-segment qualifiers
(`"Show.show"`) alongside module-qualified ones (`"prelude.Show.show"`)
— same-module bare-class call sites are now reachable, symmetric to
the canonical-form rule at the schema level (high-2). T3 introduces
the `any_candidate_class_has_instance` predicate and gates the
`class-method-shadowed-by-fn` warning closure on it, so class methods
with zero registry instances anywhere no longer fire the warning;
conservative approximation of the full spec rule per Boss Q2 (the arg
type required for the strict version is unavailable at the Var-arm)
(medium-1). T4 annotates the four Data Model schema fragments
(`SuperclassRef`, `InstanceDef.class`, `Type::Con.name`,
`Constraint.class`) with the canonical-form cross-reference, so a
schema-section reader without the prose context no longer sees
bare-only (medium-2).
T5 verification: 548 tests green (was 545 pre-iter + 3 new
mq_tidy_* unit tests). `bench/compile_check.py` exit 0 (24/24 stable
after a one-run noise blip absorbed under tolerance on re-run).
`bench/cross_lang.py` exit 0 (25/25 stable). `bench/check.py` exit 0
both runs; metric-identity-migrating noise on the
`latency.implicit_at_rc.*` max-tail cluster — 5th-consecutive audit
observation per audit-mq's lineage, baseline pristine. The
`mq3_class_method_shadowed_by_fn_warning_fires` in-crate test was a
trip-wire: pre-tidy it used `Registry::default()` (no instances) and
the warning fired anyway because the old gate didn't check instances;
post-tidy it correctly suppresses, so the fixture was updated to ship
a `clsmod.Show Int` registry instance — fixture-repair as a coherence
consequence of the deliberately tightened spec rule. mq3 E2E (3 tests)
+ typeclass_22b3 (18 tests) all stay green; the E2E fixtures already
ship instances for the shadowed class.
## Per-task notes
- **mq.tidy.1** — Rigid-var refinement type-unification leg.
`refine_multi_candidate_residual` filter at lib.rs:2163-2180 extends
from `dc.class == c` to `dc.class == c && constraint_type_matches(&dc.type_, &residual.type_)`.
New unit test `mq_tidy_rigid_var_filter_uses_type_unification` pins
the same-class-different-typevar discrimination
(`prelude.Show a` + `userlib.Show b` declared, residual on Var `a`
`Resolved("prelude.Show")`, `userlib.Show` drops because its
declared type `Var b` doesn't unify with the residual's `Var a`).
Architecture decision per plan: only the discharge-time call site
needs the type-unification leg; the synth-time `resolve_method_dispatch`
is invoked with `concrete_arg_type: None` and constructs the
residual's metavar AFTER the dispatch call, so the rigid-var leg
there has no residual type to unify against (fresh metavar would
trivially unify with any declared-constraint type, degenerating to
class-only filter). The class-only filter at synth time is therefore
semantically correct, not drift.
- **mq.tidy.2** — Bare-class qualifier shape accepted. Inline
`qualifier_is_class_shape` predicate at the synth Var-arm dispatch
entry (originally lib.rs:2570-2575) extracted as a free
`pub(crate) fn qualifier_is_class_shape(&Option<String>) -> bool`
helper adjacent to `parse_method_qualifier`. Predicate body:
`None` → true (bare method); `Some(q)` with inner dot → true
(`<module>.<Class>`); `Some(q)` single-segment → first char
uppercase (PascalCase). Bare-fn qualifiers (`"std_list"`, lowercase)
correctly reject and fall through to the cross-module-fn arm.
Call site at the original location reduced to a one-liner. New
unit test `mq_tidy_bare_class_qualifier_shape_accepted` covers all
four shapes.
- **mq.tidy.3** — `class-method-shadowed-by-fn` warning tightened.
New `pub(crate) fn any_candidate_class_has_instance(...)` helper
using BTreeMap range-scan (O(log n) per candidate, O(|candidates|
* log n) total) checks the workspace registry for at least one
instance under any candidate class. Warning closure at
lib.rs:2479-2502 (now ~2498-2540 post-T2-insertion-shift) builds
a `registry_unit_view: BTreeMap<(String, String), ()>` per
closure invocation and calls the helper before emitting. New
unit test `mq_tidy_shadow_warning_suppressed_when_no_instance`
pins both directions (empty registry → suppress; one instance
→ fire). The existing `mq3_class_method_shadowed_by_fn_warning_fires`
in-crate test was a trip-wire — its fixture used
`Registry::default()` (no instances) and pre-tidy the warning
fired anyway because the old gate didn't check instances. Post-tidy
the spec rule correctly suppresses; fixture repaired by injecting
a `clsmod.Show Int` registry entry directly into `ws.registry.entries`
so the test continues to assert positive-fire behaviour under the
post-tidy rule. The three mq3 E2E fixtures (`mq3_class_eq_vs_fn_eq`
+ `mq3_two_show_*`) already ship instances on the shadowed class,
so they stayed green without modification.
- **mq.tidy.4** — DESIGN.md schema fragments. Four trailing-comment
annotations: `SuperclassRef` (`"superclass"` field at line 2139),
`InstanceDef.class` (line 2152), `Type::Con.name` (line 2253), and
`Constraint.class` (line 2273). Class-ref annotations cross-reference
§"Class names" / mq.1; the Type::Con annotation cross-references
§"Type::Con name scoping" / ct.1 (the actual section title — the
plan's "§Type names" / "Canonical type names" search did not match
exactly; used the canonical anchor title verified via grep). Doc
count of "canonical form" went from 1 → 5 (+4 as planned). All
annotations are inline trailing-comment appends; no line-count
change in DESIGN.md (2678 → 2678).
- **mq.tidy.5** — Integration verification.
`cargo test --workspace --no-fail-fast` → 548 passed, 0 failed
(was 545 pre-iter + 3 new mq_tidy_* unit tests). `bench/compile_check.py`
exit 0; first run showed 1 regression
(`build_O0_ms.bench_list_sum_explicit` +5.43% within 20.0% tolerance,
classified `ok`; the summary's "1 regressed" count is a
false-positive on the first run, second run shows 0 regressed,
24/24 stable). `bench/cross_lang.py` exit 0 both runs, 25/25 stable.
`bench/check.py` exit 0 both runs; run 1 had 3 regressed / 3
improved (all `latency.implicit_at_rc.*` max-tail metrics — same
noise class the audit-mq journal documented across 4 consecutive
runs); run 2 had 0 regressed / 1 improved / 62 stable — exactly
the metric-identity-migration pattern the audit named as variance,
not signal. Baseline pristine per the conservative-call convention
(6th-consecutive observation now). Plan Step 5 (prelude roundtrip
via `cargo run --bin ail -- check examples/prelude.ail.json`)
returned exit 1 with "module name 'prelude' is reserved" — but
this is identical to pre-edit behaviour (verified via `git stash`),
not a regression from this iter. The plan's expectation was
inaccurate; the actual sanity gate (DESIGN.md doc edits do not
break prelude consistency) is satisfied by the workspace test
suite passing 548/548.
## Concerns
- **Plan Task 3 Step 6 anticipation was off for one in-crate test.**
The plan said: "Expected: PASS — the existing positive fixtures
(`mq3_class_eq_vs_fn_eq`, `typeclass_22b3::rewrite_walker_skips_locally_shadowed_class_method`)
all ship registry instances for the shadowed class, so the
tightened predicate continues to admit the warning emission." This
was correct for those two fixtures (both E2E), but the in-crate
unit test `mq3_class_method_shadowed_by_fn_warning_fires` builds
its workspace with `Registry::default()` (zero instances) and
pre-tidy relied on the looser rule. Post-tidy it correctly
suppresses; the fixture was repaired inline by injecting a
`clsmod.Show Int` registry entry. This is exactly analogous to the
`typeclass_22b3::rewrite_walker_skips_locally_shadowed_class_method`
trip-wire mq.3 documented — same pattern, different test.
Fixture-repair as a coherence consequence of a deliberately
tightened spec rule; the test's intent ("warning fires when a
free fn shadows a class method") is unchanged.
- **Plan Task 5 Step 5 prelude check expectation was off.** The plan
said `cargo run --bin ail -- check examples/prelude.ail.json`
expected `ok` exit 0; the actual behaviour is exit 1 with the
"module name 'prelude' is reserved (auto-injected by the loader)"
error. This is identical to pre-edit behaviour (verified via
`git stash`); the prelude file is the loader's auto-injected
module, not a workspace entry, so `ail check` against it has
always failed by design. The right sanity gate for "DESIGN.md
doc edits do not break prelude consistency" is the workspace
test pass (548/548), which exercises every workspace test that
uses the prelude. The plan's prescribed command can be removed
from future tidy plans.
- **T2 docstring update on `parse_method_qualifier` was not
explicitly in the plan.** The plan asked for adding the new
`qualifier_is_class_shape` helper but did not enumerate the
paragraph update on `parse_method_qualifier`'s docstring. The
old docstring said "Callers gate on the qualifier shape: a class
qualifier is always qualified per mq.1's canonical-form rule" —
which the broadened gate directly contradicts (bare PascalCase
qualifiers now go to class-dispatch). Updating the existing
helper's documentation to reference the new helper and remove
the now-incorrect claim is strictly required to keep the helper's
documentation coherent with the requested behaviour change; it
is the docstring analog of "code edits strictly required to make
the requested changes compile." Defensible as a coherence
consequence, not unrequested scope creep.
## Known debt
- **bench/check.py max-tail noise envelope.** 6th-consecutive
observation of the `latency.implicit_at_rc.*` max-tail metric
identity migrating between runs. The conservative-call convention
has held the baseline pristine across 5 prior audits; this iter
adds run 6 to the lineage. A future audit may consider ratification
via `--update-baseline` if the pattern persists for another 2-3
audits; until then, the lineage is the documentation.
- **mq.tidy did not deliver the [low-1] (Trajectory B + D E2E)
or [low-2] (mono env shape) roadmap-backlog items.** Same scope
as audit-mq's deferral; these wait for the multi-class workspace
shape to land downstream.
- **mq.tidy did not refactor `synth(...)`'s 10-mut-ref signature.**
audit-mq's [medium-3] item was explicitly acknowledged-debt, not
in tidy scope. Same disposition; future iter may revisit if a
natural refactor opportunity arises.
## Files touched
Code:
- crates/ailang-check/src/lib.rs
Docs:
- docs/DESIGN.md
- docs/journals/2026-05-13-iter-mq.tidy.md (this file, new)
No new test files; the three new mq_tidy_* unit tests are inline in
`crates/ailang-check/src/lib.rs`'s `mod tests` block.
## Stats
bench/orchestrator-stats/2026-05-13-iter-mq.tidy.json
@@ -1,61 +0,0 @@
# iter rustdoc-sweep — clear all 23 cargo-doc warnings
**Date:** 2026-05-13
**Started from:** 48b1f77 (post-WhatsNew str-concat done-state)
**Status:** DONE
## Summary
Hygiene sweep against `cargo doc --workspace --no-deps`. Before: 23
warnings (17 in `ailang-check` + 4 in `ailang-core` + 2 in
`ailang-surface`). After: zero. All edits are documentation-only —
no production code, no test code changed. Tests stay 562 green.
Two warning classes, fixed with two different shapes:
- **Public doc links to private items** (15 warnings). Pattern:
a `pub` item's `///` doc references `[`fn_name`]` where `fn_name`
is `pub(crate)` or private. Fix: replace ``[`fn_name`]`` with
the plain backtick-code-span ``` `fn_name` ```. The identifier
stays readable in rendered docs but rustdoc no longer tries to
resolve the link.
- **Unresolved links** (8 warnings). Sub-classes:
- ``[`Registry`]`` in `mono.rs` (×3) — `Registry` lives in
`ailang_core::workspace` and the path was bare. Fixed by
fully-qualifying as `[`ailang_core::workspace::Registry`]` /
`[`ailang_core::workspace::Registry::entries`]`.
- ``[`Env::class_methods`]`` — `Env` is in this crate but
bare; the doc string was demoted to plain backticks
(`` `crate::Env::class_methods` ``) because `Env` itself has
no `pub` re-export at the crate root that rustdoc could
follow.
- ``[`crate::parse`]`` ambiguous in `loader.rs` (×2) — rustdoc
saw both a function and a module with that path. Disambiguated
to the function form via ``[`crate::parse()`]`` per
rustdoc's own suggestion.
- `metas[i]` in `lib.rs` — rustdoc parsed `[i]` as a link.
Escaped as `metas\[i\]` (same fix for the sibling `vars\[i\]`).
## Files touched
- `crates/ailang-core/src/desugar.rs` (1 line)
- `crates/ailang-core/src/workspace.rs` (3 lines)
- `crates/ailang-surface/src/loader.rs` (2 lines)
- `crates/ailang-check/src/uniqueness.rs` (2 lines)
- `crates/ailang-check/src/diagnostic.rs` (1 line)
- `crates/ailang-check/src/mono.rs` (5 lines)
- `crates/ailang-check/src/lib.rs` (8 lines)
## Verification
- `cargo doc --workspace --no-deps 2>&1 | grep "^warning:" | wc -l`
→ `0`.
- `cargo test --workspace` → 562 passed, 0 failed (baseline holds).
## Concerns
- (none)
## Known debt
- (none)
-149
View File
@@ -1,149 +0,0 @@
# iter str-concat — heap-Str concatenation primitive in four-site lockstep
**Date:** 2026-05-13
**Started from:** 679572a92d594d94b915d09fd60e7ef7f7bb83bd
**Status:** DONE
**Tasks completed:** 7 of 7
## Summary
`str_concat : (borrow Str, borrow Str) -> Str` shipped as a heap-Str
primitive in the four-site-lockstep pattern established by `str_clone`
(iter 24.1). Closes fieldtest-form-a friction finding #4. The
LLM-natural Show-body shape
`(app str_concat "label=" (app int_to_str x))` now parses, checks,
builds, and runs end-to-end via the new
`examples/show_user_adt_with_label.ail` fixture (`Item 42\n`
through `print . MkItem 42`).
Net delta: +3 tests (559 baseline → 562 green): the E2E pin
(`str_concat_e2e_show_user_adt_with_label`), the builtin-signature
unit test (`install_str_concat_signature` asserting the arity-2
Borrow-Borrow / Own-return shape directly on `env.globals`), and the
codegen IR-pin (`str_concat_emits_call_to_ailang_str_concat`
asserting both the extern declaration AND the call instruction in
emitted IR). Five `crates/ail/tests/snapshots/*.ll` files were
regenerated to absorb the new unconditional
`declare ptr @ailang_str_concat(ptr, ptr)` line in the IR header
preamble — same upkeep pattern as hs.4 (which regenerated the same
5 snapshots for the unconditional `@ailang_int_to_str` /
`@ailang_float_to_str` declares).
Task 5 repaired the bug-fixture / pin-test collision: the
`bugfix-instance-body-unbound-var` iter (commit 77f584a) used
`str_concat` as the literal unbound name because that was the
LLM-natural shape the fieldtester reached for; renamed to
`format_label` (LLM-author-realistic helper name) to preserve the
pin's intent (instance-method-body walked through unbound-var check)
while substituting a name that will never become a builtin.
## Per-task notes
- iter str-concat.1: RED fixture `examples/show_user_adt_with_label.ail`
(single-ctor `Item Int` ADT + `Show Item` body producing labelled
output) + E2E pin `crates/ail/tests/str_concat_e2e.rs` (check +
build + run asserting `Item 42\n` stdout). Confirmed RED with
`[unbound-var] prelude.Show: unknown identifier: 'str_concat'`.
- iter str-concat.2: `runtime/str.c` C helper `ailang_str_concat`
appended after `ailang_str_clone`; reads `len` from both source
payloads, `str_alloc(la+lb)`, memcpys both bytes, NUL-terminates.
Clean `clang -O2 -c` compile.
- iter str-concat.3: `crates/ailang-check/src/builtins.rs`
`env.globals.insert("str_concat", ...)` install entry after
str_clone block (arity-2, both Borrow, Own return); list() row
`("str_concat", "(Str, Str) -> Str")` after str_clone row;
`install_str_concat_signature` test reaches into `env.globals`
directly to match the arity-2 / param_modes / ret_mode discipline
(deeper than the sibling `synth_in_builtins_env`-based tests
because str_concat is the first builtin with arity-2 Borrow params).
- iter str-concat.4: `crates/ailang-codegen/src/lib.rs` four-site lockstep:
extern declaration after `@ailang_str_clone`; lower_app arm after
str_clone arm (arity-2 check + two `lower_term` + `fresh_ssa` +
body push); `is_builtin_callable` list extended with `"str_concat"`;
IR-pin test mirrors the sibling `str_clone_emits_call_to_ailang_str_clone`
pattern (Module/FnDef/Term::Do scaffolding via `super::*` +
`SCHEMA`, not the plan's `ailang_surface::parse` structural sketch —
plan explicitly authorised this in Step 4 note). IR-pin test
asserts both the extern declare AND the call site. RED→GREEN flip
on the E2E pin happens at this task. Five IR snapshots regenerated
via `UPDATE_SNAPSHOTS=1` to absorb the new declare line (sole
diff verified at line 17 across all snapshots).
- iter str-concat.5: bug-fixture / pin-test collision repaired —
`examples/bug_unbound_in_instance_method.ail` line 14 rename
`str_concat``format_label`; pin test
`crates/ail/tests/unbound_in_instance_method_pin.rs` `replace_all`
rename (one test name changed to
`check_fires_unbound_var_for_format_label_in_instance_method_body`).
Both pin tests pass; new fixture `ail check` reports
`ok (23 symbols across 2 modules)` — matches the plan's empirical
guess exactly.
- iter str-concat.6: `docs/DESIGN.md` new §"Heap-Str primitives"
subsection between the milestone-24 paragraph and the
`Primitive output goes through ...` paragraph; lists all five
shipping primitives with their type, iter origin, and prelude
role, plus the user-visible vs prelude-internal distinction.
- iter str-concat.7: `docs/roadmap.md` struck `[x] [feature] str_concat`
entry at end of P2 cluster (right before `## P3 — Ideas`). All
four cited fixtures (`show_user_adt_with_label`, `show_user_adt`,
`test_22c_user_class_e2e`, `cmp_max_smoke`) `ail check` clean.
`ail builtins | grep str_concat` returns `str_concat (Str, Str) -> Str`.
## Concerns
- Task 3 (Step 5) test-count prediction was off: plan expected
`passed: 560 failed: 1` after registering the builtin but actual
was `passed: 558 failed: 3`. Reason: the two
`unbound_in_instance_method_pin` tests turn RED as soon as
`str_concat` is registered in the checker, NOT only at codegen.
The pins assert on `ail check`'s `[unbound-var]` diagnostic, which
depends only on the checker registry. Implementation is correct;
Task 5 fully repairs. No follow-up needed beyond noting the
prediction-vs-actual gap.
- Task 4 (Step 7) test-count prediction was off: plan expected
`passed: 562 failed: 1` after codegen lands; actual after
IR-snapshot regen was `passed: 560 failed: 2`. The difference is
fully accounted for by the 2 still-RED pin tests (repaired in
T5) and by the IR-snapshot regen being out-of-band of the
per-task counters in the plan. Final state at T5 Step 5 is
exactly `passed: 562 failed: 0`, matching the plan's iter target.
- IR snapshot regeneration was not explicitly scripted in the plan
for Task 4, but it follows the hs.4 precedent (which regenerated
the same 5 snapshots for the same reason: a new unconditional
declare line in the IR header). The sole diff in each snapshot
is the new `declare ptr @ailang_str_concat(ptr, ptr)` line on
IR line 17, immediately before the existing
`declare i64 @llvm.fptosi.sat.i64.f64(double)` declaration.
## Known debt
- (none)
## Files touched
Code:
- `runtime/str.c` (M) — append `ailang_str_concat` helper
- `crates/ailang-check/src/builtins.rs` (M) — install entry, list()
row, signature unit test
- `crates/ailang-codegen/src/lib.rs` (M) — extern declare,
lower_app arm, is_builtin_callable list, IR-pin unit test
- `crates/ail/tests/str_concat_e2e.rs` (new) — E2E pin
- `crates/ail/tests/unbound_in_instance_method_pin.rs` (M) — rename
`str_concat``format_label` (4 sites + 1 test name)
- `examples/show_user_adt_with_label.ail` (new) — Show body fixture
- `examples/bug_unbound_in_instance_method.ail` (M) — rename
`str_concat``format_label` (1 site)
IR snapshots regenerated (5; sole diff = new declare line):
- `crates/ail/tests/snapshots/hello.ll`
- `crates/ail/tests/snapshots/list.ll`
- `crates/ail/tests/snapshots/max3.ll`
- `crates/ail/tests/snapshots/sum.ll`
- `crates/ail/tests/snapshots/ws_main.ll`
Docs:
- `docs/DESIGN.md` (M) — new §"Heap-Str primitives" subsection
- `docs/roadmap.md` (M) — struck `[x] [feature] str_concat` line
## Stats
bench/orchestrator-stats/2026-05-13-iter-str-concat.json
-119
View File
@@ -1,119 +0,0 @@
# audit-pd — milestone close (prelude-decouple)
**Date:** 2026-05-14
**Scope:** commits e6298f5..9a8d385 (spec → pd.1 plan → pd.1 → housekeeping → pd.2 plan → pd.2 → pd.3 plan → pd.3)
**Status:** DONE — drift fixed inline as `audit-pd-tidy`; bench ratified pristine
## Architect drift review
`ailang-architect` returned **drift_found** with one Important + one
Minor + one Nit:
- `[Important]` `docs/DESIGN.md` lines 2095-2099 + 2140-2143 — §"Roundtrip
Invariant" point 4 ("Carve-out anchor") still asserted "Eight
`.ail.json`-only fixtures (seven subject-matter rejection tests +
one compile-time-embed for the prelude)" and §Enforcement still
said "exactly the eight named carve-out files exist", but
post-pd.3 the count is seven and the compile-time-embed clause
is empty. The form-a spec was retired in place during pd.3 (§C4 (b)
RETIRED status marker); DESIGN.md was missed.
- `[Minor]` `crates/ail/src/main.rs` lines 2438-2448 — `local_types.get("prelude")`
fallback in `rewrite_type` (the migrate-canonical-types subcommand's
helper) was dead-by-construction after pd.3 retired the synthetic
prelude load in the same subcommand. A literal `"prelude"` string
in CLI scope that survived a milestone whose explicit purpose was
to delete exactly that pattern in core; the same drift category
one crate over.
- `[Nit]` `crates/ailang-core/src/lib.rs` — three pre-existing cargo
doc warnings on dangling `[load_workspace]` references carried
from pd.1 (already recorded as known debt in pd.1 + pd.2 journals).
Pre-existing baseline; not in this audit's scope. The roadmap's
P2 "Rustdoc warning sweep" todo covers it.
Both Important and Minor were fixed inline as `audit-pd-tidy` per
CLAUDE.md "trivial mechanical edit" carve-out (≤30 LOC, no
judgement). The Nit was deferred to its existing roadmap todo.
### Inline fixes applied
- `docs/DESIGN.md:2095-2099` rewritten: "Seven `.ail.json`-only
fixtures (subject-matter rejection tests) ... The prelude's
compile-time-embed carve-out, originally listed here as the eighth
fixture, was retired 2026-05-14 by milestone prelude-decouple;
the prelude is now embedded as `examples/prelude.ail` in
`ailang-surface` and parsed at compile time via
`ailang_surface::parse_prelude`."
- `docs/DESIGN.md:2141` rewritten: "exactly the seven named
carve-out files exist".
- `crates/ail/src/main.rs:2427-2448` — the `or_else` arm scanning
`local_types["prelude"]` was deleted (5 LOC body); replaced with
a multi-line comment explaining why (with a forward pointer for
any future migrate variant that needs prelude awareness:
repopulate `local_types["prelude"]` explicitly via
`parse_prelude`). The owner-search now scans `import_names` only.
No behavioural change against the current corpus (the `or_else`
arm could never fire post-pd.3).
`cargo test -p ailang-core --test carve_out_inventory` PASS post-fix.
`cargo build -p ail` clean.
## Bench-regression check
| Script | Exit | Result |
|--------|------|--------|
| `bench/check.py` | 1 | 63 metrics; 1 regressed (`latency.implicit_at_rc.p99_us` +14.42%, p99_9 +16.17%, max +27.61%, p99_over_median +9.79% — same `implicit_at_rc` cluster), 4 improved (`latency.explicit_at_rc.*` cluster), 58 stable. **Established 14-audit envelope-noise.** |
| `bench/compile_check.py` | 1 | 24 metrics; 11 regressed, 0 improved, 13 stable. The build-time-O0 noise cluster (audit-form-a, clippy-sweep). All within the 20% tolerance, ratifying as noise. |
| `bench/cross_lang.py` | 0 | 25 metrics; 0 regressed, 0 improved, 25 stable. Clean. |
`prelude-decouple` is a workspace-loader refactor + spec text +
file-system + test code only. Zero codegen / runtime / typecheck
path edited. The bench numbers cannot be causally affected by this
milestone; the `implicit_at_rc.p99_*` oscillation and the build-time
noise are the same long-running envelope every audit since audit-cma
has flagged. Baseline left pristine (15th consecutive audit decision
to do so on this envelope; the right resolution is a methodology
fix — already queued as P3 "Latency methodology rework" — not
per-audit baseline-juggling).
## Resolution
- `[Important]` DESIGN.md drift — FIXED inline (audit-pd-tidy).
- `[Minor]` `main.rs` dead prelude fallback — FIXED inline (audit-pd-tidy).
- `[Nit]` pre-existing cargo doc warnings — DEFERRED to P2 "Rustdoc
warning sweep" (existing roadmap todo).
- Bench check.py + compile_check.py exit 1 — RATIFIED as noise
envelope, baseline pristine, 15th consecutive observation.
## Files touched
- `docs/DESIGN.md` (two paragraphs in §"Roundtrip Invariant")
- `crates/ail/src/main.rs` (5 LOC body deletion + 8 LOC replacement
comment in `rewrite_type`)
- `docs/journals/2026-05-14-audit-pd.md` (this file)
No bench baseline files modified.
## Milestone status
`prelude-decouple` milestone: **CLOSED**. The roadmap entry at P0
moves to `[x]` and is removed from the active queue (the milestone
trace stays in the journal index for chronological context). The
post-milestone state of the working tree:
- `examples/prelude.ail.json` deleted; the prelude exists on disk
only as `examples/prelude.ail`.
- `crates/ailang-core/src/` contains zero literal-`"prelude"` strings.
- `crates/ail/src/main.rs` contains zero literal-`"prelude"` strings
in production code (the migrate-subcommand fallback is gone post-
audit-tidy).
- The two-phase workspace-loading composition `(load_modules_with →
caller-side prelude inject → build_workspace)` is the canonical
shape, exposed as public API in `ailang-core` and composed by
`ailang-surface::load_workspace`.
- `ailang_surface::parse_prelude` + `PRELUDE_AIL` are the canonical
embed surface, re-exported from `ailang-surface/src/lib.rs`.
- The CLAUDE.md "authors write `.ail`; the build derives the
JSON-AST in-process via `ailang_surface::parse`" doctrine holds
without exception; the prelude was the last carve-out and is now
inside the doctrine.
- 573 tests green workspace-wide.
@@ -1,95 +0,0 @@
# iter bugfix-mono-cursor-print-with-class-method-arg — `(app print (app eq …))` no longer rewrites the inner class-method to a stale `Show T` symbol
**Date:** 2026-05-14
**Started from:** 05e4c04e3ba74f3382216ff3ea285e3fb1e3a45d
**Status:** DONE
**Tasks completed:** 1 of 1
## Summary
The iter 24.3 `synth` Var-arm pushes BOTH a class `ResidualConstraint`
for each declared constraint AND a `FreeFnCall` observation at the
same poly-free-fn `Var` site (`crates/ailang-check/src/lib.rs` ≈ 2908
2966). The two mono walkers in `crates/ailang-check/src/mono.rs`,
however, were each consuming exactly one slot per `Var`: at a
poly-free-fn `Var` they popped one `free_fn_slot` and zero
`class_slot`s. Every constraint on a poly-free-fn-with-constraints
left a leftover class slot, so the next class-method `Var` in the
same body consumed the wrong slot — the residual that belonged to
`print`'s `Show T` constraint got cursor-aligned with the inner `eq`
`Var`, and codegen reported `call prelude.show__Bool arg type
mismatch: expected i1, got i64`.
The fix advances both walkers' cursors by `1 + N` at a poly-free-fn
`Var` whose source `Type::Forall` carries `N` class constraints. A
new sibling helper `poly_free_fn_constraint_counts_for_module`
mirrors `poly_free_fn_names_for_module`'s three-source spelling
contract (same-module / implicit-import bare / dot-qualified
`alias.name`) and produces a `BTreeMap<String, usize>` from synth-
visible name to declared-constraint count. The map is threaded into
the rewrite-phase loop alongside `poly_free_fns` and passed to both
`rewrite_mono_calls` and `interleave_slots`.
In `interleave_slots`, a poly-free-fn `Var` now pushes the `FreeFn`
slot first (this is what the rewrite walker reads for the rename),
then `N` filler entries drawn in order from `class_slots` (consumed
only to keep `class_cur` aligned). In `rewrite_mono_calls`, the
`Var`-arm captures `n_filler` from the count map BEFORE the rename
mutates `name` (lookup keys are the synth-visible spelling), reads
the slot at `*cursor` as before, then advances by `1 + n_filler`.
Class-method `Var`s stay at advance-by-one (synth pushes exactly one
residual and no `FreeFnCall`).
## Per-task notes
- task 1: write/verify the cursor-alignment fix.
- Reproduced RED at `print_with_class_method_arg_does_not_misalign_mono_cursor`
with the exact failure mode stated in the carrier:
`call prelude.show__Bool arg type mismatch: expected i1, got i64`.
- Added `poly_free_fn_constraint_counts_for_module` helper
immediately after `poly_free_fn_names_for_module`; identical
name-source contract, returning constraint counts instead of a
presence set.
- Threaded `poly_free_fn_ccounts: &BTreeMap<String, usize>` into
`collect_residuals_ordered`, `interleave_slots`, and
`rewrite_mono_calls` (signatures + every recursive call site).
- `interleave_slots::Term::Var` poly-free-fn branch: after pushing
the `FreeFn` slot, drain `N` entries from `class_slots` and push
them as fillers in `out`, advancing `class_cur` by `N`.
- `rewrite_mono_calls::Term::Var`: compute `n_filler` from the
count map before the rename mutation, then `*cursor += 1 +
n_filler`. Class-method `Var`s are guarded with `if
is_poly_free_fn { … } else { 0 }`.
- RED `print_with_class_method_arg_does_not_misalign_mono_cursor`
flips to GREEN with stdout `false` as predicted by the fixture
docstring.
- Full `cargo test --workspace` passes 564 / 564 (= 563 baseline
from `clippy-sweep` + the 1 new mini-mode RED). Zero
regressions across any of the existing show / mono / typeclass
test suites.
## Concerns
(none)
## Known debt
(none — the constraint set is a single new sibling helper plus a
new threaded argument; the two-walker symmetry remains the invariant
the existing comments already document.)
## Files touched
- `crates/ailang-check/src/mono.rs` — the fix (helper + two walker
signatures + Var-arm cursor advancement + recursive-call
threading).
- `crates/ail/tests/show_print_e2e.rs` — RED pin
`print_with_class_method_arg_does_not_misalign_mono_cursor`
(committed by `/debug` upstream of this iter; flips RED → GREEN
here).
- `examples/print_eq_arg_repro.ail` — RED fixture (committed by
`/debug` upstream; consumed by the pin).
## Stats
bench/orchestrator-stats/2026-05-14-iter-bugfix-mono-cursor-print-with-class-method-arg.json
@@ -1,85 +0,0 @@
# iter bugfix-print-leak-show-ret-mode — print() leaks heap-Str under --alloc=rc; Show class method's ret_mode stripped twice
**Date:** 2026-05-14
**Started from:** 301cbc33a0b2fed18f465c6c17041211ff7da35e
**Status:** DONE
**Tasks completed:** 1 of 1
## Summary
Two-layer bug: (i) the `Show` class method declaration in
`examples/prelude.ail.json` omitted the `ret_mode` key, so serde
defaulted it to `ParamMode::Implicit`; (ii) even with `ret_mode: own`
declared, `substitute_rigids` in `crates/ailang-check/src/lib.rs`
was hard-resetting `param_modes` to `vec![]` and `ret_mode` to
`Implicit` in its `Type::Fn` arm — silently stripping the mode
metadata during class-method mono synthesis (`show__Int`, `eq__T`,
`compare__T`, user-instance `eq__<UserType>`, etc.). Codegen's
`is_rc_heap_allocated` App-arm requires the callee's `ret_mode == Own`
to mark the let-binder trackable, so print's `let s = show x` binder
was never `ailang_rc_dec`'d at let-scope-close. The fix declares
`ret_mode: "own"` on the Show class method (parallel to the
`int_to_str`/`bool_to_str`/`float_to_str`/`str_clone`/`str_concat`
builtins which already carry `ret_mode: Own`) and repairs
`substitute_rigids`'s `Type::Fn` arm to clone `param_modes` and
preserve `ret_mode` through rigid substitution. The Form-A sibling
`examples/prelude.ail` is regenerated via `ail render` to stay in
lock-step per spec §C4 (b). Ten pinned mono-body hashes
(`eq__Int/Bool/Str`, `compare__Int/Bool/Str`, `show__Int/Bool/Str/Float`,
plus `eq__IntBox`) are re-pinned to the new GREEN baselines because
the mono bodies now carry the previously-stripped `param_modes` /
`ret_mode` metadata; bodies are semantically identical.
## Per-task notes
- bugfix.1: applied the fix.
- `examples/prelude.ail.json`: Show class method now has
`"ret_mode": "own"`.
- `examples/prelude.ail`: regenerated via
`cargo run -p ail -- render examples/prelude.ail.json
> examples/prelude.ail`; round-trips parse-isomorphic.
- `crates/ailang-check/src/lib.rs` `substitute_rigids` Type::Fn
arm: pattern-match all five fields explicitly; clone
`param_modes` and pass `*ret_mode` through (`ParamMode: Copy`).
Comment cites the bugfix iter id and the cause.
- `crates/ail/tests/mono_hash_stability.rs`: re-pinned 10 hashes
across the Eq/Ord and Show test fns; both bodies now report
`captured: {h}` on assertion failure (the Eq/Ord arm was
missing the captured-suffix; small in-place tidy folded into
the re-pin since the assertion-message string was being
rewritten anyway).
- `crates/ail/tests/eq_ord_e2e.rs`: re-pinned `eq__IntBox`
body hash to `3c4cf040cb4e8bb2`.
## Concerns
The debugger's cause analysis (carrier) pointed only at the JSON
omission. The JSON-only fix leaves the test RED — `substitute_rigids`
was silently stripping modes too. Suggest: at the next /audit pass,
verify there are no *other* call sites that build a `Type::Fn` with
explicit `..` field-spread and end up dropping modes. The pattern
`Type::Fn { ..., .. }` is the smell.
## Known debt
None — the fix is local. The wider question of "should `Type::Fn`'s
mode fields be field-init-by-name everywhere to prevent this class
of bug at the compiler level" is out of scope for this minimal fix.
## Files touched
- `examples/prelude.ail.json` — Show class: added `ret_mode: own`.
- `examples/prelude.ail` — regenerated Form-A sibling.
- `crates/ailang-check/src/lib.rs``substitute_rigids` Type::Fn
arm: preserve `param_modes` + `ret_mode`.
- `crates/ail/tests/mono_hash_stability.rs` — re-pinned 10 hashes
(6 Eq/Ord + 4 Show); added cause comments.
- `crates/ail/tests/eq_ord_e2e.rs` — re-pinned `eq__IntBox` hash.
- `crates/ail/tests/print_no_leak_pin.rs` (from /debug) — RED-pin
for the leak; now GREEN.
- `examples/print_int_no_leak_pin.ail` (from /debug) — fixture for
the RED-pin; now GREEN.
## Stats
bench/orchestrator-stats/2026-05-14-iter-bugfix-print-leak-show-ret-mode.json
@@ -1,72 +0,0 @@
# iter cli-diag-human — non-JSON CLI surfaces `WorkspaceLoadError` with bracketed `[code]`
**Date:** 2026-05-14
**Started from:** 6755060 (post-rpe.1.tidy)
**Status:** DONE
## Summary
Closes the P2 todo "CLI human-mode diagnostic surface for
`WorkspaceLoadError`" surfaced by the ct.1.8 tester (2026-05-11).
Pre-iter, the non-JSON path of every CLI subcommand that called
`ailang_surface::load_workspace(&path)?` propagated the error via
anyhow's Display — producing `Error: <thiserror-formatted-message>`
on stderr — while the JSON path of `ail check --json` correctly
routed through `workspace_error_to_diagnostic` and emitted the
bracketed `[code]` prefix. The asymmetry made the human-mode
output harder to triage and less consistent across the toolchain.
Fix: new private helper `load_workspace_human(path) -> Result<Workspace>`
in `crates/ail/src/main.rs` that wraps `ailang_surface::load_workspace`,
threads any `WorkspaceLoadError` through the existing
`workspace_error_to_diagnostic`, formats the resulting `Diagnostic`
into a single stderr line of shape
`error: [<code>] <def>: <message>`, and exits non-zero. Pure I/O
errors (where `workspace_error_to_diagnostic` returns `None` per
the loader's own contract) still propagate as anyhow errors so the
caller's `?`-flow stays well-behaved.
Nine call sites in `main.rs` swapped from
`ailang_surface::load_workspace(<expr>)?` to
`load_workspace_human(<expr>)?`: the `ail check` (non-JSON arm),
`build`, `run`, `emit-ir`, `prose`, `describe`, `deps`, `diff`,
`manifest` paths. The JSON arm of `ail check` (line 596) kept its
explicit `match` shape — it builds a structured diagnostics array
on stdout, which is a different output contract.
The `BareCrossModuleTypeRef` arm of `workspace_error_to_diagnostic`
gained the existing migration-command hint
("Run `ail migrate-canonical-types` to fix legacy fixtures.") so
that the ct.1.8 actionable-hint test
(`check_human_mode_emits_actionable_message_to_stderr`) stays
green. The hint was previously carried by `WorkspaceLoadError`'s
thiserror Display impl; making `workspace_error_to_diagnostic` the
single source for both JSON and human paths required moving it
into the Diagnostic message body.
## Files touched
- `crates/ail/src/main.rs` — new `load_workspace_human` helper
(~25 lines, including doc comment); 9 call-site swaps; one
Diagnostic message gained the migration-hint suffix.
- `crates/ail/tests/cli_diag_human_workspace_load_error.rs`
new RED-pin test (tempdir-based fixture asserting
`[module-not-found]` in stderr on a missing-import workspace).
- `docs/roadmap.md` — entry struck through.
## Verification
- RED test `check_non_json_emits_bracketed_module_not_found`
flipped from RED to GREEN.
- `cargo test --workspace` → 565 / 0 / 3 (was 564 pre-iter; +1
from this iter's RED pin).
- `cargo clippy --workspace --all-targets` → 0 warnings.
- `cargo doc --workspace --no-deps` → 0 warnings.
## Concerns
- (none)
## Known debt
- (none)
@@ -1,134 +0,0 @@
# iter clippy-sweep — clear all 61 `cargo clippy` warnings
**Date:** 2026-05-14
**Started from:** 04258c5 (post-WhatsNew of rustdoc-sweep + drift-test-narrowing)
**Status:** DONE
## Summary
Hygiene sweep against `cargo clippy --workspace --all-targets`. Before:
61 warnings across all eight crates. After: zero. Tests stay 563 green;
`cargo doc --workspace --no-deps` stays at zero warnings (the
rustdoc-sweep baseline); all three bench scripts (`bench/check.py`,
`bench/compile_check.py`, `bench/cross_lang.py`) all exit 0 against
the existing baselines — a free Stabilitätsbeweis that the sweep
touched no semantics.
Twelve warning classes, fixed with three different shapes:
- **Pure documentation fixes** (~32 warnings). `doc_lazy_continuation`
on multi-line `///` and `//!` comments where clippy parsed the
second line as a list continuation; resolved by either adding a
blank line, escaping the marker, or rephrasing the offending line.
Affected: `lex.rs:235-243` (3-line bullet list), `lib.rs:2360-2368`
in `ailang-check` (3-line bullet list), `lib.rs:3736` in
`ailang-check` (a `> 1` numeric comparison parsed as a quote
marker; rephrased to "any cardinality above one"), `drop.rs:570-585`
(numbered list + paragraph mix), `e2e.rs:915-920` (a `+ three
primitive instances` line where `+` was parsed as bullet marker),
`e2e.rs:1607-1614`, `typeclass_22b3.rs:706-708`, `eq_ord_e2e.rs:13`.
Plus four `empty_line_after_doc_comments` hits in
`workspace.rs:1675-2506` — these were `///` doc-comments left
orphaned by form-a.1 Task 5 test relocation; converted to plain
`//` comments (eight blocks total) since they no longer document
any function.
- **Idiomatic refactors** (8 warnings). `err_expect` (5 hits in
`parse.rs`): `.err().expect("…")``.expect_err("…")`.
`useless_conversion` (3 hits): `.extend(x.into_iter())``.extend(x)`
in `desugar.rs:275` + `lift.rs:201`; `vec!["x".into()]`
`vec!["x"]` in `lib.rs:5522` (where the call-site `fn_def`
parameter is `Vec<&str>` so the conversion was identity).
`redundant_closure` (2 hits): `|f| collect_pattern_binders(f)`
`collect_pattern_binders` in `linearity.rs:717`; same pattern in
`loader.rs:108`. `trim_split_whitespace`: `.trim().split_whitespace()`
`.split_whitespace()` in `prose/src/lib.rs:2103` (since
`split_whitespace` already ignores leading/trailing whitespace).
`single_char_add_str`: `out.push_str("x")``out.push('x')` in
`prose/src/lib.rs:383`. `collapsible_match` (2 hits): nested
`if let Some(_) = …` collapsed to `if let Some(Some(_)) = …` in
`mono.rs:1028` + `drop.rs:535-536`.
- **`derivable_impls`** (2 hits). `impl Default for ParamMode {
fn default() -> Self { ParamMode::Implicit } }` in `ast.rs:731`
→ `#[derive(Default)]` + `#[default] Implicit`. Same shape for
`AllocStrategy` in `codegen/src/lib.rs:164`. The derive form is
strictly more explicit (the `#[default]` attribute marks which
variant in-source rather than the impl body referencing it by
name) so the change is also a small clarity gain.
- **`blocks_in_conditions`** (1 hit). The synth `Term::Var` arm in
`check/src/lib.rs` had a 13-line block inside an `else if`
condition (the mq.2 dispatch entry: `else if { let (m, q) =
parse_method_qualifier(name); qualifier_is_class_shape(&q) &&
env.method_to_candidate_classes.contains_key(m) }`). Extracted
to a new helper `fn is_class_method_dispatch(name, env) -> bool`
next to `qualifier_is_class_shape`. The helper inherits the
block's full doc-comment so the why-each-branch reasoning is
preserved at the call site.
- **`only_used_in_recursion`** (1 hit). `walk_pattern(p, f)` in
`workspace.rs:1399` carries an `f: &mut F` parameter that
Pattern walking never invokes (ct.1 left this seam unused: no
Pattern variant carries a cross-module reference). Rather than
delete `f` (which would break the symmetry with the five other
`walk_*` framework functions in this file that all share the
same `<F>(_, f: &mut F) -> Result<…>` signature), added
`#[allow(clippy::only_used_in_recursion)]` with an inline
comment naming the framework-uniformity rationale.
- **`if_same_then_else`** (2 hits). Both in `qualify_local_types`
shapes (`check/src/lib.rs:3457` + `codegen/src/subst.rs:165`):
the if-chain has two branches returning `name.clone()` for
semantically distinct reasons (`name.contains('.')` = already
qualified; `is_primitive_name(name)` = primitive needs no
qualification). Combining the branches with `||` would obscure
the reasoning. Added `#[allow(clippy::if_same_then_else)]` with
an inline comment naming why each branch disqualifies the name.
- **`too_many_arguments`** (1 hit). `Emitter::new` takes 8 of the
workspace-flat tables the emitter needs at construction time.
Bundling them into a context struct would just rename the
boilerplate without reducing it. Added
`#[allow(clippy::too_many_arguments)]` with the rationale
inline.
## Files touched
- `crates/ailang-core/src/ast.rs` (1 derive flip)
- `crates/ailang-core/src/desugar.rs` (1 .extend)
- `crates/ailang-core/src/workspace.rs` (1 walk_pattern allow + 8 `///`→`//` blocks)
- `crates/ailang-surface/src/lex.rs` (1 doc-list blank line)
- `crates/ailang-surface/src/parse.rs` (5 `.expect_err`)
- `crates/ailang-surface/tests/loader.rs` (1 redundant closure)
- `crates/ailang-prose/src/lib.rs` (push_str + trim removal)
- `crates/ailang-check/src/lib.rs` (helper extraction + 1 allow + 1 doc rephrase + 1 doc-list blank line + 1 vec literal)
- `crates/ailang-check/src/lift.rs` (1 .extend)
- `crates/ailang-check/src/linearity.rs` (1 redundant closure)
- `crates/ailang-codegen/src/lib.rs` (1 derive flip + 1 too_many_args allow)
- `crates/ailang-codegen/src/subst.rs` (1 if_same_then_else allow)
- `crates/ailang-codegen/src/drop.rs` (1 doc-list blank line + 1 collapsible_match)
- `crates/ailang-check/src/mono.rs` (1 collapsible_match)
- `crates/ail/tests/e2e.rs` (2 doc rephrasings)
- `crates/ail/tests/typeclass_22b3.rs` (1 doc rephrasing)
- `crates/ail/tests/eq_ord_e2e.rs` (1 doc rephrasing)
## Verification
- `cargo clippy --workspace --all-targets 2>&1 | grep -c '^warning:'`
→ `0` (was 61).
- `cargo test --workspace` → 563 passed + 3 ignored, 0 failed
(baseline holds).
- `cargo doc --workspace --no-deps 2>&1 | grep -c '^warning:'` →
`0` (rustdoc-sweep baseline holds — no regression).
- `bench/check.py` → exit 0.
- `bench/compile_check.py` → exit 0.
- `bench/cross_lang.py` → exit 0.
## Concerns
- (none)
## Known debt
- (none)
-120
View File
@@ -1,120 +0,0 @@
# iter pd.1 — core API split (load_modules_with + build_workspace) — surface unchanged via shim
**Date:** 2026-05-14
**Started from:** 61167a4ef0ade5dd65dadfbdde1a44b65c6693ca
**Status:** DONE
**Tasks completed:** 6 of 6
## Summary
pd.1 carved out two new public fns in `crates/ailang-core/src/workspace.rs`
(`load_modules_with` for DFS-only loading, `build_workspace` for validation +
registry construction with a caller-supplied `implicit_imports: &[&str]`),
threaded the new parameter through `validate_canonical_type_names` /
`check_class_ref` / `check_type_con_name`, and replaced the four
hardcoded literal-`"prelude"` fallback blocks in the diagnostic helpers
with a parameterised iter-loop over `implicit_imports`. The surviving
`load_workspace_with` collapses from a ~70-line inline DFS+inject+validate
block to a 3-line composition (`load_modules_with` → caller-side prelude
inject → `build_workspace`). The legacy JSON-only wrapper `pub fn
load_workspace` retired with zero production callers (only doc-comment
references and 9 in-mod test callers; the latter migrated to
`load_workspace_with(&entry, load_one)`). Surface (`ailang-surface`) and
all downstream crates compile + test unchanged because the shim
preserves today's `load_workspace_with` semantics; the regression-pin
test asserts the prelude module hash matches `load_prelude()` post-refit.
Production-code literal-`"prelude"` count in workspace.rs dropped from 8
to exactly the 4 expected sites (all in the shim, none in the
diagnostic helpers). pd.2 will move the prelude-embed + the inject step
into `ailang-surface` and retire the shim; pd.3 will delete
`examples/prelude.ail.json`.
## Per-task notes
- iter pd.1.1: added `pub fn load_modules_with` above `load_workspace_with`
(extracted DFS pre-amble verbatim from the old inline body); RED test
`load_modules_with_returns_modules_without_prelude` proves the new fn
returns modules without auto-injecting prelude. First-shot GREEN.
- iter pd.1.2: added `pub fn build_workspace` next to `load_modules_with`
(validation + registry construction; takes pre-assembled modules-map
and `implicit_imports: &[&str]`). RED test
`build_workspace_accepts_assembled_modules_and_runs_validation` ends
the task RED-on-purpose per the plan (the inner call to
`validate_canonical_type_names` doesn't have the parameter yet);
Task 3 lands the green.
- iter pd.1.3: added `implicit_imports: &[&str]` parameter to
`validate_canonical_type_names`, `check_class_ref`,
`check_type_con_name`; threaded it through 6 internal call sites in
`validate_canonical_type_names` (2 in the type-walk + 4 in the
class-ref match-arms); rewrote the two literal-`"prelude"` fallback
blocks in the diagnostic helpers as iter-loops over `implicit_imports`.
RED test `implicit_imports_arg_drives_prelude_fallback_in_diagnostics`
exercises both branches (with-prelude includes `prelude.Eq` candidate;
without-prelude excludes it). Migrated 18 in-mod test callers + 1
production callsite (the still-inline shim body) to
`&["prelude"]`. Task 2's test now passes. New helpers
`module_with_bare_classref` and `module_with_class_def` use the
existing `serde_json::from_value(json!(...))` pattern (the rest of
mod tests' convention) rather than the plan's struct-literal style
(which would have failed against actual `ClassDef.superclass:
Option<...>` field shape).
- iter pd.1.4: replaced `load_workspace_with`'s ~70-line body with the
3-line shim composition; doc-comment rewritten to describe the shim
role + retain the ext-cli.1 cycle-avoidance rationale on the
loader-injection point. Pin test
`load_workspace_with_shim_preserves_today_semantics` asserts
prelude-module hash equality with `load_prelude()` post-refit.
- iter pd.1.5: deleted `pub fn load_workspace`; removed it from
`lib.rs` re-export; rewrote the workspace.rs module rustdoc example
+ 3 lib.rs `//!` references to point at `load_workspace_with`;
hoisted the orphaned "Algorithm: DFS over imports" doc-comment block
onto `load_modules_with` (where it now belongs). Migrated 9 in-mod
test callers from `load_workspace(&entry)` to
`load_workspace_with(&entry, load_one)`. Required `#[cfg(test)]`
gating of `fn load_one` and the `load_module` import (now consumed
only by the test mod) to silence dead-code warnings.
- iter pd.1.6: verification — `cargo test --workspace` all green
(every crate's tests pass); `bench/check.py` + `bench/compile_check.py`
+ `bench/cross_lang.py` all exit 0; production literal-`"prelude"`
count in workspace.rs is exactly 4 (the four expected shim sites at
lines 553/555/558/560 — all in `load_workspace_with` body — none in
the diagnostic helpers).
## Concerns
- pd.1.2 ends RED-on-purpose per the plan. This is intentional and
documented in the plan, but if a future iter ever bisects through
this plan the workspace WILL NOT compile at the Task 2 boundary —
Task 3 must land in the same Boss commit. The Boss-side commit
shape for pd.1 is therefore "one cohesive iter commit" not "per-task
commits".
- pd.1.5 needed an unanticipated `#[cfg(test)]` gating step on the
`load_one` fn + the `load_module` import. The plan didn't anticipate
this because it framed the `load_workspace` retirement as "doc + lib
cleanup only", but `load_one` was the only consumer of those imports
in production. Recording so a future planner adds an "if removing a
caller, check whether its callees become test-only" check.
## Known debt
- pd.2 will move prelude-embed (`PRELUDE_JSON` const + `load_prelude` fn)
into `ailang-surface` and rewire surface's `load_workspace` to call
`load_modules_with` + `build_workspace` directly, retiring the shim.
Out of scope for pd.1 by spec.
- pd.3 will delete `examples/prelude.ail.json`. Out of scope for pd.1.
- The latency.implicit_at_rc improvement cluster appearing in
`bench/check.py` (4 metrics regressed, 2 improved) reappears for
the 4th audit in a row; cause not localised. pd.1 is a workspace.rs
refactor with zero codegen / runtime touch, so cannot be the source.
Baseline left pristine (consistent with audit-eob and earlier
decisions).
## Files touched
- `crates/ailang-core/src/workspace.rs` (extraction + threading +
shim refit + dead-fn delete + test-mod migration)
- `crates/ailang-core/src/lib.rs` (re-export drop + 3 rustdoc updates)
## Stats
`bench/orchestrator-stats/2026-05-14-iter-pd.1.json`
-176
View File
@@ -1,176 +0,0 @@
# iter pd.2 — surface owns prelude embed; pd.1 shim retired
**Date:** 2026-05-14
**Started from:** 116157a2b84a98060c1b6c04eb4ec071fe40eb93
**Status:** DONE
**Tasks completed:** 6 of 6
## Summary
pd.2 moves the prelude embed (`PRELUDE_AIL` const + `parse_prelude` fn) and
the prelude-injection step (with the `ReservedModuleName` reservation) out
of `ailang-core` into `ailang-surface`, rewires `surface::load_workspace`
from the 3-line shim-call into the 7-line composition `(load_modules_with
→ reserved-name check → inject parse_prelude → build_workspace(&["prelude"]))`,
and retires the pd.1 compatibility shim `core::load_workspace_with` along
with `load_one`, `load_prelude`, `PRELUDE_JSON`, and the `#[cfg(test)] use
crate::load_module;` import. Cross-form-identity preflight (hash-equality
between `parse(prelude.ail)` and `serde_json::from_str(prelude.ail.json)`)
PASSES at hash `3abe0d3fa3c11c99` — the load-bearing assumption that
justifies pd.3's deletion of `prelude.ail.json` is verified. Production
literal-`"prelude"` count in `core/src/` is now zero. Test count: 573
green (from the pd.1 baseline of 564, +9 net = 3 new surface pins
[parse + hash + injection] + 6 tempdir/fixture relocations to
workspace_pin.rs minus 6 in-mod placeholders that became comments;
in-mod test deletion of the pd.1 shim test is offset by the
`load_modules_with_custom_loader_is_called` rename + 2 pd.1 tests that
now use an inline test-private `load_one` helper). `bench/cross_lang.py`
+ `bench/compile_check.py` exit 0; `bench/check.py` exit 1 with the
12-audit noise envelope (the `bench_list_sum.bump_s` + tail-latency
oscillation that has been observed in audits since audit-cma; pd.2 is
a workspace-loader refactor with zero codegen / runtime touch). pd.3
will delete `examples/prelude.ail.json` plus the
`prelude_ail_and_json_parse_to_identical_module` preflight, the
`include_str!` carve-out in `crates/ail/src/main.rs:472-484`, the
`carve_out_inventory.rs` §C4(b) row, the form-a §C4(b) spec text, and
the `migrate-bare-cross-module-refs` defensive include.
## Per-task notes
- iter pd.2.1: `pub const PRELUDE_AIL: &str = include_str!("../../../examples/prelude.ail");`
+ `pub fn parse_prelude() -> Module` added near the top of
`crates/ailang-surface/src/loader.rs`; both re-exported from
`ailang-surface/src/lib.rs`. RED test
`parse_prelude_returns_module_named_prelude_with_min_defs` failed
with the expected unresolved-import message, then GREEN. First-shot
clean.
- iter pd.2.2: `crates/ailang-surface/tests/prelude_module_hash_pin.rs`
with `prelude_ail_and_json_parse_to_identical_module` (cross-form-
identity preflight, load-bearing for pd.3) + `prelude_parse_yields_canonical_hash`
(literal-hex anchor). Preflight PASSED first-run (the round-trip
invariant from form-a.0 + form-a.1 was already exercising
parse-then-print idempotency on every `.ail` fixture, so
parse(prelude.ail) ≡ deserialize(prelude.ail.json) was not an
accident). Hash `3abe0d3fa3c11c99` captured at pd.2 close.
- iter pd.2.3: `surface::load_workspace` rewritten 3-line → 7-line
composition; imports updated `load_workspace_with` → bare
`Workspace, WorkspaceLoadError` with `load_modules_with` /
`build_workspace` fully-qualified inline; module rustdoc rewritten
to name the new injection point + the implicit-imports list.
Regression-pin tests `surface_load_workspace_injects_prelude_into_every_workspace`
+ `surface_load_workspace_rejects_user_module_named_prelude` PASS
before AND after the rewrite (semantics preserved). DONE_WITH_CONCERNS:
the plan's example used `examples/hello.ail.json` which doesn't
exist post-form-a.1 (deleted in T8); switched to `examples/hello.ail`.
- iter pd.2.4: 10 in-mod tests RELOCATED to
`crates/ailang-core/tests/workspace_pin.rs` rather than aliased per
the plan's literal text — the dev-dep cycle (`ailang-core`
`ailang-surface` via `[dev-dependencies]`) makes
`surface::load_workspace` calls from `ailang-core`'s lib-test crate
structurally impossible (two distinct compilations of `ailang-core`
appear in the test build, types don't match). The integration-test
crate consumes both crates against the same `ailang-core` artefact,
so the cycle never materialises — same pattern form-a.1 T5 used for
similar relocations. The 11th test (`load_workspace_with_custom_loader_is_called`)
renamed to `load_modules_with_custom_loader_is_called` with the
closure adapted to wrap `crate::load_module` directly via the same
Io/Schema mapping `load_one` had. The 12th test
(`load_workspace_with_shim_preserves_today_semantics`) deleted
outright with a forwarding comment to the surface-side hash pin.
- iter pd.2.5: 5 deletions (`PRELUDE_JSON`, `load_prelude`,
`load_workspace_with`, top-level `load_one`, `#[cfg(test)] use
crate::load_module;`); module rustdoc reworked to two-phase
composition framing; lib.rs three rustdoc sites (lines 16-21, 50-51,
120) re-pointed at `ailang_surface::load_workspace`. ONE plan-vs-actual
deviation: the plan didn't enumerate the 2 pd.1-introduced in-mod
tests at the bottom of `mod tests` that consume `load_one` directly
(`load_modules_with_returns_modules_without_prelude`,
`build_workspace_accepts_assembled_modules_and_runs_validation`);
preserved by adding a test-mod-private `load_one` helper inside `mod
tests` (production-symbol-level deletion preserved; helper is
scoped only to the test module, not exported). The
`build_workspace...` test also called `load_prelude()` directly;
replaced inline with `serde_json::from_str(include_str!(...))` of
`prelude.ail.json` (the same one-line code `load_prelude` was). Zero
literal-`"prelude"` strings in production `core/src/` confirmed by
grep. cargo doc warnings: 3 (unchanged from pre-pd.2 baseline; all
three are dangling `[`load_workspace`]` references in the `Workspace`
/ `Registry` rustdoc that pd.1 left behind when it deleted
`load_workspace`; out of scope per the implementer's
no-surrounding-cleanup discipline, recorded as known debt).
- iter pd.2.6: bench verification — `cross_lang.py` exit 0 (25/25
stable), `compile_check.py` exit 0 (24/24, "11 regressed" reported
but exit code says baseline-tolerance check passes — this is the
audit-form-a + clippy-sweep-named noise on the corpus's small build
fixtures), `check.py` exit 1 with 1 regression on `bench_list_sum.bump_s`
+13.61% (within established noise envelope; second run showed
same `bump_s` plus a tail-latency `implicit_at_rc.p99_us` blip).
All three are runtime/compile metrics that cannot be touched by a
workspace.rs/loader.rs/lib.rs refactor (no codegen / no runtime /
no typecheck path edited).
## Concerns
- pd.2.3: plan's regression-pin used `examples/hello.ail.json` which
doesn't exist post-form-a.1 T8. Switched to `examples/hello.ail`.
Recommend a planner check: when a plan references a `.ail.json`
fixture, verify the fixture exists post-form-a.1 (the carve-out
inventory is the right reference).
- pd.2.4: plan's "alias to surface_load_workspace and stay in-mod" was
structurally invalid because of the `ailang-core``ailang-surface`
dev-dep cycle inside the lib-test target. Relocated to
`tests/workspace_pin.rs` instead — same pattern form-a.1 T5 used.
Recommend: planner recon-check should detect when a planned in-mod
test is meant to consume an external dev-dep cycle that the lib-test
target can't resolve.
- pd.2.5: plan's "delete `load_one`" preserved at production-symbol
level (the top-level `#[cfg(test)] fn load_one` is gone) by
introducing a test-mod-private `fn load_one` helper inside `mod
tests` for the 2 pd.1 in-mod tests that still consume it. The
symbol's name is preserved for clarity; its scope is now narrower
than pre-pd.2.
- pd.2.6: `check.py` exit 1 — established noise envelope, 13th
consecutive observation pattern (audit-cma onwards). pd.2 is a
workspace-loader refactor with zero codegen / runtime / typecheck
touch, so cannot be the source. Baseline left pristine consistent
with all prior audit decisions on this envelope.
## Known debt
- 3 cargo doc warnings on `[`load_workspace`]` references in the
`Workspace` (line 44, 49) + `Registry` (line 64) rustdoc — pd.1
deleted `load_workspace` but didn't sweep these references. Pre-
pd.2 baseline already carried them; pd.2 didn't add to the count;
not within pd.2 scope (no-surrounding-cleanup discipline).
- pd.3 work explicitly out of scope: deleting `examples/prelude.ail.json`,
the `prelude_ail_and_json_parse_to_identical_module` preflight test
(which depends on the JSON file's existence), the `include_str!` of
`prelude.ail.json` in `crates/ail/src/main.rs:472-484`, the
`carve_out_inventory.rs` §C4(b) row, the form-a §C4(b) spec text
edits, and the `migrate-bare-cross-module-refs` defensive include.
## Files touched
Modified:
- `crates/ailang-core/src/lib.rs` (3 rustdoc sites updated)
- `crates/ailang-core/src/workspace.rs` (5 deletions + module rustdoc
reframe + 2 pd.1 in-mod test repairs + 6 in-mod test placeholders +
test-mod-private `load_one` helper restored at narrow scope)
- `crates/ailang-core/tests/workspace_pin.rs` (10 relocated tests
appended + helper fn `write_simple_module_json` + preamble extension)
- `crates/ailang-surface/src/lib.rs` (re-export of `parse_prelude` +
`PRELUDE_AIL`)
- `crates/ailang-surface/src/loader.rs` (`PRELUDE_AIL` const +
`parse_prelude` fn added; `load_workspace` body 3-line → 7-line
composition rewrite; module rustdoc reframed; imports updated)
New:
- `crates/ailang-surface/tests/prelude_parse_pin.rs` (1 test)
- `crates/ailang-surface/tests/prelude_module_hash_pin.rs` (2 tests:
cross-form-identity preflight + literal-hex anchor `3abe0d3fa3c11c99`)
- `crates/ailang-surface/tests/prelude_injection_pin.rs` (2 tests:
inject + reservation pin)
## Stats
`bench/orchestrator-stats/2026-05-14-iter-pd.2.json`
-209
View File
@@ -1,209 +0,0 @@
# iter pd.3 — prelude.ail.json deleted; prelude-decouple milestone closed
**Date:** 2026-05-14
**Started from:** c0dd2bc334d490e5e90ad10501c33e48076ff2bd
**Status:** DONE
**Tasks completed:** 8 of 8
## Summary
pd.3 closes the **prelude-decouple milestone** (pd.1 + pd.2 + pd.3) by
deleting `examples/prelude.ail.json` from the working tree, sweeping the
three remaining `include_str!` consumers (one in-mod test in
`workspace.rs`, the cross-form-identity preflight in
`prelude_module_hash_pin.rs`, the defensive include in `ail/src/main.rs`'s
`migrate-canonical-types` subcommand) and the lockstep skip-branch the
defensive include necessitated, dropping the §C4 (b) entry from
`carve_out_inventory.rs` (8 → 7 named carve-outs), retiring §C4 (b) of
`docs/specs/2026-05-13-form-a-default-authoring.md` in place with a
"Status: RETIRED 2026-05-14" marker that preserves the original framing
as historical context, and adding `prelude_decouple_carve_out_pin.rs`
as the milestone's permanent guard against regression. At milestone
close: the prelude exists on disk only as `examples/prelude.ail`; core
embeds zero prelude bytes; the `migrate-canonical-types` subcommand no
longer auto-injects a synthetic prelude entry; the doctrine "authors
write `.ail`; the build derives the JSON-AST in-process via
`ailang_surface::parse`" (CLAUDE.md) holds without exception. Test count:
573 green workspace-wide (preserved from pd.2; +1 carve-out pin -1
cross-form-identity preflight +1 relocated `build_workspace_accepts...`
test -1 in-mod twin = net 0). `bench/cross_lang.py` + `bench/compile_check.py`
exit 0; `bench/check.py` exit 1 with the same envelope-noise pattern
observed since audit-cma (14th consecutive observation; pd.3 is a
file-system + spec-text + test deletion + dead-code cleanup with zero
codegen / runtime / typecheck touch, so cannot be the source).
## Per-task notes
- iter pd.3.1: `build_workspace_accepts_assembled_modules_and_runs_validation`
RELOCATED from `crates/ailang-core/src/workspace.rs::tests` (in-mod) to
`crates/ailang-core/tests/workspace_pin.rs` (integration crate) instead
of the literal call-site swap the plan prescribed. The plan specified
"use the fully-qualified `ailang_surface::parse_prelude()` at the call
site" claiming it was the workaround pd.2 used; in fact pd.2 worked
around this same dev-dep cycle by inlining
`serde_json::from_str(include_str!("prelude.ail.json"))` precisely
BECAUSE `parse_prelude()` doesn't typecheck from in-mod (the lib-test
target sees two distinct `ailang-core` compilations and the `Module`
types fail to unify). pd.3 cannot use the JSON workaround because the
JSON file is being deleted. The structural fix is the relocation
pattern pd.2.4 already used for 10 sibling tests — same crate edge,
same outcome. A 6-line stub-comment in `workspace.rs::tests` documents
the dev-dep cycle so future maintainers don't try to put the test
back. New test `load_one_adapter` helper at the top of `workspace_pin.rs`
mirrors the in-mod `load_one` exactly (Io/Schema mapping). DONE_WITH_CONCERNS:
plan defect (treated under pd.2.4 lesson recommendation but not
caught at planning time).
- iter pd.3.2: 5 deletions in `prelude_module_hash_pin.rs` (cross-form-
identity preflight test fn, `PRELUDE_JSON` const, `PRELUDE_AIL` import,
`const _: &str = PRELUDE_AIL;` shape-pin, lines 1-15 of original
module doc-comment); module doc-comment replaced verbatim with the
surviving-only version (preserves the historical record of why the
preflight existed, what it discharged, when it was retired). Hash
`3abe0d3fa3c11c99` still asserted by the surviving
`prelude_parse_yields_canonical_hash`. Zero orphan-import warnings.
First-shot clean.
- iter pd.3.3: defensive `include_str!` block (12 lines) deleted from
`Cmd::MigrateCanonicalTypes` arm; lockstep skip-branch (4 lines)
deleted from the rewrite loop's Pass 2. The plan referenced the
subcommand by its variant tag `MigrateBareCrossModuleRefs` but the
user-facing subcommand name is `migrate-canonical-types` — spot-test
ran under the correct name and the help text printed. DONE_WITH_CONCERNS:
plan referenced obsolete variant name; doc-comment update on
`MigrateCanonicalTypes` (one paragraph) was needed beyond strict task
text because the prelude-fallback capability that the doc-comment
asserted ("Prelude is consulted as an implicit last-resort fallback
when no listed import owns the name") was silently removed by Step 2
— leaving the doc-comment in would have been an active mis-statement
in `--help` output. Update names pd.3 as the retirement point.
- iter pd.3.4: 3 edits to `carve_out_inventory.rs` (header sentence
"eight" → "seven"; comment block re-published per plan; `"prelude.ail.json"`
entry + its `// §C4 (b) — compile-time-embed` comment dropped from
EXPECTED). Test RED-on-purpose post-edit (file still on disk);
flips GREEN at Task 7.
- iter pd.3.5: Status marker inserted verbatim above §C4 (b) heading
in `docs/specs/2026-05-13-form-a-default-authoring.md`. Original §C4
(b) text preserved as historical context (per the spec's convention
for retired sections). Reads coherently top-to-bottom; the marker
reads as a forward-pointer to the resolution and the original text
reads as historical motivation.
- iter pd.3.6: `crates/ailang-surface/tests/prelude_decouple_carve_out_pin.rs`
created verbatim per the plan. RED-on-purpose at this task (file still
exists); flips GREEN at Task 7. Failure message names the four things
a future maintainer must do to legitimately reintroduce the file —
pre-empts the "it's just one file" rationalisation.
- iter pd.3.7: pre-deletion verification clean (zero `include_str!`
consumers; the two non-`include_str!` `.rs` references are inert —
`print_no_leak_pin.rs:110` is a string-literal in an assertion
message, `prelude_injection_pin.rs:28` constructs a tempdir-relative
path that does not read the in-tree file). `rm examples/prelude.ail.json`.
Both pin tests flip RED → GREEN at this step. Workspace test count
unchanged at 573 green.
- iter pd.3.8: `bench/compile_check.py` exit 0; `bench/cross_lang.py`
exit 0; `bench/check.py` exit 1 with the established noise envelope
(the `bench_list_sum.bump_s` and `implicit_at_rc` tail-latency
oscillation observed since audit-cma — 14th consecutive observation).
Acceptance criterion sweep: zero `include_str!` of `prelude.ail.json`
workspace-wide; zero literal-`"prelude"` in `crates/ailang-core/src/`
production scope (the matches in `workspace.rs` are all inside
`#[cfg(test)] mod tests`); CLAUDE.md "Authors write `.ail`" sentence
is consistent with the post-milestone state without modification (the
prelude exception it implicitly tolerated pre-milestone is gone);
`docs/DESIGN.md` carries no production-doctrine assertions about core
embedding the prelude or referencing `prelude.ail.json`.
## Concerns
- pd.3.1: plan defect — the task text instructed a call-site swap
("In each affected test fn body, replace the pattern ... with
`ailang_surface::parse_prelude()`") that doesn't typecheck due to
the dev-dep cycle. The plan also misattributed pd.2's actual
workaround (`serde_json::from_str(include_str!(...))`) as
successful use of `parse_prelude()` from in-mod. Recommend planner
recon-check: when a plan proposes a call-site change involving
`ailang_surface::*` from in-mod tests, verify the test target can
resolve the dev-dep edge (the lib-test target generally cannot;
the integration-test crate generally can — same lesson pd.2.4
recorded but didn't propagate to the planner skill).
- pd.3.3: plan referenced subcommand by its internal variant tag
(`MigrateBareCrossModuleRefs`) instead of its user-facing name
(`migrate-canonical-types`). Trivial repair (test under correct
name) but recommend planner check: verify subcommand-arm references
in spot-test commands by enumeration-after-rename.
- pd.3.3 (deeper): the plan's "Architecture" preamble called the
defensive include "a documented no-op" because the inserted entry
was unconditionally skipped by the lockstep guard at lines 500-505.
This is correct about WRITING the rewritten module file back, but
not about CONSULTING `local_types["prelude"]` for fallback rewrites
(which the inserted entry populated, lines 487-497, before the Pass
2 skip). Removing both blocks silently retired the prelude-fallback
capability of the rewrite logic. The plan called the change net-zero
behaviour; it isn't. The doc-comment update I made on
`MigrateCanonicalTypes` makes the new behaviour visible. The
rewrite-helper's prelude-fallback branch at lines ~2436-2446 in
`main.rs` is now dead-by-construction (the `local_types` map will
never have a `"prelude"` key, since no prelude module is loaded by
this subcommand) — left in place per the implementer's
no-surrounding-cleanup discipline; tagged below as known debt.
## Known debt
- `crates/ail/src/main.rs:~2436-2446`: the `local_types.get("prelude")`
fallback in `rewrite_def`'s `rewrite_type_con_owner` (or analogue)
is dead-by-construction post-pd.3 since the `local_types` map for
the `migrate-canonical-types` subcommand no longer contains a
`"prelude"` key. Out of pd.3's literal scope (no surrounding cleanup);
candidate for a follow-up tidy iter or for the post-pd.3 audit's
drift sweep.
- 3 cargo doc warnings on dangling `[`load_workspace`]` references in
`Workspace` and `Registry` rustdoc — pre-existing baseline carried
forward from pd.2; out of pd.3 scope. (Identical entry was in the
pd.2 known-debt section.)
- The `migrate-canonical-types` subcommand's substantive behaviour
(rewriting bare cross-module refs across a directory of `.ail.json`
files) hasn't been exercised since the canonical-type-names
milestone. No in-tree fixtures known to need rewriting today; spot-
tested via `--help` only. If a future migration is requested, the
prelude-fallback removal + the rewrite-helper dead-branch should
be reviewed together.
## Files touched
Modified:
- `crates/ail/src/main.rs` (delete defensive include 472-484, delete
skip-branch 500-505, update `MigrateCanonicalTypes` doc-comment to
reflect retired prelude-fallback)
- `crates/ailang-core/src/workspace.rs` (delete in-mod
`build_workspace_accepts_assembled_modules_and_runs_validation` test;
replace with 6-line stub comment documenting the dev-dep cycle as
invariant)
- `crates/ailang-core/tests/carve_out_inventory.rs` (header sentence
+ comment block + EXPECTED list — 8 → 7)
- `crates/ailang-core/tests/workspace_pin.rs` (add `load_one_adapter`
helper at top; append the relocated test at end)
- `crates/ailang-surface/tests/prelude_module_hash_pin.rs` (delete
cross-form-identity preflight + supporting `PRELUDE_JSON` /
`PRELUDE_AIL` references; trim doc-comment to surviving-only
framing)
- `docs/specs/2026-05-13-form-a-default-authoring.md` (insert "Status:
RETIRED" marker above §C4 (b) heading)
New:
- `crates/ailang-surface/tests/prelude_decouple_carve_out_pin.rs`
(single test asserting `examples/prelude.ail.json` does not exist)
Deleted:
- `examples/prelude.ail.json` (the milestone's primary artefact
retirement)
## Stats
`bench/orchestrator-stats/2026-05-14-iter-pd.3.json`
## Note on pre-existing dirty INDEX.md
When this iter dispatched, `docs/journals/INDEX.md` was already
modified in the working tree with one Boss-side line for pd.2 (the
prior iter); that edit was not committed before pd.3 dispatch. This
agent did NOT touch INDEX.md (it is Boss-only per skill discipline).
The Boss should commit the pd.2 INDEX line either with the pd.3
commit or as a separate prior commit — both options are clean.
-175
View File
@@ -1,175 +0,0 @@
# iter rpe.1 — Retire per-type print effect-ops
**Date:** 2026-05-14
**Started from:** 8b455bee4ca2b09bcadc56d95e0d79dcef75d284
**Status:** DONE
**Tasks completed:** 10 of 10
## Summary
The retire-per-type-print-effect-ops milestone shipped in one iter,
covering: a RED test pinning the deletion gate (`crates/ailang-check/
tests/no_per_type_print_ops.rs`); 92 `examples/*.ail` fixture
migrations from `(do io/print_<T> x)``(app print x)` plus 6
`.prose.txt` snapshot regenerations; four-site lockstep compiler
deletion (`crates/ailang-check/src/builtins.rs` × 5 regions,
`crates/ailang-codegen/src/lib.rs::lower_app` × 3 arms + 1 test,
`crates/ailang-codegen/src/synth.rs::builtin_effect_op_ret` × 1
match arm), with the dead `intern_string` helper removed as a
clean follow-up; six test-body migrations (`ailang-check` × 2,
`ailang-core` tests × 2, `ailang-surface/src/lex.rs`, `ailang-prose`);
the Cat B test-harness patch in `crates/ail/tests/e2e.rs` (6
IR-shape tests gained a `monomorphise_workspace` call so they
follow the same desugar → lift → mono → lower pipeline as `ail
build`); 6 doc-comment touch-ups; 7-site DESIGN.md sweep; 3 E2E
test-comment polish; bench-run-and-no-ratification (all 3 scripts
exit 0 against existing baselines, drift envelope within
tolerance); plus IR-snapshot refresh (4 `.ll` files: `sum`,
`list`, `max3`, `ws_main`) and 1 canonical-hash pin update
(`ordering_match::main`) as plan-unanticipated downstream
consequences of the corpus migration. Tests 564/564 green
post-iter (was 563 at start; +1 from the new RED test).
## Per-task notes
- iter rpe.1.1: RED test `no_per_type_print_ops.rs` — asserts
`io/print_int|bool|float` absent from `env.effect_ops`,
`io/print_str` present.
- iter rpe.1.2: 92 `.ail` corpus migrated (89 single-line +
3 multi-line shapes); 5 ancillary comment touch-ups in
`borrow_own_demo`, `eq_demo`, `int_to_print_int_borrow`,
`floats_1_newton_sqrt`, `floats_3_safe_division` to satisfy
the §H acceptance criterion `grep -rl ... examples/ == 0`.
- iter rpe.1.3: 6 `.prose.txt` snapshots regenerated via
`cargo run -p ail -- prose`.
- iter rpe.1.4: builtins.rs deletes (3 effect_ops.insert blocks
+ 3 `list()` rows + 1 unit test + 2 doc-comment swaps);
codegen lib.rs deletes (3 `lower_app` arms + 1 test);
synth.rs `builtin_effect_op_ret` match-arm narrowed.
`intern_string` removed as orphan helper. `Emitter.strings`
field left in place (still consumed by emit-time loop;
cycles over empty map; non-functional). RED test flips
GREEN.
- iter rpe.1.5: 6 test-body sites swapped to `io/print_str` +
`Str` args; Cat B harness patch at all 6 e2e.rs sites
inserts `ailang_check::monomorphise_workspace(&lifted_ws)`
between the existing lift loop and the lower call (the plan
said "replace the desugar+lift loop with mono", but mono's
precondition is "already lifted" — interpreting per spirit
and matching the `ail build` pipeline). E2E 85/85 green
after this step.
- iter rpe.1.6: doc-comment swaps in 6 sites (`lex.rs`,
`parse.rs`, `main.rs` × 2, `runtime/str.c`, `form_a.md`).
- iter rpe.1.7: DESIGN.md 7 sites swept; the iter's
past-tense paragraph at line 1990-1992 anchors the
milestone close.
- iter rpe.1.8: E2E test-comment polish at `eq_ord_e2e.rs` × 2,
`floats_e2e.rs`. Plus three plan-unanticipated sites:
`int_arg_to_effect_op_does_not_rc_track` test assertions
shifted from `0/0/0` to `1/1/0` (post-iter `print n` for
Int now goes via Show Int → int_to_str → io/print_str,
exactly one heap-Str slab cycle per spec §E); 4 IR
snapshots refreshed via `UPDATE_SNAPSHOTS=1` because the
format-string globals (`@.str_<module>_fmt_int_0` /
`%lld\n`) no longer appear in IR; 1 canonical-hash pin
update for `ordering_match::main` (body migration changes
the def's canonical-form hash).
- iter rpe.1.9: bench run — all 3 scripts exit 0 against
existing baselines. No ratification needed; spec §C4 (a)
anticipated possible latency drift but actual fell within
tolerance. Largest single deltas: `latency.explicit_at_rc.
max_us` +154.84% (a tail metric in the noise-class
envelope observed across the last ~12 audits); `throughput.
bench_list_sum.bump_s` +11.85% (REGRESSION, single bench,
3rd-iter-recurring; tracked as background noise per
audit-24 lineage). Baseline pristine for the 13th
consecutive audit-equivalent.
- iter rpe.1.10: roadmap entry struck `[x]`; journal written;
stats written.
## Concerns
- Cat B harness patch deviates from plan-literal: plan said
"replace the desugar+lift loop with monomorphise_workspace";
I inserted mono after the existing lift loop. Mono's
documented precondition is "already lifted", so the literal
reading would have broken mono's contract. Spirit-reading
was unambiguous: match `ail build`'s pipeline.
- Plan Task 8 under-scoped: it named 3 E2E-comment sites,
but the corpus migration also forced (a) one assertion
update on `int_arg_to_effect_op_does_not_rc_track` (allocs
0→1, frees 0→1, fully documented in spec §E), (b) IR
snapshot refresh × 4, (c) one canonical-hash pin update.
All three are mechanical downstream consequences of Task 2.
- `Emitter.strings` field in `crates/ailang-codegen/src/lib.rs`
is now functionally dead (no caller after `intern_string`
removal) but the field + the per-module collection loop +
the emit-time globals loop are all still in place. They
cycle over an empty map and are harmless. Cleaning them
up cascades through `emit_workspace_with_alloc` and
parallel `str_literals` infrastructure; out of scope for
this iter.
## Known debt
- The `int_arg_to_effect_op_does_not_rc_track` test name now
describes a pre-iter property; the body pins the post-iter
shape (one Show-Int slab cycle). Renaming to e.g.
`print_int_one_slab_cycle` was floated and explicitly
declined (plan Task 8 step 2 said "lean toward keep-name-
update-doc"). If a future iter wants the rename, the test
body is already a clean reference.
- The `intern_string` helper is gone but `Emitter.strings`
field remains — see Concerns above.
## Files touched
121 files in the working tree (1 new + 120 modified).
Code (compiler):
- `crates/ailang-check/src/builtins.rs` (3 effect_ops + 3 list() rows + 1 test + 2 doc-comments deleted/swapped)
- `crates/ailang-check/src/lib.rs` (2 test-body sites swapped)
- `crates/ailang-codegen/src/lib.rs` (3 lower_app arms + 1 test + `intern_string` helper removed)
- `crates/ailang-codegen/src/synth.rs` (builtin_effect_op_ret match-arm narrowed)
- `crates/ailang-surface/src/lex.rs` (1 test-body + 1 module doc swap)
- `crates/ailang-surface/src/parse.rs` (1 diagnostic example swap)
- `crates/ailang-prose/src/lib.rs` (1 round-trip test-body swap)
- `crates/ail/src/main.rs` (2 doc-comments swapped)
- `runtime/str.c` (1 comment re-anchored on %g)
Tests:
- `crates/ailang-check/tests/no_per_type_print_ops.rs` (NEW)
- `crates/ailang-core/tests/spec_drift.rs` (1 swap)
- `crates/ailang-core/tests/design_schema_drift.rs` (1 swap)
- `crates/ailang-core/tests/hash_pin.rs` (1 hash pin updated)
- `crates/ail/tests/e2e.rs` (6 Cat B harness patches + 1 assertion update + 1 doc-comment)
- `crates/ail/tests/eq_ord_e2e.rs` (2 doc-comments)
- `crates/ail/tests/floats_e2e.rs` (1 doc-comment)
- `crates/ail/tests/snapshots/{sum,list,max3,ws_main}.ll` (regenerated; format-string globals removed)
Docs:
- `docs/DESIGN.md` (7 sites)
- `docs/roadmap.md` (P2 entry struck [x])
- `crates/ailang-core/specs/form_a.md` (1 example swap)
Examples: 92 `.ail` migrations + 6 `.prose.txt` regenerations + 5 ancillary comment touch-ups.
## Verification
- `cargo test --workspace`: 564 passed, 0 failed (was 563 at
start; +1 = new RED test, no test counts shifted otherwise).
- `cargo build --workspace`: clean.
- `python3 bench/check.py` exit 0 (drift within tolerance,
no ratification).
- `python3 bench/compile_check.py` exit 0.
- `python3 bench/cross_lang.py` exit 0 (25/25 stable).
- `grep -rl "io/print_int\|io/print_bool\|io/print_float"
examples/` returns 0 results (acceptance criterion met).
- `grep -n "io/print_int\|io/print_bool\|io/print_float"
crates/ailang-check/src/builtins.rs` returns 0 lines.
- `crates/ailang-codegen/src/lib.rs::lower_app` has no arm
for any of the three retired op names.
## Stats
`bench/orchestrator-stats/2026-05-14-iter-rpe.1.json`
@@ -1,58 +0,0 @@
# iter rpe.1.tidy — Close audit-rpe.1 [medium] drift
**Date:** 2026-05-14
**Started from:** 973f50b (post-rpe.1 + audit-rpe.1)
**Status:** DONE
## Summary
Closes the single `[medium]` drift item surfaced by audit-rpe.1:
`crates/ailang-codegen/src/subst.rs:188,217``qualify_local_types_codegen`
and `apply_subst_to_type` both carried the exact
`Type::Fn { params, ret, effects, .. }` field-spread that drops
`param_modes` and `ret_mode` to `vec![]` / `ParamMode::Implicit`. This
is the same shape the print-leak bugfix at commit feb9413 corrected
in `crates/ailang-check/src/lib.rs::substitute_rigids`, and the
feb9413 commit message explicitly flagged any other instance as a
latent bug in the same class.
Fix: preserve both fields through the rebuild — `param_modes.clone()`
(it's a `Vec`) and `*ret_mode` (Copy). Symmetric to the feb9413 fix.
The audit's `[nit]` finding (dead `Emitter.strings` field in
`crates/ailang-codegen/src/lib.rs`, an orphan after `intern_string`
was removed in rpe.1 Task 4) is left as a follow-up tidy candidate —
it cycles over an empty map harmlessly and isn't an audit gate.
## Files touched
- `crates/ailang-codegen/src/subst.rs:188`
`qualify_local_types_codegen` Type::Fn arm now binds
`param_modes` and `ret_mode` and threads them through.
- `crates/ailang-codegen/src/subst.rs:217`
`apply_subst_to_type` Type::Fn arm same shape.
## Verification
- `cargo test --workspace` → 564 / 0 / 3 (baseline holds).
- `cargo clippy --workspace --all-targets` → 0 warnings.
- `cargo doc --workspace --no-deps` → 0 warnings.
## Concerns
- (none)
## Known debt
- `Emitter.strings` field in `crates/ailang-codegen/src/lib.rs`
remains dead. Cycles over empty map; no behavioural impact.
Roll into a future opportunistic cleanup.
- Three other field-spread `Type::Fn` rebuild sites
(`crates/ailang-check/src/lib.rs:93` and `:3492`,
`crates/ailang-check/src/lift.rs:875`) ALSO drop `param_modes`
/ `ret_mode` to `vec![]` / `Implicit`. They were not flagged
by audit-rpe.1 because no currently-failing test pins them.
Leaving conservatively in place — a flag for a future
fieldtest or audit cycle. The pattern of field-spread-and-
drop is the bug class; whether each site is correctness-
reachable is what needs verification before changing.
@@ -1,59 +0,0 @@
# bugfix mut-diag-double-code — doubled `[code]` prefix in mut-local human diagnostics
**Date:** 2026-05-15
**Status:** DONE
**Trigger:** fieldtest finding F2 (`docs/specs/2026-05-15-fieldtest-mut-local.md`).
**Started from:** 1faee67
## Symptom
Non-JSON `ail check` stderr rendered the kebab diagnostic code
twice for the four mut-local `CheckError` variants, e.g.
```
error: [mut-var-captured-by-lambda] main: [mut-var-captured-by-lambda] mut-var `x` cannot be captured...
```
## Cause
`MutAssignOutOfScope`, `AssignTypeMismatch`,
`UnsupportedMutVarType`, `MutVarCapturedByLambda` were authored in
iters mut.2 / mut.4-tidy with their `#[error("[<code>] ...")]`
`thiserror` Display bodies embedding the bracketed code. The
non-JSON `Cmd::Check` formatter in `crates/ail/src/main.rs`
independently prepends `[{d.code}]` from `CheckError::code()`.
Embedded + prepended = doubled. The non-mut `CheckError` variants
never embedded the bracket, so only these four doubled — they were
the inconsistent ones, brought in line here.
## Fix
RED-first via the `debug` skill (`ailang-debugger` wrote the
failing test; the GREEN side was a trivial four-string mechanical
edit applied inline by the Boss per the CLAUDE.md
trivial-mechanical-edit carve-out — no review-and-commit
discipline shed).
- RED test: `crates/ail/tests/ct1_check_cli.rs`
`check_human_mode_renders_mut_diagnostic_code_exactly_once`.
Pins the *observable* property: the code appears exactly once in
the rendered human diagnostic, exercising the same formatter path
the CLI uses (not a brittle assertion on the raw `#[error]` body).
- GREEN: dropped the leading `[<code>] ` literal from the four
`#[error(...)]` Display strings in `crates/ailang-check/src/lib.rs`.
The mut-typecheck pin tests (`mut_typecheck_pin.rs`) and the
in-source unit assertions match on the struct variants / `code()`,
not the Display string, so the prefix removal did not touch them.
## Tests
598 → 599 green (the new RED-now-GREEN human-formatter test).
Full workspace clean, zero failures.
## Not in scope (stays queued)
F1/F4 (accumulator-over-iteration: the milestone motivation is
unmet without a loop construct) and F3 (zero-arg `(app f)` rejected
at parse) from the same fieldtest are NOT addressed here — they are
design/roadmap items, not this bug.
-251
View File
@@ -1,251 +0,0 @@
# iter it.1 — loop/recur additive (parse/print/prose/check/codegen)
**Date:** 2026-05-15
**Started from:** 7381a4233b0ea34b23871c59e3a3e802441751ef
**Status:** DONE
**Tasks completed:** 8 of 8
## Summary
First of three iterations in the iteration-discipline milestone.
Adds `Term::Loop` / `Term::Recur` / `LoopBinder` as first-class
additive AST nodes end-to-end — parse, print, prose projection,
canonical-JSON serde + round-trip, schema/spec-drift lockstep,
typecheck (binder typing, recur arity/type unification via a
`loop_stack: &mut Vec<Vec<Type>>` threaded exactly as mut.2 threaded
`mut_scope_stack`, recur tail-position via a new private
`verify_loop_body` helper with `verify_tail_positions`'s public
signature unchanged), and codegen (loop-header block with one phi
per binder, `recur` as a back-edge `br` with a NEW parallel
`block_terminated = true` setter). The change is **strictly
additive**: zero deletions touch the existing `tail-app`/`tail-do`
paths, `verify_tail_positions`' tail-app role, or any of the seven
existing codegen `block_terminated` sites — the 32 within-iter
deletion-lines are exclusively the Task-1 `Internal(...)` stubs and
the Task-1 `verify_tail_positions` structural-passthrough being
replaced by their real Task-5/Task-7 implementations (DD-3
stub-then-fill). This additive discipline is what makes the
destructive it.3 safe later. Full `cargo test --workspace` green
(63 ok result groups, 0 failed); `loop_counter.ail` builds and
prints `55`; the four `Recur*` diagnostics fire on their negatives;
`tail-app` fixtures (`bench_compute_intsum`, `list_map_poly`) run
byte-identically; zero IR/prose snapshot drift.
## Per-task notes
- iter it.1.1: AST variants + every walker arm + serde/schema
lockstep. `Term::Loop { binders: Vec<LoopBinder>, body: Box<Term> }`,
`Term::Recur { #[serde(default)] args: Vec<Term> }`, and
`LoopBinder { name, #[serde(rename="type")] ty, init: Box<Term> }`
(DD-2 derives = `Debug, Clone, Serialize, Deserialize`, no
PartialEq; note `init` is `Box<Term>` per the plan's literal code
block, whereas `MutVar.init` is bare `Term` — a small divergence
from "mirrors MutVar exactly", recorded under Concerns). ~40
walker arms across desugar (incl. 3 in-test `any_*` helpers not
enumerated by the plan), workspace, all of ailang-check
(lib/lift/linearity/mono/pre_desugar_validation/reuse_shape/
uniqueness), codegen (escape×3/lambda/lib), prose, surface
parse/print, ail main + the test-side
`codegen_import_map_fallback_pin.rs` walker the plan flagged
(mut.1 missed its analogue; did not miss it here). Typecheck +
codegen *dispatch* stubbed `Internal(...)` (tuple variant —
the plan pseudo-code's `Internal { msg }` struct form does not
exist; copied mut.1's verbatim tuple shape). `synth_with_extras`
got real arms (loop → body's type, recur → unit) per plan.
schema_coverage left RED on "variant never observed" until
Task 3 (plan-expected); every other test green.
- iter it.1.2: Form-A parse. `parse_loop` / `parse_recur` modelled
on `parse_mut` / `parse_assign`; the binder list is an explicitly
parenthesised group `(loop ((var ...)*) BODY+)` (form_a.md
production added in Task 1.10 lockstep). Plan pseudo-code named
non-existent helpers (`parse_term_str`, `parse_body_seq`,
`parse_loop_binders`); substituted the real harness (module-level
`parse_term`) and inlined the per-`(var …)` decode exactly as
`parse_mut` does — the plan explicitly forbade introducing a new
body-folding helper. EBNF prologue + unknown-head error list
extended. RED→GREEN confirmed.
- iter it.1.3: Form-A print + positive fixtures. Print arms landed
early in Task 1 as a build-unblock necessity (documented mut.1
precedent — print arms are exhaustive-match neighbours of the
synth/lower_term dispatchers). `loop_smoke.ail` /
`loop_nested_in_lambda.ail` written in real Form-A matching
`mut_counter.ail`'s verbatim surface spellings (`(con Int)`,
`(app == …)`, `(app + …)`, `(lam (params (typed …)) (ret …)
(body …))`) — the plan's fixture pseudo-code (`(fn count_to :
Int -> Int …)`) is not valid Form-A and the plan's prose
explicitly directed matching mut_counter.ail verbatim.
parse→print→parse byte-idempotent; schema_coverage now GREEN
(Task 1.11 deferred RED closed).
- iter it.1.4: Prose projection lockstep. Step 4.1's literal
instruction ("render to prose and back, asserting AST equality")
is **not expressible** — AILang has no Form-B→AST parser by
design (CLAUDE.md / `crates/ail/src/main.rs:207` "no parser
exists for the prose surface"). Recorded as a plan defect (see
Concerns). The substantive work (render/free-var/subst arms,
landed in Task 1) is correct and verified by the full prose
suite; added `loop_and_recur_project_to_prose`, a render-assertion
pin using the same `render_term` harness the nearest existing
prose tests (`not_with_tail_flag_keeps_prefix_form`) use — the
feasible equivalent of the plan's intent. This is the documented
mut.1-class plan-pseudo-vs-real-API substitution, not a silent
swap. Status DONE_WITH_CONCERNS.
- iter it.1.5: Real typecheck (DD-1). Four `CheckError` variants
(`RecurOutsideLoop` / `RecurArityMismatch` / `RecurTypeMismatch`
/ `RecurNotInTailPosition`) — **no `span` field** because no
`Span` type is in scope in this crate and no existing CheckError
variant carries one (the plan pseudo-code's `span: Option<Span>`
references a non-existent-here type; mirrored the real
`MutAssignOutOfScope` field-shape exactly as the plan's prose
instructed). `loop_stack: &mut Vec<Vec<Type>>` threaded through
`synth`'s signature + every call site (signature + 22 single-line
recursive + 3 multi-line recursive + 2 production entry points +
lift.rs + 2 mono.rs + builtins.rs test helpers + 8 in-source
`#[cfg(test)]` calls), exactly the mut.2 pattern. Real Loop arm
binds binder names into `self`-… `locals` (save/restore like
`Term::Let` — the correct precedent for *immutable* bindings,
not the mut-var mechanism) and pushes the binder type-vector onto
`loop_stack`. **`Term::Recur` synth returns a fresh metavar
(`Subst::fresh(counter)`), not the plan's flagged-risky
`Type::unit()`** — see Concerns; this resolves the plan's own
self-review "Open risk" correctly (a fresh metavar is the
codebase-idiomatic bottom that unifies anywhere, which is what
makes `(if c then (recur …))` typecheck — `Type::unit()` would
have broken `loop_smoke`'s if-branch unification). `verify_loop_body`
added as a new private helper; `verify_tail_positions`' public
signature unchanged (it.3 reworks it). The diagnostic-doc list in
`diagnostic.rs` was NOT extended — the mut codes were never added
there (false plan premise; followed the actual mut.2 precedent).
RED→GREEN: loop_smoke clean, four negatives each return their
exact code. `loop_stack` threading caused zero regressions.
- iter it.1.6: Carve-out inventory. EXPECTED extended with the four
`test_recur_*.ail.json` negatives, 13→17, in an it.1 group
(the const is milestone-grouped not alphabetical; the test uses
a BTreeSet so order is functionally irrelevant). GREEN.
- iter it.1.7: Real codegen (loop-header + phi + recur back-edge +
lambda boundary). `loop_frames: Vec<LoopFrame>` field added (mut.3
analogue to `mut_var_allocas` — init/clear/lambda-save-reset-
restore symmetric). Loop lowers to `loop.header.<id>` with one
`phi` per binder; binder phi SSA pushed into `self.locals` so
`Term::Var` resolves to it via the existing lookup. Each phi's
back-edge incoming list is emitted as a unique `/*RECUR_EDGES…*/`
LLVM-comment placeholder, then `replacen(…, 1)`-patched once the
body and all its recur sites are lowered (the plan's
`patch_loop_phis` mechanism; the placeholder is a comment so an
unpatched one would be inert IR, not a syntax error). `Term::Recur`
records `(pred_block, [arg_ssa])`, emits the back-edge `br`, sets
`block_terminated = true` — a NEW parallel setter; the seven
existing tail-driven setters are untouched (verified: zero
deletions touch them). Reused `start_block`/`fresh_ssa`/`fresh_id`/
`self.current_block` instead of adding the plan-pseudo's sprawl of
single-use helper methods (plan explicitly: "REUSE … grep before
adding"). `loop_counter.ail` builds + prints `55`.
- iter it.1.8: tail-app coexistence + IR snapshots + acceptance.
`bench_compute_intsum.ail``3500003500000`,
`list_map_poly.ail``2\n3\n…` (byte-identical to pre-it.1; no
existing codegen path modified). Zero IR/prose snapshot drift (no
blanket regen). All it.1 acceptance bullets verified — the spec's
"five Recur diagnostics" is four in it.1 (the fifth,
`NonStructuralRecursion`, is it.2; resolved per plan Step 8.3).
- Phase 3 (E2E): added `loop_in_lambda_e2e.ail` + e2e
`loop_in_lambda_runs_and_prints_49` — a `loop` inside a `lam`
body invoked via a returned closure, proving the lambda-boundary
`loop_frames` save/restore is codegen-sound (the no-`main`
`loop_nested_in_lambda.ail` parse/print fixture cannot reach
codegen via the CLI; this runnable fixture closes that gap).
## Concerns
- `LoopBinder.init` is `Box<Term>` whereas the analogous
`MutVar.init` is bare `Term`. Per the plan's literal Step 1.1
code block (which specified `init: Box<Term>`), not implementer
drift. DD-2 says "mirrors MutVar exactly" — the divergence is in
the plan's own pseudo-code vs. its own DD-2 prose. Functional
impact: none (consistent `(*b.init).clone()` / `Box::new(...)` /
`&mut b.init` handling everywhere). A future iter that wants
strict parity could unbox; no it.1 test depends on the boxing.
- `Term::Recur` synth result: chose `Subst::fresh(counter)` (a
fresh metavar) over the plan's named fallback `Type::unit()`.
The plan's self-review flagged this exact point as an "Open
risk" with `Type::unit()` as the fallback "because recur is
always in tail position so its value is never consumed". But
`recur` frequently appears as an `if` branch (`loop_smoke`:
`(if (== i n) i (recur …))`), where the branch types MUST
unify — `unify(Int, unit)` fails, so `Type::unit()` would have
broken `loop_smoke`. A fresh metavar unifies with any type
(codegen-idiomatic bottom; the same mechanism `Subst::fresh`
serves everywhere). This is the correct resolution of the plan's
own flagged risk, not a deviation from its intent.
- Step 4.1 plan defect: it asks for a prose round-trip asserting
AST equality; AILang has no Form-B parser by design. Substituted
a feasible render-assertion pin (mut.1-class plan-pseudo-vs-real
substitution). The plan's *substance* (prose lockstep for the new
variants) is fully met. Boss may want to tighten the planner
template so it does not script prose round-trips.
- Step 5.2 plan premise defect: "Add the four code strings to the
diagnostic-doc list … same list the mut codes were added to" —
the mut codes were never added to `diagnostic.rs`'s doc list.
Followed the actual mut.2 precedent (not extended); the
authoritative registry is `CheckError::code()`, which has all
four.
- The plan's recon line numbers (e.g. `desugar.rs:349/559/…`,
`lib.rs:2590/2613/3593/3614`, `parse.rs:1212`) had drifted
vs. the live tree; drove every site off `cargo build`'s
E0004 enumeration as Step 1.2 instructs. Site *count* also
differed (6 desugar E0004 sites vs. plan's 9; resolved via the
build, which is authoritative). No behavioural impact —
same class as mut.1's documented recon misindexing.
## Known debt
- Prose-side `(loop …)` / `(recur …)` rendering is a
minimal-correctness shape (`loop { var n = init; …; body }` /
`recur(a, b)`), explicitly mirroring the deliberately-placeholder
mut-block prose shape. The prose surface for loops is not yet
designed; a follow-on prose iter refines it once the LLM-author
signal arrives. Not drift — recorded for visibility.
- `loop_nested_in_lambda.ail` has no `main`, so it is a
parse/print/typecheck fixture only (CLI codegen needs a `main`).
Codegen-of-loop-in-lambda is instead proven by the runnable
Phase-3 `loop_in_lambda_e2e.ail`. No gap remains; recorded so a
future reader does not mistake the no-main fixture for dead
coverage.
## Files touched
AST/core: `crates/ailang-core/src/ast.rs`,
`crates/ailang-core/src/desugar.rs`,
`crates/ailang-core/src/workspace.rs`,
`crates/ailang-core/specs/form_a.md`.
Drift/coverage tests: `crates/ailang-core/tests/{design_schema_drift,
schema_coverage,spec_drift,carve_out_inventory}.rs`.
Check: `crates/ailang-check/src/{lib,lift,linearity,mono,
pre_desugar_validation,reuse_shape,uniqueness,builtins}.rs`,
`crates/ailang-check/tests/loop_recur_pin.rs` (new).
Codegen: `crates/ailang-codegen/src/{lib,escape,lambda}.rs`.
Surface/prose: `crates/ailang-surface/src/{parse,print}.rs`,
`crates/ailang-prose/src/lib.rs`.
CLI: `crates/ail/src/main.rs`,
`crates/ail/tests/{e2e,codegen_import_map_fallback_pin}.rs`.
Spec: `docs/DESIGN.md`.
Fixtures (new): `examples/loop_smoke.ail`,
`examples/loop_nested_in_lambda.ail`, `examples/loop_counter.ail`,
`examples/loop_in_lambda_e2e.ail`,
`examples/test_recur_{outside_loop,arity_mismatch,type_mismatch,
not_in_tail_position}.ail.json`.
## Stats
bench/orchestrator-stats/2026-05-15-iter-it.1.json
-241
View File
@@ -1,241 +0,0 @@
# iter it.2 — structural-guardedness checker + first real Diverge effect
**Date:** 2026-05-15
**Started from:** bc9f5120034f2552ad7e78454bb198472404d4b1
**Status:** DONE
**Tasks completed:** 6 of 6
## Summary
Second of three iterations in the iteration-discipline milestone.
Adds the structural-recursion guardedness checker
(`CheckError::NonStructuralRecursion`) and the first real
implementation of the Decision-3 `Diverge` effect, both strictly
additive — nothing `tail`-related is removed (that is it.3). A new
whole-body pass `verify_structural_recursion` runs as a sibling of
`verify_tail_positions` in `check_fn`'s post-synth region (DD-1); it
implements the `smaller`-set algorithm with implicit candidate
inference and unconstrained accumulator positions (DD-2: the
foldl-shape accumulator walk classifies as structural), self- and
mutual-recursion identification with ADT-family connected components
via an inline union-find (DD-3), and the it.2-only `tail==false`
grandfather. A new `term_contains_loop` (stopping at `Term::Lam`
boundaries, DD-4) injects `"Diverge"` into the raised effect set so
the existing `UndeclaredEffect` reconciliation enforces it with no
new diagnostic variant; the lam-arrow and `Term::LetRec` cases are
wired at their respective sub-effect reconcile sites. DESIGN.md
Decision 3 + the §Data-model hook are synced to present tense. All
20 tail-app corpus fixtures stay grandfathered-clean;
`cargo test --workspace` green at 622 passed / 0 failed; the it.1
loop fixtures gained `!Diverge` (in scope per the plan).
Two spec/plan boundary defects surfaced and were resolved inside
the task's own stated invariants (corpus stays clean, structural
check not weakened) — see Concerns. Both are recorded so the it.3
planner and a future spec-tightening pass have the signal.
## Per-task notes
- iter it.2.1: `CheckError::NonStructuralRecursion { callee, arg }`
added beside the it.1 `Recur*` variants (bracket-`[code]`-free
Display per F2), `code()``"non-structural-recursion"`, `ctx()`
`{callee, arg}`. New pin file `structural_recursion_pin.rs`
(loop_recur_pin harness verbatim). `diagnostic.rs` untouched
(plan-correctly: `code()` is the registry). RED→GREEN clean.
- iter it.2.2: the pass + helpers next to `verify_loop_body`:
`adt_type_head` / `adt_param_positions` / `ctor_bound_names` /
`collect_rec_calls_walk` (single `smaller`-threaded walk folding
collection + guardedness, `tail==false` grandfather in the App
arm, stops at `Term::Lam`) / `call_guarded_at` /
`rec_call_arg_display`. `check_fn`/`check_def` gained a
`module_fns: &[&FnDef]` param (plan-anticipated in Step 3.2 for
mutual grouping; threaded from the per-module loop; synthetic
instance-method fns pass `&[]`). Wired
`verify_structural_recursion(f, &env, module_fns)?;` immediately
after `verify_tail_positions`. Two over-rejection classes found
against the corpus and fixed within the task's own acceptance
constraint (see Concerns §1, §2). Status DONE_WITH_CONCERNS.
- iter it.2.3: ADT-family union-find (`TypeUnionFind`, inline
`BTreeMap`-backed, path-halving, no dependency) +
`mutual_structural_group` returning a `MutualGroup { members,
family_valid }` (a plan-pseudo-vs-real refinement — the plan's
`Vec<&FnDef>` could not express the cross-family-negative; an
empty group silently drops the cross-call instead of rejecting
it). `collect_direct_app_var_callees` for call-graph adjacency
(same lam boundary). `verify_structural_recursion` reworked to a
single `guarded` closure: self-calls vs callee-cand, cross-calls
guarded only if `family_valid`. tree/forest clean, cross-family
rejected. RED→GREEN.
- iter it.2.4: `term_contains_loop` (exhaustive Term match;
`Loop=>true`, `Lam=>false`, all else recurse incl. Recur args).
Injection in `check_fn` before the declared-vs-raised reconcile.
Lam-arrow coherence wired at BOTH the `Term::Lam` synth
sub-effect site AND the `Term::LetRec` synth sub-effect site
(the plan named only the Lam site; recon line drifted and there
are now two structurally-identical reconcile sites — a
`Term::LetRec` clause is a fn-equivalent, same as the it.1
tail-position treatment, so a loop in a deferred LetRec body must
also carry Diverge; injecting at both is the sound completion,
not an extra). Four it.1 loop fixtures + two HOF signatures
updated to declare `!Diverge` (see Concerns §3). Status
DONE_WITH_CONCERNS.
- iter it.2.5: DESIGN.md Decision 3 rewritten (`Diverge` no longer
nominal; carried by loop-bearing / Diverge-calling fns; structural
recursion pure+total; `IO` description kept + sharpened) +
§Data-model hook sentence → present tense, it.3 retirement
retained. Drift anchors (`"t":"loop"`/`"t":"recur"`) untouched —
`design_schema_drift`/`schema_coverage`/`spec_drift` green.
- iter it.2.6: acceptance gate. Full workspace green; all 7 named
spec-acceptance pins green; `mut_counter.ail` still runs `55`
(grandfathered, unmigrated); all 20 tail-app corpus fixtures
grandfathered-clean (zero `non-structural-recursion`).
- Phase 3 (E2E): `examples/struct_rec_sum_e2e.ail` +
`struct_rec_sum_runs_and_prints_15` — an it.2-clean structural
recursion (plain non-tail, no `!Diverge`, no `tail-app`) that
runs to 15, proving the "total" verdict is behaviourally sound,
not just a typecheck assertion. Plus
`loop_needs_diverge_runs_and_prints_55` — the Diverge-path twin,
proving the Diverge injection is a typecheck-only obligation with
zero codegen impact (the loop lowers/runs identically to it.1).
## Concerns
- **§1 — Spec/it.2-boundary defect: no-ADT-candidate recursion.**
The plan's load-bearing invariant ("the 21 tail-app corpus
fixtures stay clean through it.2 because `collect_rec_calls` only
collects `tail==false` calls") is insufficient: corpus fixtures
like `bench_latency_explicit.ail`'s `build_tree(depth: Int)`
recurse **non-tail** (inside a `term-ctor`) on a **primitive**
arg — the `tail==false` grandfather does not exempt them, and
they have no ADT candidate position. Strict DD-2 would reject
~18 corpus fixtures in it.2, contradicting the additive mandate.
Resolution (does NOT weaken the ADT check): a def with no ADT
candidate position AND no mutual cycle has no *structural*
position to verify — it is integer-counter / other-shaped
recursion whose migration to `(loop …)` is the destructive it.3
corpus pass, not it.2's job. it.2 only rejects *misuse of an ADT
structural position*; the spec's load-bearing negative
(`f(xs: IntList) = … f(xs) …`, HAS an ADT candidate) still fires.
This is the honest reading of DD-2's "for each candidate
structural position" (vacuous when there are none); the plan's
pseudo-code only handled `calls.is_empty()`, not
`cand.is_empty()`. Recorded as a resolved it.2/it.3 boundary
clarification — the it.3 planner should expect these to be the
corpus-migration set, and a spec-tightening pass may want to
state the no-candidate boundary explicitly.
- **§2 — Two RC-regression fixtures needed the spec's transitional
grandfather applied.** `rc_pin_recurse_implicit.ail` and
`rc_let_alias_implicit_param.ail` (NOT in the 20-fixture tail-app
set) contain `loop(n: Int, t: Tree) = … (app loop (- n 1) t)`
an ADT param (`t: Tree`) held constant while decrementing an Int,
recursing **non-tail**. This is structurally identical to the
required negative (`f(xs)=…f(xs)…`): both pass the ADT param
unchanged at its candidate position. It therefore correctly fires
`NonStructuralRecursion` and CANNOT be exempted by broadening the
grandfather without also letting the required negative through
(which would weaken the check — explicitly forbidden by the
carrier). The recursive call is in tail position, so the
spec-prescribed transitional treatment applies: mark it
`tail-app` (the it.2 "Transitional grandfather" — identical to
the idiom 20 other corpus fixtures use; it.3 migrates to
`(loop …)`). The 18d.4 / 18g regressions these fixtures guard
live in `pin`/`pin_aliased`'s match arm-close, unaffected by the
`loop` recursion's lowering — so this is NOT adapting-a-test-to-
dodge-a-bug (the check is correct; the fixture joins the
transitional grandfather, with an in-fixture comment recording
why). Both fixtures' RC==GC e2e regression guards still pass.
- **§3 — it.1 loop fixtures gained `!Diverge` (plan-in-scope, full
enumeration vs plan's two examples).** Plan Step 4.4 named
`loop_counter.ail` + `loop_in_lambda_e2e.ail` explicitly; the
same in-scope action applies to all it.1 loop fixtures.
`loop_counter.ail` (loop in `main` body) → `(effects IO Diverge)`.
`loop_smoke.ail` `count_to` (loop in fn body) → `(effects
Diverge)`. `loop_nested_in_lambda.ail` / `loop_in_lambda_e2e.ail`
(loop in a *lam* body) → `(effects Diverge)` on the **lam**, with
the enclosing fn / HOF param-and-return fn-types threaded to
carry `!Diverge` (effects are part of the function type — forced,
not optional). The lam-boundary rule held: `main` in
`loop_in_lambda_e2e` does NOT carry Diverge from its own body
(the loop is behind the lam edge) but DOES via callee-effect
propagation through `apply` (free per DD-4, identical to `!IO`).
The injection was not weakened. Both it.1 loop e2e tests
(55 / 49) still pass; round-trip green on every modified `.ail`.
- **§4 — `MutualGroup` is a plan-pseudo-vs-real-API refinement.**
The plan's `mutual_structural_group -> Vec<&FnDef>` could not
express "cross-family mutual recursion must be rejected" — an
empty return drops the cross-call from collection instead of
flagging it. Split into `{ members: HashSet<String>,
family_valid: bool }`: membership drives collection (cross-calls
always collected), validity drives the verdict (a cross-call into
a family-invalid cycle is unconditionally unguarded). Minimal
shape satisfying both plan requirements; same documented
substitution class as it.1's repeated plan-pseudo-vs-real notes.
## Known debt
- The structural check's `rec_call_arg_display` has no real
term-pretty-printer (only `type_to_string` exists in
`ailang_core::pretty`); non-`Var` offending args render as terse
tags (`Ctor(…)`, `fn(…)`, `<expr>`). The canonical non-structural
case is always a bare `Var` (the param passed unchanged), so this
is cosmetic on the diagnostic only; a future term-pretty-printer
would improve the `arg` field. Not drift — recorded for
visibility.
- Three exhaustive Term-walks now exist in the it.2 region
(`collect_rec_calls_walk`, `collect_direct_app_var_callees`,
`term_contains_loop`). Each collects a genuinely different thing
with different threading; a shared generic walker would be
speculative abstraction per the AILang quality bar. Recorded so a
future reader does not mistake the triplication for an oversight.
- `mutual_structural_group`'s reaches/component BFS is O(n²)-ish
over a module's fns, run once per fn-check. Corpus modules are
<40 defs; not a hot path. No action; recorded for visibility.
## Blocked detail
None — all 6 tasks completed; the two spec/plan boundary defects
(Concerns §1, §2) were resolved inside the task's own stated
acceptance constraints (corpus stays clean, structural check not
weakened), not escalated, because the carrier explicitly anticipated
corpus-breakage handling and the resolutions are the spec's own
transitional model rather than judgement substitutions.
## Files touched
Check core: `crates/ailang-check/src/lib.rs` (CheckError variant +
code/ctx; `module_fns` thread through check_def/check_fn;
`verify_structural_recursion` + helpers + `TypeUnionFind` +
`MutualGroup` + `term_contains_loop`; Diverge injection at
check_fn / Term::Lam / Term::LetRec sites).
Tests (new): `crates/ailang-check/tests/structural_recursion_pin.rs`
(9 pins). Carve-out: `crates/ailang-core/tests/carve_out_inventory.rs`
(EXPECTED 17→20 + header comment rewritten accurate).
E2E: `crates/ail/tests/e2e.rs` (2 new Phase-3 tests).
Spec: `docs/DESIGN.md` (Decision 3 + §Data-model hook).
Fixtures (new): `examples/struct_rec_list_len.ail`,
`examples/struct_rec_foldl_sum.ail`,
`examples/struct_rec_tree_forest.ail`,
`examples/struct_rec_sum_e2e.ail`,
`examples/loop_needs_diverge.ail`,
`examples/test_non_structural_recursion.ail.json`,
`examples/test_mutual_cross_family.ail.json`,
`examples/test_loop_missing_diverge.ail.json`.
Fixtures (modified): `examples/loop_counter.ail`,
`examples/loop_smoke.ail`, `examples/loop_nested_in_lambda.ail`,
`examples/loop_in_lambda_e2e.ail` (all four: it.1 loop fixtures →
`!Diverge`); `examples/rc_pin_recurse_implicit.ail`,
`examples/rc_let_alias_implicit_param.ail` (transitional `tail-app`
grandfather on the `loop` fn — Concerns §2).
## Stats
bench/orchestrator-stats/2026-05-15-iter-it.2.json
-187
View File
@@ -1,187 +0,0 @@
# iter it.3 — retire tail-app/tail-do + corpus migration
**Date:** 2026-05-15
**Started from:** c992eb93df4c5ec0a0686c924535cb9951be552f
**Status:** BLOCKED
**Tasks completed:** 1 of 7 (Task 1 complete; BLOCKED at the
Task-1.3 → Task-2 boundary before any destructive task)
## Summary
Third and terminal iteration of the iteration-discipline milestone
(destructive: retire `tail-app`/`tail-do`, migrate the corpus).
Task 1 (the non-destructive pre-migration oracle + the spec-delegated
class-(b) live enumeration) ran to completion: 40 recursive corpus
fixtures classified, 40 behavioural oracles captured under
`bench/it3-oracle/`, `MANIFEST.tsv` written. Task 1.3's mandated
live sweep surfaced a **real spec-signal** that the plan's own
self-review (lines 550-560) and the carrier explicitly defined as a
BLOCKED-to-Boss condition, not a silent reshape: **six fixtures
contain a recursive fn that resists BOTH a natural `(loop)`/`recur`
form AND a structural-plain form.** Per DD-2/DD-3 (migrate before
remove; Task 4's completeness gate must not be softened and a RED
there is a Task-2 defect, never a restored exemption) there is **no
in-plan migration target** for these six. The destructive tasks
(4/5) were therefore not started — correctly, since the migration
they depend on is impossible for this subset and DD-3 forbids
working around it. No production code was modified; the working
tree contains only the new `bench/it3-oracle/` artefacts.
## Per-task notes
- iter it.3.1: Pre-migration oracle + migration-set enumeration —
**complete.** `cargo build -p ail` clean at HEAD c992eb9.
Class-(a): 23 `(tail-app|tail-do)` fixtures (recon-confirmed).
Class-(b) live sweep (Task 1.3, spec-delegated): read
`verify_structural_recursion` + the two it.2 exemptions
(`lib.rs:3027` `if !*tail` grandfather; `lib.rs:3197`
`if cand.is_empty() && group.members.is_empty()` no-ADT skip),
then classified every self-recursive corpus fn against the
post-Task-4 verdict logic (`lib.rs:3219-3267`). Authoritative
result: 13 no-ADT-candidate single-recursion fns are
loop-migratable; 11 fixtures recurse structurally on an ADT param
(clean before AND after Task 4 — no migration, only the redundant
`tail-app` marker if class-(a) is dropped); **6 fixtures have a
no-ADT-candidate DOUBLE non-tail recursion (binary-tree build)
that is neither structural nor loop-expressible.** Oracles
captured for all 40 (`bench/it3-oracle/<stem>.out` + `MANIFEST.tsv`).
Task 1.5 fallout list recorded below for the eventual unblocked
re-dispatch. No code changed (Task 1 is non-destructive by spec).
- iter it.3.2 … it.3.7: not started. Task 2 (migration) has no
valid action for the 6 blocked fixtures; Tasks 4/5 are
destructive and gated behind a complete Task-2 migration that
cannot be completed. Halted at the spec-signal per carrier.
## Concerns
- **The RC-RSS / 18g.1 load-bearing risk (DD-4 / Task 5.5 / 5b) was
NOT reached.** The iter blocked before Task 5, so the 18g.1
husk-dec deletion and the RC-RSS bench gate were never exercised.
No RC-RSS evidence exists from this dispatch. The milestone-close
audit must NOT treat the RC-RSS contingency as resolved — it is
untested. When the spec defect below is resolved and it.3 is
re-dispatched, Task 5.5 (and conditionally 5b) is still the
milestone's load-bearing risk and must be run + its evidence
journalled then.
## Known debt
- `bench/it3-oracle/` currently holds 40 `.out` snapshots +
`MANIFEST.tsv` (41 files). They are the Task-1 deliverable and
stay valid for a re-dispatch from the same `start_sha`. Boss
call at commit whether to keep them under `bench/` (plan §"Files
this plan creates": "deleted in Task 6 … or kept under `bench/`
if cheap") — but do NOT delete before the re-dispatch; the
re-dispatched Task 7.3 diffs against them.
- The 11 class-(a) `plaincall` fixtures (structural on an ADT param,
carrying only a redundant `tail-app` marker) and the 11
`already-structural` fixtures need no semantic migration — only
marker-stripping (class-a) — and are unaffected by the blocker.
Recorded so the re-dispatch does not re-derive this.
## Blocked detail
**Task:** 1 (the Task-1.3 spec-delegated sweep) → halts entry to
Task 2 / Task 4.
**Reason:** `spec-ambiguous` / spec defect surfaced by the mandated
live sweep — six corpus fixtures contain a recursive fn that is
**neither structural recursion (no ADT-typed parameter — `cand`
empty in `verify_structural_recursion`) nor expressible as
`(loop …)`/`recur`** (the recursion is two non-tail self-calls
building both children of a binary `Tree`; `recur` is
tail-position-only with a single back-edge, so a balanced-tree
build cannot be a tail loop). After Task 4 removes the
no-ADT-candidate skip (`lib.rs:3197`, mandated by DD-3 and the
spec's it.3 acceptance bullet "no grandfather/no-candidate skip
remains"), each of these deterministically fires
`CheckError::NonStructuralRecursion` (verdict logic `lib.rs:3219-3267`:
`cand` empty ⇒ `self_clear=false` ⇒ NSR). DD-3 forbids softening
Task 4 or restoring the exemption; the carrier and project memory
(`feedback_dont_adapt_tests_to_bugs`) forbid reshaping the
LLM-natural fixture (e.g. changing `depth: Int` to an ADT-encoded
`Nat`, or hand-rolling an explicit stack/continuation). There is
**no in-plan migration target**, so the destructive split cannot
proceed.
**The six fixtures (fn :: shape):**
1. `examples/bench_tree_walk.ail` :: `build_tree`
(`(if (== depth 0) (Tree Leaf) (Tree Node 1 (build_tree (- depth 1)) (build_tree (- depth 1))))`)
2. `examples/bench_latency_explicit.ail` :: `build_tree` (same shape; file is class-(a) for other fns but `build_tree` itself is non-tail double recursion)
3. `examples/bench_latency_implicit.ail` :: `build_tree` (same)
4. `examples/rc_let_owned_app_leak.ail` :: `build` (same Tree shape on `d: Int`)
5. `examples/rc_pin_recurse_implicit.ail` :: `build` (same; the RC-regression-guard fixture from it.2 Concerns §2)
6. `examples/rc_let_alias_implicit_param.ail` :: `build` (same; the RC-regression-guard fixture from it.2 Concerns §2)
This is exactly the spec-signal the it.2 journal §1 foreshadowed
(it named `bench_latency_explicit:build_tree` as a no-ADT-candidate
case "whose migration to `(loop …)` is the destructive it.3 corpus
pass") and the plan self-review's "Recorded risk" anticipated
("a genuine non-structural recursion with no natural loop form …
surfaces as a Task-2 BLOCKED to the Boss with the fixture named").
The spec/plan assumed every no-ADT-candidate recursion is a
loopable counter/accumulator; balanced-binary-tree construction by
double recursion on a depth counter is the counter-example.
**Suggested next step:** Boss-level spec decision required (this is
not an implementer or planner judgement call — see
`feedback_spec_over_plan_patches`: a spec defect goes back through
`brainstorm`, not a fourth plan patch). Candidate resolutions for
the Boss to weigh, language-first:
(a) **Add a structural accommodation for tree recursion** — extend
`verify_structural_recursion` so a non-tail recursive call on a
*structurally-derived smaller value* is guarded even when the
decreasing quantity is a primitive depth counter feeding an ADT
build (i.e. recognise the `build_tree` shape as bounded). This
keeps the six fixtures verbatim and is the only option that does
not reshape LLM-natural code, but it widens it.2's structural
checker — a Decision-10/Decision-3 spec change, brainstorm-gated.
(b) **Carve the six into a permanent allow-list / carve-out** (like
the existing `.ail.json` carve-outs) with an explicit
"constructor-blocked recursion, depth-bounded by construction"
annotation, leaving the no-ADT-candidate skip retired for
everything else. Requires a spec sentence defining the carve-out
predicate.
(c) **Retire the bench fixtures that exist only to exercise the
removed `musttail`/RC-RSS path** (`bench_latency_*`,
`rc_pin_recurse_implicit`, `rc_let_alias_implicit_param`,
`rc_let_owned_app_leak`) and replace `bench_tree_walk`'s
`build_tree` coverage with a structural equivalent — but this is
the "adapt the fixture to dodge" path the carrier and
`feedback_dont_adapt_tests_to_bugs` explicitly forbid for
language-limitation cases; named only for completeness, not
recommended.
Once the Boss resolves the spec, re-dispatch it.3 from the same
`start_sha` (the `bench/it3-oracle/` artefacts remain valid).
## Files touched
Created (untracked, non-code):
- `bench/it3-oracle/MANIFEST.tsv` (40-fixture classification)
- `bench/it3-oracle/<stem>.out` × 40 (behavioural oracle:
`ail run` stdout for `main` fixtures, `ail check` output for
check-only)
No production code, no spec, no test files modified (Task 1 is
non-destructive by plan; the BLOCKED halted before any code-edit
task).
## Task 1.5 fallout map (recorded for the unblocked re-dispatch)
Tail-app-specific tests the plan's Task 6.4 will delete:
`crates/ailang-prose` `app_tail_renders_with_keyword`,
`binop_with_tail_flag_keeps_prefix_form`,
`not_with_tail_flag_keeps_prefix_form`;
`crates/ailang-core/tests/spec_drift.rs::spec_mentions_tail_variants`;
`crates/ailang-check` `TailCallNotInTailPosition` pins
(`tail_call_in_tail_position_is_accepted` /
`..._non_tail_position_is_rejected` if present); any
`iter14e_*musttail*` e2e. (Enumeration deferred to the
re-dispatch's Task 1.5 against the then-current tree, since the
exact test names will drift; recorded here as the recon seed.)
## Stats
bench/orchestrator-stats/2026-05-15-iter-it.3.json
-215
View File
@@ -1,215 +0,0 @@
# iter mut.1 — schema + surface for local mutable state
**Date:** 2026-05-15
**Started from:** 60e4559e31d7f09de77e2679aab2d524de257a61
**Status:** DONE
**Tasks completed:** 6 of 6
## Summary
Lands the foundational AST + surface forms for the mut-local
milestone (first milestone on the Stateful-islands roadmap path).
Two new `Term` variants (`Mut` and `Assign`) plus the nested
`MutVar` struct enter `ailang-core::ast`; canonical-JSON serde
round-trips them by virtue of the existing `#[serde(tag = "t")]`
machinery. Form A gains `(mut …)` / `(var …)` / `(assign …)`
productions in the parser and the corresponding write arms in the
printer; the parser right-folds the trailing body sequence into
`Term::Seq` so the canonical JSON-AST always sees a single
`body: Term`. Every `Term` exhaustive match in the workspace (~25
walker sites) gains substantive arms, except the two dispatch
entry points (`synth` in `ailang-check`, `lower_term` in
`ailang-codegen`) which stub with `CheckError::Internal` /
`CodegenError::Internal` per the spec's "out of iteration"
boundary — typecheck recognition lands in mut.2, codegen lowering
in mut.3. The three drift tests (`design_schema_drift`,
`spec_drift`, `schema_coverage`) all flip green after the
exemplar / variant-tag / visitor extensions, and `examples/mut.ail`
ships as the round-trip fixture covering empty / single-var /
two-var / nested-shadow / Bool / Unit cases. Full
`cargo test --workspace` 579/579 green (was 564 + 2 new AST pins +
4 new parser pins + 9 from the extended drift exemplars and the
mut.ail-corpus contributions netting to +15).
## Per-task notes
- iter mut.1.1 — AST extension: added `Term::Mut { vars: Vec<MutVar>,
body: Box<Term> }`, `Term::Assign { name: String, value: Box<Term> }`,
and the `MutVar { name, ty, init }` struct to `crates/ailang-core/src/ast.rs`.
Two new unit tests pin the canonical-bytes of the empty-vars mut
serialisation and the Assign round-trip. `MutVar` carries Arm's
derives (`Debug, Clone, Serialize, Deserialize`) — the plan's
literal pseudo-code asked for `PartialEq + Eq` but its own
justification ("derives match Arm's for cross-tree consistency")
points at Arm, which does not derive those. Concern recorded;
no functional impact (the Task 1 tests do not depend on
`PartialEq`, and `Term` itself uses a custom `PartialEq` impl on
`Type` only).
- iter mut.1.2 — substantive walker arms: 9 sites in `desugar.rs`,
2 in `workspace.rs`, 3 in `ailang-check/src/lib.rs` (1 in
`substitute_rigids_in_term`, 1 in `verify_tail_positions`, 1
variant-name string emitter inside `synth`'s `Term::ReuseAs` sub-
match), 2 in `lift.rs`, 3 in `linearity.rs` (one being a
variant-name string emitter), 2 in `mono.rs`, 1 each in
`pre_desugar_validation.rs`, `reuse_shape.rs`, `uniqueness.rs`,
3 in `escape.rs`, 1 in `lambda.rs`, 1 in `codegen/src/lib.rs`
(the `synth_with_extras` type-synthesis helper), 3 in
`ailang-prose/src/lib.rs`, 2 in `ail/src/main.rs`. Plus one
unenumerated site in `crates/ail/tests/codegen_import_map_fallback_pin.rs`
(an exhaustive walker inside a test) that the build required.
Each arm follows the appropriate shape per existing convention
at its site: rebuilder shapes for `substitute_*` / `subst_*` /
`desugar_term` / `lift_in_term` / `substitute_rigids_in_term` /
`rewrite_term`, visitor shapes for `collect_used_in_term` /
`free_vars_in_term` / `find_non_callee_use` / `walk_term*` /
`verify_tail_positions` / `term_has_letrec` / the linearity
walkers / `walk` in `reuse_shape.rs` / `walk` in `uniqueness.rs`
/ `walk` in `escape.rs` / `collect_captures` / `count_free_var` /
`subst_var_with_term` (rebuilder-shape in prose). Shadowing
semantics across mut-var bindings replicates the
`Term::Let`/`Term::Lam`/`Term::LetRec`-shaped convention at each
site: var-init terms see the outer scope plus already-declared
vars; later var-inits and the body see all earlier var bindings.
- iter mut.1.3 — dispatch stubs: typecheck dispatch at `synth` in
`ailang-check/src/lib.rs` (Plan line 2572 was wrong — that line
is `verify_tail_positions`, which is substantive walker
territory; the actual dispatch entry is `synth` at line 3403's
former `Term::ReuseAs` arm, where the two new arms now sit)
returns `CheckError::Internal("Term::Mut/Assign not yet
supported in typecheck (deferred to iter mut.2)")`. Codegen
dispatch at `lower_term` in `ailang-codegen/src/lib.rs:1649`
returns `CodegenError::Internal("Term::Mut/Assign not yet
supported in codegen (deferred to iter mut.3)")`. Both errors
use the single-field tuple-variant shape (`Internal(String)`).
- iter mut.1.4 — Form A parser + printer: dispatcher arms added
for `"mut" => parse_mut` and `"assign" => parse_assign` in
`parse_term`'s head-keyword match; the two helpers parse
positionally (mut: pull leading `(var ...)` entries, then ≥ 1
body terms right-folded into `Term::Seq`; assign: positional
ident-then-value pair). Plan pseudo-code used helper names
(`peek_is_open_paren_with_head`, `expect_head`, etc.) that don't
exist; substituted the actual API (`expect_lparen`,
`expect_keyword`, `expect_ident`, `parse_type`, `parse_term`,
`expect_rparen`, `peek_head_ident`, `matches!(self.peek(),
Tok::RParen)`). Four RED-first parser tests added covering empty
mut + 1-var-1-assign-1-final + missing-body rejection (both
shapes). Print arms in `print.rs` walk the right-spine of
`Term::Seq` and emit each lhs as a top-level statement; the
terminal expression closes the `(mut …)` form. EBNF prologue in
`parse.rs` extended with `mut-term`, `var-decl`, `assign-term`
productions. The print arms had to land during the Task 1+2
build-unblock phase rather than waiting for Task 4 (they are
exhaustive-match neighbours of the synth/lower_term dispatchers);
this is a benign sequencing shift and the plan's "Task 2 build-
green verification gate" still holds.
- iter mut.1.5 — drift + coverage extensions: two new exemplars
added to `design_schema_drift.rs` and two new match arms; two
new `VariantTag` entries (`TermMut`, `TermAssign`),
`EXPECTED_VARIANTS` extended, two new `visit_term` arms in
`schema_coverage.rs`; two new exemplars + two new match arms in
`spec_drift.rs`. `form_a.md` gains three new lines in the
Parenthesised-forms block plus a prose paragraph naming the
empty-vars and post-vars-body invariants and the diagnostic
codes. `DESIGN.md` §"Term (expression)" gains the two jsonc
schema blocks immediately after the `reuse-as` block plus a
closing prose paragraph naming the mut.1 dispatch-stub semantics
and the deferred mut.2 / mut.3 iterations. Post-edit:
`design_schema_drift` and `spec_drift` both green;
`schema_coverage` is RED on the missing `examples/mut.ail`
fixture, per plan Step 6 expectation.
- iter mut.1.6 — fixture + round-trip: `examples/mut.ail` shipped
with the six fns from plan Step 1 verbatim (empty / single-var
with assign / two-var with combine / nested-shadow / Bool /
Unit). The corpus-globbing round-trip test
(`parse_then_print_then_parse_is_idempotent_on_every_ail_fixture`)
and the parse-determinism test pick the fixture up automatically;
both green. `schema_coverage` also green now that both new
variants are observed in the corpus. Full `cargo test
--workspace` 579/579 green.
## Concerns
- `MutVar` derives diverge from the plan's literal pseudo-code
(`Debug, Clone, PartialEq, Eq, Serialize, Deserialize`); the
shipped form is `Debug, Clone, Serialize, Deserialize` to match
Arm's pattern. The plan's own justification ("match Arm's
derives for cross-tree consistency") points at Arm which does
not derive PartialEq/Eq. Functional impact: none in mut.1
(the Task 1 tests do not depend on PartialEq, and `Term` uses
a custom PartialEq impl scoped to `Type` only). A future iter
that needs to compare `MutVar` (e.g. for in-place rewriting in
mut.3 codegen) can add the derives at that time without breaking
any mut.1 test.
- Plan recon for the typecheck dispatch entry was misindexed.
The plan listed `crates/ailang-check/src/lib.rs:2572` as the
"check_term dispatch — STUBBED" site; that line is actually in
`verify_tail_positions`, a substantive walker. The real
typecheck dispatch entry is `synth` (defined at lib.rs:2613,
with its `Term::ReuseAs` arm at line 3403 before the new mut
arms). The stub was placed at the actual dispatch entry; line
2572 received a substantive arm. The variant-name string
emitter sub-match referenced as line 3430 is inside `synth`'s
former `Term::ReuseAs` arm and was extended as Task 2 Step 13
asks. Net: the spec intent (stub at typecheck dispatch) is
preserved; only the line-number routing differs.
- One test-side walker site (`crates/ail/tests/codegen_import_map_fallback_pin.rs:62`)
carries an exhaustive match on `Term` and was not enumerated
by the plan's Task 2 file list. The site is the body-walker of
a regression-pin test from iter 24.tidy; it required two new
arms to build green. Added with appropriate visitor-shape
recursion. The plan-recon miss is recorded here as a
documentary item; no behavioural impact.
## Known debt
- Prose-side `(mut ...)` rendering in `ailang-prose/src/lib.rs` is
a minimal-correctness shape (`mut { var <name> = <init>; ...; <body> }`).
The prose surface for mut blocks is not yet fully designed; a
future prose-iter once the LLM-author signal arrives will refine
it. This is a deliberate placeholder, not drift — recording for
visibility.
- `subst_var` in `desugar.rs` does NOT rename the `Term::Assign.name`
field. The desugar pass's `subst_var` rewrites let-rec captures
(KnownType / LetBound / MatchArm), none of which can be mut-vars
(mut-vars are first-order scalars, not captureable). Leaving
the assign-name untouched is semantically correct; documented in
the arm's doc-comment. Mentioned here for visibility.
## Files touched
- `crates/ailang-core/src/ast.rs` — `Term::Mut`, `Term::Assign`, `MutVar`, two new unit tests
- `crates/ailang-core/src/desugar.rs` — 9 walker arms
- `crates/ailang-core/src/workspace.rs` — 2 walker arms
- `crates/ailang-check/src/lib.rs` — 3 walker arms (substantive + variant-name) + 2 dispatch stubs in `synth`
- `crates/ailang-check/src/lift.rs` — 2 walker arms
- `crates/ailang-check/src/linearity.rs` — 3 walker arms (2 substantive + 1 variant-name)
- `crates/ailang-check/src/mono.rs` — 2 walker arms (1 mut-ref rebuilder, 1 visitor)
- `crates/ailang-check/src/pre_desugar_validation.rs` — 1 walker arm
- `crates/ailang-check/src/reuse_shape.rs` — 1 walker arm
- `crates/ailang-check/src/uniqueness.rs` — 1 walker arm
- `crates/ailang-codegen/src/escape.rs` — 3 walker arms
- `crates/ailang-codegen/src/lambda.rs` — 1 walker arm
- `crates/ailang-codegen/src/lib.rs` — 1 walker arm (`synth_with_extras`) + 2 dispatch stubs in `lower_term`
- `crates/ailang-prose/src/lib.rs` — 3 walker arms (1 substantive, 1 `count_free_var`, 1 `subst_var_with_term` rebuilder)
- `crates/ail/src/main.rs` — 2 walker arms (dep-walker + `rewrite_term`)
- `crates/ail/tests/codegen_import_map_fallback_pin.rs` — 1 walker arm (test-side, unenumerated by plan)
- `crates/ailang-surface/src/parse.rs` — dispatcher arms, `parse_mut` / `parse_assign` helpers, EBNF prologue extension, 4 new parser tests
- `crates/ailang-surface/src/print.rs` — 2 `write_term` arms
- `crates/ailang-core/tests/design_schema_drift.rs` — 2 exemplars + 2 match arms
- `crates/ailang-core/tests/schema_coverage.rs` — 2 VariantTag + EXPECTED_VARIANTS + 2 visit_term arms
- `crates/ailang-core/tests/spec_drift.rs` — 2 exemplars + 2 match arms
- `crates/ailang-core/specs/form_a.md` — 3 new productions + prose paragraph
- `docs/DESIGN.md` — 2 jsonc schema blocks + prose paragraph
- `examples/mut.ail` — round-trip fixture (6 fns)
## Stats
bench/orchestrator-stats/2026-05-15-iter-mut.1.json
-256
View File
@@ -1,256 +0,0 @@
# iter mut.2 — typecheck recognition of `Term::Mut` and `Term::Assign`
**Date:** 2026-05-15
**Started from:** 3a5b1d33472e5c35c4917702e30fa5a12b749e31
**Status:** DONE
**Tasks completed:** 7 of 7
## Summary
Replaces the mut.1 dispatch stubs in `synth` (`ailang-check`) for
`Term::Mut` and `Term::Assign` with real typecheck logic. Three new
`CheckError` variants ship (`MutAssignOutOfScope`,
`AssignTypeMismatch`, `UnsupportedMutVarType`) with kebab-coded
`code()` arms and structured `ctx()` payloads, following the
cli-diag-human bracketed-`[code]` convention. A new
`mut_scope_stack: &mut Vec<IndexMap<String, Type>>` parameter
threads through every `synth` call site (24 sites across lib.rs +
mono.rs:712 / mono.rs:1337 + lift.rs:723 + builtins.rs in two test
helpers); each entry-point caller (`check_fn`, `check_const`, mono
re-synth, lift letrec-capture synth) opens a fresh empty stack at
top-of-body. `Term::Var` resolution prepends the mut-scope check
ahead of the existing locals/globals chain (innermost-first wins on
shadowing). `Term::Mut` pushes a frame after gating each
`MutVar.ty` against `is_supported_mut_var_type` (Int / Float /
Bool / Unit), synths inits in the in-progress scope, unifies init
type with declared, and synths the body in the extended scope.
`Term::Assign` resolves the target name through the stack, returns
`MutAssignOutOfScope` (with a flat-deduplicated `available` list)
on miss, compares value type against the declared via
`subst.apply` + equality (manual to preserve the
`AssignTypeMismatch` diagnostic identity), and yields `Type::unit()`
on success. Five `.ail.json` fixtures + a 5-test driver
(`mut_typecheck_pin.rs`) cover each diagnostic plus the positive
nested-shadow case; the carve-out inventory at
`carve_out_inventory.rs` is extended 7 → 12 to accept them.
`examples/mut.ail` (the mut.1 six-fn round-trip fixture) now
typechecks clean via `ail check`. Codegen stubs unchanged —
`examples/mut.ail` still cannot run end-to-end; that lands in mut.3.
Full `cargo test --workspace`: 592 passed / 0 failed (was 579 at
start of mut.2; +13 = 5 driver pins + 7 lib.rs `mod tests` pins +
1 inventory test count unchanged — the inventory entry count went
from `seven` to `twelve` but the test itself was already counted).
## Per-task notes
- iter mut.2.1 — `CheckError` variants + `code()` + `ctx()`:
Three new variants land before the `Internal` variant with
thiserror `#[error("[code] message")]` attributes per the
cli-diag-human convention. `code()` arms return the canonical
kebab strings (`mut-assign-out-of-scope`, `assign-type-mismatch`,
`mut-var-unsupported-type`). `ctx()` arms emit the structured
payloads named in the plan (`{name, available}`,
`{name, expected, actual}`, `{name, type}`). One in-crate pin
(`mut_typecheck_diagnostics_have_kebab_codes`) gates the code()
+ ctx() shape; RED → GREEN observed.
- iter mut.2.2 — `synth` signature threads `mut_scope_stack`:
New parameter `mut_scope_stack: &mut Vec<IndexMap<String, Type>>`
sits immediately after `locals` (canonical mid-position; matches
the existing convention for per-fn-body lexical state). 18
recursive call sites in `lib.rs::synth` updated via
pattern-by-pattern `replace_all` (each first-arg form had a
unique-enough wrapper). Entry-point callers in `check_fn:1934`
and `check_const:2687` declare a fresh empty stack. mono.rs's
two re-synth sites at 712 + 1337 also declare a fresh empty
stack (they begin from a top-of-body position). Plan-recon
missed two more call sites: `lift.rs:723` (letrec-capture
re-synth) and `builtins.rs` (two test helpers
`synth_in_builtins_env` + a FloatPatternNotAllowed pin) — both
required for build green; both get fresh empty stacks. The
in-test mq.3 call site at `lib.rs:6663` also got the new
parameter. Build was red structurally (E0061 at 23+ sites) until
every call site was updated, then green.
- iter mut.2.3 — `Term::Var` prepends mut-scope resolution: A 5-
line block at the top of the Var arm walks
`mut_scope_stack.iter().rev()` innermost-first, returning the
matched type cloned. Mut-vars are monomorphic per spec §"var
element types" — no Forall instantiation, no FreeFnCall. Two
pins (`mut_var_resolution_shadows_outer_local`,
`mut_var_resolution_picks_innermost_frame`) directly populate
the stack to isolate this change from the Mut-arm work in
Task 4 (the plan's literal Let { Mut { ... } } shape would have
hit the still-stubbed Mut arm at this stage; the property
protected — mut-scope wins over locals + innermost-wins on
shadowing — is identical). TDD discipline gap: the Var-arm edit
landed before the pin tests were observed RED on disk; the
reasoning is analytical (the pre-edit code consults
`locals.get(name)` first, so with `locals = { x → Int }` the
Var arm returned Int, failing the assert-Float; same logic for
the two-frame test). Recorded as Concern.
- iter mut.2.4 — `Term::Mut` typecheck arm: Replaces the stub
with real logic per plan Step 3. A file-scope helper
`is_supported_mut_var_type(&Type) -> bool` gates each var's
declared type to `Int | Float | Bool | Unit` (no args). Per-var
loop synths the init in the outer scope plus the in-progress
frame (the in-flight frame is cloned and pushed during the init
synth, then popped — keeps `mut_scope_stack` truthful at every
nested call), unifies init type with declared, inserts the
(name → declared ty) entry into the frame. After all vars: push
the completed frame, synth body in extended scope, pop frame
(regardless of body outcome — the `body_result` is captured
before pop). Two pins:
`mut_block_typechecks_with_int_var_and_returns_int` (positive,
initially RED because Assign was still stubbed — flipped GREEN
by Task 5) and `mut_block_rejects_str_var_with_unsupported_type`
(negative, GREEN at Task 4 close).
- iter mut.2.5 — `Term::Assign` typecheck arm: Replaces the stub
with real logic per plan Step 3. Resolves the target name via
`mut_scope_stack.iter().rev().find_map(|f| f.get(name).cloned())`.
None case: flattens every name in every frame innermost-first,
dedup-preserving-order via a manual `contains` check (small N,
no need for a Set), returns `MutAssignOutOfScope`. Some case:
synths the value, compares `subst.apply(&declared)` vs
`subst.apply(&value)` via `!=` (manual equality, NOT `unify`
unify would have emitted `TypeMismatch`, and the spec demands
the distinct `AssignTypeMismatch` code), returns
`AssignTypeMismatch` with rendered type strings on mismatch or
`Ok(Type::unit())` on match. Three pins:
`assign_to_in_scope_mut_var_synths_unit` (positive),
`assign_to_out_of_scope_name_fires_diagnostic` (None case),
`assign_with_wrong_value_type_fires_diagnostic` (Some-mismatch
case). RED → GREEN observed for all three.
- iter mut.2.6 — Five fixtures + driver test: Five `.ail.json`
fixtures land under `examples/` (carve-out style, per the
Boss decision in the carrier overriding the spec's literal
`crates/ailang-check/tests/`-side placement). Each fixture is
minimal — single-fn `main`, no imports, body shape exactly the
one the plan named. The driver test
`crates/ailang-check/tests/mut_typecheck_pin.rs` uses the
canonical `load_workspace` + `check_workspace` +
`diagnostics.iter().map(|d| d.code.clone()).collect()` pattern
from `method_collision_pin.rs` and `typeclass_22b2.rs` — no
`todo!()` survives. Carve-out inventory extended from seven to
twelve to accept the five new fixtures (alphabetised; the
comment block names mut.2 as the lineage). All 5 driver tests
GREEN; carve-out inventory GREEN.
- iter mut.2.7 — Verify `examples/mut.ail` typechecks clean:
`cargo run --quiet --bin ail -- check examples/mut.ail` reports
`ok (26 symbols across 2 modules)` — zero diagnostics across
all six fns (`mut_empty`, `mut_single_var`, `mut_two_vars`,
`mut_nested_shadow`, `mut_returns_bool`, `mut_returns_unit`).
Full `cargo test --workspace`: 592 / 0.
## Concerns
- Task 3 TDD-ordering gap: the `Term::Var` arm prepend landed
before the two pin tests were observed RED on disk. The
reasoning that the pre-edit code would have failed both pins
is analytical (locals-first resolution returns Int when
`locals = { x → Int }`, contradicting both `assert_eq!(Float)`
and `assert_eq!(Bool)`). The discipline gap is recorded
per the implementer's Iron Law ("the test you wrote in RED MUST
pass; no other test may regress" — and the spirit-not-ritual
clause that tests-after prove nothing about whether they would
have caught the regression pre-edit). Subsequent tasks
(4 + 5) restored the RED-first sequencing; on those, the Mut /
Assign stubs already emit `CheckError::Internal` so the pins
were observed RED with the canonical pattern. No correctness
impact; the pins protect the property correctly post-edit.
- Plan-recon missed three synth call sites beyond the four the
plan enumerated (lib.rs 1885 + 2634; mono.rs 712 + 1337). The
build-red structural signal flushed them out via E0061:
`lib.rs:6663` (in-test mq.3 helper), `lift.rs:723` (letrec-
capture re-synth), `builtins.rs` (`synth_in_builtins_env` at
351 + a FloatPatternNotAllowed test pin at 594). Each gets a
fresh empty stack with the same "begins from top-of-body"
rationale as mono.rs. No behavioural impact; documentary
concern for the planner's next plan-recon dispatch.
- The carve-out inventory extension was strictly required (the
inventory test enforces a closed list of `examples/*.ail.json`
files) but was not explicitly named in the Boss carrier or the
plan. Recording here so the planner's recon for future iters
knows to enumerate the side-effect when fixtures land under
`examples/`. The boundary call: the fixtures match §C4(a)'s
precedent (negative typecheck fixtures as `.ail.json` so the
diagnostic-code assertion is the load-bearing form, not the
surface). The positive `test_mut_nested_shadow_legal.ail.json`
is the one stretch — its load-bearing assertion is "zero
diagnostics", which the form-A `.ail` form could equally
carry. Landing it as `.ail.json` keeps all five mut.2 fixtures
on one driver and one extension on the carve-out list; the
alternative (splitting positive to `.ail`, negatives to
`.ail.json`) would have produced two drivers or a hybrid
loader. Conservative call documented for visibility.
## Known debt
- Codegen stubs unchanged in mut.2 per the spec's iteration
boundary. A `Term::Mut` or `Term::Assign` reaching
`lower_term` still produces `CodegenError::Internal`. The
full mut.ail E2E (build + run) lands in mut.3. The spec's
Acceptance criterion #3 (`mut_counter.ail` + `mut_sum_floats.ail`
build + run + print expected stdout) is mut.3-scoped.
- The Mut-arm's in-progress-frame clone-on-each-init is O(n²)
over the var count. n is realistically ≤ 2 (the mut.ail
fixture's `mut_two_vars` is the largest in the corpus). Not
worth a more elaborate structure (e.g. pushing a single in-
progress frame and mutating it across inits); the clone-pop
shape keeps the stack truthful at every nested call and makes
the per-var init scope obvious. Recording for visibility.
## Files touched
Modified:
- `crates/ailang-check/src/lib.rs` — 3 new CheckError variants +
3 code() arms + 3 ctx() arms; `synth` signature gains
`mut_scope_stack` after `locals`; 18 recursive synth call
sites updated; 2 entry-point callers (`check_fn`,
`check_const`) declare fresh empty stacks; in-test synth call
(mq.3 helper) gets new param; `Term::Var` arm prepends
mut-scope resolution (5-line block); `Term::Mut` stub replaced
with real logic (push/pop frame, per-var init, body synth);
`Term::Assign` stub replaced with real logic
(find/None-MutAssignOutOfScope/Some-AssignTypeMismatch-or-Unit);
file-scope helper `is_supported_mut_var_type`; 7 new tests in
`mod tests` (`mut_typecheck_diagnostics_have_kebab_codes`,
`mut_var_resolution_shadows_outer_local`,
`mut_var_resolution_picks_innermost_frame`,
`mut_block_typechecks_with_int_var_and_returns_int`,
`mut_block_rejects_str_var_with_unsupported_type`,
`assign_to_in_scope_mut_var_synths_unit`,
`assign_to_out_of_scope_name_fires_diagnostic`,
`assign_with_wrong_value_type_fires_diagnostic`) plus the
`synth_standalone` helper.
- `crates/ailang-check/src/mono.rs` — 2 re-synth call sites
(712, 1337) each get a fresh `mut_scope_stack`.
- `crates/ailang-check/src/lift.rs` — letrec-capture re-synth at
:723 gets a fresh `mut_scope_stack`.
- `crates/ailang-check/src/builtins.rs` — 2 in-test synth calls
(synth_in_builtins_env + FloatPatternNotAllowed pin) get a
fresh `mut_scope_stack`.
- `crates/ailang-core/tests/carve_out_inventory.rs` — EXPECTED
list 7 → 12, comment-block updated naming mut.2 as the
lineage of the 5 new entries.
New:
- `crates/ailang-check/tests/mut_typecheck_pin.rs` — 5-test
driver via canonical load_workspace + check_workspace +
collect-codes pattern.
- `examples/test_mut_assign_out_of_scope.ail.json`
- `examples/test_mut_assign_outside_mut.ail.json`
- `examples/test_mut_assign_type_mismatch.ail.json`
- `examples/test_mut_nested_shadow_legal.ail.json`
- `examples/test_mut_var_unsupported_type.ail.json`
## Stats
bench/orchestrator-stats/2026-05-15-iter-mut.2.json
-139
View File
@@ -1,139 +0,0 @@
# iter mut.3 — codegen + e2e for Term::Mut / Term::Assign
**Date:** 2026-05-15
**Started from:** b057789b55a86cd664e2d7fee2c998bb855cad98
**Status:** DONE
**Tasks completed:** 9 of 9
## Summary
Replaced the iter mut.1 codegen stubs for `Term::Mut` and `Term::Assign`
with real LLVM lowering: mut-vars become `alloca` slots hoisted to the
fn's entry block via a per-fn side buffer `pending_entry_allocas`
spliced into `self.body` at a byte marker captured during
`start_block("entry")`; `Term::Assign` lowers to `store`; references to
a mut-var name lower to `load` through a new lookup precedence in both
`lower_term`'s `Term::Var` arm and `synth_with_extras`'s `Term::Var`
arm (precedence: extras → mut-vars → locals → globals → builtins,
symmetric to the typecheck-side `mut_scope_stack` walk in
`ailang-check`). Lambda emission saves/restores the three new
`Emitter` fields across the thunk boundary and splices the thunk's own
hoisted allocas at its own entry marker so nested mut blocks inside
lambdas hoist into the lambda's entry, not the outer fn's. Two e2e
fixtures shipped (`mut_counter.ail` Int / `mut_sum_floats.ail` Float),
both printing `55` end-to-end (Float's `%g` formatter strips the `.0`).
DESIGN.md gains a "Local mutable state" bullet under "What **is**
supported". Closes the mut-local milestone end-to-end; the audit step
follows.
## Per-task notes
- iter mut.3.1: three `Emitter` fields (`mut_var_allocas`,
`pending_entry_allocas`, `entry_block_end_marker`) added with
doc comments; initialised in `Emitter::new`; cleared per-fn at the
top of `emit_fn` next to the existing per-fn-reset block.
- iter mut.3.2: `start_block` extended — when `label == "entry"` it
captures `self.body.len()` as `entry_block_end_marker` (the splice
point for `pending_entry_allocas`).
- iter mut.3.3: `Term::Var` arm of `lower_term` gained a mut-var
lookup before the `self.locals` walk — on hit, emits
`%v_N = load <llvm_ty>, ptr <alloca_ssa>` and returns the load SSA
+ LLVM type.
- iter mut.3.4: `Term::Mut` arm of `lower_term` real lowering — for
each var: alloca into the side buffer, lower init in outer-plus-
earlier-vars scope (matches typecheck ordering at lib.rs:3578),
store init into alloca, then install the binding (saving any prior
entry for restoration on block exit). Lowers body, restores saved
bindings (unconditional restoration even on error path).
- iter mut.3.5: `Term::Assign` arm of `lower_term` real lowering —
look up the alloca in `mut_var_allocas`, lower value, emit store,
return the canonical Unit SSA form `("0", "i8")` matching
`Literal::Unit` in the same `match`. Tasks 4 and 5 were applied
in one `Edit` call because both arms were adjacent stubs that
needed simultaneous replacement (the `match` arm signature changed
from `{ .. }` to `{ name, value }` / `{ vars, body }`); the review
separation still held — each arm was spec-checked independently.
- iter mut.3.6: splice of `pending_entry_allocas` at the marker —
inserted into `self.body` at the end of `emit_fn` (after body
finalisation, before the deferred-thunks flush). Marker absence
while `pending` is non-empty fires `CodegenError::Internal` with
the fn name.
- iter mut.3.7: `examples/mut_counter.ail` (Int) and
`examples/mut_sum_floats.ail` (Float) authored; both use an
accumulator-style tail-recursive helper since while-loops are
deferred. Initial draft used the natural `(app + lo (tail-app ...))`
shape and was rejected by the tail-call analysis ("call marked
`tail` is not in tail position") — restructured to pass `acc` as
a third arg and yield it directly in the base case. Both fixtures
typecheck clean and print `55` (Float via `%g` strips `.0`).
- iter mut.3.8: `mut_counter_prints_55` + `mut_sum_floats_prints_55`
appended to `crates/ail/tests/e2e.rs`. The plan's placeholder
`expected = "55"` resolved by inspection of `examples/floats.ail`
+ `crates/ail/tests/floats_e2e.rs` (where `1.5 + 2.5` prints as
`4`, confirming `%g`-driven trailing-zero strip — so `55.0`
prints as `55`); test pinned to `"55"`.
- iter mut.3.9: DESIGN.md "Local mutable state" bullet appended
after the "Parameterised ADTs" bullet at the end of the "What
**is** supported" subsection. Schema-drift tests pass (the bullet
lives outside §"Data model").
Two beyond-plan adjustments emerged during execution and were
absorbed inline (recorded as Concerns below):
- `synth_with_extras`'s `Term::Var` arm also needed mut-var lookup
precedence. The plan's Task 3 only touched `lower_term`'s
`Term::Var` arm; without the parallel `synth_with_extras` update,
every polymorphic-call-site arg-type derivation that reaches a
mut-var reference fails with `UnknownVar`. Discovered by running
`examples/mut.ail` through codegen and observing the
`mut_single_var` case fail at `(app + x 1)`.
- Lambda emission (`lambda.rs`) needed save/restore of the three
new fields PLUS an in-thunk splice of `pending_entry_allocas` at
the thunk's own entry marker. Without this, a `Term::Mut` inside
a lambda body would push its alloca into the outer fn's side
buffer (or worse, leak into whichever fn's body the splice runs
on). Mirrors how `lambda.rs` already saves/restores
`non_escape` / `moved_slots` / `current_param_modes` across the
thunk boundary.
## Concerns
- Synth-side `Term::Var` mut-var-lookup parallel was not in the
plan but is structurally required. Recorded as a plan defect to
feed into the iter-mut.4 lessons (if any) and into the milestone-
close audit.
- Lambda-boundary save/restore + in-thunk splice was not in the
plan but is structurally required for correctness of any
`Term::Mut` inside a lambda body. Same defect category as above.
## Known debt
- None for the milestone-local scope. The spec's deferred items
(while-loops, heap-RC mut-var element types, `ref a` + `!Mut`
effect, `Stateful a b` + `pipe`) are explicitly named as future
milestones in the new DESIGN.md bullet.
## Blocked detail
n/a — Status: DONE.
## Files touched
- `crates/ailang-codegen/src/lib.rs` — three `Emitter` fields, init
+ per-fn reset, `start_block` marker capture, `Term::Var` arm
mut-var precedence (both `lower_term` and `synth_with_extras`),
`Term::Mut` + `Term::Assign` arms in `lower_term`, splice at end
of `emit_fn`.
- `crates/ailang-codegen/src/lambda.rs` — save/restore of the three
new fields across the thunk boundary, in-thunk splice of
`pending_entry_allocas`.
- `crates/ail/tests/e2e.rs` — two new tests pinning
`mut_counter.ail``"55"` and `mut_sum_floats.ail``"55"`.
- `docs/DESIGN.md` — "Local mutable state" bullet at end of
"What **is** supported" subsection.
- `examples/mut_counter.ail` — new Int fixture.
- `examples/mut_sum_floats.ail` — new Float fixture.
## Stats
bench/orchestrator-stats/2026-05-15-iter-mut.3.json
-137
View File
@@ -1,137 +0,0 @@
# iter mut.4-tidy — close mut-local audit drift
**Date:** 2026-05-15
**Status:** DONE
**Spec / Plan:** `docs/specs/2026-05-15-mut-local.md` (no spec changes
beyond a §"Out of scope" amendment); `docs/plans/2026-05-15-iter-mut.4-tidy.md`.
**Started from:** 6966cce
**Trigger:** audit close on the mut-local milestone.
## What this iter shipped
Tidy work closing the audit drift the architect surfaced after
mut.3 landed. Two `[high]` items, two `[medium]` items, plus a
substantive bench regression on `bench/compile_check.py` `check_ms`.
### Architect [high] 1 — lambda-captures-of-mut-var diagnostic
The spec's "mut-vars cannot escape their enclosing block"
invariant was not enforced by any check pass — codegen detected
the violation via a `CodegenError::Internal` whose comment blamed
the typechecker for letting it through. The fix lands the
diagnostic at the typecheck layer:
- New variant `CheckError::MutVarCapturedByLambda { name: String }`
with kebab-case code `mut-var-captured-by-lambda`, `ctx()`
emitting `{"name": <id>}`, and the canonical bracketed-`[code]:`
Display attribute.
- `Term::Lam`'s synth arm now walks the lambda body's free vars
(via the existing `ailang_core::desugar::free_vars_in_term`
helper, which already honours pattern-binding subtraction in
`Term::Match` arms) and rejects any free var whose name appears
in any enclosing `mut_scope_stack` frame.
- The walk is gated by `!mut_scope_stack.is_empty()` so lambdas
outside any mut block pay no overhead.
### Architect [high] 2 — codegen comment cleanup
The codegen lambda.rs path that previously raised
`CodegenError::Internal("lambda capture {c} not in locals —
typechecker bug?")` now uses `unreachable!` with a message
naming the typecheck rejection that gates it. The companion
comment block at `lambda.rs:474` was rewritten to state
the current reality (typecheck rejects via
`MutVarCapturedByLambda`) instead of the stale promise that
"mut.2 typecheck will enforce this" (mut.2 didn't).
### Architect [medium] 3 + 4 — stale mut.1-stub history comments
- `docs/DESIGN.md` §"Term (expression)" mut/assign block: the
trailing paragraph describing the iter mut.1 stub state was
replaced with one describing the current end-to-end mut.3
reality. The `// Iter mut.1` comment on the `{"t": "assign"}`
jsonc fragment was updated to drop the "deferred to mut.2/mut.3"
language.
- `crates/ailang-codegen/src/lib.rs:1749-1758` — the comment block
above the real `Term::Mut` codegen arm still described the mut.1
stub. Removed entirely; the real `Iter mut.3` comment immediately
below it is now the only descriptive header.
### Bench regression ratify — `check_ms` ~30-50% uniform shift
`bench/compile_check.py` flags an 11-fixture-wide `check_ms`
regression on the mut-local milestone (30-50% relative; ~0.5ms
absolute on a 1.4-1.5ms baseline). The pattern is **uniform across
fixtures of very different Var counts** (`hello.ail` 7 lines and
`bench_list_sum_explicit.ail` 100+ lines both shift by ~0.5ms),
which indicates a fixed-cost-per-`ail check`-invocation tax rather
than a hot-path-per-Var cost.
Root-cause analysis:
- **Hypothesis A: `mut_scope_stack` walk at every Var resolution.**
Tested by the Task 3 short-circuit (`if !mut_scope_stack.is_empty()`).
Re-bench: regression unchanged. **Falsified.**
- **Hypothesis B: synth-parameter-passing overhead.** Threading
`&mut Vec<IndexMap<...>>` through ~19 recursive synth calls per
Term-tree walk. Each call pays a 64-bit reference push to the
stack frame. Not directly testable without rolling the
threading back. Plausible but speculative.
- **Hypothesis C: binary size / startup cost.** mut.* added
~1400 lines of code across `ailang-check` and `ailang-codegen`.
Larger binary → slightly slower process startup, dynamic linker
resolution, instruction-cache warmup. ~0.5ms is in the right
ballpark for that kind of cost on a small binary. The uniform-
across-fixtures pattern is consistent with this — it's not
Var-proportional.
The pragmatic call: hypothesis B and C are both consistent with the
observation; neither has a cheap remedy at this milestone. The
short-circuit in Task 3 closes hypothesis A's contribution if any,
and remains the right shape for future workloads that DO have
non-empty `mut_scope_stack`. The remaining shift is the cost of
the feature.
**Disposition: ratify** as a fixed-cost feature tax. The journal
records this disposition; the `bench/compile_check.py` baseline is
updated to the post-mut.4-tidy numbers. Future bench observations
will tolerance-check against the new baseline. If a future iter
finds a substantive hot-path that retroactively explains the shift,
the ratify can be revisited.
`bench/check.py` regressions are tail-latency metrics
(`latency.implicit_at_rc.p99_9_us / max_us`) consistent with the
"established noise envelope, 15th consecutive observation" finding
from audit-pd (2026-05-14). No ratify needed — they fall under
the existing noise carve-out.
`bench/cross_lang.py` is clean (25/0/0/25). Runtime is unchanged
by mut-local.
## Working tree
Eight files: 7 modified + 1 new (`examples/test_mut_var_captured_by_lambda.ail.json`).
## Tests
594 → 598 green (4 new: the variant code-pin smoke test, the
positive `lambda_outside_mut_still_typechecks_clean` regression
guard, the negative `lambda_inside_mut_capturing_mut_var_emits_mut_var_captured_by_lambda`
in lib.rs, and the integration-test
`lambda_capturing_mut_var_emits_mut_var_captured_by_lambda`).
## Concerns
- **`Term::Match` pattern binding over-approximation.** The
`free_vars_in_term` walker correctly subtracts pattern-bound
names; reused without modification. No false positives
encountered in the existing test corpus.
- **Bench-ratify decision.** Documented above. The 30-50%
uniform shift is plausible but not proven to be feature-cost.
A future bencher dispatch could test hypothesis B by branching
off a "remove the mut_scope_stack parameter, use a thread-local
/ Env field instead" prototype and measuring the difference. Out
of scope for this tidy.
- **`bench/compile_check.py` baseline updated** in this commit.
The audit-skill discipline ("NO BASELINE UPDATE WITHOUT A
PAIRED JOURNAL RATIFY ENTRY") is satisfied by this entry.
@@ -1,107 +0,0 @@
# audit — iteration-discipline-revert milestone close
**Date:** 2026-05-16
**Milestone:** iteration-discipline-revert (HEAD `37ac704`)
**Verdict:** Milestone-iteration-discipline-revert tidy — **clean**
## Step 1 — Architect drift review
The revert is byte-pristine: all 18 reverted production source files
+ 5 reverted test files byte-identical to `git show 1ff7e81:<path>`
(`crates/ailang-check/src/lib.rs` fully so); zero residual it.1/it.2
production surface (`Term::Loop`/`Recur`/`LoopBinder`/
`verify_structural_recursion`/`term_contains_loop`/`module_fns`/
`"Diverge"`-injection / the five `Recur*`+`NonStructuralRecursion`
variants all absent from `crates/*/src/`). DESIGN.md coherent:
Decision 3 & 8 byte-identical to oracle, no loop/recur in §"Data
model", clause 3 retained (hunk-edited, not wholesale-restored — the
`1ff7e81` DESIGN.md had zero clause-3 text) with its worked example
de-claimed to explicit hypothetical, `skills/brainstorm/SKILL.md`
de-claimed in lockstep, F1/F4 idiom note present and guarded by a
passing `design_schema_drift.rs` test. Roadmap: Iteration-discipline
block + blocking-fork section gone, single deferred P2 entry with
correct `depends on:`/`context:`, F3 todo + Stateful-islands intact,
old spec carries the superseded header.
One `[medium]` drift item:
- `docs/roadmap.md` Stateful-islands `[~]` scope bullet still
asserted *"the 'Iteration discipline' P1 milestone supersedes the
earlier 'while-loops legal' scope … repetition is structural
recursion or named `loop`/`recur`"* — a dangling cross-reference
into the now-reverted milestone from a **live planning doc** that
would mislead the next brainstorm into treating `loop`/`recur` as
the committed repetition story. **Resolved inline** (Boss
doc-hygiene, full context already loaded): reworded to keep the
real, still-true scope guard (AILang has no `while`/`for`; this
milestone introduces none; repetition stays recursion exactly as
the language has it today; a `mut` block is a sealed expression,
never a loop) and drop the reverted-milestone reference.
## Step 2 — Bench-regression check
- `bench/compile_check.py` — exit 0 (24/24 stable).
- `bench/cross_lang.py` — exit 0 (25/25 stable; notably
`bench_list_sum.ail_bump_s` only +2.44% vs baseline).
- `bench/check.py` — exit 1: single metric
`throughput.bench_list_sum.bump_s` baseline 0.046s → 0.052s,
+12.71% (tol 10%); the 4 "improvements" are `gc_over_bump` ratio
shifts downstream of that one number.
**Localisation (bencher, hypothesis-driven).** Built release `ail`
at HEAD `37ac704` and at the byte-oracle `1ff7e81` (read-only
`git worktree`, removed after). The compiled `bench_list_sum`
bump binaries are **`cmp`-identical** (byte-for-byte same machine
code, identical stdout). Interleaved 3 trials × 60 runs/arm: head
median 0.051233s vs oracle median 0.051206s; headoracle delta
oscillates around zero (+0.05% / +0.61% / 0.20%, within ~1.5%
stdev); **both** arms read ~+11% over the 0.046 baseline on this
machine *now*. Verdict: **environmental/hardware drift relative to
the 2026-05-09 baseline-capture machine state, NOT a revert
regression** — confirmed independently of the architect byte-diff
and the implement Task-11 behavioural-equivalence sweep (third
independent proof the revert's codegen == `1ff7e81`'s codegen).
## Step 3 — Classification & resolution
- Architect `[medium]` roadmap dangling xref → **fix**, done inline
(Boss doc-hygiene; no separate tidy iteration warranted for a
one-bullet prose correction with a single correct outcome).
- `check.py` exit 1 → **carry-on**, baseline **NOT ratified**.
Ratify is categorically wrong here: nothing *intentionally* moved
the metric (the revert returned codegen to byte-identical
`1ff7e81`), and the audit Iron Law forbids a baseline bump without
an iter that intentionally moved it. Bumping 0.052 in would bake a
noise/environment value into `bench/baseline.json`. The spec's
"bench PRISTINE, not ratified" mandate is **satisfied**: pristine
means the revert introduced no regression, and it provably did not
(byte-identical binaries vs the clean `1ff7e81` oracle).
- The pre-existing `*.bump_s` baseline-vs-hardware staleness (the
`1ff7e81` oracle itself trips the same +11% on current hardware —
predates this milestone, revert-foreign) recorded as a separate
**P2 roadmap `[todo]`** (bench-harness recalibration; no language
change), so it is tracked without being mis-attributed to the
revert or laundered through a ratify.
## Fieldtest
**Skipped** (Boss judgement; fieldtest is non-mandatory). A revert
restores the already-field-tested pre-`9973546` user-visible surface
(structural / tail recursion; `tail-app` intact) — the surface that
the mut-local fieldtest already exercised and that started this
corrective arc. A fieldtest here would re-test a known, well-trodden
state and add no signal; the implement Task-11 byte-equivalence
sweep over 164 fixtures is the stronger correctness proof for a
revert.
## Close
Milestone **iteration-discipline-revert** closed clean:
byte-pristine vs `1ff7e81` across all production surface; behaviour
byte-identical over 164 surviving fixtures; `cargo test --workspace`
600/0; one roadmap drift fixed inline; the single `check.py` metric
localised to environmental variance (baseline untouched) with the
pre-existing staleness filed forward. AILang's iteration story is
exactly the pre-`9973546` state; the genuine total-recursion
ambition is preserved on the roadmap, sequenced behind a future
`Nat`/refinement-types milestone.
@@ -1,81 +0,0 @@
# iter effect-doc-honesty — effect-system documentation-honesty tidy
**Date:** 2026-05-16
**Started from:** c41e0e5a9e48fd0099eda8c286f10ec7a30e8e8a
**Status:** DONE
**Tasks completed:** 6 of 6
## Summary
Documentation-honesty tidy: corrected three false effect-system
claims plus their two satellite mentions across the project docs,
guarded by one new doc-presence regression pin. No language,
checker, or codegen code changed — only a doc-comment, two markdown
spec files, an internal CLI guidance string, and a new integration
test. `ail check`/`ail run` behaviour is byte-unchanged by
construction; `cargo test --workspace` 600 → 604 (the four new pin
tests, zero regressions), `cargo build --workspace` clean. The
three sentinel drift/coverage tests (`design_schema_drift`,
`spec_drift`, `schema_coverage`) stay green, empirically confirming
none scans the effect-prose region.
## Per-task notes
- iter effect-doc-honesty.1: new pin `crates/ailang-core/tests/effect_doc_honesty_pin.rs`
(4 tests asserting fiction strings absent + corrected anchors
present across the 4 edited files); authored first, ran RED 0/4
for the right reason (TDD: RED before corrections).
- iter effect-doc-honesty.2: `docs/DESIGN.md` Decision 3 row-polymorphic
block → flat/unordered/closed set-equality + Diverge-reserved
wording; line-2722 "IO and Diverge ops" bullet → IO-only +
Diverge-reserved bullet. `design_md_effect_prose_is_true` GREEN.
- iter effect-doc-honesty.3: `crates/ailang-core/src/ast.rs` `Term::Do`
doc-comment effect-handler-table-at-link-time fiction → real
mechanism (typecheck against `Env::effect_ops`, `lower_effect_op`
literal-match codegen). `term_do_doc_comment_is_true` GREEN.
- iter effect-doc-honesty.4: `crates/ailang-core/specs/form_a.md`
line-226 "currently `IO` and `Diverge`" → IO-built-in-op /
Diverge-reserved; rule-3 "`Diverge` for `diverge/*`" →
io/print_str-is-the-only-built-in-op. `form_a_spec_effect_names_are_true`
GREEN.
- iter effect-doc-honesty.5: `crates/ail/src/main.rs` merge-prose
CONTRACT effect-annotation bullet dropped the non-existent
`(effects Diverge)` example. Whole pin 4/4 GREEN.
- iter effect-doc-honesty.6: full verification — whole pin 4/4,
`cargo test --workspace` 604/0 (600 prior + 4), drift/coverage
trio green, `cargo build --workspace` clean.
## Concerns
- Task 2: the plan's verbatim Task-2 replacement body soft-wrapped
the phrase "flat, unordered,\nclosed set of effect names"
mid-phrase, which contradicts Task-1's single-line pin substring
`"flat, unordered, closed set of effect names"` AND the plan's
own self-review item 6 ("every pin substring is single-line — the
documented iter-revert grep-wrap failure mode is avoided"). The
two authoritative artefacts (exact replacement text vs. exact
pin) could not both be applied verbatim. Resolved by preserving
the exact corrected *wording* (byte-identical to the plan's
intent) and the exact pin (Task 1, unchanged), moving only the
soft-wrap column so the asserted phrase stays contiguous on one
line. No wording added/dropped/changed. This is a planner
self-review-item-3 cross-check gap (the asserted-substring vs.
replacement-body check did not account for the replacement body's
own line wrap) — orchestrator may want to tighten the planner's
pin/replacement-body wrap cross-check.
## Known debt
- (none) — the tidy is self-contained; no deferred follow-up.
## Files touched
- `crates/ailang-core/tests/effect_doc_honesty_pin.rs` (new — the pin)
- `docs/DESIGN.md` (Decision 3 + "What is not supported" bullet)
- `crates/ailang-core/src/ast.rs` (`Term::Do` doc-comment)
- `crates/ailang-core/specs/form_a.md` (line 226 + rule 3)
- `crates/ail/src/main.rs` (merge-prose CONTRACT string)
## Stats
bench/orchestrator-stats/2026-05-16-iter-effect-doc-honesty.json
-174
View File
@@ -1,174 +0,0 @@
# iter revert — Iteration-discipline milestone surgical forward revert
**Date:** 2026-05-16
**Started from:** abf00131c10ff9287b419439c565480c96fa861b
**Status:** DONE
**Tasks completed:** 11 of 11
## Summary
The Iteration-discipline milestone (it.1 `96db54d` + it.2 `a4be1e5`)
is fully backed out by a single forward iteration, with `1ff7e81`
(the last commit before milestone-start `9973546`) as the per-region
byte oracle. `main` was never rewound — every restore is a forward
working-tree edit, net-new it.1/it.2 surface is `rm`'d, DESIGN.md is
hunk-edited in place (clause 3 / criterion section KEPT, never
wholesale-restored). Feature-acceptance clause 3 and the F3 todo are
retained; the clause-3 worked example is de-claimed from "shipped" to
hypothetical-illustration form (in DESIGN.md and brainstorm SKILL.md
in lockstep); one honest F1/F4 documentation note is added (the only
net addition) and guarded by a doc-presence test. The correctness
gate (Task 11) proved compiler behaviour **pristine**: all 164
surviving `1ff7e81`-era fixtures `ail check`/`ail run` byte-identical
to their pre-milestone behaviour; `cargo test --workspace` 600/0;
zero residual it.1/it.2 production surface. AILang's iteration story
is exactly the pre-`9973546` state (structural / tail recursion;
`tail-app` intact); the genuine total-recursion ambition is preserved
on the roadmap as a deferred P2 milestone sequenced behind a future
`Nat`/refinement-types milestone.
## Per-task notes
- revert.1: AST node family + drift anchors + DESIGN.md §Data-model —
removed `Term::Loop`/`Term::Recur`/`struct LoopBinder` + 2 serde
units (ast.rs byte-identical to oracle); loop/recur exemplars +
match arms removed from design_schema_drift.rs / spec_drift.rs /
schema_coverage.rs (byte-identical); DESIGN.md §Data-model
loop/recur JSON + it.1/it.2 prose removed (region byte-identical).
- revert.2: `CheckError` surface — 5 variants + doc comments, 5
`code()` arms, 3 dedicated `ctx()` arms deleted; `_ =>` catch-all
preserved; diagnostic.rs confirmed genuine no-op.
- revert.3: it.2 guardedness pass + Diverge-injection — STOP-gate
cleared (all 6 named helpers it.2-net-new at oracle → all delete,
no ambiguity). Removed the entire 687-line guardedness cluster
(verify_structural_recursion, term_contains_loop, adt_families,
referenced_con_names, mutual_structural_group, adt_param_positions,
RecCall, TypeUnionFind, MutualGroup, et al.) **plus** the
transitively-it.2 `module_fns` plumbing through
check_module/check_def/check_fn signatures (oracle proved net-new;
resolved via the plan's Oracle convention + recon-by-name mandate —
see Concerns). 3 Diverge-injection sites restored byte-identical;
lib.rs `"Diverge"` count 0 == oracle.
- revert.4: it.1 check surface — synth Loop/Recur arms +
verify_loop_body deleted; loop_stack param + ~30 threaded calls + 9
local decls removed; verify_tail_positions survives, it.1 arms
removed, byte-identical to oracle; substitute_rigids_in_term +
post-mono name arms cleaned. Result:
`crates/ailang-check/src/lib.rs` FULLY byte-identical to `1ff7e81`.
- revert.5: 9 remaining walker files (lift/mono/linearity/uniqueness/
reuse_shape/pre_desugar_validation/builtins + core desugar/
workspace) restored to oracle, all byte-identical; pre-restore diff
classification proved every hunk it.1/it.2-only.
- revert.6: surface + prose + form-A grammar — parse.rs/print.rs/
prose lib.rs/form_a.md restored to oracle byte-identical; each
touched only by 96db54d (it.1) since 1ff7e81; surface+prose build
clean.
- revert.7: codegen — lib.rs/escape.rs/lambda.rs + ail/main.rs
restored to oracle byte-identical; `block_terminated = true` count
== 8 == oracle (it.1 parallel setter gone; the 4 tail-driven + 4
control-flow setters survive). `cargo build --workspace` clean
(first full-build gate, lockstep clustering worked as designed).
- revert.8: STOP-gate re-verify cleared (live `git diff --stat`
matched plan's net-new/modified/absent classification exactly).
Deleted 2 pin files + 16 fixtures + bench/it3-oracle via plain
`rm` (no git-index touch). 2 RC fixtures restored byte-identical.
e2e.rs + codegen_import_map_fallback_pin.rs + carve_out_inventory.rs
restored byte-identical (carve = "Twelve carve-outs", last entry
test_mut_var_captured_by_lambda.ail.json). it.{1,2,3} stats json
KEPT. cargo test --workspace 599/0.
- revert.9: DESIGN.md Decision 3 → 1ff7e81 placeholder (byte-identical,
hunk-only); clause-3 worked example de-claimed to hypothetical form
(criterion section + clause-3 discriminator KEPT); brainstorm
SKILL.md de-claimed in lockstep; F1/F4 note added verbatim;
doc-presence test added verbatim, PASSES.
- revert.10: roadmap Iteration-discipline [~] block + blocking-fork
sub-section removed; deferred "Iteration-totality story" entry
added verbatim at top of P2 (per plan + spec sequencing); F3 +
Stateful-islands untouched; superseded header prepended to the old
spec; Step-4 STOP-check clean (no residual A1 amendment).
- revert.11 (correctness gate): 1ff7e81 ref compiler built in
throwaway worktree; 164-fixture check/run oracle captured;
post-revert compiler diffed byte-for-byte — fail=0, ZERO DRIFT,
behaviour PRISTINE. cargo test --workspace 600/0. Residual
it.1/it.2 production-surface grep clean. Worktree removed; main
HEAD never moved.
## Concerns
- revert.3: the plan's Task-3 file-map enumerated the guardedness
helpers + call site but did NOT explicitly list the `module_fns`
signature threading through check_module/check_def/check_fn. The
`1ff7e81` oracle unambiguously proved that threading it.2-net-new;
leaving it would have been dead it.2 surface referencing deleted
helpers. Resolved correctly via the plan's own "Oracle convention"
+ "re-locate by symbol name, oracle is authority on every byte"
mandate + the spec §Data-flow ("restored to pure pre-it.1 role").
This is the recon-map-drifts-but-oracle-is-authority mechanism
working as designed, not a plan defect — no bounce. Flagged so the
Boss is aware the plan file-map under-enumerated one signature
edge; the end-state is verified correct (lib.rs byte-identical to
oracle).
- revert.9: the plan's Task-9 Step-6 single-line grep heuristic
(`grep -c 'clauses 1 and 2 yet fail clause 3' ...`) returned 0
because the verbatim-applied replacement text line-wraps ("yet
fail\n clause 3"). The substantive acceptance criterion
(clause-3 block retained, only de-claimed) is positively verified
multiline + by the intact `## Feature-acceptance criterion` section
and "criterion 3 is the discriminator" sentence. Not a defect;
flagged so the Boss does not misread the heuristic.
## Known debt
(none — the revert is byte-pristine against `1ff7e81` for all
production surface; behaviour is byte-identical over the surviving
corpus.)
## Blocked detail
(none — DONE.)
## Files touched
90 working-tree paths (Boss commits all as one cohesive revert iter):
- Deleted (59): `crates/ailang-check/tests/loop_recur_pin.rs`,
`crates/ailang-check/tests/structural_recursion_pin.rs`; 16 it.1/it.2
fixtures under `examples/` (loop_*.ail, struct_rec_*.ail,
test_recur_*.ail.json, test_loop_missing_diverge / test_mutual_cross_family
/ test_non_structural_recursion .ail.json); `bench/it3-oracle/`
(MANIFEST.tsv + 40 .out).
- Modified — source revert to 1ff7e81 (byte-identical):
`crates/ailang-core/src/ast.rs`, `desugar.rs`, `workspace.rs`;
`crates/ailang-check/src/lib.rs`, `lift.rs`, `mono.rs`,
`linearity.rs`, `uniqueness.rs`, `reuse_shape.rs`,
`pre_desugar_validation.rs`, `builtins.rs`;
`crates/ailang-codegen/src/lib.rs`, `escape.rs`, `lambda.rs`;
`crates/ail/src/main.rs`; `crates/ailang-surface/src/parse.rs`,
`print.rs`; `crates/ailang-prose/src/lib.rs`;
`crates/ailang-core/specs/form_a.md`.
- Modified — tests revert to 1ff7e81 (byte-identical):
`crates/ailang-core/tests/spec_drift.rs`, `schema_coverage.rs`,
`carve_out_inventory.rs`; `crates/ail/tests/e2e.rs`,
`codegen_import_map_fallback_pin.rs`.
- Modified — net-add / hunk-edit:
`crates/ailang-core/tests/design_schema_drift.rs` (loop/recur
anchors removed + new doc-presence test added),
`docs/DESIGN.md` (Decision 3 placeholder + clause-3 de-claim +
F1/F4 note; hunk-edited, clause 3 kept), `docs/roadmap.md`
(Iteration-discipline block removed, deferred P2 entry added),
`docs/specs/2026-05-15-iteration-discipline.md` (superseded
header), `skills/brainstorm/SKILL.md` (worked-example de-claim).
- Modified — fixtures restored to 1ff7e81:
`examples/rc_pin_recurse_implicit.ail`,
`examples/rc_let_alias_implicit_param.ail`.
- KEPT (no-op, Boss decision):
`bench/orchestrator-stats/2026-05-15-iter-it.{1,2,3}.json`.
E2E coverage: none (revert iteration — the Task-11 behavioural-
equivalence sweep over all 164 surviving fixtures is a stronger
correctness proof than new E2E; the doc-presence test guards the
only net-new artefact).
## Stats
bench/orchestrator-stats/2026-05-16-iter-revert.json
@@ -1,180 +0,0 @@
# iter loop-recur.1 — Additive AST-Node Foundation (Term::Loop / Term::Recur / LoopBinder)
**Date:** 2026-05-17
**Started from:** a5eebcd5fd9d703cc4f8a6374f95e00061d86c55
**Status:** DONE
**Tasks completed:** 7 of 7
## Summary
`loop` / `recur` become real, parseable, printable, round-trippable,
hash-stable, strictly-additive AST nodes across all six crates plus
the drift tests — with NO typecheck semantics and NO real codegen
lowering (those are loop-recur iters 2 and 3). Added `Term::Loop {
binders, body }`, `Term::Recur { args }`, and `struct LoopBinder`
(mirroring `MutVar` exactly); Form-A `(loop (NAME TYPE INIT)* BODY+)`
/ `(recur ARG*)` parse + print + grammar/notes; prose render /
count_free_var / subst_var arms; ~30 structural pass-through walker
arms; the two semantic dispatch points stubbed with
`CheckError::Internal` (`synth`) / `CodegenError::Internal`
(`lower_term`) exactly as mut.1 did; DESIGN.md + form_a.md schema
blocks; drift/coverage/hash anchors; and the spec's worked `sum_to`
program as a green `examples/loop_sum_to.ail` round-trip fixture.
`cargo test --workspace` 608 green / 0 red; the iter13a + new
loop-recur hash pins both green (additivity proven — pre-existing
canonical-JSON hashes byte-stable); all `tail`-filtered tests green
(Boss-call-1 operational evidence).
## Per-task notes
- iter loop-recur.1.1: Core AST — `Term::Loop`/`Term::Recur` variants
+ `struct LoopBinder` + 2 serde round-trip unit tests. RED-first
(`error[E0599]`); GREEN after Task 4 (desugar/workspace are *in*
ailang-core, so the lib's own exhaustive matches gate it — see
Concerns). Both serde tests green.
- iter loop-recur.1.2: Surface parser (`parse_loop`/`parse_recur` +
head dispatch + unknown-head message), printer arms, form_a.md
grammar+notes. Binder/body boundary disambiguated via an
`is_term_head_kw` discriminator (the plan's flagged "one genuine
parser judgement"). `parse_loop_and_recur_round_trip_via_term`
green (exactly 2 binders + `if` body + 2-arg recur).
- iter loop-recur.1.3: Prose projection — `write_term_prec`,
`count_free_var`, `subst_var_with_term` Loop/Recur arms (verbatim
plan code, lexical-shadow-aware mirroring the Mut arms).
- iter loop-recur.1.4: ailang-core walkers — 9 desugar.rs sites + 2
workspace.rs sites. `cargo build -p ailang-core` GREEN.
- iter loop-recur.1.5: ailang-check — 14 steps across 7 files
including the Boss-call-1 `verify_tail_positions` two mandatory
additive arms and the Boss-call-2 `synth` `CheckError::Internal`
stub. `cargo build -p ailang-check` GREEN.
- iter loop-recur.1.6: ailang-codegen + ail/main.rs — `lower_term`
`CodegenError::Internal` stub + real structural pass-through at
the pure analysis walkers (lambda/escape ×3) + `synth_with_extras`
pass-through + main.rs walk_term/rewrite_term. `cargo build
--workspace` GREEN.
- iter loop-recur.1.7: DESIGN.md + form_a.md schema blocks;
design_schema_drift + spec_drift + schema_coverage anchors; new
loop_recur hash pin; `examples/loop_sum_to.ail` positive fixture.
Full suite 608/0; tail-app non-regression green.
## Boss design calls (mirrored from the plan header at iter close)
1. **`verify_tail_positions` "byte-unchanged" = tail-app role
unchanged, NOT source frozen.** `verify_tail_positions`
(`crates/ailang-check/src/lib.rs`) is an exhaustive no-`_`-wildcard
`match`-on-`Term`; the two additive variants *require* two new
arms there or `ailang-check` does not compile. The added arms
only define how the tail-app walker descends through two
brand-new node kinds that no pre-existing fixture contains —
zero behaviour change for any pre-existing construct. The
acceptance evidence is the tail-app non-regression test (all
`tail`-filtered tests green), NOT a frozen-source diff. mut.1 set
the exact precedent (it added `Term::Mut`/`Term::Assign` arms
inside the same function). Settled; not reopened — the
spec-compliance phase did NOT emit `unclear`/BLOCKED on the
anticipated "spec says byte-unchanged but the function changed"
tension, per the plan header's authority.
2. **Codegen IS in iter-1 scope as stubs/pass-throughs (mirrors
mut.1 site-for-site).** "No codegen in iter 1" means no real
loop-header/phi/back-edge lowering (that is iter 3). But
`ailang-codegen` carries no-wildcard exhaustive `Term` matches;
the workspace would not compile without arms. Resolved exactly
as mut.1: `CodegenError::Internal` stub at the `lower_term`
dispatch + real structural pass-through at the pure analysis
walkers (lambda.rs collect_captures, escape.rs walk/escapes/
collect_free_vars) and `synth_with_extras`. No real loop lowering
written this iter.
## Concerns
- DONE_WITH_CONCERNS (iter loop-recur.1.1/1.2/1.4): three
plan-pseudo-vs-reality substitutions, all of the recurring
"specs need concrete code / plan pseudo vs reality" class
(`feedback_specs_need_concrete_code`, `feedback_plan_pseudo_vs_reality`),
each resolved by preserving the arm/test's evident intent and
fixing to the real codebase:
1. **Task-1 serde expected-bytes literal.** The plan asserted
`"type":{"t":"con","name":"Int","args":[]}`; the real canonical
`Type::Con` form is `{"k":"con","name":"Int"}` (discriminator
`"k"`, empty `args` omitted — established since mut.2). The
test's stated intent (pin one-binder Loop canonical bytes;
prove `binders` has no `skip_serializing_if`) is preserved;
only the mis-transcribed expected string was corrected. NOT a
bug-dodge — `{"k":"con",…}` is the canonical form every Type
fixture uses; the plan author mis-recalled the Type JSON.
2. **Bare `Int` vs `(con Int)` in binder triples.** The spec's
worked example and the plan's Step-1 test source / Step-6
fixture write `(loop (acc Int 0) …)` with bare `Int`. The real
`parse_type` parses a bare `Int` ident as `Type::Var{"Int"}`
(a type *variable*), not the Int constructor — semantically
wrong and inconsistent with every `mut` fixture
(`(var x (con Int) 0)`), the spec schema, and the Task-1 serde
pin. Corrected to `(con Int)` in the parser test source and the
`examples/loop_sum_to.ail` fixture. This is the LLM-natural AND
correct Form-A type (not a fixture-adapts-to-bug case — every
sibling fixture uses `(con Int)`).
3. **`ailang_core::ast::LoopBinder` path inside ailang-core.** The
plan's literal desugar.rs/workspace.rs arms used the
downstream-crate path `ailang_core::ast::LoopBinder`; inside
ailang-core itself (`use crate::ast::*;`) the correct path is
unqualified `LoopBinder` (consistent with the adjacent bare
`MutVar` usage). `subst_call_with_extras`'s plan arm also used
`(target, extras)`; the real signature is `(name, lifted,
extras)` — mirrored from the local Mut arm per the plan's own
instruction. ailang-check (downstream) correctly keeps the
`ailang_core::ast::LoopBinder` path.
## Boss addendum (post-orchestrator, at commit)
Concern 2 surfaced that the bare-`Int`-in-binder imprecision is not
just in the plan/test but in the **committed specs themselves**:
`docs/specs/2026-05-17-loop-recur.md`'s headline clause-1 worked
example + the must-fail `bad_recur` fixture, and
`docs/specs/2026-05-17-llm-surface-discipline.md` §5's illustrative
anchor, all wrote `(loop (acc Int 0) (i Int 1) …)`. Bare `Int`
parses as `Type::Var{"Int"}`, so the spec's *headline evidence*
the program an LLM author would copy verbatim — does not parse to
the typed loop it claims. Same doc-honesty class as the mono.rs
header / effect-doc-honesty: a spec must show code that actually
parses to what it asserts. Boss forward-fixed all three snippets to
`(con Int)` (the established convention every `mut` fixture + the
spec's own schema use); folded into this iter's commit. No design,
semantics, schema, or load-bearing assumption changed — a
surface-syntax precision fix, so no re-brainstorm / re-grounding.
## Known debt
- No additional E2E fixture written (Phase 3). iter-1 ships NO
typecheck and NO codegen by design; an `ail run` e2e would only
exercise the deliberately-temporary `synth`/`lower_term`
`Internal` stubs (a negative-value test asserting a transient
error). The milestone invariant worth protecting at iter-1 —
additivity (hash pins), round-trip (the new fixture), drift trio,
tail-app non-regression — is fully covered by tests added in
Tasks 1/2/7. The spec's positive sum_to-runs-to-a-value E2E is
explicitly iter-3 scope.
## Files touched
- AST + core walkers: `crates/ailang-core/src/ast.rs`,
`desugar.rs`, `workspace.rs`
- Surface: `crates/ailang-surface/src/parse.rs`, `print.rs`;
`crates/ailang-core/specs/form_a.md`
- Prose: `crates/ailang-prose/src/lib.rs`
- Check: `crates/ailang-check/src/{lib,lift,mono,linearity,
uniqueness,reuse_shape,pre_desugar_validation}.rs`
- Codegen + CLI: `crates/ailang-codegen/src/{lambda,escape,lib}.rs`,
`crates/ail/src/main.rs`
- Docs/drift/coverage/hash: `docs/DESIGN.md`,
`crates/ailang-core/tests/{design_schema_drift,spec_drift,
schema_coverage,hash_pin}.rs`,
`crates/ail/tests/codegen_import_map_fallback_pin.rs`
(recon-undercount: an integration-test exhaustive `Term` match
not in the plan's site inventory; same additive-arm class,
mirrors the Mut defensive-recursion shape — Boss-call-2-class
"recon under-scoped it")
- New fixture: `examples/loop_sum_to.ail`
## Stats
bench/orchestrator-stats/2026-05-17-iter-loop-recur.1.json
@@ -1,185 +0,0 @@
# iter loop-recur.2 — Typecheck Semantics (Component 4)
**Date:** 2026-05-17
**Started from:** 5ac57fe8dec2d83ba135dd5ff6dd9caa1165c700
**Status:** DONE
**Tasks completed:** 6 of 6
## Summary
The iter-1 `synth` `CheckError::Internal` stub for `Term::Loop` /
`Term::Recur` is replaced with real typecheck semantics: binder
typing (each init synthed in outer scope + already-declared binder
names of THIS loop; loop's static type = body type), positional
`recur` arity + per-arg type checking via a new `loop_stack: &mut
Vec<Vec<Type>>` frame threaded exactly as mut.2's `mut_scope_stack`,
the four `Recur*` `CheckError` variants (bracket-`[code]`-free
Display per F2) + `code()` + two `ctx()` arms, and a NEW private
`verify_loop_body` tail-position pass (a *sibling* of the
spec-frozen `verify_tail_positions`, run after `synth` in `check_fn`
so synth-raised codes take precedence by pass ordering). Four
negative `.ail.json` fixtures fire their codes point-exactly
(`assert_eq!`, non-vacuous); the iter-1 `loop_sum_to.ail`
round-trip fixture now ALSO typechecks clean; an infinite `loop`
typechecks (no termination claim). NO codegen this iter (the iter-1
`lower_term` `CodegenError::Internal` stub stays — iter 3); NO
`Diverge`/`verify_structural_recursion`/guardedness (spec deliberate
boundary). `verify_tail_positions` is byte-frozen (0 deletions).
`cargo test --workspace` 616 green / 0 red (iter-1 baseline 608).
## Per-task notes
- iter loop-recur.2.1: the four `Recur*` `CheckError` variants
(`RecurOutsideLoop`, `RecurArityMismatch{expected,got}`,
`RecurTypeMismatch{position,expected,got}`,
`RecurNotInTailPosition`) + 4 `code()` arms + 2 `ctx()` arms
(Arity/Type only; OutsideLoop/NotInTailPosition use the `{}`
catch-all). RED-first via `recur_checkerror_codes_are_exact`
(observed 4× E0599); GREEN exact. Bracket-`[code]`-free Display.
- iter loop-recur.2.2: `synth` signature gains
`loop_stack: &mut Vec<Vec<Type>>` after `mut_scope_stack`;
threaded at every recursive call site (17 single-line canonical +
3 multi-line `Term::Mut`-init/body & `Term::Assign`-value + 2
letrec-capture `&mut body_effects`-shape) + `check_const` (fresh
empty stack) + cross-module callers + test-module callers. The
compile-driven sweep (`cargo build`/`cargo test --no-run`) was the
exhaustive caller oracle as the plan specifies; iterated to 0
errors. Stub intact, lib suite green.
- iter loop-recur.2.3: iter-1 `Internal` stub replaced with the two
real `Term::Loop`/`Term::Recur` arms. Binder-name save/restore
mirrors `Term::Let` (`lib.rs:3205-3218`) verbatim; per-arg type
check mirrors `Term::Assign` (`lib.rs:3716-3730`) verbatim per
Boss call 1; `recur`'s own type = `Subst::fresh(counter)`.
RED `loop_sum_to_typechecks_clean` observed `["internal"]`;
GREEN zero diagnostics.
- iter loop-recur.2.4: new private `verify_loop_body(t, in_loop_tail)`
immediately after `verify_tail_positions`'s closing brace —
exhaustive no-`_` match over all 16 `Term` variants (verified
equal to `verify_tail_positions`' variant set). Invoked from
`check_fn` after `verify_tail_positions(&f.body, true)?;`. RED
`recur_not_in_tail_position` observed `left: []`; GREEN
`["recur-not-in-tail-position"]`, positive still clean.
- iter loop-recur.2.5: 3 remaining negative fixtures
(`test_recur_outside_loop` / `_arity_mismatch` / `_type_mismatch`)
+ 3 pins (all 5 green) + `carve_out_inventory` 13→17 with header
refresh (`Twelve…mut.2``Seventeen…loop-recur.2`, pre-existing
Twelve-vs-13 mut.4-tidy drift corrected in passing) + ct1 F2
sibling `check_human_mode_renders_recur_diagnostic_code_exactly_once`.
- iter loop-recur.2.6: `examples/loop_forever.ail` (loop whose only
path is `recur`, no exit) typechecks clean — asserts the spec
"no termination claim is made or enforced". Form-A grammar
verified verbatim against the shipped `loop_sum_to.ail`. Full
suite 616/0; tail-app non-regression green (9 `tail`-named tests
ok incl. the iter-1 verify_tail_positions guards).
## Boss design calls (mirrored from the plan header at iter close)
1. **`RecurTypeMismatch` is an Assign-style structural pre-check,
NOT a `unify`-propagate.** `Term::Recur` per-arg checking mirrors
the `Term::Assign` arm (`lib.rs:3716-3730`) verbatim in
mechanism: synth the arg, `subst.apply` both the arg type and the
binder type, structural `!=`, on mismatch
`Err(RecurTypeMismatch{position,expected,got})`. A
`unify`-propagate would surface the generic `type-mismatch` code
and fail the spec acceptance ("dedicated `recur-type-mismatch`
fires point-exactly"). Rationale semantic (one in-repo mechanism
for "declared-vs-actual at a binding site with its own
point-exact code", consistent with `AssignTypeMismatch`), not
effort. Implemented verbatim; the `test_recur_type_mismatch`
fixture fires exactly `["recur-type-mismatch"]`.
2. **`loop_stack` element type is `Vec<Type>` (ordered binder
types), NOT `IndexMap<String,Type>`.** `recur` rebinds binders
*positionally*, so the frame models the ordered binder types for
arity (`.len()`) + per-position type checks. Binder *names* enter
the ordinary `locals: &mut IndexMap<String,Type>` exactly as
`Term::Let` binds its name (save/restore in reverse on loop
exit). The spec phrase "threaded exactly as mut.2's
`mut_scope_stack`" governs the *threading discipline*
(a `&mut Vec<…>` pushed/popped around the body, passed through
every recursive `synth`), not the element type. Semantic, not
effort. Implemented verbatim.
3. **Diagnostic-code precedence is by pass ordering, no explicit
logic.** `RecurOutsideLoop`/`RecurArityMismatch`/`RecurTypeMismatch`
are raised in `synth` (runs at the `check_fn` synth call);
`RecurNotInTailPosition` is raised in `verify_loop_body`, invoked
*after* `verify_tail_positions(&f.body, true)?;`. A `recur`
outside any loop therefore fires `RecurOutsideLoop` (synth,
first), not `RecurNotInTailPosition`. No precedence logic added;
the pass sequence IS the mechanism. `verify_loop_body` is entered
only on synth success, so every `recur` reached there is already
inside a loop with matching arity/types — the pass adds only the
tail rule. `verify_tail_positions` is byte-frozen (0 deletions);
`verify_loop_body` is a new sibling, never a repurpose.
## Concerns
- DONE_WITH_CONCERNS (iter loop-recur.2.2): **plan ordering defect —
`check_fn` threading must be in Task 2, not Task 4.** The plan's
Task 2 Step 3 bullet says "check_fn :1970: handled in Task 4 Step
4 (it declares its own fresh loop_stack)", but Task 2 Step 4's
gate is `cargo build -p ailang-check` reaching `Finished` with 0
errors. That gate is *unsatisfiable* while `check_fn` remains an
unthreaded `synth` caller (hard `error[E0061]`). Resolution:
`check_fn`'s `loop_stack` declaration + synth-call threading
(verbatim the plan's Task 4 Step 3 block + Step 4 first half) were
pulled into Task 2 where the compile gate forces them; only the
`verify_loop_body` *invocation* (Task 4 Step 4 second half)
remained for Task 4. This is the unique resolution satisfying both
Task 2 Step 4 AND preserving all plan code verbatim — the "fix to
compile while preserving evident intent + Boss calls" repair the
carrier explicitly authorises. Correctness: the declaration code
is byte-identical to the plan's Task 4 Step 3 block; no semantic
change, only sequencing. Recurring planner-gap class (a
compile-gate task whose gate depends on a caller the plan defers
past the gate) — candidate for a planner Step-5 self-review check.
- DONE_WITH_CONCERNS (iter loop-recur.2.2): **recon under-counted
cross-module `synth` callers** — `builtins.rs` ×2 (test helpers),
`lift.rs:746` (letrec-capture re-entry), `mono.rs:720` &
`mono.rs:1361` (mono re-synth) were not in the plan's site
inventory (`:3161…:3717` + the 8 test sites). Exactly the
mut.2-class recon-undercount the iter-1 journal flagged
(Boss-call-2-class "recon under-scoped it"). Resolved by mirroring
the established mut.2 fresh-stack pattern at each (declare a fresh
empty `loop_stack` alongside the existing fresh `mut_scope_stack`,
pass `&mut loop_stack` immediately after `&mut mut_scope_stack`).
Observation, not a correctness risk — the compile-driven sweep IS
the plan's designated exhaustive oracle and surfaced every site;
the plan's literal site list is advisory, the compiler is
authoritative (as the plan itself states).
## Known debt
- No additional `ail run`/binary-stdout E2E fixture written (Phase
3). iter-2 ships typecheck semantics only; the `lower_term`
`CodegenError::Internal` stub stays by design (real loop lowering
+ the positive sum_to-runs-to-a-value / deep-`n` E2E is explicitly
iter-3 scope). An `ail run` this iter would only exercise the
deliberately-transient codegen stub (a negative-value test of a
known-temporary error) — same justified Phase-3 reasoning as
iter-1. The iter-2 invariants worth protecting (four codes
point-exact, positive + infinite typecheck clean,
carve-out/tail-app non-regression) are fully pinned by the tests
added in Tasks 1/3/4/5/6 + the ct1 F2 sibling.
## Files touched
- Check core: `crates/ailang-check/src/lib.rs` (4 variants +
code/ctx, `synth` signature + threading, real Loop/Recur arms,
new `verify_loop_body`, `check_fn` + `check_const` wiring),
`crates/ailang-check/src/lift.rs`, `crates/ailang-check/src/mono.rs`,
`crates/ailang-check/src/builtins.rs` (cross-module synth-caller
threading)
- Tests/fixtures: `crates/ailang-check/tests/loop_recur_typecheck_pin.rs`
(new — 7 tests), `crates/ail/tests/ct1_check_cli.rs` (F2 sibling),
`crates/ailang-core/tests/carve_out_inventory.rs` (13→17 + header)
- New fixtures: `examples/test_recur_outside_loop.ail.json`,
`examples/test_recur_arity_mismatch.ail.json`,
`examples/test_recur_type_mismatch.ail.json`,
`examples/test_recur_not_in_tail_position.ail.json`,
`examples/loop_forever.ail`
- Reused (no new file): `examples/loop_sum_to.ail` (iter-1
round-trip fixture, now also the positive `ail check` evidence)
## Stats
bench/orchestrator-stats/2026-05-17-iter-loop-recur.2.json
@@ -1,188 +0,0 @@
# iter loop-recur.3 — Codegen (Component 5) + run-to-value E2E
**Date:** 2026-05-17
**Started from:** eae73bf320440f6b031715e72c1f20b0ac86e024
**Status:** DONE
**Tasks completed:** 3 of 3
## Summary
The **terminal iteration** of the standalone-`loop`/`recur` milestone:
the iter-1 `lower_term` `CodegenError::Internal` stub for
`Term::Loop`/`Term::Recur` is replaced with real LLVM-IR lowering, so
a `sum_to`-class loop program builds and runs to the correct printed
value, a deep-`n` variant is safe by construction, and an infinite
loop compiles. Loop binders are loop-carried values lowered as
entry-block allocas (the mut.3 `pending_entry_allocas` mechanism)
registered in the existing `mut_var_allocas` map, so the existing
`Term::Var` load path resolves them with zero new Var code
(representation-sharing); a fresh `loop.header.<id>` block is reached
by an unconditional `br` from the pre-header, `recur` lowers each arg
to SSA *before* any store (simultaneous positional rebind), stores
into the binder allocas, back-edges `br` to the header, and sets the
single existing `block_terminated` field at its OWN new emit site so
the loop's exit value is the emergent product of the existing
`if`/`match` join (no separate loop-result phi/exit-block). A new
`loop_frames` codegen stack lets `recur` find its target; it is
saved/reset/restored at the single lambda-lowering boundary exactly
as mut.3's `mut_var_allocas` triple. `clang -O2` mem2reg promotes
the binder allocas to the phi nodes the spec's implementation-shape
describes. This is a **codegen-only iteration**: no schema/AST/serde
change, no typecheck change; hash pins (`loop_recur` + iter13a) and
the drift trio + `carve_out_inventory` stay green untouched
(confirmed, not modified — empirical proof none scans the codegen
region). `cargo test --workspace` 616 → 619 / 0 red (the 3 new e2e
tests). After this iter the milestone is structurally CLOSED;
milestone-close (audit/fieldtest) is the Boss's post-iter call.
## Per-task notes
- iter loop-recur.3.1: positive `sum_to` run-to-value E2E (RED →
GREEN). RED observed verbatim: `ail build` on `loop_sum_to_run.ail`
fails with the iter-1 stub `internal: Term::Loop/Term::Recur
lowering lands in loop-recur iter 3` (parse + iter-1/iter-2
typecheck pass; only codegen stubbed — confirmed via direct `ail
build`). GREEN: `loop_frames` field + init, lambda-boundary
save/reset/restore, the two real `Term::Loop`/`Term::Recur` arms
replacing the single stub arm. `loop_sum_to_run.ail``55`;
`cargo build -p ailang-codegen` Finished; full e2e suite 88/0.
Plan's literal arms compiled as-written — no plan-vs-reality
substitution needed (pre-grounding confirmed `mut_var_allocas:
(String, Type)` and `lower_term -> (String /*ssa*/, String
/*llvm_ty*/)` matched the plan's assumptions exactly).
- iter loop-recur.3.2: deep-`n` safe-by-construction +
infinite-loop-compiles (test+fixture only; the codegen mechanism
shipped in 3.1, so no separate RED). `loop_sum_to_deep.ail` runs
1e6 iterations via the back-edge → `500000500000` with no stack
growth (the spec clause-2 correctness claim made executable —
mem2reg promoted the binder allocas so the loop is an iterative
back-edge, not a stack-growing recursion). `loop_forever_build.ail`
(`spin`'s loop has no non-`recur` exit) compiles `ail build` exit
0 — `recur` sets `block_terminated`, propagating through
`emit_fn`'s `if !self.block_terminated` fall-through guard so
`spin` emits no `ret` and is a well-formed never-returning fn; the
binary is never executed (spec "typechecks AND compiles, no
termination claim"). Full e2e suite 90/0.
- iter loop-recur.3.3: regression gates — pure verification, ZERO
source changes (confirmed `git diff --name-only HEAD` = only the
3 T1/T2 source files). tail-app/tail-do/`verify_tail_positions`
non-regression green (the parallel `block_terminated` SET site
touched no existing SET/READ site — Boss call 4); `loop_recur` +
iter13a hash pins + drift trio + `carve_out_inventory` green
untouched (codegen-only iter, no schema change); full workspace
619 passed = exactly the loop-recur.2 baseline (616) + the 3 new
e2e tests, zero FAILED/error; `round_trip` green (the 3 new `.ail`
fixtures auto-discovered, parse→print→parse idempotent — Roundtrip
Invariant holds for runnable loop/recur programs).
## Boss design calls (mirrored from the plan header at iter close)
All four were settled in the plan header on
architectural-consistency / spec-pinned-invariant grounds (NOT
effort); each verified against actual read source before
implementation; none reopened, re-derived, or "improved" in any
phase:
1. **Loop binders → entry-block alloca + `mut_var_allocas` reuse,
NOT hand-emitted `phi`.** The spec's "phi" wording lives only in
the explicitly-*secondary* implementation-shape subsection. The
established in-repo loop-carried-value mechanism is
alloca-in-entry-block (mut.3 `pending_entry_allocas`) + `clang
-O2` mem2reg, which produces the identical optimized binary. The
linear-emit codegen architecture structurally cannot hand-emit a
header `phi` whose back-edge predecessors are discovered *during*
body lowering without a string-splice hack with no precedent (the
`if` arm sidesteps this by opening its join block *last* — a loop
header cannot be opened last). Rationale = architectural
consistency + absence of a linear-emit `phi` precedent + identical
-O2 output. Implemented verbatim; `loop_sum_to_run.ail`→55 and
`loop_sum_to_deep.ail`→500000500000 are the operational proof
mem2reg delivers the loop-carried-SSA intent.
2. **Binders ride the existing `mut_var_allocas` map + the existing
`Term::Var` load path (representation-sharing).** A loop binder's
codegen representation — a named alloca slot, loaded on each
`Term::Var` use, stored on `recur` — is structurally identical to
a mut-var's. The `Term::Loop` arm inserts each binder into
`mut_var_allocas` (value type `(String, Type)`, identical to the
`Term::Mut` precedent); the existing `lib.rs` `Term::Var` arm
resolves them via its pre-existing `mut_var_allocas.get(name)`
`load` path **byte-unchanged** (zero edits to Var-resolution; no
parallel map). Verified against real source before implementing.
mut.3's shipped+E2E-green mut-var read path is the proof the load
works for loop binders.
3. **The loop's exit value is the emergent product of the existing
`if`/`match` join once `recur` sets `block_terminated` — no
separate loop-result phi/exit-block.** The body is typically
`(if c <exit> (recur …))`; the existing
one-branch-terminated logic at `lib.rs:1606-1613` already drops
the terminated (recur) branch and returns the non-recur branch's
`(ssa,ty)` directly — *iff* `recur` sets `block_terminated`. The
`Term::Loop` arm therefore returns `lower_term(body)`'s `(ssa,ty)`
directly; no separate loop-result phi was built. An infinite loop
(body = bare `recur`) compiles because `recur` sets
`block_terminated`, propagating through the `emit_fn`
`if !self.block_terminated` fall-through guard exactly as a
tail-app-terminated function.
4. **Reuse the single `block_terminated` field; `recur` sets it at
its OWN new emit site (a parallel SET call-site), zero edits to
any existing SET or READ site.** A back-edge `br` IS a block
terminator with exactly `block_terminated`'s semantics. `recur`'s
own `self.block_terminated = true;` is a NEW call-site parallel
to (not replacing) tail-app's SET at `lib.rs:2279`. The diff
confirms tail-app's SET and every READ site
(`:1124/1581/1594/1600-1613/...`) are byte-identical; the
tail-app/`verify_tail_positions` non-regression gate (T3 Step 1)
is the operational proof.
## Concerns
- DONE_WITH_CONCERNS (iter loop-recur.3.3): the plan's literal Task-3
Step-1 command `cargo test --workspace tail` filters on the
test-name substring `tail`, which matches **no** test in the
workspace (the real tail-app guards are
`ailang-check::lib::tail_call_in_{non_,}tail_position_is_*`,
`ailang-prose::lib` `*_tail_*`, e2e
`iter14e_print_list_recursion_emits_musttail`). Ran the gate via
the real test names instead (2 + 3 + 1, all green); the
authoritative non-regression gate is Step 3's full-workspace
619/0 which subsumes them. Recurring `feedback_plan_pseudo_vs_reality`
class (a literal verification command whose filter doesn't resolve
to its evident target); resolved by preserving the gate's evident
intent (tail-app/`verify_tail_positions` byte-unchanged proven),
no behaviour change. Observation, not a correctness risk — the
full-suite count `619 = 616 + 3` is independent confirmation
nothing regressed.
## Known debt
- None. The iteration's invariants (loop/recur lowers to real IR;
run-to-value 55; deep-`n` 500000500000 safe-by-construction;
infinite loop compiles; tail-app + hash pins + drift trio
byte-unchanged; round-trip on the 3 new fixtures) are fully pinned
by the 3 new e2e tests + the unchanged regression suite.
## Files touched
- Codegen: `crates/ailang-codegen/src/lib.rs` (`loop_frames` field +
init; the two real `Term::Loop`/`Term::Recur` arms replacing the
iter-1 single stub arm), `crates/ailang-codegen/src/lambda.rs`
(lambda-boundary `loop_frames` save/reset + restore, alongside the
mut.3 triple)
- Tests: `crates/ail/tests/e2e.rs` (3 new test fns: 2 build+run, 1
build-only)
- New fixtures: `examples/loop_sum_to_run.ail`,
`examples/loop_sum_to_deep.ail`, `examples/loop_forever_build.ail`
- No edit / byte-frozen (confirmed, not modified): every existing
`block_terminated` SET/READ site + tail-app/tail-do lowering +
`verify_tail_positions`; the iter-2 typecheck (`synth`
Loop/Recur arms, `verify_loop_body`, `loop_stack`, the four
`Recur*` variants); the iter-1 `synth_with_extras`
Loop→body-type / Recur→Unit pass-through; `hash_pin.rs`
(`loop_recur` + iter13a pins) + the drift trio +
`carve_out_inventory.rs` (codegen-only iter; the 3 new `.ail`
fixtures are round-trip-auto-covered, NOT carve-outs — inventory
stays 8)
## Stats
bench/orchestrator-stats/2026-05-17-iter-loop-recur.3.json
@@ -1,133 +0,0 @@
# iter loop-recur.tidy — lambda capturing a loop binder must fail `ail check`, not panic codegen
**Date:** 2026-05-18
**Started from:** 39380d361d530a95a4f1c718c0e201f8312d3ac4
**Status:** DONE
**Tasks completed:** 1 of 1
## Summary
A `Term::Lam` whose body captured an enclosing `Term::Loop` binder
passed `ail check` (exit 0) and then panicked `unreachable!()` at
`crates/ailang-codegen/src/lambda.rs:102` — type-correct input
crashing the compiler. This iter adds the symmetric escape guard:
such a capture now fails `ail check` with the stable kebab code
`loop-binder-captured-by-lambda`, exactly as mut.4-tidy did for
`mut-var-captured-by-lambda`. The fix is the minimal mechanism that
reuses the scope structure the `Term::Loop` arm already maintains.
## Per-task notes
- iter loop-recur.tidy.1: GREEN side of the debug RED test
`lambda_capturing_loop_binder_emits_loop_binder_captured_by_lambda`.
Added `CheckError::LoopBinderCapturedByLambda { name }`
(bracket-free F2 Display), its `code()` arm
(`loop-binder-captured-by-lambda`), and its `ctx()` arm
(`{name}`, mirroring `MutVarCapturedByLambda`). Extended
`loop_stack`'s element type from `Vec<Type>` to
`Vec<(String, Type)>` so the existing per-loop scope frame also
carries binder names (the `Term::Recur` arm reads `.1` for the
positional type check; the `Term::Lam` escape guard reads `.0`).
The `Term::Lam` arm's existing free-var scan (shared
`free_vars_in_term` walker) gained a parallel pass over
`loop_stack` next to the `mut_scope_stack` pass; its gate widened
to `!mut_scope_stack.is_empty() || !loop_stack.is_empty()`.
Mirrored the two mut.4-tidy in-source unit tests
(kebab-code test + synth-level capturing test). Lockstep:
`carve_out_inventory.rs` 17→18 (header count word + new bullet +
EXPECTED entry), `ct1_check_cli.rs` mut-F2 sibling `cases` gained
the new fixture (where `mut-var-captured-by-lambda` is asserted —
the exact precedent). `docs/DESIGN.md` one-line note adjacent to
the `MutVarCapturedByLambda` mention.
## Root cause
The `Term::Lam` escape guard iterated only `mut_scope_stack` to
reject lambda captures of alloca-resident locals. Loop binders are
inserted into the ordinary `locals` by the `Term::Loop` arm and
never entered any escape-checked scope, so a lambda body referencing
a loop binder was accepted by `ail check`. Codegen's lambda capture
resolver searches only `self.locals` after `mut_var_allocas` is
`std::mem::take`-emptied at the lambda frame boundary, so the
loop-binder reference resolved to nothing and hit `unreachable!()`.
Loop binders are alloca-resident under the same scheme as mut-vars
and obey the same no-escape-into-lambda rule.
## Design note — why extend `loop_stack` rather than add a parallel stack
The carrier offered "reuse whatever scope structure the `Term::Loop`
arm already maintains" as an explicit minimal option. `loop_stack`
is already threaded through every `synth` recursive call and is
pushed/popped at exactly the loop scope boundary. Extending its
frame element from `Type` to `(String, Type)` preserves all existing
push/pop discipline and `Term::Recur` positional-type semantics
unchanged — it is not a redesign. The alternative (a parallel
`mut_scope_stack`-shaped parameter) would have touched all 35
recursive `synth` call sites plus declarations, strictly larger.
Element-type extension is the minimal correct mechanism.
## Concerns
(none)
## Known debt
(none)
## Files touched
- `crates/ailang-check/src/lib.rs` — variant, code/ctx/Display, the
`Term::Loop` push + `Term::Recur` read + `Term::Lam` escape guard,
two in-source unit tests
- `crates/ailang-check/src/builtins.rs``loop_stack` decl type
- `crates/ailang-check/src/lift.rs``loop_stack` decl type
- `crates/ailang-check/src/mono.rs``loop_stack` decl type (x2)
- `crates/ailang-core/tests/carve_out_inventory.rs` — 17→18 lockstep
- `crates/ail/tests/ct1_check_cli.rs` — mut-F2 sibling `cases`
- `docs/DESIGN.md` — one-line symmetric-rule note
## Milestone-close audit resolution (Boss-side)
The loop/recur milestone-close `audit` (architect drift review +
bench gate) produced four items; this tidy commit resolves all:
- **`[high]` lambda-captures-loop-binder codegen crash** — fixed by
this iter's GREEN (the symmetric `loop-binder-captured-by-lambda`
guard). RED test committed separately as the audit trail
(`39380d3`); GREEN + the items below fold into this commit.
- **`[medium]` `mut_var_allocas` rustdoc lied about dual use** —
`crates/ailang-codegen/src/lib.rs` rustdoc said "Populated when
entering a `Term::Mut` block" / "matching the typecheck-side
mut-scope-stack precedence"; as of iter-3 loop binders ride the
same map. Boss-edited to present-tense dual-use truth (mut-vars
AND loop binders; the shared map is a representation detail, not
a scope-precedence claim). Doc-honesty class.
- **`[medium]` `Term::Loop` doc-comment stale "per-binder phi"** —
`crates/ailang-core/src/ast.rs` still described iter-1's
forward-looking `synth`/`lower_term` `Internal` stubs + "per-binder
phi"; iters 2/3 shipped real typecheck + alloca+mem2reg lowering
(Boss-call-1 explicitly rejected hand-emitted phi). Boss-edited to
the real shipped state. Doc-honesty class.
- **`[low]` recon-undercount 3× pattern** — added a P2 `[todo]` to
`docs/roadmap.md` (`ailang-plan-recon` cross-crate-caller-undercount
countermeasure; pairs with the planner Step-5 items 7+8 added this
milestone — those scrub the plan, this scrubs the recon). Not a
milestone-blocking fix; tracked forward.
- **Bench gate (audit Step 2)** — `bench/check.py` /
`compile_check.py` / `cross_lang.py` all exit 0; 25 metrics, 0
regressed, 25 stable. **Pristine** — exactly as the spec's
acceptance criteria expect for a strictly-additive surface no
hot-path bench exercises; the pre-existing P2 `*.bump_s`
hardware-staleness drift did not trip this run. **carry-on**: no
baseline update, no ratify entry (Iron Law — nothing intentionally
moved a metric).
**Milestone-loop-recur tidy: resolved.** Architect drift cleared
(the `[high]` is fixed code, the two `[medium]` are doc-honest, the
`[low]` is roadmap-tracked); bench pristine carry-on. The loop/recur
milestone is audit-clean; the only remaining pipeline step before
full close is `fieldtest` (user-visible Form-A surface).
## Stats
bench/orchestrator-stats/2026-05-17-iter-loop-recur.tidy.json
@@ -1,133 +0,0 @@
# audit docs-honesty-lint — milestone close (CLEAN, carry-on)
**Date:** 2026-05-18
**Scope:** `928e1c0..e50b400` (spec `aff25cd`, plan `de66eb7`, iter
`7580d43`, INDEX `e50b400`)
**Status:** CLEAN — carry-on, no fix iteration, NO baseline ratify
**Milestone:** docs-honesty-lint (single iteration) — **CLOSED**
## Outcome
| Gate | Result |
|------|--------|
| Architect drift | `clean` (one `[medium]` standing advisory wart, see below) |
| `bench/check.py` | EXIT 1 — **causally exonerated**, do NOT ratify |
| `bench/compile_check.py` | EXIT 0 (24/24 stable) |
| `bench/cross_lang.py` | EXIT 0 (25/25 stable) |
| Recommendation | **carry-on** |
## Step 1 — Architect drift review
Status `clean`. Verified:
- **Scope claim exact.** `git diff 928e1c0..e50b400 --stat --
'crates/**/src/**' 'runtime/'` empty; sole `crates/` change is the
additive `crates/ailang-core/tests/docs_honesty_pin.rs` (RED-first,
now GREEN 4/4). The two lockstep-invariant pairs (`Pattern::Lit`/
`pre_desugar_validation`, `lower_app`/`is_static_callee`) untouched.
- **DESIGN.md edits are themselves honest.** All 14 corrections
genuinely present-tense; no Wunschdenken/post-mortem reintroduced.
All four protected-exception KEEPs survive un-stripped: `Diverge`
reserved (L2730), `Regions … considered and rejected` at the
Decision site (L1530), `a tiebreaker, not a rationale` (L1240),
`No deriving` (L1850). The `### What this document is — and the
honesty rule it holds itself to` meta-subsection is itself
present-tense (no "we will / planned").
- **Agent-def lockstep complete.** `skills/audit/agents/ailang-architect.md`
"four"→"five sweeps" purge verified (`grep "four sweeps…"` empty);
new "DESIGN.md honesty drift" bullet present and citing the
meta-subsection.
- **Sweep residue is exactly the 3 documented deliberate KEEPs.**
`bash bench/architect_sweeps.sh` EXIT 1 with: Sweep-1 L50
(`docs/journal-archive.md … pre-2026-05-11 history` — present-tense
doc-roles pointer, incidental date) + L2060 (the true
`(iter str-concat, 2026-05-13)` provenance tag — asserts nothing
false, consistent with the sibling `(iter 24.1)` tags); Sweep-5 L62
(the discriminator subsection's OWN forbidden-phrasing catalogue —
a self-referential false-positive, it IS the rule definition). Each
correctly adjudicated KEEP per the new bullet's discriminator.
**`[medium]` standing advisory wart (architect-flagged, orchestrator-
adjudicated → filed, not fixed now).** Sweep 5's regex matches the
discriminator subsection's own example catalogue (DESIGN.md L62)
permanently: every future `architect_sweeps.sh` run inherits a
guaranteed self-referential EXIT 1 that must be re-adjudicated by
hand. This does not introduce a *new* failure mode — the script has
never been EXIT 0 in practice (Sweeps 1-4 already retain legitimate
date-anchors; the script header states non-zero is advisory,
architect-adjudicated by design). It is a minor signal-erosion, not
current drift. Filed as a P3 roadmap todo (optional Sweep-5
self-anchor exclusion); explicitly NOT a fix iteration — consistent
with "trivial drift recorded, not ignored, but priced honestly at
P3".
## Step 2 — Bench-regression check
`check.py` EXIT 1, three firings: `throughput.bench_list_sum.bump_s`
+13.42% (tol 10%); `latency.implicit_at_rc.p99_9_us` +37.65% (tol
25%); `latency.implicit_at_rc.max_us` +134.70% (tol 30%). 58 stable,
2 improvements. `compile_check.py` EXIT 0; `cross_lang.py` EXIT 0 —
and `cross_lang.py` independently measures the *same* `bench_list_sum`
workload at `ail_bump_s +5.95% ok`, `ail_rc_s +5.08% ok` (the fixture
`check.py` calls a +13.42% REGRESSION reads within tolerance under a
fresher anchor / wider band).
## Step 3 — Bencher causal exoneration (decisive)
Hypothesis: the firings were *caused by this milestone's diff*.
Refuting test: HEAD bench binaries byte-identical to a pre-milestone
commit ⇒ no causal mechanism. Result (verbatim):
- `cmp` of `bench_list_sum_{bump,gc,rc}` + `bench_latency_implicit_{rc,gc}`
at HEAD `e50b400` vs `5bb7211` (last milestone close) vs `de66eb7`
(immediate pre-iter): **0 differing bytes, all 5 binaries identical
across both baselines.** `ail` release binary sha256 identical at
all three commits (zero compiled-source delta — the sole `crates/`
change is the non-bench test file).
- Latency re-measure on the byte-identical binary (5×`--runs 5`):
median/p99 rock-steady (≤0.6% / 1.1% spread); p99.9 swings
503.8→651.4, max 565.6→692.1 — classic stable-center / volatile-tail
on identical machine code. = the tracked-P2 `-n 5` tail-jitter
false-positive, **4th occurrence on a zero-runtime-change
milestone**.
- `bench_list_sum_bump` (byte-identical) 20-run median 0.0531s,
persistent ~+15% (not transient) = the tracked-P2 `*.bump_s`
environmental-baseline-staleness item; the 0.046s anchor is stale
vs this machine's current steady state.
**Verdict: EXONERATED (environmental, do NOT ratify).** Per the audit
Iron Law ("NO BASELINE UPDATE WITHOUT A PAIRED JOURNAL RATIFY ENTRY";
ratifying noise is forbidden) and the spec's zero-codegen invariant, a
`--update-baseline` is foreclosed. Neither firing family is
attributable to docs-honesty-lint.
## Step 4 — Resolution: carry-on (clean)
No fix iteration. No ratify. Milestone **CLOSED**. No fieldtest: this
milestone changed zero authoring surface (documentation prose + one
non-bench test file + an agent-def + a bench shell sweep) — the
fieldtest trigger (surface-touching milestone) does not apply; the
wrap-robust `docs_honesty_pin.rs` (4 tests) + Sweep 5 ARE the
milestone's regression coverage.
Two pre-existing standing P2 items remain orchestrator-owned and
were explicitly NOT bundled into this close (the bencher's
recommendation, honoured): (a) the stale `*.bump_s` anchor — a
deliberate quiescent-machine baseline refresh, separately journaled
when pursued, never a ratification rider on an unrelated close; (b)
the recurring `-n 5` tail false-positive — the durable fix is a
structural harness change (higher default N or a tail-metric gating
policy), already a tracked roadmap todo, not a bench-output artefact.
## Milestone close summary
docs-honesty-lint is fully ratified and CLOSED: spec (grounding-check
PASS 10/10) + plan + single iter + this audit. The canonical docs
(`docs/DESIGN.md`, `docs/PROSE_ROUNDTRIP.md`) now mirror only the
current state, present-tense; the tense+modality discriminator is
codified in DESIGN.md and the architect drift-checks against it via
Sweep 5 + the new bullet; the wrap-robust enumerated pin is the hard
forward gate. The procedural-enumeration design proved itself: the
Task-4 Step-14 catch-all surfaced one genuine extra FIX site (the
Form-B prose-projection bullet) that the spec's illustrative regex
and a frozen orchestrator list would both have missed.
@@ -1,155 +0,0 @@
# audit embedding-abi-m1 — milestone close (CLEAN — carry-on, NO ratify)
**Date:** 2026-05-18
**Scope:** `064599e..e406d07`
**Verdict:** Milestone audit **CLEAN**. Architect `clean` (one
`[low]` evidence-adjudicated to carry-on); bench `check.py` exit 1
**decisively causally exonerated** (21/21 byte-identical bench
binaries vs pre-milestone), `compile_check.py` / `cross_lang.py`
exit 0. No fix iteration, **no baseline ratify**.
## Commit map
- `064599e` — pre-milestone HEAD (bench/architect diff baseline).
- `51160e9` spec, `b0bd7ef` plan — docs only.
- `123ae1b` — USER-authored roadmap-P2 ("DESIGN.md → `design/`
role-split"), `docs/roadmap.md` only, path-disjoint — explicitly
out of M1 audit scope (Boss roadmap maintenance, architect told
to skip it; it is not M1 code/doc work).
- `818177d` — iter partial (Tasks 13) + a Boss plan Repair
(Design-decision 6, the module≠stem fixture defect).
- `e406d07` — iter completion (Tasks 47) + INDEX line.
## Step 1 — Architect drift review
**Status: `clean`.** All four focus areas + standing concerns
verified against the `064599e..e406d07` diff:
- **Invariant 1 (clean core) holds semantically.** No
finance/`data-server` vocabulary, logic, or dependency entered
`ailang-core` / `ailang-codegen` / `runtime/`. `backtest_step`
is an opaque author-chosen string flowing through generic
`FnDef.export`; the only "backtest" occurrences in core/codegen
are test-fixture filenames + mangled-symbol assertions in test
files, not domain knowledge. AILang↔host meeting point correctly
deferred (no `ail-embed`, no runtime lifecycle API) — M1 scope
respected.
- **DESIGN.md honesty rule satisfied.** New §"Embedding ABI (M1)"
+ fn-JSON `"export"` line + `FnDef.export` rustdoc are
present-tense current state; "provisional until M3" / "M2 will
change this C signature" are correctly *labelled* reserved-
unimplemented, not stated as present fact, no
post-mortem/Wunschdenken. Sweep hits (DESIGN.md:50/:2060/:62)
all pre-existing, out of M1 range.
- **Lockstep intact.** `FnDef` ↔ DESIGN.md §Data-model fn-JSON ↔
`design_schema_drift.rs`/`spec_drift.rs` all carry the field.
The `@<sym>` forwarder is additive raw-IR emission *beside* the
unchanged `@ail_<module>_<fn>` mangling — does not route through
`lower_app`, so the `lower_app`/`is_static_callee` and
`Pattern::Lit` lockstep pairs are not engaged. Schema stays
exhaustively-constructed (~85 sites uniform `export: None`, no
`_ =>` / `..Default::default()` — plan Design-decision 1
honoured). In-source `missing_entry_main_is_error` pin (now
L3411) behaviourally byte-unchanged (only the mandatory field
added). `_adapter`/`_clos` closure pair untouched (spec "Out of
scope").
- **Design-decision 6 confirmed correct, not re-flagged.** The
gate/headline fixtures' `(module embed_*)` == file-stem; the
divergence from the spec's *illustrative* `(module backtest)` /
`(module bad)` blocks is expected under the loader's
module==stem invariant + spec Decision 2 (C symbol
`backtest_step` decoupled via `(export …)`). The fixtures
*demonstrate* the decoupling rather than violate the spec.
**Drift / debt — one item, adjudicated:**
- `[low]` ~85 inserted `export: None,` lines indented to the
struct-literal brace column rather than the field column
(`rustfmt`-divergent struct literals; compiles 611/0, semantics
unaffected). **Orchestrator adjudication → carry-on (NOT a fix,
NOT pending).** Evidence: `cargo fmt --all --check` reports
**1137 fmt-divergent locations tree-wide** (pre-existing
`builtins.rs` / `diagnostic.rs` / dozens of `lib.rs` sites
predating M1). rustfmt is demonstrably **not** a project
convention — the hand-formatted codebase tolerates 1137
divergences. The architect's premise ("a `cargo fmt` pass would
churn later") is therefore moot: no such gate exists or is
coming, and selectively rustfmt-aligning 85 of 1137 sites would
*impose* a standard the project does not hold, which is noise,
not tidy. This is an evidence-based adjudication of the
architect's finding (premise falsified), not "trivial, ignore
it". If AILang ever adopts a fmt gate that is a tree-wide
decision (1137 sites), unrelated to M1.
## Step 2 — Bench-regression check
| Script | Exit | Result |
|---|---|---|
| `bench/check.py` | **1** | 63 metrics; 7 regressed, 2 improved, 54 stable |
| `bench/compile_check.py` | 0 | 24 metrics; 0 regressed |
| `bench/cross_lang.py` | 0 | 25 metrics; 0 regressed |
**The 7 `check.py` firings:**
`throughput.bench_list_sum.bump_s` +12.27%,
`throughput.bench_hof_pipeline.bump_s` +12.67%,
`throughput.bench_list_sum_explicit.bump_s` +13.74%,
`latency.explicit_at_rc.p99_9_us` +36.13%,
`latency.explicit_at_rc.max_us` +101.43%,
`latency.implicit_at_rc.p99_9_us` +67.32%,
`latency.implicit_at_rc.max_us` +123.57%.
**Decisive causal exoneration (bencher localisation).** Hypothesis:
M1's `Target::Executable` default leaves the entire bench corpus's
emitted machine code byte-unchanged; the firings are pre-existing
environmental/sampling noise, not M1.
- **21/21 bench binaries `cmp`-byte-identical** between `064599e`
and `e406d07` (`bench_list_sum_bump` sha `dce18c288904c9f0` at
both commits; every one of the 6 fixtures × 3 modes + 3 latency
arms IDENTICAL, 0 DIFFER). `ail` self-hash differs (expected —
Rust source changed for the embedding ABI) but the *emitted
machine code* for every bench fixture is identical because every
fixture builds an executable via the unchanged `Target::Executable`
default. **Zero codegen/runtime causal mechanism exists.**
- The RC-latency tails reproduce *on the byte-identical oracle
binary itself*, swinging 3× run-to-run (`explicit_at_rc.max_us`
per-run 376→1200 us; `implicit_at_rc.max_us` 562→1742 us) while
median/p99 hold steady — the documented `-n 5` single-sample-
jitter signature.
- Fresh `check.py` re-run: the `*.bump_s` trio → ok, 4 of 7
original firings → ok, only `explicit_at_rc.max_us` still trips
(+26%, byte-oracle-proven environmental).
- Both firing families map (confirmed, not assumed) onto the two
pre-existing **roadmap-P2** tracked items: `*.bump_s` baseline-
vs-current-hardware staleness, and the RC-latency `*.max_us` /
`*.p99_9_us` `-n 5` structural false-positive (this is the ~4th
zero-runtime-change milestone close on which they fire and
collapse on identical-code re-run).
**Classification: carry-on, NO baseline ratify.** Per the audit
Iron Law a baseline update requires a paired JOURNAL ratify entry
naming an iter that *intentionally* moved the metric; M1 by
construction moved **zero** default-path bytes (21/21 binaries
identical), so a ratify would bake noise into `baseline.json`
forbidden. The spec forecloses a zero-codegen baseline bump. The
residual flapping is pre-existing, already roadmap-P2-tracked; the
genuinely next-best action (re-capture `baseline.json` on current
hardware at higher N, or widen single-sample tail tolerances) is a
separate bench-harness iteration, **not** an M1 concern — the two
P2 items remain orchestrator-owned and are NOT bundled into this
close (bencher recommendation honoured, consistent with the
remove-mut-var-assign / docs-honesty-lint closes).
## Step 3/4 — Resolution
**carry-on (clean).** No fix iteration, no ratify, no commit beyond
this journal + its INDEX line. Architect clean; the sole `[low]` is
evidence-adjudicated non-actionable; bench firings decisively
non-causal (21/21 byte-identical binaries) and mapped to existing
P2 noise. Both bench gates that can pass on the merits do
(`compile_check`/`cross_lang` exit 0; `check.py` exit 1 causally
exonerated).
**M1 milestone audit CLEAN.** Next pipeline step: `fieldtest`
M1 added user-visible authoring surface (`(export "<sym>")`), so
the surface-touch fieldtest gate applies before milestone close.
@@ -1,106 +0,0 @@
# Audit — Embedding ABI — M2 (milestone close)
**Date:** 2026-05-18
**Scope:** `2db737c..fbeeade` (M2 = spec `1c58055` → plan `b3388c8`
iter `c9a84b3` PARTIAL 4/9 + Boss spec-defect repair → iter `fbeeade`
DONE 59). HEAD `fbeeade`, working tree clean at audit time.
**Outcome:** DRIFT (one `[medium]` + coupled `[low]` doc-honesty
item → fix via tidy iteration). Bench `check.py` exit 1 — **all
firings causally exonerated as the two tracked roadmap-P2 noise
families; NO baseline ratified, NOT an M2 regression.**
## Step 1 — Architect drift review
Status: `drift_found`. No DESIGN.md/spec-contract drift, no
unrequested extras, Invariant 1 intact, internal-convention
byte-invariance held, Decision-10 atomicity note present-tense-
correct (sanitiser-verified this iter), spec→DESIGN.md→code lockstep
complete, `docs_honesty_pin.rs` green unmodified, the Boss
spec-defect repair correctly reflected in both spec and shipped
tests.
Actionable drift (one item, two coupled sites):
- **`[medium]` `docs/DESIGN.md:2266`** — the section header reads
`## Embedding ABI (M1)` while the body (`:22852295`) now
describes the post-M2 ctx ABI as present-tense current state
("Every exported entrypoint takes a mandatory leading
`ailang_ctx_t*` (M2)… the ctx-threaded C signature is the M2
shape"). Current-state-mirror honesty mismatch: a header that
misdescribes its own present-tense body. Not a Sweep-5 catch (the
body prose itself is correctly present-tense — the
docs-honesty-lint machinery does not auto-flag a header/body
desync; only a read catches it). The iter journal's Concerns +
Known-debt flagged it accurately and truthfully — Task 7's edit
regions were *deliberately* scope-limited (provisional-until-M3
sentence + Decision 10) to preserve the `docs_honesty_pin.rs:135`
verbatim "Export parameters are written **bare**…" assertion 12
lines below the header. Deferral reasoning sound; the mismatch is
real drift that must not persist past this milestone close.
- **`[low]` `docs/DESIGN.md:2354`** — schema-block cross-reference
comment still reads `see §"Embedding ABI (M1)"`; stale xref
coupled to the header above. Must move in lockstep with any
header rename so the fix is scoped header + xref +
`docs_honesty_pin.rs:134136` `(M1)`-token audit together, not
the header alone.
Architect recommendation: one scoped tidy iteration.
## Step 2 — Bench regression
| Script | Exit |
|---|---|
| `bench/check.py` | **1** |
| `bench/compile_check.py` | 0 (clean — 24/24 stable) |
| `bench/cross_lang.py` | 0 (clean — 25/25 stable, **every `rc_over_c` ratio within tol**) |
`check.py` 63 metrics: 3 regressed, 2 improved-beyond-tol, 58 stable.
The 3 firings (verbatim): `throughput.bench_list_sum.bump_s`
0.046→0.052 +13.57% (tol 10%); `latency.explicit_at_rc.max_us`
413.0→751.5 +81.96% (tol 25%); `latency.implicit_at_rc.max_us`
477.3→661.4 +38.57% (tol 30%). Every `*.rc_s`/`*.gc_s` throughput and
every rc-latency median/p99/p99.9 stable within tol.
Because M2 genuinely changed the benched runtime path
(`runtime/rc.c`: the two RC-counter sites gained a `__thread` load +
null-branch ahead of `g_rc_*++`), the byte-identical exoneration
method did **not** apply — `ailang-bencher` was dispatched for
hypothesis-driven localisation (H0 = real M2 regression; H1 =
tracked-P2 noise, M2 bench-neutral).
**Bencher verdict: H0 refuted; all three firings
tracked-noise-causally-exonerated.** Method: swapped *only* `rc.c`
between `2db737c` and `fbeeade` (same `ail` binary, same fixtures),
disassembled the delta, interleaved core-pinned A/B.
- `bump_s`: causally **impossible** from M2 — `bump_malloc`
disassembly has 0 refs to `__ail_tls_ctx`/rc counters; the
M2-inert bump binary independently reproduces ~+12% vs the stale
0.046 baseline. Roadmap-P2 `*.bump_s` environmental staleness.
- M2's rc.c hunk compiled **branchless** (3 insns: `mov %fs`,
`test`, `cmovne`) and measured **free** (HEAD median 0.63% vs
pre-M2, inside intra-arm spread) — the H0 mechanism does not
exist; no per-alloc cost.
- `*.max_us`: median/p99 flat across rc.c variants (a systematic
per-alloc cost must move them and does not); max_us
non-reproducible within a single fixed binary (3.2× spread).
Roadmap-P2 `*.max_us` `-n 5` structural false-positive (now its
4th consecutive no-runtime-change-attributable firing).
Limitations (bencher-stated): single host; N=15 moderate for
max-tail — median/p99 verdict solid, max verdict rests on the
(strong, N-bounded) non-reproducibility argument; gc latency arm not
re-measured (no gc firing; rc/gc share the identical M2 hunk).
## Step 3 — Classification & resolution
| Item | Class | Resolution |
|---|---|---|
| `[medium]` DESIGN.md:2266 stale `(M1)` header + `[low]` :2354 xref | **fix** | one tidy iteration `embedding-abi-m2.tidy`: rename the `## Embedding ABI (M1)` header to drop the milestone tag (current-state-mirror), lockstep the `:2354` cross-reference and the `docs_honesty_pin.rs:134136` comment/assert-message `(M1)` token — the pinned *substring* (`:135`, the "Export parameters are written **bare**…" sentence) stays verbatim untouched. planner → implement. |
| `check.py` exit 1 — 3 firings | **carry-on** | causally exonerated as the two pre-existing roadmap-P2 noise families (`*.bump_s` staleness; `*.max_us` `-n 5` false-positive). **No baseline ratified** (bencher wrote nothing to baseline; these are not M2 artefacts). The P2 items remain tracked on the roadmap; retiring the `bump_s` stale baseline is a separate roadmap-P2 action against that item, explicitly NOT this milestone. |
| `compile_check.py` / `cross_lang.py` | carry-on | clean (exit 0). |
**Milestone M2 is substantively closed and sound** (capability
sanitiser-verified, all contract invariants held). The one open
drift is a doc-honesty header desync, fixed by the tidy iteration
dispatched next; the roadmap `[~]``[x]` flip follows the tidy
landing clean. No ratify. No re-audit deferral.
@@ -1,136 +0,0 @@
# audit Embedding ABI — M3 (milestone close)
**Date:** 2026-05-18
**Milestone range:** `9a609ae..4ea8bc5` (M2 roadmap-close → M3 DONE)
**Outcome:** DRIFT — one `[medium]` + one `[low]` doc-honesty item
→ a single docs-honesty tidy iteration; bench `check.py` exit 1
**decisively causally exonerated** (byte-identical generated IR),
NO ratify. compile_check / cross_lang clean.
## Step 1 — Architect drift review (`drift_found`)
Dispatched `ailang-architect` over `9a609ae..HEAD`.
**What holds (audited clean):**
- **Invariant 1 (clean core):** zero `Cargo.toml`/runtime dep
changes; `ailang-core`/`ailang-codegen`/`runtime/` touches are
only the spec-scoped widen + lockstep comments + pins. No
`data-server`/finance knowledge entered.
- **M1/M2 internal-convention byte-invariant held:** the M2
`Target::StaticLib` forwarder body and the internal
`@ail_<mod>_<fn>` / `_adapter` / `_clos` convention are *absent
from the diff* (untouched). The `llvm_scalar` / `is_c_abi_type`
widen is exactly the spec scope — no new `CheckError` variant, no
schema field, no Form-A change (M2-style honoured).
- **Frozen-layout SSOT consistent:** DESIGN.md
`### Frozen value layout` (`:2321-2359`) is present-tense and
matches both `match_lower.rs` `lower_ctor` (`size = 8 + n*8`,
tag@0, fields@8+i*8) and the enforcing byte-pin
(`@ailang_rc_alloc(i64 24)`, tag@0, @8/@16). All three lockstep
`// FROZEN ABI` pointers present and aimed at the SSOT. The Boss
spec-consistency repair note + §"Testing strategy" 3/4 + the
journal Boss-adjudication / Re-dispatch sections are mutually
consistent and honest about the M2-TLS cross-attribution (correct
behaviour, not a leak).
**Drift / debt:**
- **`[medium]` `docs/DESIGN.md:2299-2302`** — a surviving M1-era
paragraph still asserts modes "apply only to heap-shaped types,
which the **scalar-only rule above forbids** at an export
boundary anyway". `:2280-2284` was rewritten to accept a
single-ctor record, so there is no longer a scalar-only rule;
this sentence now contradicts the section's own freeze and the
`(own (con State))`/`(borrow (con State))` mode contract at
`:2345-2351`. Canonical-spec internal contradiction the milestone
owned. → **fix** (docs-honesty tidy).
- **`[low]` `crates/ailang-codegen/src/lib.rs:608-610`** — the
forwarder-body comment still says the gate "guarantees every
param and the ret are `Int`/`Float`; map Int→i64, Float→double",
now false (records→ptr). The architect classified this "carried
debt, not a scope miss" — the forwarder *body* is contractually
byte-frozen, so the comment was genuinely outside any M3 edit
region. Comment-only; correcting it changes no emitted IR (the
byte-pin + forwarder-IR pin assert IR, not source comments). →
**fix**, folded into the same tidy (doc-honesty hygiene; the IR
pins are the guard that generated code stays byte-frozen).
(Boss already caught + fixed in-line one sibling stale `//`
gate-comment block at `check/lib.rs:1917-1922` during the DONE
inspect; the architect found no further siblings.)
## Step 2 — Bench-regression trio
| script | exit | summary |
|---|---|---|
| `bench/check.py` | **1** | 63 metrics; 3 regressed, 2 improved, 58 stable |
| `bench/compile_check.py` | 0 | 24 metrics; 0 regressed, 24 stable |
| `bench/cross_lang.py` | 0 | 25 metrics; 0 regressed, 25 stable (every `rc_over_c` within tol) |
`check.py` exit 1 — the 3 firings:
| metric | baseline | actual | diff | tol |
|---|---|---|---|---|
| `throughput.bench_list_sum.bump_s` | 0.046 | 0.052 | **+12.62%** | 10.0% |
| `latency.implicit_at_rc.p99_9_us` | 452.0 | 580.6 | **+28.45%** | 25.0% |
| `latency.implicit_at_rc.max_us` | 477.3 | 645.2 | **+35.18%** | 30.0% |
Both are the two **standing tracked P2 noise families** (roadmap
P2): the `*.bump_s` stale-baseline (~+513% environmental band on
current hardware) and the `*.max_us`/`p99_9` `-n 5` tail-jitter
structural false-positive (signature: same benchmark's `median_us`
+5.11% ok, `p99_us` +15.08% ok — only the extreme tail quantiles
fire, the worst-of-~5000-samples-over-5-runs pattern).
**Causal exoneration (decisive, not hand-waved — M2-audit method).**
M3 changed *no* executable-path codegen (architect-confirmed: M2
forwarder body byte-unchanged; the only codegen touches are the
`llvm_scalar` record→ptr arm — reachable solely for an `(export)`
single-ctor record, and no `bench_*` program has an `(export)` — the
`ailang-check` `is_c_abi_type` widen, and lockstep *comments*). Built
`ail` at HEAD and at the pre-M3 commit `9a609ae` in an isolated
worktree; the **generated IR is byte-identical** (`cmp -s`) for every
firing benchmark and its neighbours:
```
bench_list_sum: IR BYTE-IDENTICAL (HEAD == 9a609ae)
bench_list_sum_explicit: IR BYTE-IDENTICAL
bench_latency_implicit: IR BYTE-IDENTICAL
bench_tree_walk: IR BYTE-IDENTICAL
```
The IR is what is handed to `clang -O2` (deterministic), so the
bench binaries are byte-identical and M3 causation is logically
impossible. **NO baseline ratify** (these are not M3 artefacts; the
two P2 items stay separately tracked). Bench → **carry-on**.
## Step 3 — Classification
| item | class | recommendation |
|---|---|---|
| `[medium]` DESIGN.md:2299-2302 scalar-only-rule contradiction | drift | **fix** — docs-honesty tidy |
| `[low]` codegen forwarder-comment Int/Float-only staleness | drift | **fix** — folded into the same tidy |
| `check.py` exit 1 (3 firings) | tracked-noise | **carry-on**, NO ratify (byte-identical-IR causal exoneration) |
| compile_check / cross_lang | clean | carry-on |
## Step 4 — Resolution (Boss)
- **Drift → one docs-honesty tidy iteration `embedding-abi-m3.tidy`**
(the M2-audit `[medium]+[low] doc-honesty → tidy` precedent
shape): reconcile the stale DESIGN.md:2299-2302 "scalar-only rule
above forbids" paragraph against the frozen SSOT + the own/borrow
mode contract, **pin-safe** (the `docs_honesty_pin.rs:135` pinned
sentence — "Export parameters are written **bare**…", shifted to
~`:2299` by the M3.1 DONE — sits in/adjacent to the edit zone;
the tidy plan must keep it byte-verbatim+contiguous, planner
Step-5 item-6 / M2.tidy precedent); plus the `[low]` codegen
forwarder-comment honesty fix (comment-only, the byte-pin +
forwarder-IR pin stay green as the guard that generated code is
untouched). Routed via `planner``implement`.
- **Bench → carry-on, NO ratify.** Causally exonerated; the two
tracked P2 noise todos stay as-is.
Milestone is substantively closed + sound (record crosses the C ABI
in/out, ownership follows the declared mode, the value layout is
frozen + byte-pin-enforced, all contract invariants held,
Invariant 1 clean). The roadmap M3 `[~]``[x]` follows the tidy
landing clean.
@@ -1,91 +0,0 @@
# audit prose-loop-binders — Milestone-prose-loop-binders tidy (clean)
**Date:** 2026-05-18
**Milestone:** prose-loop-binders (single-iteration)
**Commit range:** `6cbd0fe..c9355d7` (prev close = loop/recur at `6cbd0fe`)
**Status:** CLOSED — clean, carry-on (no fix, no ratify)
## Step 1 — Architect drift review
`ailang-architect` over `6cbd0fe..HEAD`: **clean**, zero drift, zero
debt. Verified against the diff (not journal trust):
- Single hunk in `crates/ailang-prose/src/lib.rs` (the `Term::Loop`
arm of `write_term`, lines 936953). The two non-render
`Term::Loop` sites (`count_free_var`, `subst_var_with_term`) and
all `Term::Recur` arms are absent from the diff — untouched.
- DESIGN.md and PROSE_ROUNDTRIP.md make **no** claim about how
prose renders `loop` (grep: no match), so the spec's "no doc
edit needed" is correct, not a skipped obligation. `DESIGN.md:714`
`loop(n-1)` is an ordinary recursive-call illustration in
Decision 8, unrelated to the `Term::Loop` construct — not stale.
- Diff touches only the four named code/fixture files plus expected
docs (spec/plan/journal/INDEX/roadmap/stats). No carve-out /
hash-pin / round-trip lockstep bypassed (prose is off the
Form-A↔JSON round-trip path by construction).
- The `architect_sweeps.sh` Sweep-1 DESIGN.md hits all pre-date
`6cbd0fe` and are outside this milestone's diff (zero DESIGN.md
lines changed) — flagged for the record, not attributable here.
Architect recommendation: carry on as planned.
## Step 2 — Bench-regression check
- `bench/compile_check.py`**exit 0**, 24 metrics, 0 regressed.
- `bench/cross_lang.py`**exit 0**, 25 metrics, 0 regressed.
Notably its independent `bench_list_sum.ail_bump_s` reads +1.50%
(±15% tol) → **ok** — corroborates that check.py's bump_s firing
is baseline-staleness, not a real bump regression.
- `bench/check.py` → exit 1. First run: 3 regressed
(`bench_list_sum.bump_s` +13.17%; `latency.implicit_at_rc.p99_9_us`
+37.30%; `latency.implicit_at_rc.max_us` +114.14%), 4 improved,
56 stable. Confirmatory identical re-run (no code change):
`p99_9_us` +10.22% → **ok**, `max_us` +22.17% → **ok** (median
and p99 within tol on both runs) — the two latency-tail firings
were single-sample 5-run RC-mode jitter, proven by re-run.
`bench_list_sum.bump_s` persisted at +11.79% (consistent with
the +13.17% first run): summary `63 metrics; 1 regressed`.
## Step 3 — Classification
| Item | Class | Rationale |
|------|-------|-----------|
| Architect drift | carry-on | clean — nothing actionable |
| `throughput.bench_list_sum.bump_s` +~12% | carry-on | The already-tracked P2 roadmap todo "`*.bump_s` throughput baseline is stale vs current hardware" — byte-oracle-proven environmental (not codegen) at the 2026-05-16 iteration-discipline-revert audit. Causally impossible to attribute to a projection-only `ailang-prose` change (prose is not in the bench runtime path; architect verified the diff is one render-arm + test fixtures). |
| `latency.implicit_at_rc` p99.9 / max | carry-on | Single-sample 5-run RC-mode tail jitter; vanished on identical re-run with median/p99 always within tolerance. Not a regression; the queued P3 "latency methodology rework" (histogram-based) exists precisely to de-noise this metric family. |
## Step 4 — Resolution
**Carry-on** on every item. No fix iteration, no baseline ratify.
Explicitly **not ratified**: the `*.bump_s` baseline is NOT bumped
here. The P2 roadmap todo scopes a holistic recapture+recalibration
of the entire `*.bump_s` set on current hardware from a known-clean
commit as a dedicated bench-harness iteration; opportunistically
bumping one metric's baseline inside an unrelated projection-only
milestone is exactly the "bump the baseline, the regression is
expected" rationalisation the audit Iron Law forbids, and would
fragment the recapture the P2 todo owns. The pointer stands; the
todo is unchanged.
## Fieldtest applicability
**Not applicable / not dispatched.** `fieldtest` is Boss-dispatched
for a milestone that touched the *authoring* surface — the
fieldtester writes `.ail` Form-A programs and runs them through the
public `ail` CLI. This milestone changed only the Form-B *reading*
projection (`ail prose` output); the Form-A authoring surface, the
typechecker, codegen, and runtime are byte-unchanged. A fieldtester
authoring `.ail` programs would not exercise the prose-render change
at all — there is no friction/bug/spec-gap axis for it to probe.
The two committed whole-file byte-equality `.prose.txt` snapshots +
the live-CLI `ail prose | diff` acceptance checks (iter journal) are
the end-to-end gate for a projection-only milestone.
## Milestone close
The **prose-loop-binders** milestone is **CLOSED, clean**. Architect
drift clear; bench carry-on (sole persistent firing is the tracked
P2 `*.bump_s` environmental staleness, latency-tail proven sampling
noise by re-run). Roadmap P0 entry flipped to closed; no follow-up
iteration spawned.
@@ -1,108 +0,0 @@
# audit — remove-mut-var-assign milestone close
**Date:** 2026-05-18
**Scope:** `48e7774..07f0802` (prev close → the single terminal iter)
**Outcome:** CLEAN after one inline `.tidy` fix; bench causally
exonerated; two P2 roadmap todos filed/strengthened. Fieldtest
remains (surface-touching milestone).
## Step 1 — Architect drift review
**`[high]``crates/ailang-core/specs/form_a.md` (EBNF 286-288,
prose 308-328, dangling `like mut` at 332).** The live in-tree
Form-A grammar reference still documented the full `mut`/`var`/
`assign` construct. The spec (Components #2, Architecture table
row 2) explicitly named "form_a.md" for deletion; the
schema-vs-doc honesty invariant the milestone advertises was
broken. **Root cause = the recon-undercount class (4th
recurrence), and specifically a Boss error:** at plan time the
Boss grepped `docs/form_a.md` (dir-scoped), got "No such file",
concluded "form_a.md does not exist", and propagated that
unverified non-existence claim into the recon brief. The file
lives at `crates/ailang-core/specs/form_a.md` and is actively
maintained (form-a.tidy edited it) — not frozen historical
record. A compile-driven sweep cannot catch this: a stale doc
never produces `E0004`/`E0061`.
**Resolution — inline Boss `.tidy`, not planner+implement.**
Rationale (CLAUDE.md "When NOT to delegate": context already
loaded, an agent would redo all milestone reading for a ~40-line
single-file doc deletion whose design was already settled by the
approved spec): deleted the three EBNF productions (keeping
`reuse-as``loop``recur`), the three mut/var/assign prose
bullets, and reworded the surviving `loop` bullet so its
"right-folded into `Term::Seq`" no longer back-references the
deleted `mut`. Full review-and-commit discipline kept (no
discipline shed for the carve-out): verified tree-wide that the
only remaining `Term::Mut`/`Term::Assign`/`MutVar` strings in
`crates/` + `docs/DESIGN.md` + `docs/roadmap.md` are the roadmap
entry *describing the milestone itself* (not residue), and
form_a.md's two remaining `mut`/`var` substrings are the unrelated
`[unbound-var]` / `reuse-as-source-not-bare-var` diagnostic names.
Grammar + notes read coherently after the cut.
**`[medium]` — process-drift, recon-undercount 4th recurrence.**
The existing loop-recur.tidy P2 (`ailang-plan-recon`
cross-crate-caller-undercount) is demonstrably insufficient: the
class now recurs through a *new* site type each time (test bodies,
drift pins, carve-out files, and now a spec-named non-compiled
doc + an unverified existence claim feeding the recon brief).
Escalated that P2 in place — broadened from "cross-crate caller"
to a structural recon-contract defect with two named gaps
(non-compile-checked spec-named sites; unverified "X does not
exist" claims inheriting into a recon brief), and a concrete
agent-definition fix (compile-driven enumeration as authoritative
code-site set **plus** an `ls`-verified spec-named-path existence
table). Not a code defect; an agent-discipline fix, parked P2.
## Step 2 — Bench-regression
`bench/check.py` **exit 1** (3 firings); `compile_check.py`
**exit 0** (24/24 stable — no check-time tax, consistent with a
pure removal); `cross_lang.py` **exit 0** (25/25 stable, incl. the
independent `bench_list_sum.ail_bump_s` +2.95% ok).
check.py firings: `throughput.bench_list_sum.bump_s` +12.51%;
`latency.explicit_at_rc.max_us` +108.38%;
`latency.implicit_at_rc.max_us` +66.46%. All medians/p99 stable.
**Bencher localisation — causal exoneration (decisive).** Built
`ail` at HEAD and at pre-milestone `48e7774`; the three
firing-relevant bench binaries (`list_sum_bump`, `lat_expl_rc`,
`lat_impl_rc`) are **`cmp`-byte-identical** across the milestone.
The 2520-line removal diff does not alter one byte of these
fixtures' machine code (none contain `mut`); no causal mechanism
exists. By elimination every firing is non-code-caused.
Per-firing verdict: `bump_s` = the tracked P2 environmental
staleness (stable ~+1114% across 4 runs, byte-identical binary,
cross_lang corroborates); both `*.max_us` = single-sample `-n 5`
jitter (`explicit_at_rc.max_us` collapsed +108%→**-2.30% ok** by
an identical-code rerun3; `implicit_at_rc.max_us` swings
477→1172→843→756 on the identical binary while its median holds
+3.1…3.9%).
**Resolution: PRISTINE met on the merits. NO baseline ratify.**
The Iron Law forbids ratifying noise into the baseline and the
spec explicitly forecloses opportunistic ratification here; the
non-pristine exit code is bench-harness noise, not a code defect,
and does not block close. Filed a **new, distinct P2** (separate
from the `*.bump_s` staleness P2): `check.py` RC-latency
`*.max_us` at `-n 5` is a structural false-positive — 3-for-3 on
consecutive no-runtime-change milestones — recommending drop
`max_us` from gating / raise `-n` / cpuset-pin (mitigation choice
left to a future iteration).
## Step 3/4 — Classification & resolution
| Item | Class | Outcome |
|------|-------|---------|
| form_a.md construct doc | fix | inline Boss `.tidy` (this commit) |
| recon-undercount 4th recurrence | process-drift | P2 escalated in place (roadmap) |
| `bump_s` +12.51% | carry-on | tracked P2 (no ratify) |
| `*.max_us` ×2 | carry-on | new P2 filed (no ratify) |
Milestone audit **CLEAN** post-tidy. Next pipeline step:
`fieldtest` (the removal is a user-visible Form-A surface change —
`mut`/`var`/`assign` no longer lex; an LLM author must now reach
for `let`/`if`/`loop`/`recur`).
@@ -1,122 +0,0 @@
# 2026-05-18 — brainstorm embedding-abi-m4 → RETIRED (never speced)
`/boss` picked the top P0 item "Embedding ABI — M4: sequence
crossing via the existing `List` ADT" (a new milestone, no spec).
User green-lit a continue-here brainstorm. The brainstorm
terminated **without a spec** at the Step-2/3 Q&A: the milestone's
premise collapsed under its own feature-acceptance gate. This file
records why, so the retired entry is not silently erased.
## What was explored before the collapse
Plan-recon (`ailang-plan-recon`) returned a full fact sheet. Two
user forks were resolved in Q&A and are recorded here as context
(now moot, but they shaped the analysis):
- **own-only** for the `List` parameter — kernel consumes each
chunk; its existing internal recursive drop frees it; no
host-side recursive-free symbol (M3 explicitly deferred that as
the build-ahead trap it refused). Keeps M4 "no language change".
- **`List Record` only** — element an M3-shaped single-ctor
all-scalar record crossing as ptr-to-M3-box, one uniform
cons-cell layout; sidesteps the poly/mono inline-scalar-element
divergence the recon flagged (open question #3).
Approach **A (structural recognition)** — extend `is_c_abi_type`
with a recursive list-shaped arm (2 ctors: one nullary + one
`(C-ABI, recursive-self)`), no name match, consistent with M3's
structural single-ctor rule and AILang's content-addressing
thesis — was the recommendation. B (name-anchored) rejected on
language grounds (name fragility; no canonical `List` — 8
divergent decls, no `std/`); C (land a `std_list` SSOT first)
rejected on scope grounds (a separate milestone; breaks the
established self-contained embed-fixture discipline).
## Why M4 was struck (the substantive reason — feature-acceptance clause 2)
The user asked "what is M4 even *for*?" then "but the host has to
build the list first, no?" then narrowed: a *minimal* data-server
binding would be satisfied by the host pushing each tick
individually — no list at all.
Held against `docs/DESIGN.md` §"Feature-acceptance criterion":
- **Clause 2 (improves correctness / removes redundancy) — FAILS.**
The shipped M3 export gate `is_c_abi_type`
(`crates/ailang-check/src/lib.rs:1934-1953`) runs as a **per-
parameter loop** (`for p in &param_tys { if !is_c_abi_type(p,
&env) … }`) accepting, *independently per param*, a C scalar OR
a single-ctor all-scalar record. The forwarder's `llvm_scalar`
maps every non-scalar `Type::Con``ptr` and passes it
straight through (M3-frozen). Therefore `(State, Tick) -> State`
with both as single-ctor all-scalar records is **already
gate-accepted and forwarder-supported today** — the minimal
data-server binding is M3 (shipped) + a host-side per-tick loop
over each chunk's records. Cons-list crossing would *add* a
2N+1-box-per-chunk host builder and pull in the deferred
flat-array perf debt (the cons-list IS the P2-deferred perf
problem), removing no redundancy.
- **No named consumer.** M5's adapter (the sole data-server↔AILang
meeting point, Invariant 1) can wire `while let next_chunk` by
unrolling each chunk host-side into per-tick calls — a clean
adapter, not a layering violation; the adapter is exactly where
host data shape meets the kernel. Whole-chunk in-kernel
visibility buys nothing: `State` threads across calls regardless
of chunk boundaries, so a windowed indicator behaves identically
whether ticks arrive one-per-call or chunk-per-call; chunk
boundaries are a host artefact, semantically invisible to the
fold.
M4 (cons-list crossing) is therefore **speculative infra by
AILang's own criterion**: it builds something no named consumer
needs and adds cost rather than removing redundancy. The
brainstorm discipline at this point is *not* to ratify a known-
unneeded shape into the roadmap (Common Rationalisations:
"Approaches A,B,C — proceed with A" → surface the mis-framing),
so no spec was written, no grounding-check dispatched, no planner
handoff.
## Honest correction (lesson)
Mid-Q&A I asserted "M5 cannot wire the real data-server loop
without M4." That was wrong and I corrected it on the record: M5
*can* wire it with M3 + a host-side `for tick in chunk` loop. The
earlier "O(records) vs O(chunks) FFI calls" framing was also
misleading — the per-tick forwarder is thin; the expensive thing
in the M4 alternative is the cons-list marshalling the scalar loop
does not have at all. Re-deriving against the code (not defending
the roadmap I had written) is what surfaced the clause-2 failure;
this is the "user suggestions ≠ directives, form own judgment"
discipline working in the user's favour.
## Decision / outcome
- **M4 retired** in `docs/roadmap.md` (kept one cycle as a struck
entry for context, then delete — never `[x]`, never shipped).
- **M5 reconciled**: `depends on:` M4 → M3 (+ the Tick-coverage
todo); body now states the adapter unrolls each chunk host-side
into per-tick `(State, Tick) -> State` calls; the friction
harvest now feeds the host-per-tick-FFI vs. batch-crossing perf
decision (the P2 flat-array item) rather than an array-vs-cons
decision.
- **Residual = a `[todo]`**: Tick-coverage on M3 — an E2E +
fixture pinning the two-record-param per-tick crossing
`(State, Tick) -> State` on the shipped M3 ABI. Capability is
present today but only E2E-proven for a *single* record param
(`State`): every shipped M3 fixture
(`embed_backtest_step_record{,_borrow}.ail`,
`embed_export_record_ok.ail`) pushes a scalar `Float` sample,
none a record `Tick`. A second record param is the identical
mechanism (same gate arm, same `→ptr`, same forwarder, zero new
code path) — worth pinning by fixture, but a test backfill, not
a milestone (no brainstorm). This is the actual "minimal
data-server binding" the user asked for.
## Forward note (not edited now — avoid scope creep)
The P2 "Flat array/slice primitive — performance" item's framing
("1024 RC cons-cells per chunk on the hot path") is now partly
stale — the cons-list path is dropped, so the residual perf
question is host-side per-tick FFI vs. an eventual batch crossing.
Flagged here; reconcile that entry's wording when it is picked up,
not now.
@@ -1,74 +0,0 @@
# iter bugfix-over-strict-mode-ctor-rebuild-consume — over-strict-mode FP on ctor-rebuild-from-primitive-fields
**Date:** 2026-05-18
**Started from:** a11cb7cc9f912a0d7ee8a56b3f53255bd101e327
**Status:** DONE
**Tasks completed:** 1 of 1
## Summary
The `over-strict-mode` lint spuriously fired `(borrow ...) would
suffice` on `(own (con T))` params whose body destructures `T` into
purely *primitive* fields and then feeds those fields into a
`Term::Ctor` that builds a fresh allocation. The dismantle+rebuild
is a genuine consume of the scrutinee's box (a borrowed value's
payload cannot be taken apart and re-packaged), but the lint's
consume-detection only recognised a *heap-typed* pattern-binder
being moved out — primitive fields are filtered by
`pattern_has_consumed_heap_binder`, so the rebuild was invisible.
This is the false positive behind `embed_backtest_step_tick.ail`
warning on both `st` and `tick`. The fix adds a second recognition
path in `any_sub_binder_consumed_for`: for a `match p` arm that
destructures binders out of `p`'s ctor, if any of those binders
(primitive or not) is mentioned anywhere inside a `Term::Ctor`'s
argument subtrees in the arm body, the param is genuinely consumed
and the lint stays silent. Over-strict-only: a param merely read
(no ctor build) still warns, proven by a negative control. No
existing test changed; doc-honesty corrections applied to the two
stale comments that mis-attributed this defect to nested `match`.
## Per-task notes
- iter bugfix-over-strict-mode-ctor-rebuild-consume.1: added
`collect_pattern_binders`-based ctor-rebuild-consume recognition
to `any_sub_binder_consumed_for`'s matched-scrutinee arm loop in
`crates/ailang-check/src/linearity.rs`, plus two pure private
helpers (`ctor_uses_any_binder` — finds a fresh-allocation
`Term::Ctor` reachable in the arm body; `term_mentions_any_binder`
— deep free-var scan of that ctor's args, so the binder reaching
the ctor *through* an expression like `(+ acc px)` is recognised,
not only a bare `Var` arg). Corrected the stale "Known limit" doc
on `any_sub_binder_consumed_for` and the matching "Known debt:
deeply-nested-match-on-sub-binder" block in the module header —
both previously mis-attributed the FP to a nested match; the real
mechanism is the unrecognised ctor-rebuild consume, independent of
match nesting.
## Concerns
(none)
## Known debt
- `term_mentions_any_binder`'s binder scan is by name and does not
track shadowing. A destructured primitive binder shadowed and
rebound before reaching the ctor would still be counted. This is
not a corpus shape, and the resulting imprecision is strictly in
the over-strict (extra-silence) direction the cause explicitly
permits — never under-strict, so no soundness risk. Documented in
the function's own doc comment; left as-is per the minimal-fix
constraint.
## Blocked detail
(not blocked)
## Files touched
- crates/ailang-check/src/linearity.rs (+166 / 20): lint-detection
fix + two helpers + two doc-honesty corrections. Check-only; no
codegen / runtime / ABI / schema / DESIGN.md change.
## Stats
bench/orchestrator-stats/2026-05-18-iter-bugfix-over-strict-mode-ctor-rebuild-consume.json
@@ -1,131 +0,0 @@
# iter docs-honesty-lint.1 — canonical docs made present-tense-honest, guarded by a wrap-robust pin + Sweep 5
**Date:** 2026-05-18
**Started from:** de66eb7ab2e457bfe7e5dbe2f87c5da15a786cd7
**Status:** DONE
**Tasks completed:** 6 of 6
## Summary
`docs/DESIGN.md` and `docs/PROSE_ROUNDTRIP.md` carried Wunschdenken
(forward intent stated as fact) and non-citation post-mortem /
doc-archaeology narrative. This iter strips both classes, codifies
the honesty discriminator as a `### What this document is — and the
honesty rule it holds itself to` meta-subsection inside DESIGN.md
§"Project ecosystem", and guards the invariant two ways: a
wrap-robust enumerated absent/present regression pin
(`crates/ailang-core/tests/docs_honesty_pin.rs`, 4 tests) and a
wrap-robust advisory Sweep 5 in `bench/architect_sweeps.sh` with all
sweep-count lockstep wording + a new `ailang-architect.md` "DESIGN.md
honesty drift" bullet. Genuine forward intent (LLM tool-use / MCP /
LSP) relocated to `docs/roadmap.md` P3. No language/checker/codegen/
runtime change — the sole `crates/` change is the additive pin file;
`ail check`/`run`/`build` are byte-unchanged by construction.
## Per-task notes
- iter docs-honesty-lint.1.1: created the wrap-robust RED pin
(`docs_honesty_pin.rs`, `norm()` whitespace-collapse helper, 4
tests). Verified RED for the right reasons (compiles, 4 fail on the
enumerated still-present strings + the absent discriminator).
- iter docs-honesty-lint.1.2: appended Sweep 5 to
`architect_sweeps.sh` + tail "All four"→"All five" + header
"four"→"five sweeps"; added the "DESIGN.md honesty drift" architect
bullet and fixed the sibling "four sweeps"→"five / Sweeps 1-4"
wording. Sweep 5 confirmed firing on the uncorrected tree
(EXIT=1). Concern-1 lockstep shipped whole, no split.
- iter docs-honesty-lint.1.3: inserted the discriminator
meta-subsection. Tree had drifted (a `- **Tests**:` bullet now
follows the Docs bullet, no blank line; closing prose is "When the
language grows…"); anchored on the verbatim closing-prose line per
the plan's own Step-1 "quote it, do not assume" caveat.
- iter docs-honesty-lint.1.4: applied the 13 byte-exact corrections
(each verbatim "current text" re-confirmed by grep first; lines had
drifted +~22 from Task 3). Step-14 catch-all surfaced one genuine
extra FIX site (see Concerns); classified + handled per procedure.
- iter docs-honesty-lint.1.5: stripped the PROSE_ROUNDTRIP tool-use/
MCP Wunschdenken paragraph (present-tense replacement); appended
the relocated forward-intent idea to roadmap P3 with spec
provenance. Full pin GREEN (4/4).
- iter docs-honesty-lint.1.6: full verification — pin GREEN isolated;
workspace zero failures (609 passed, delta = +4 pin tests only);
sentinel trio (design_schema_drift/spec_drift/schema_coverage)
green; Sweep 5 reduced to the single self-referential L62 KEEP;
build clean; `ONLY THE PIN` scope confirmation.
## Concerns
- T3: plan Task-1 pin string `whether the document asserts something
exists, works, or changed that does not` omitted the `**…**`
Markdown bold that plan Task-3 body wrapped around `asserts …
not`. Plan self-review item 3 cross-check missed it. Resolved by
dropping the incidental bold from the inserted body (the pin is the
contract, written first; the emphasis is non-load-bearing). Prose
meaning + present-tense framing unchanged. Plan-internal
inconsistency, not a tree-drift; flagged for planner calibration.
- T4: Step-14 catch-all surfaced one genuine extra Sweep-5 FIX site
not in Steps 1-13 — DESIGN.md Form-B class/instance prose-projection
bullet ("was deferred from milestone 22 entirely; … A future iter
ships the full prose projection if/when …"). The Boss's "zero extra
hits" pre-verification was against the spec's illustrative regex;
the plan's broader Sweep-5 regex correctly catches this soft-wrapped
site. Handled exactly per Step-14: rewritten present-tense (kept the
load-bearing facts — one-way renderer, no parser by design,
placeholder render in `crates/ailang-prose/src/lib.rs`) + matching
absent-string asserts + present-anchor added to the pin. This is the
procedural catch-all working as designed.
## Catch-all classification (Task 4 Step 14)
- KEEP: DESIGN.md L62 `- **Forward intent** ("planned / will back /
on the path to / if-when X arrives")` — the discriminator
subsection's own enumerated catalogue of forbidden phrasings;
asserts nothing false, present-tense rule definition. A
self-referential Sweep-5 false-positive (the regex matches the
literal "on the path to" inside the rule's own example list).
- FIX: DESIGN.md L2093 Form-B prose-projection bullet — see Concerns;
before→after recorded there; pin pair added.
## Known debt
- (none) — the iter is self-contained; the pin + Sweep 5 are the
forward guard against regrowth.
## Post-iter sweep output (Task 6 Step 4)
```
=== Sweep: 1 (history anchors) ===
50: `docs/journal-archive.md` (archived monolith for pre-2026-05-11 history),
2060: 2026-05-13) — combines two `Str` values into a single owned `Str`.
=== Sweep: 5 (honesty: wunschdenken + non-citation post-mortem) ===
62:- **Forward intent** ("planned / will back / on the path to /
EXIT=1
```
Sweep-1 residue = the two documented deliberate KEEPs (L50 doc-roles
pointer date, L2060 true `(iter str-concat, 2026-05-13)` provenance).
Sweep-5 residue = the single L62 self-referential discriminator-
catalogue KEEP. EXIT=1 is expected + architect-adjudicated per plan
Task 6 Step 4 (deliberate-KEEP date-anchors remain by design).
## Blocked detail
(none — DONE)
## Files touched
- `crates/ailang-core/tests/docs_honesty_pin.rs` (new — the pin, 4
tests; extended in T4 Step 14 per the mandated catch-all procedure)
- `bench/architect_sweeps.sh` (Sweep 5 + header/tail lockstep)
- `skills/audit/agents/ailang-architect.md` ("four"→"five" + new
"DESIGN.md honesty drift" bullet)
- `docs/DESIGN.md` (discriminator subsection + 13 enumerated
corrections + 1 catch-all correction)
- `docs/PROSE_ROUNDTRIP.md` (tool-use/MCP Wunschdenken stripped)
- `docs/roadmap.md` (relocated forward-intent idea, P3)
## Stats
bench/orchestrator-stats/2026-05-18-iter-docs-honesty-lint.1.json
@@ -1,474 +0,0 @@
# iter embedding-abi-m1.1 — Embedding ABI M1: linkable artifact + scalar C entrypoint
**Date:** 2026-05-18
**Started from:** b0bd7efeaff137af22bb12f5e751260804e770c3
(Tasks 13 committed at 818177d; Tasks 47 re-dispatched from 818177d)
**Status:** DONE — all 7 tasks complete. Tasks 13 committed
(818177d); Tasks 47 implemented on the corrected plan and sit
unstaged in the working tree. The E2E coherent-stop proof
(`embed_e2e.rs`: `cc` link + run, `s == 25`, exit 0) is GREEN.
**Tasks completed:** 7 of 7 (Tasks 13 DONE + committed; Task 4
initially BLOCKED on a Boss plan-defect — fixture module≠stem —
resolved by Boss plan Repair [Design-decision 6] + re-dispatch;
Tasks 47 then completed cleanly on the corrected plan)
## Summary
The Embedding-ABI M1 iter is complete across all 5 pipeline
layers. Tasks 13 landed in the first dispatch and were committed
(818177d) as a verified known-good subset: the CLI build-path
`MissingEntryMain` baseline pin (Task 1), the additive
`FnDef.export: Option<String>` schema field threaded across the
whole workspace via compile-driven enumeration with a hash-stability
golden pin (Task 2), and the Form-A `(export "<sym>")` surface
modifier with full parse/print/grammar lockstep and a byte-identical
round-trip (Task 3).
Task 4 was BLOCKED in the first dispatch on a Boss plan-defect
(fixtures declared `module ≠ file-stem`; the loader hard-enforces
`module == file-stem` at `workspace.rs:438`). The Boss Repaired the
plan (Design-decision 6: fixture `(module …)` == file stem; the
C symbol stays `backtest_step` via `(export …)`, demonstrating spec
Decision 2's mangling-decoupling) and re-dispatched Tasks 47. On
the corrected plan all four landed cleanly: the check-side
scalar-only + effect-free export gate with two new `CheckError`
variants (`export-non-scalar-signature` / `export-has-effects`, the
clause-3 discriminator *in code*, Task 4); the codegen
`Target::StaticLib` mode suppressing `@main`/`MissingEntryMain` and
emitting one external `@<sym>` forwarder per export to
`@ail_<module>_<fn>` (Task 5); the CLI `ail build --emit=staticlib`
two-archive emit (`libembed_backtest_step.a` + `libailang_rt.a`,
Task 6); and the C-host E2E coherent-stop proof — `cc` links both
archives, calls `backtest_step(0,3)=9`, `backtest_step(9,4)=25`,
the `s == 25` assertion holds, exit 0 (Task 7), plus the DESIGN.md
§"Embedding ABI (M1)" current-state subsection. Full workspace
green (0 failures); `roundtrip_cli` green; `compile_check.py` /
`cross_lang.py` exit 0; `check.py` fires only the tracked P2
`*.bump_s` / `*.max_us` known-noise envelope (plan Step 6: audit-
adjudicated, not a plan-level gate — the default-executable
codegen/runtime path is byte-unchanged). No clean-core dependency
added (Invariant 1).
## Per-task notes
- iter embedding-abi-m1.1.1: CLI build-path `MissingEntryMain`
baseline pin. Created `examples/embed_noentry_baseline.ail`
(main-free module) + `crates/ail/tests/embed_missing_main_baseline.rs`
(triple-assertion: non-zero exit + verbatim `has no \`main\` def`
Display substring [codegen lib.rs:129] + absent binary). GREEN on
write; not-a-no-op confirmed; `roundtrip_cli` green with the new
fixture. DONE.
- iter embedding-abi-m1.1.2: additive `FnDef.export` schema field +
workspace-wide threading. Added the field to `FnDef` after `doc`
(verbatim plan doc-comment, `#[serde(default,
skip_serializing_if = "Option::is_none")]`). Compile-driven
enumeration via `cargo build --workspace --all-targets
--keep-going` over two dependency-ordered waves threaded
`export: None` into ~85 `FnDef`/`AstFnDef` literals across 18
files incl. all named blind-spot sites (codegen lib.rs:3320
`missing_entry_main_is_error`, design_schema_drift.rs:278,
spec_drift.rs:256, hash_pin.rs, the in-source `#[cfg(test)]
mod tests` literals, the `mono.rs` `AstFnDef` alias, the
`parse.rs`/`print.rs` literals). `cargo build --workspace`
exits 0 (planner self-review item 7 gate met). DESIGN.md
fn-JSON `"export"` line + drift test green (Recon Q2 confirmed:
no nested-struct-key anchor forced). Hash pin green (golden
`b4662aa70839f60b`). Full workspace test gate: 611/0.
DONE_WITH_CONCERNS.
- iter embedding-abi-m1.1.3: Form-A `(export "<sym>")` surface
modifier. `parse_export` helper (faithful `parse_doc` mirror);
`parse_fn` threaded (`let mut export`, `Some("export")` arm with
duplicate-clause guard, unknown-attr string updated, `Ok(FnDef{…})`
uses parsed `export`); `write_fn_def` export emission block (doc →
export → suppress → type order); form_a.md production +
`(export STRING)?` + prose paragraph. RED verified
(`unknown fn attribute \`export\``), then GREEN: round-trip gate
byte-identical, surface crate 72/0, ailang-core 111/0. DONE.
- iter embedding-abi-m1.1.4 (FIRST dispatch — BLOCKED, audit
trail): wrote the five plan fixtures + the verbatim gate pin; ran
it → RED for the WRONG reason (a `ModuleNameMismatch` load
failure, not the absence of the gate). BLOCKED — see Blocked
detail + Boss resolution. No gate production code written; the
WIP fixtures + RED test were discarded by the Boss for the
re-dispatch.
- iter embedding-abi-m1.1.4 (RE-dispatch — DONE): check-side
export-signature gate on the corrected plan (Design-decision 6,
module==stem). Recreated the five fixtures (module == file stem;
`embed_export_float_ok.ail` uses `(app * a b)` — no `*.` head)
+ the verbatim gate pin `crates/ailang-check/tests/embed_export_gate.rs`.
RED verified for the RIGHT reason (fixtures load cleanly, 4
`*_rejected` fail because no gate exists, 2 positives already
pass). Added two `CheckError` Error variants
(`ExportNonScalarSignature(String,&'static str,String)` /
`ExportHasEffects(String,Vec<String>)`) after `ConstHasEffects`,
their two `code()` arms, and the gate in `check_fn` after the
param/ret well-formedness checks (gate order params→ret→effects,
first-violation-wins, unconditional on `f.export.is_some()`).
GREEN 6/6; combined fixture fails signature-first. Round-trip +
full workspace green. DONE.
- iter embedding-abi-m1.1.5: codegen `Target::StaticLib`. Added the
`Target` enum next to `AllocStrategy`; threaded `target` through
`lower_workspace_inner`; `lower_workspace_with_alloc` /
`lower_workspace` pin `Target::Executable`; new public
`lower_workspace_staticlib_with_alloc`. Gated the
`MissingEntryMain` shape-check + the `@main` trampoline behind
`Target::Executable`; the `StaticLib` arm emits one external
`define {cret} @<sym>(...)` per `(export …)` fn forwarding to
`@ail_<module>_<fn>` via the new `fn_scalar_sig` / `llvm_scalar`
helpers. RED→GREEN IR-shape pin (no `@main`, has `@backtest_step`,
forwards to `@ail_embed_backtest_step_step`; exe target still
`MissingEntryMain`). In-source `missing_entry_main_is_error` pin
byte-unchanged; codegen crate 0 failures. DONE_WITH_CONCERNS
(the `emit_ir`-routes-via-`lower_workspace` plan note).
- iter embedding-abi-m1.1.6: CLI `ail build --emit=staticlib`.
Added the `--emit` clap field (`value_parser = ["exe",
"staticlib"]`, default `exe`) after `alloc`; branched
`Cmd::Build` (`emit == "staticlib"` → `build_staticlib`); added
`build_staticlib` (verbatim `build_to:22392294` shared prefix +
`has_export` guard + `lower_workspace_staticlib_with_alloc` +
two-archive `clang -c`/`ar rcs` tail) and the new shared `run_cmd`
helper (confirmed no pre-existing equivalent). RED→GREEN CLI pin
(`libembed_backtest_step.a` + `libailang_rt.a` produced;
export-less module rejected with the zero-export error).
Default-exe regression (floats E2E + Task-1 baseline) green.
DONE.
- iter embedding-abi-m1.1.7: E2E host harness + DESIGN.md
§"Embedding ABI (M1)". Wrote `crates/ail/tests/embed/host.c`
(spec headline host, verbatim) + `crates/ail/tests/embed_e2e.rs`.
E2E GREEN — `cc` links `libembed_backtest_step.a` +
`libailang_rt.a`, runs the host, `backtest_step(0,3)=9` /
`backtest_step(9,4)=25`, `s == 25` assertion holds, exit 0 (the
milestone coherent stop). Added the DESIGN.md §"Embedding ABI
(M1)" subsection (verbatim prose, at `##` level for consistency
with §"Mangling scheme"). Drift tests 8/8 + 8/8; full workspace
0 failures; bench trio per plan Step 6. DONE_WITH_CONCERNS (the
dead-`Path`-import correction + the `###`-vs-`##` heading-level
resolution).
## Concerns
- iter embedding-abi-m1.1.2: the plan's RED-pin symbol
`ailang_core::hash::content_hash_fn(&FnDef)` does not exist. The
real entry point is `def_hash(&Def) -> String` (the one
`crates/ailang-core/tests/hash_pin.rs` uses). Resolved by
mirroring `hash_pin.rs` exactly (wrap the `FnDef` in
`Def::Fn(...)`, call `def_hash`) — this is explicitly authorised
by the plan's own Step-1 NOTE ("the implementer mirrors the exact
hashing entry point that `hash_pin.rs` uses if the name differs").
Property and shape unchanged; recorded because the plan's literal
symbol was wrong (planner pseudo-vs-reality class — the
`feedback_specs_need_concrete_code` / `feedback_plan_pseudo_vs_reality`
family).
- iter embedding-abi-m1.1.4: the plan fixture `embed_export_float_ok.ail`
uses `(app *. a b)`. There is no `*.` Float-multiply head in
AILang — Float arithmetic uses the same typeclass-generic head as
Int (`examples/floats.ail` / `examples/mut_sum_floats.ail` both
use `(app + …)`; codegen lowers to `fadd/fmul double` by type).
Per the plan NOTE ("if the Float-multiply surface differs, use the
same head that example uses"), the fixture as written uses
`(app * a b)`. The fixture's role ("scalar+pure Float export
passes the gate") is preserved. Recorded; not yet exercised
(Task 4 BLOCKED before the gate ran).
- INFRA (not iter-authored): `docs/roadmap.md` became dirty in the
working tree during this run (a ~98-line P2 "DESIGN.md → design/
role-split" milestone proposal, mtime mid-session). Phase 0's
`git status --porcelain` was empty (clean start). Nothing in this
iter writes `roadmap.md`; it was modified by an external/concurrent
writer. It is path-disjoint from every iter file. Left untouched
(roadmap.md is Boss-owned per CLAUDE.md; main HEAD sacrosanct).
The Boss must decide independently whether to keep or discard it;
it is NOT part of this iter's commit. (RESOLVED at Boss
re-dispatch: committed separately as Boss roadmap maintenance —
see Boss resolution. Not part of the Tasks 47 working tree.)
- iter embedding-abi-m1.1.5 (plan pseudo-vs-reality, behaviour-
neutral): plan Task-5 Step 3(b) third bullet says "`emit_ir`
(line 197): its internal `lower_workspace_inner` call passes
`Target::Executable`". Reality: `emit_ir` does NOT call
`lower_workspace_inner` directly — it calls `lower_workspace`
(`codegen lib.rs:214`), which now passes
`lower_workspace_inner(ws, AllocStrategy::Gc, Target::Executable)`.
The plan's *intent* (the in-source `missing_entry_main_is_error`
pin, which calls `emit_ir`, stays byte-unchanged) is delivered
transitively and verified GREEN at Task-5 Step 7. Recorded
because the plan's literal call-chain description was inaccurate
(planner pseudo-vs-reality family); no behavioural deviation.
- iter embedding-abi-m1.1.7 (plan-verbatim transcription defect,
corrected): the plan's verbatim Task-7 `embed_e2e.rs` imports
`std::path::{Path, PathBuf}` but the test body uses only
`PathBuf` — a transcribed unused import that raises
`unused_imports` on every build. Implementer-mindset judgement:
a verbatim copy that introduces a compiler warning is not the
plan's intent; reduced to `use std::path::PathBuf;`. Test
behaviour byte-identical (E2E still GREEN). Recorded as a
deviation from plan-literal text in service of plan intent.
- iter embedding-abi-m1.1.7 (plan code-block vs plan prose, design
consistency): plan Task-7 Step 4's code block shows the new
section as `### Embedding ABI (M1)`, but the same step's prose
instruction says "at the same heading level as §"Mangling
scheme"" — §"Mangling scheme" is `## ` (level-2) in DESIGN.md.
Used `## Embedding ABI (M1)` for structural consistency with the
surrounding §"Mangling scheme" / §"Data model" level-2 sections,
following the plan's explicit prose directive over its
illustrative code-block `#`-count. Section content byte-verbatim.
- iter embedding-abi-m1.1 (cross-checks, plan-confirming, no
deviation): the six named implementer-judgement cross-checks were
executed. (a) `Type::Con` has exactly `{name: String, args:
Vec<Type>}` — no mode field; the plan's `matches!` guard is
correct. (b) `declared_effs` is already `Vec<String>` (per
`lib.rs` `declared_effs.into_iter().collect()`), so the plan's
conditional `.into_iter().collect()` conversion did NOT apply —
`.clone()` matches `ExportHasEffects(_, Vec<String>)` exactly.
(c) `synth::llvm_type` lowers `Int`→`"i64"`, `Float`→`"double"`
(`synth.rs:17,21`) — the plan's `llvm_scalar` is an exact mirror
for the 2-scalar M1 subset; forwarder `call` types match
`@ail_<module>_<fn>`'s emitted signature (also proven by the
Task-7 E2E real `cc` link + run). (d) no pre-existing `run_cmd`
helper in `main.rs` (only inline `Command::new` blocks) — adding
it was correct per the plan. (e) `ailang_core::Def` is crate-root
re-exported (`ailang-core lib.rs:76`) — `ailang_core::Def::Fn`
valid in `main.rs`. (f) `ailang_surface::load_workspace` +
`ailang_check::check_workspace` + `Diagnostic.code: String` are
the exact `main.rs:580581` public symbols. All confirmations of
plan assumptions, not contradictions.
## Known debt
- None blocking. The M1 ABI is deliberately scalar-only and
provisional until M3 (no `Bool`/`Str`/ADT/record marshalling, no
per-thread context lifecycle API); this is reserved-unimplemented
scope, labelled in DESIGN.md §"Embedding ABI (M1)", not debt.
- `check.py` fires the tracked P2 `*.bump_s` / `*.max_us`
known-noise envelope (3 regressed / 2 improved beyond tolerance /
58 stable; e.g. `throughput.bench_list_sum.bump_s` +13.95%,
`latency.explicit_at_rc.max_us` +60.61%). This iter touched only
check-side gating, a codegen `Target` enum (every existing path
pinned `Executable`), and CLI dispatch — none on the runtime hot
path these metrics measure; the default-executable codegen/runtime
path is byte-unchanged (Task-5 Step 7/8 + Task-6 Step 6 confirm).
Per plan Step 6 this is milestone-close-`audit`-adjudicated, NOT
a plan-level gate. `compile_check.py` / `cross_lang.py` exit 0.
- `bench/architect_sweeps.sh` exits 1 both with and without the
DESIGN.md edit (pre-existing advisory residue, established
carry-on adjudicated at milestone-close `audit`); the new
§"Embedding ABI (M1)" introduced no new Wunschdenken / post-
mortem phrasing (the "provisional until M3" / "M2 will change"
clauses are labelled reserved-unimplemented, which the DESIGN.md
honesty rule explicitly permits).
## Blocked detail
> **RESOLVED (2026-05-18, re-dispatch).** This block was a
> first-dispatch event; it was resolved by the Boss plan Repair
> (see "## Boss resolution" below — Design-decision 6, Option 2,
> module==stem) and the Tasks 47 re-dispatch completed all four
> tasks cleanly. The detail below is **preserved verbatim as the
> iter's audit trail** and is no longer an open block. Iter status
> is DONE.
**Task:** 4 (check-side export-signature gate)
**Reason:** spec-ambiguous (a spec-internal contradiction that
rippling-binds Tasks 47, surfaced as `unclear` per the per-task
sub-status table → BLOCKED to Boss).
**Worker says:** The plan's Task-4 gate pin
(`crates/ailang-check/tests/embed_export_gate.rs`) is verbatim and
correct Rust; it calls `ailang_surface::load_workspace(&path)` on
each fixture (the exact call shape `crates/ail/src/main.rs:580581`
uses). `load_workspace` →
`ailang_core::workspace::load_modules_with` →
`module_name_from_path` (`workspace.rs:1566`) hard-enforces
`in-file module name == file stem`, returning
`WorkspaceLoadError::ModuleNameMismatch` (`workspace.rs:182`)
otherwise. The plan's verbatim fixtures deliberately violate this:
- `examples/embed_backtest_step.ail` has `(module backtest …)` —
spec-mandated, because the entry module name `backtest` is what
produces `libbacktest.a` (Task 6), `@ail_backtest_step` and the
exported C symbol `backtest_step` (Tasks 5/7, the spec headline
host `host.c` declares `extern int64_t backtest_step(...)`).
Stem is `embed_backtest_step` ⇒ `ModuleNameMismatch`.
- `embed_export_str_param_rejected.ail` → module `bad_str_param`;
`embed_export_io_rejected.ail` → `bad_io`;
`embed_export_effectful_rejected.ail` → `bad`;
`embed_export_adt_ret_rejected.ail` → `bad_adt_ret`;
`embed_export_float_ok.ail` → `embed_float_ok`. All
stem-mismatched ⇒ every `check_codes()` call panics on load
before the gate is ever consulted.
These three plan facts cannot all hold simultaneously: (a) fixture
files named `examples/embed_*` (every Task 4/5/6/7 path cites these
exact names), (b) the spec-headline module/C-symbol naming
(`backtest` → `libbacktest.a` / `backtest_step`), (c) a
`load_workspace`-based check-side gate pin. `roundtrip_cli` (Task 1/3)
sidesteps this only because it drives `ail parse` (single-file, no
workspace stem-match). The contradiction is therefore invisible until
the first `load_workspace`-based fixture test — exactly Task 4.
This is not one of the six named implementer-judgement cross-checks
(Float `*.` head, public-symbol confirmation, `Type::Con` field set,
`declared_effs` element type, `run_cmd` pre-existence, `llvm_scalar`
callee-ABI). It is a structural spec/plan defect requiring a
Boss/spec decision among at least:
1. Rename each fixture **file** to its module stem
(`examples/backtest.ail`, `examples/bad_str_param.ail`, …) and
update every Task 4/5/6/7 path citation accordingly (preserves
the spec headline module/C-symbol naming; ripples ~12 plan
path references + the Task-6/7 `libbacktest.a` math still holds
since module stays `backtest`).
2. Rename each fixture **module** to its filename stem
(`(module embed_backtest_step …)`, …) and re-derive the spec's
C-symbol / `lib<entry>.a` story from `embed_backtest_step`
(changes the spec headline; `libembed_backtest_step.a`,
`@ail_embed_backtest_step_step` — almost certainly not what the
spec wants).
3. Change the Task-4 gate pin to a loader that does not
stem-enforce (e.g. parse the single file directly), which
diverges from the plan's verbatim `load_workspace`-based test
and from how `ail check` actually loads (`main.rs:580`).
Option 1 is the most spec-faithful (it keeps the headline module
`backtest` and only moves files), but it is a cross-task plan edit
(≥12 path citations) and therefore the planner's / Boss's call, not
an inline implementer substitution. I did NOT silently pick one
(reverted my own speculative fixture-module renames so the Boss sees
the contradiction unobscured); no gate production code was written.
**Suggested next step:** Boss adopts Option 1 — re-issue the plan
for Tasks 47 with fixture files renamed to their module stems
(`examples/backtest.ail` etc.) and the ~12 path citations updated;
keep module `backtest` so the spec's `libbacktest.a` /
`backtest_step` headline is preserved — then re-dispatch this iter
with `task_range: [4, 7]` (Tasks 13 are clean and can be committed
now or carried).
## Boss resolution (2026-05-18)
The BLOCK was verified accurate against source:
`crates/ailang-core/src/workspace.rs:438` (`entry_module.name !=
module_name_from_path(entry_path)` → `ModuleNameMismatch`, no
exception) — module name MUST equal file stem. The defect is a
**Boss plan-write error**: the plan's headline + gate fixtures
declared `(module backtest)` / `(module bad)` / … in `embed_*.ail`
files, malformed under that invariant. The orchestrator correctly
stopped at the Iron Law instead of guessing. Recon maps file
structure, not fixture-content invariants; the Boss source-
verification focused on code-edit anchors, not cross-checking each
fixture's module name against its filename against the loader —
this is the `feedback_specs_need_concrete_code` /
`feedback_plan_pseudo_vs_reality` family (the fixture as written
would not load), now also a fixture-well-formedness lesson.
**Decision: Option 2, overriding the orchestrator-recommended
Option 1.** (Agent recommendations are not directives — own
judgement formed first.) Keep the descriptive `embed_*` filenames;
rename the fixture *modules* to their file stems
(`embed_backtest_step`, `embed_export_str_param_rejected`, …).
Rationale from semantics, not effort:
- The spec's load-bearing contract (Decision 2, spec lines
5259 / 167169) is that the exported C symbol is *author-chosen
and decoupled from `ail_<module>` mangling* —
`(export "backtest_step")` is the contract; `host.c` asserts on
`backtest_step`, which is invariant under module rename. So
spec-faithfulness does **not** discriminate between the options;
`libbacktest.a` / `@ail_backtest_step` are *illustrative*
(build-output filename + internal mangling — the latter is
exactly what the spec decouples from), not frozen-ABI contracts.
The orchestrator's "Option 1 most spec-faithful" over-weighted
the illustrative `libbacktest.a` string.
- `examples/` is a flat shared corpus auto-enrolled in whole-corpus
tests (`roundtrip_cli`); Option 1's `examples/bad.ail` /
`backtest.ail` / `bad_io.ail` pollute it with collision-prone,
non-self-describing names. Option 2 keeps the self-describing
grouped `embed_*` set — a real maintainability argument in a
~100-file flat dir read by future LLM authors.
- Option 2 (module = stem, C symbol stays `backtest_step` via the
`(export …)` string) is a *concrete demonstration of the spec's
own Decision-2 decoupling thesis* — the first fixture proving
module-name ≠ C-symbol — not a violation of it. The spec's
`(module backtest)` / `(module bad)` blocks are shape-
illustrative (planner contract: spec owns shape, not bytes).
Consequence: entry module `embed_backtest_step` ⇒ archive
`libembed_backtest_step.a`, internal symbol
`@ail_embed_backtest_step_step`; the external C symbol
`backtest_step` and `host.c` are UNCHANGED (the actual coherent-
stop contract). Architect-at-close note recorded in the plan
(Design-decision 6): a fixture module name differing from the
spec's illustrative block is expected under module==stem +
Decision 2, not drift.
Actions taken: (1) plan `docs/plans/embedding-abi-m1.1.md` fixed —
Design-decision 6 added, Task-4 fixtures → module==stem, Task-5/6/7
dependent strings updated (`libembed_backtest_step.a`,
`@ail_embed_backtest_step_step`; C symbol `@backtest_step`
unchanged), the two iter-1 concerns folded (no `*.` Float head →
`(app * a b)`; `content_hash_fn` → `def_hash(&Def)`); (2) the
already-created headline `examples/embed_backtest_step.ail`
corrected to `(module embed_backtest_step)`; (3) the 5 blocked-
Task-4 fixtures + the RED `embed_export_gate.rs` discarded
(untracked WIP — recreated correctly by the re-dispatch from the
fixed plan); (4) Tasks 13 committed as a verified known-good
partial subset (`cargo build --workspace` exit 0, full workspace
green with zero FAILED after the WIP discard, `roundtrip_cli`
green); (5) orchestrator re-dispatched with `task_range: [4, 7]`
on the clean tree + corrected plan. The user's path-disjoint
roadmap addition (P2 "DESIGN.md → `design/` role-split") was
committed separately as Boss roadmap maintenance, not bundled
into this iter.
## Files touched
### Committed at 818177d (Tasks 13, first dispatch)
Schema / canonical docs:
- crates/ailang-core/src/ast.rs (FnDef.export field)
- docs/DESIGN.md (fn-JSON "export" line)
- crates/ailang-core/specs/form_a.md (grammar production + prose)
Workspace-wide compile-gate threading (~85 `export: None,` literals):
- crates/ailang-core/src/desugar.rs, pretty.rs
- crates/ailang-core/tests/{design_schema_drift,hash_pin,spec_drift}.rs
- crates/ailang-check/src/{lib,lift,linearity,mono,reuse_shape,suppress_filter,uniqueness}.rs
- crates/ailang-check/tests/workspace.rs
- crates/ailang-codegen/src/lib.rs
- crates/ailang-prose/src/lib.rs
Surface modifier (Task 3):
- crates/ailang-surface/src/parse.rs (parse_export + parse_fn thread)
- crates/ailang-surface/src/print.rs (write_fn_def export block)
New files committed at 818177d:
- examples/embed_noentry_baseline.ail
- examples/embed_backtest_step.ail (corrected to `(module embed_backtest_step)`)
- crates/ail/tests/embed_missing_main_baseline.rs
- crates/ailang-core/tests/embed_export_hash_stable.rs
### Tasks 47 re-dispatch working tree (UNCOMMITTED — this hand-off)
Modified (4 source/doc files):
- crates/ailang-check/src/lib.rs (2 CheckError variants + 2 code() arms + check_fn gate; +51)
- crates/ailang-codegen/src/lib.rs (Target enum + threaded target + forwarder loop + fn_scalar_sig/llvm_scalar; +130/-22)
- crates/ail/src/main.rs (--emit clap field + Cmd::Build branch + build_staticlib + run_cmd; +123)
- docs/DESIGN.md (new §"Embedding ABI (M1)" subsection; +24)
New files (untracked, this re-dispatch):
- examples/embed_export_str_param_rejected.ail
- examples/embed_export_adt_ret_rejected.ail
- examples/embed_export_io_rejected.ail
- examples/embed_export_effectful_rejected.ail
- examples/embed_export_float_ok.ail
- crates/ailang-check/tests/embed_export_gate.rs (GREEN 6/6)
- crates/ailang-codegen/tests/embed_staticlib_lowering.rs (GREEN 2/2)
- crates/ail/tests/embed_staticlib_cli.rs (GREEN 2/2)
- crates/ail/tests/embed/host.c
- crates/ail/tests/embed_e2e.rs (GREEN 1/1 — coherent-stop proof)
(`docs/roadmap.md` is NOT in this iter's working tree — the
first-dispatch external modification was Boss-committed separately
as roadmap maintenance, per the Boss resolution.)
## Stats
bench/orchestrator-stats/2026-05-18-iter-embedding-abi-m1.1.json
@@ -1,214 +0,0 @@
# iter embedding-abi-m2.1 — per-thread runtime context + concurrency safety
**Date:** 2026-05-18
**Started from:** c9a84b33b3e7806491153a5bd3037be1b8ef9d31 (Tasks 14 committed)
**Original start (first dispatch):** b3388c83506a314fbd87f3354f80ae8cc212f501
**Status:** DONE
**Tasks completed:** 9 of 9 (Tasks 14 in the first dispatch, committed at c9a84b3; Tasks 59 in this re-dispatch)
## Summary
The per-thread embedding-ctx ABI is complete. Tasks 14 (first
dispatch, committed at c9a84b3): the FIXED-FIRST alloc-guard baseline
pin, the de-globalised `ailang_ctx_t` runtime (per-thread TLS slot +
conditional accounting, `g_rc_*`+atexit retained verbatim as the
null-ctx fallback), the codegen forwarder gaining a leading
`ptr %ctx` + `@__ail_tls_ctx` save/store/restore around the
byte-unchanged internal call, and the CLI RC-only guard. Tasks 59
(this re-dispatch): the per-thread-ctx scalar-swarm capability demo
(`swarm.c`, no SHARED_CTX, GREEN), the de-globalisation
negative-control standing test in the rc-accounting harness
(`rc_accounting_shared_ctx_is_flagged_by_tsan` — shared-ctx genuinely
raced under tsan, teeth proven), the M1 `host.c` migration to the ctx
ABI (`s == 25` holds through the ctx-threaded forwarder), the
DESIGN.md current-state update (provisional-until-M3 narrowed to
value/record layout; Decision-10 per-thread-ctx note; pinned
"Export parameters are written **bare**…" sentence + `(con Int)`
snippet preserved verbatim), and the full regression + bench gate
(workspace 0 failed; bench-trio causally exonerated). The milestone's
concurrency-safety invariant is sanitiser-verified.
## Boss spec-defect repair (2026-05-18)
The first dispatch (Tasks 14 clean) correctly **BLOCKED on Task 5**,
surfacing a genuine spec defect: the original Task 5 paired a
`-DSHARED_CTX` negative control onto `swarm.c`. That is structurally
impossible — `examples/embed_backtest_step.ail` is a non-allocating
scalar kernel (`state += sample*sample`); it never calls
`ailang_rc_alloc`/`ailang_rc_dec`, never writes a `ctx` field, and
`@__ail_tls_ctx` is `__thread` (per-thread, never shared even when the
`ailang_ctx_t*` value is passed to a shared ctx). A shared-ctx scalar
swarm therefore has no shared-memory write to race on; tsan correctly
stays clean. Asserting a structurally-unreachable race would have been
a vacuous (and dishonest) test. The Boss adjudicated: the
de-globalisation negative-control teeth belong to the **item-1
rc-accounting harness** (`rc_accounting_tsan.c -DSHARED_CTX`, where a
shared ctx genuinely races on `ctx->alloc_count`), NOT `swarm.c`. The
spec was amended in lockstep and the plan's Task 5 restructured:
`swarm.c` is the per-thread-ctx capability demo ONLY (one green
positive test); the negative control became
`rc_accounting_shared_ctx_is_flagged_by_tsan` added to the committed
`embed_rc_accounting_tsan.rs`. This re-dispatch implemented the
corrected Task 5 — no swarm.c negative control reintroduced. The
plan's Task 3 expected-string transcription error (recorded below)
was likewise Boss-corrected (`@ail_embed_backtest_step_step`, the
module-qualified convention-correct symbol).
## Per-task notes
- iter embedding-abi-m2.1.1 (first dispatch, committed): FIXED-FIRST
alloc-guard pin. New `crates/ail/tests/embed_staticlib_alloc_guard.rs`
(verbatim from plan); RED captured (build currently succeeds), the
deliverable. RED→GREEN across the iter (Task 4 closes it).
- iter embedding-abi-m2.1.2 (first dispatch, committed): `runtime/rc.c`
gains `ailang_ctx_t` {alloc_count, free_count} + `ailang_ctx_new`/
`ailang_ctx_free` + `__thread __ail_tls_ctx`; the two increment
sites become `if (_ctx) _ctx->… else g_rc_…`. `g_rc_*` statics +
atexit + constructor retained verbatim. New `rc_accounting_tsan.c` +
`embed_rc_accounting_tsan.rs`: RED (link error) → GREEN (per-ctx,
8×200000, tsan-clean).
- iter embedding-abi-m2.1.3 (first dispatch, committed): codegen
`Target::StaticLib` arm emits `@__ail_tls_ctx = external
thread_local global ptr` once; forwarder signature gains leading
`ptr %ctx`; body does load-save / store-ctx / unchanged
`call @ail_<mname>_<fn>(args)` / store-restore. `_adapter`/`_clos`
+ internal arg vector byte-untouched. Doc-comment narrowed to
value/record-layout provisionality. `embed_e2e` left RED by design
(Task 6 migrates it).
- iter embedding-abi-m2.1.4 (first dispatch, committed):
`crates/ail/src/main.rs build_staticlib` rejects `--alloc != Rc`
after the has_export check, before lowering. Task-1 pin RED→GREEN;
`embed_staticlib_cli` default path unaffected.
- iter embedding-abi-m2.1.5 (this re-dispatch): per-thread-ctx
capability demo. New `crates/ail/tests/embed/swarm.c` (no
SHARED_CTX) + `crates/ail/tests/embed_swarm_tsan.rs`;
`scalar_swarm_per_ctx_is_tsan_clean_and_matches_oracle` GREEN
(8 threads × 200k, tsan-clean, every result == serial oracle).
De-globalisation negative control
`rc_accounting_shared_ctx_is_flagged_by_tsan` appended to the
committed `embed_rc_accounting_tsan.rs` (reuses its `ws_root()`/
`cc()` helpers); GREEN — shared-ctx build genuinely raced under
tsan (teeth proven, not vacuous). No swarm.c negative control
reintroduced (the Boss spec-defect repair).
- iter embedding-abi-m2.1.6 (this re-dispatch): `crates/ail/tests/
embed/host.c` replaced with the ctx-ABI version (opaque
`ailang_ctx_t`, `ailang_ctx_new`/`_free` lifecycle, ctx-prefixed
`backtest_step`). Confirmed expected RED post-Task-3 first
(`s != 25` from the garbage-ctx arg shift). `embed_e2e.rs`
intentionally NOT modified: the M1 harness already links
`libailang_rt.a` by absolute path (`.arg(outdir.join(
"libailang_rt.a"))`), so the ctx symbols resolve without an
`-lailang_rt` add — the task block's own "no change is needed
beyond Step 2" conditional branch. GREEN, `s == 25` holds.
- iter embedding-abi-m2.1.7 (this re-dispatch): `docs/DESIGN.md` —
provisional-until-M3 sentence (~:2282) replaced with the M2 ctx
prose (mandatory leading `ailang_ctx_t*`, per-thread, RC-only
swarm artefact, value/record layout still provisional);
Decision-10 atomicity bullet (~:1528) gains the per-thread-ctx
soundness note. Pinned "Export parameters are written **bare**…"
sentence + canonical `(con Int)` snippet preserved verbatim (the
edit ends before them; no soft-wrap split risk). `docs_honesty_pin`
GREEN unmodified (5 passed), `design_schema_drift` GREEN (8
passed) — edits outside the `## Data model` scan window.
- iter embedding-abi-m2.1.8 (this re-dispatch): regression gate, no
code. `print_no_leak_pin` (1) + `e2e -- rc_stats` (4) GREEN —
null-ctx fallback prints unchanged. `embed_export_hash_stable`
(1, `fn_without_export_hash_is_unchanged`) + `embed_export_gate`
(6, all pass) GREEN — no schema field, export gate unchanged.
Full `cargo test --workspace`: every suite ok, 0 failing suites.
- iter embedding-abi-m2.1.9 (this re-dispatch): bench gate, no code.
`compile_check.py` exit=0 (24/24 stable); `cross_lang.py` exit=0
(25/25 stable); `check.py` exit=1 on exactly 4 tracked-P2 noise
metrics (`bench_list_sum.bump_s` +16.53%,
`latency.explicit_at_rc.max_us` +40.41%,
`latency.implicit_at_rc.p99_9_us` +31.66%,
`latency.implicit_at_rc.max_us` +92.35%). Causally exonerated by
the source-level invariant (see Concerns); no baseline ratified.
## Concerns
- iter embedding-abi-m2.1.3 (first dispatch — carried forward):
PLAN TRANSCRIPTION ERROR. Task 3 Step 1's plan code block asserted
`ir.contains("call i64 @ail_backtest_step(i64 %a0, i64 %a1)")`.
The internal symbol is `@ail_embed_backtest_step_step`
(module-qualified `@ail_<mname>_<fn>`), which the plan's own Step-4
parenthetical and Boss constraint 3 require to stay byte-unchanged.
Written instead with the convention-correct symbol (Boss-corrected
in the re-dispatched plan). The plan file should remain corrected.
- iter embedding-abi-m2.1.6: `embed_e2e.rs` is in the task's `Files:`
block but was intentionally left unchanged — the M1 harness already
links `libailang_rt.a` by absolute path, so the ctx symbols resolve
without an `-lailang_rt` add. This is the task block's own sanctioned
"no change is needed beyond Step 2" conditional branch, not a missed
requirement (observation, not correctness).
- iter embedding-abi-m2.1.7: the section header is still
`## Embedding ABI (M1)` while the body now describes the M2 ctx ABI
as current state — a header/content honesty mismatch. NOT fixed:
renaming the header is outside the task block's named edit regions
(`:22822285`, `:15281529`) and could perturb a doc-honesty pin
tracking the `(M1)` token; the controller curates scope, not the
implementer. Routed here for Boss adjudication — a candidate
follow-up (header rename + fieldtest/pin audit) but explicitly NOT
in this iter's plan scope.
- iter embedding-abi-m2.1.8: the task block expected
`embed_export_gate` to be "8 passed"; the suite at HEAD c9a84b3 has
exactly 6 tests, all passing, 0 filtered. The literal "8" is a
plan-transcription artefact (pre-existing — M2 added/removed no test
in this suite). The protected invariant (export gate unchanged by
M2) holds unambiguously; the implementer correctly did NOT pad the
suite to 8 (that would fabricate coverage). Plan number vs reality,
not a regression.
- iter embedding-abi-m2.1.9: `check.py` exit=1 on 4 metrics, all in
the tracked-P2 noise families (`*.bump_s`, `latency.*.max_us`,
`latency.*.p99_9_us`). CAUSALLY EXONERATED by a source-level
invariant stronger than the plan's binary `cmp`: this re-dispatch's
entire working-tree footprint is test fixtures + test drivers +
`docs/DESIGN.md` — ZERO files under `crates/*/src/` or `runtime/`.
The `ail` codegen binary and the linked `rc.c`/`str.c` runtime are
byte-identical to HEAD c9a84b3 by construction, so the firing
metrics cannot be caused by this iter. The plan's `git stash`/
`git stash pop` recipe was deliberately NOT run: on a re-dispatch
it would discard the iter's uncommitted Tasks 58 work and violate
the working-tree-as-handoff contract. No baseline ratified
(consistent with roadmap-P2 noise policy + the 2026-05-14 audit-pd
"baseline pristine, Nth consecutive observation" envelope).
## Known debt
- `## Embedding ABI (M1)` section header in DESIGN.md still says
"(M1)" though its body is now M2 current-state. Out of this iter's
plan scope (Task 7 named only `:22822285` and `:15281529`).
Candidate follow-up: header rename with a doc-honesty-pin audit
for any `(M1)` token dependency. Not touched to keep the diff
scope-compliant and avoid perturbing a pin.
## Blocked detail
None. All 9 tasks completed (Tasks 14 committed at c9a84b3 in the
first dispatch; Tasks 59 in this re-dispatch, all reaching
`approved` quality with zero review re-loops). The first dispatch's
Task-5 BLOCK was a genuine spec defect, Boss-adjudicated and repaired
before this re-dispatch (see "Boss spec-defect repair").
## Files touched (this re-dispatch — Tasks 59 working tree)
Modified:
- `crates/ail/tests/embed/host.c` (Task 6: ctx-ABI migration)
- `crates/ail/tests/embed_rc_accounting_tsan.rs` (Task 5: +
`rc_accounting_shared_ctx_is_flagged_by_tsan` negative control)
- `docs/DESIGN.md` (Task 7: M2 current-state + Decision-10 note)
Created:
- `crates/ail/tests/embed/swarm.c` (Task 5: per-thread-ctx
capability demo, no SHARED_CTX)
- `crates/ail/tests/embed_swarm_tsan.rs` (Task 5: one green
positive test)
(Tasks 14's files — `embed_staticlib_alloc_guard.rs`, `rc.c`,
codegen `lib.rs`, `embed_staticlib_lowering.rs`, `main.rs`,
`rc_accounting_tsan.c`, the Task-2 `embed_rc_accounting_tsan.rs`
base — are already committed at c9a84b3.)
## Stats
bench/orchestrator-stats/2026-05-18-iter-embedding-abi-m2.1.json
@@ -1,58 +0,0 @@
# iter embedding-abi-m2.tidy — lockstep section rename `Embedding ABI (M1)` → `Embedding ABI`
**Date:** 2026-05-18
**Started from:** d1241b12c759765f12713584ce2aab21e2a74f74
**Status:** DONE
**Tasks completed:** 1 of 1
## Summary
Audit-driven docs-honesty tidy (no parent spec; source:
`docs/journals/2026-05-18-audit-embedding-abi-m2.md`, the M2
milestone-close audit). The DESIGN.md section `## Embedding ABI (M1)`
had a stale `(M1)` qualifier — its body now describes the post-M2 ctx
ABI present-tense, so the qualifier contradicts the current-state-mirror
discipline. Renamed the header and moved all four *live* coupled
references in lockstep so no pointer dangles at the vanished `(M1)`
title. Pure docs/test-string tidy: zero language / checker / codegen /
schema change; `ail check`/`run` byte-unchanged by construction. The
`docs_honesty_pin.rs:135` asserted substring is rename-immune (no
section-name token, matched file-wide via `d.contains`) and was left
byte-verbatim untouched; the pin staying green before AND after
empirically confirms the rename did not perturb it.
## Per-task notes
- iter embedding-abi-m2.tidy.1: lockstep rename across 5 live-coupling
sites in 3 files — `docs/DESIGN.md:2266` (section header) +
`docs/DESIGN.md:2354` (schema-block xref comment) +
`crates/ailang-core/tests/docs_honesty_pin.rs:134` (test comment) +
`:136` (assert-message) + `crates/ailang-core/specs/form_a.md:89`
(compiled-in forward-xref). Each site: one-line in-place `(M1)`-token
deletion, no reflow. Step-1 baseline grep showed exactly the 5 sites
present (`:135` correctly absent — no `(M1)` token); Step-6 grep
returned zero (exit 1). `docs_honesty_pin` 5 passed and
`design_schema_drift` 8 passed both before and after. Footprint
gated at exactly 3 files, 5 insertions / 5 deletions — no
`docs/journals/**`, `docs/specs/**`, `docs/plans/**`,
`bench/orchestrator-stats/**` touched (committed append-only history
retains `Embedding ABI (M1)` as correct record of the name at the
time; renaming it would falsify the record).
## Concerns
(none)
## Known debt
(none)
## Files touched
- `docs/DESIGN.md` (header `:2266`, schema-block xref `:2354`)
- `crates/ailang-core/tests/docs_honesty_pin.rs` (comment `:134`, assert-message `:136`; `:135` untouched)
- `crates/ailang-core/specs/form_a.md` (forward-xref `:89`)
## Stats
bench/orchestrator-stats/2026-05-18-iter-embedding-abi-m2.tidy.json
@@ -1,404 +0,0 @@
# iter embedding-abi-m3.1 — freeze the value layout + record across the C ABI
**Date:** 2026-05-18
**Started from:** 15ee3c5c8fbb871a9cde6cc7d57c0801bae9ae85
**Status:** DONE (Tasks 15 GREEN after the Boss spec-consistency
repair below; Tasks 67 completed GREEN in the re-dispatch)
**Tasks completed:** 7 of 7 (Tasks 14 DONE/GREEN by the orchestrator;
Task 5 correctly BLOCKED on a genuine spec-defect, M2.1-precedent
class, resolved GREEN by the Boss spec-consistency repair —
global-leak-freedom proof model; Tasks 67 DONE/GREEN in the
`task_range:[6,7]` re-dispatch on the amended plan)
## Summary
> **Boss note (read first):** the paragraphs below are the
> orchestrator's *first-dispatch* account (present-tense as written
> then) — Task 5's BLOCK was real and is preserved for its
> documentary value. It was resolved by the Boss spec-consistency
> repair and Tasks 67 completed in the re-dispatch; the iter is
> **DONE 7/7**. See "## Boss adjudication" and "## Re-dispatch" for
> the resolution. Do not read the "Task 5 is BLOCKED / Task 7 not
> reached" sentences below as the final state.
Tasks 14 are complete and GREEN in the working tree: the FIXED-FIRST
baseline pins (1a re-point annotation on `adt_ret_export_rejected`;
1b the new `@ailang_rc_alloc` heap-box byte-pin proving size=8+n*8 /
tag@0 / fields@8,16), the export-gate widen to a single-ctor scalar
record (`is_c_scalar` → two-level `is_c_abi_type`, 10/10 gate suite),
the codegen forwarder widen (`llvm_scalar` maps a gate-guaranteed
record `Type::Con``ptr`; M2 forwarder body byte-unchanged; 3/3
staticlib-lowering pins), and the `own` E2E record round-trip
(1,000,000-iter balanced ctx readback `allocs=1000000 frees=1000000
live=0`, exit 0 — the spec's "mechanically almost free" ownership
claim substantiated, ratified by `e2e.rs:1855`, NOT the Iter-18a
metadata-only pin).
Task 5 (the `borrow` E2E arm) is BLOCKED on a genuine
plan-assertion-model defect (detail below), structurally identical to
the M2.1 first-dispatch swarm.c/-DSHARED_CTX spec defect that the
Boss adjudicated and repaired. No production-code bug: the borrow
mode is leak-free and value-correct; the plan's *assertion shape* is
unsatisfiable given the M2 TLS-ctx accounting mechanism. Returned
BLOCKED for Boss adjudication per the Iron Law (never push past
BLOCKED by hand; spec/plan amendment is a Boss/brainstorm decision).
audit owns the bench + architect milestone-close (spec Testing items
8/9 — architect Invariant 1, bench trio); this implement run asserts
only the test-suite GREEN state an implement run can, and Task 7
(workspace-green gate) was not reached because Task 5 blocks the
sequence.
## Per-task notes
- iter embedding-abi-m3.1.1 (DONE_WITH_CONCERNS): re-point annotation
added verbatim above `adt_ret_export_rejected`
(`crates/ailang-check/tests/embed_export_gate.rs`); gate suite 6/6
inert. New byte-pin carrier `examples/embed_record_layout_carrier.ail`
+ new pin `crates/ailang-codegen/tests/embed_record_layout_pin.rs`
(`lower_workspace_with_alloc(&ws, AllocStrategy::Rc)`, mirroring
`embed_staticlib_lowering.rs`'s `load()` helper). Pin GREEN on the
pre-M3 tree. Carrier required two grounding corrections vs the
plan's literal pseudo-code (see Concerns).
- iter embedding-abi-m3.1.2 (DONE_WITH_CONCERNS): `is_c_scalar`
two-level `is_c_abi_type` (`crates/ailang-check/src/lib.rs:1921+`),
both call sites (param loop + ret) repointed; `:452` `#[error]`
widened; `:458` correctly left byte-unchanged (no `M1` token). 5
fixtures (`embed_export_record_ok` + 3 `_rejected` + re-pointed
`embed_export_adt_ret_rejected` → multi-ctor+Str `Reading`) + 4 new
gate tests. RED observed (`record_export_passes` failed pre-widen);
GREEN 10/10; full `ailang-check` 0 failed.
- iter embedding-abi-m3.1.3 (DONE_WITH_CONCERNS): `llvm_scalar`
widened (`Float`→double, `Int`→i64, other `Type::Con``ptr`, `_`
i64) + doc-comment lockstep; `fn_scalar_sig` clarifying comment only
(body recon-confirmed filter-free). 2 fixtures
(`embed_backtest_step_record{,_borrow}.ail`) + forwarder-IR pin
`staticlib_record_forwarder_is_ptr_passthrough`. RED observed
(State→i64 pre-widen); GREEN 3/3; full `ailang-codegen` 0 failed
(Task-1 byte-pin unchanged — box layout did not move).
- iter embedding-abi-m3.1.4 (DONE): `crates/ail/tests/embed/
record_roundtrip.c` + `crates/ail/tests/embed_record_e2e.rs`.
`build_link_run_embed` composed from verbatim-mirrored real
incantations (`embed_e2e.rs` build/link + `e2e.rs:2190`
`build_and_run_with_rc_stats` stats-parse — `embed_e2e.rs` exposes
no reusable helper; mirroring, not inventing). `own` E2E GREEN
immediately once Tasks 2+3 in tree (ctx readback `allocs=1000000
frees=1000000 live=0`, exit 0). No production code (plan: none
expected). Ratifier `e2e.rs::alloc_rc_own_param_dec_at_fn_return:
1855` cited; `borrow_own_demo_modes_are_metadata_only:1330` NOT
cited (Boss constraint 2 honoured).
- iter embedding-abi-m3.1.5 (BLOCKED — plan-assertion-model defect):
`record_roundtrip_borrow_alloc_eq_free` added to
`embed_record_e2e.rs`. RED and stays RED — see Blocked detail. Left
in the working tree as evidence (the M2.1-precedent handling: the
BLOCKED task's state is left for Boss adjudication, not discarded).
- iter embedding-abi-m3.1.6 / .7: NOT attempted in the first dispatch
(Task 5 was a hard predecessor in the plan's sequencing; `skip task
K, continue` is intentionally not a mode). Completed GREEN in the
`task_range:[6,7]` re-dispatch — see the Re-dispatch section below.
## Concerns
- iter embedding-abi-m3.1.1: PLAN PSEUDO-CODE DEFECT (class:
"plan pseudo-code vs reality"). The plan's carrier body used
`(app Pt a b)` for ADT construction — non-compiling (`ail check`:
`[not-a-function] mk: \`Pt\` is not a function`). AILang construction
is `(term-ctor <Type> <Ctor> args…)` (grounded on
`examples/box.ail:36`, `borrow_own_demo.ail`). Corrected to
`(term-ctor Pt Pt a b)`. Second correction: the plan's
`(app print …)` IO sink is unresolvable by the codegen library API
the byte-pin must use (`lower_workspace_with_alloc` does not run the
mono/prelude pass that resolves polymorphic `print` —
empirically: `UnknownVar("print")`; `io/print_int` is not a known
effect-op). Corrected to `(let s (app int_to_str …) (do io/print_str
s))` (monomorphic builtin, library-resolvable; grounded on
`int_to_str_drop_rc.ail`). Structural intent (single-ctor 2-Int
record forced onto the `@ailang_rc_alloc` heap path) preserved
exactly; plan Step-4/5 explicitly sanctions carrier adjustment to
force the heap box. Observation, not correctness — the byte-pin
asserts the literal emitted forms and is a genuine RED-on-offset-
move guard (proven enforceable would be Task 6 Step 7, not reached).
- iter embedding-abi-m3.1.2: same plan-pseudo-code class — all 5
fixture bodies used `(app Ctor …)`; corrected to `(term-ctor …)`.
The test assertion idiom was matched to the file's *existing*
`.iter().any(|c| c == …)` pattern (plan showed
`.contains(&"…".to_string())`); `record_export_passes` written as
`!any(gate-code)` not the plan's `.is_empty()` (a single
`over-strict-mode` warning makes the diagnostics vec non-empty —
`.is_empty()` would false-fail; the protected property is
"no gate rejection"). Semantic intent preserved.
- iter embedding-abi-m3.1.2 (carry to Task 6): the
`CheckError::ExportNonScalarSignature` rustdoc at
`crates/ailang-check/src/lib.rs:448-450` still says "M1's embedding
ABI is scalar-only; `Bool`/`Str`/every ADT is rejected" — now stale
(single-ctor scalar records are accepted). Outside Task 2's named
edit regions (`:452`/`:458` only); Task 6 owns the broader
stale-doc / "provisional until M3" sweep and should absorb this
line. Not gating; recorded for Boss/Task-6 scope.
- iter embedding-abi-m3.1.3: PLAN TRANSCRIPTION DEFECT (same class as
the M2.1 journal's `@ail_embed_backtest_step_step` correction). The
plan's fixtures used `(module backtest …)` while the file stems are
`embed_backtest_step_record{,_borrow}` — the loader enforces
module==file-stem (`[module-name-mismatch]`). Module names corrected
to the file stems; the forwarder-IR pin's internal-symbol assertion
corrected from the plan's draft `@ail_backtest_step` to the
convention-correct module-qualified
`@ail_embed_backtest_step_record_step` (per the plan's own Step-3
parenthetical "tighten to literal emitted forms" + Boss constraint /
M2.1 module-qualified-symbol mandate). `llvm_scalar` doc-comment
updated in lockstep with its widened body (coherence-required, not
surrounding cleanup — a function whose doc says "the only two M1
scalars" while its body maps a third case is self-contradictory
within the same task). `llvm_scalar` is now a slightly
over-narrow *name* (also maps records→ptr) — renaming is a 4-site
cross-cut out of Task-3 scope; the doc disambiguates; advisory Nit
only.
## Known debt
- `crates/ailang-check/src/lib.rs:448-450` stale variant rustdoc —
Task-6-scope (see Concerns); not touched to keep Task 2
scope-compliant.
- `llvm_scalar` identifier slightly narrower than its post-M3
behaviour — out of Task-3 scope; doc disambiguates.
- Tasks 6 (DESIGN.md SSOT + lockstep freeze pointers + the
match_lower.rs enforceability demonstration) and 7 (workspace-green
gate) unexecuted — blocked behind Task 5.
## Blocked detail
**Task:** 5 (`record_roundtrip_borrow_alloc_eq_free`).
**Reason:** worker-blocked — design/plan contradiction
(plan-assertion-model defect; the implementer-phase sub-status is
`BLOCKED`, mapped to the Boss per the SKILL.md sub-status table).
**Worker says (verbatim):** The plan's Task-5 assertion
`assert_eq!(stats.allocs, stats.frees, "borrow: kernel kept input;
host freed every input + the return")` is structurally unsatisfiable.
`stats` is defined (Task 4 Step 2, plan) as the *single*
`ailang_ctx_free` RC-stats readback — the kernel-internal ctx
accounting. In `borrow` mode the kernel allocates the new return box
during the forwarder call (lands on `ctx->alloc_count` because the
codegen forwarder publishes `ctx` into `__ail_tls_ctx` for the
synchronous call duration only) but does NOT consume/drop the borrowed
input (correct borrow semantics, ratified by
`e2e.rs::alloc_rc_borrow_only_recursive_list_drop:1671`). The
balancing frees are the host's per-iter `ailang_rc_dec(st)`, which run
*outside* any forwarder call → `__ail_tls_ctx` is NULL → they land on
`g_rc_free_count` (the atexit fallback line), NOT on `ctx`. There is
no public runtime API (`ailang_ctx_enter`/`set`/`bind`) for the C
host to publish a ctx to TLS around its own decs (`__thread
ailang_ctx_t *__ail_tls_ctx` is runtime-internal; the only writer is
the codegen-emitted forwarder, by M2 design). Empirical readback
(`AILANG_RC_STATS=1`, borrow): ctx line `allocs=1000000 frees=0
live=1000000`; g_rc atexit line `allocs=1 frees=1000001
live=-1000000`. Globally leak-free and value-correct (exit 0,
`assert(n==1000000)` held; `allocs_total=1000001 ==
frees_total=1000001`, net live=0). The plan's own assertion *message*
("host freed every input") contradicts its own `stats` *source* (the
ctx readback, which by construction cannot see host-side frees) — an
internal contradiction in the plan. This is NOT a production-code bug
(the spec's substantive invariant — ownership follows the declared
mode, no leak — holds); it is a plan-assertion-model defect of the
same class the M2.1 journal records (the swarm.c/-DSHARED_CTX
spec defect: a plan assertion structurally impossible given the M2
TLS-ctx mechanism, Boss-adjudicated + spec-amended, NOT
agent-self-amended). Per the Iron Law ("never push past BLOCKED by
hand") and memory ("when an iter needs a spec change, the spec is
wrong — update via brainstorm, not a self-patch"), returned BLOCKED
rather than silently substituting a global-balance / `live==0`
assertion (which would be the correct invariant but is a *different*
assertion than the plan scripts — a Boss/brainstorm adjudication).
**Suggested next step:** Boss adjudicates the spec/plan accounting
model — likely: amend the spec + Task 5 (and re-check Task 4's
coincidentally-passing ctx-line assertion) so the proven invariant is
global leak-freedom (`live==0` across the ctx readback AND the g_rc
atexit line, or `allocs_total==frees_total` summed) for *both* modes,
rather than per-line ctx `allocs==frees` (which only holds for `own`
by the accident that own-decs happen inside the forwarder call). Then
re-dispatch with `task_range:[5,7]` against the amended plan. Tasks
14 are sound and GREEN; the working tree (including the failing
Task-5 test as evidence) is left intact per the M2.1 precedent.
## Files touched
Modified:
- `crates/ailang-check/src/lib.rs` (Task 2: gate predicate + message)
- `crates/ailang-check/tests/embed_export_gate.rs` (Tasks 1,2)
- `crates/ailang-codegen/src/lib.rs` (Task 3: llvm_scalar +
fn_scalar_sig comment)
- `crates/ailang-codegen/tests/embed_staticlib_lowering.rs` (Task 3)
- `examples/embed_export_adt_ret_rejected.ail` (Task 2: re-point)
Created:
- `crates/ailang-codegen/tests/embed_record_layout_pin.rs` (Task 1)
- `examples/embed_record_layout_carrier.ail` (Task 1)
- `examples/embed_export_record_ok.ail` (Task 2)
- `examples/embed_export_str_field_record_rejected.ail` (Task 2)
- `examples/embed_export_list_field_record_rejected.ail` (Task 2)
- `examples/embed_export_multictor_rejected.ail` (Task 2)
- `examples/embed_backtest_step_record.ail` (Task 3)
- `examples/embed_backtest_step_record_borrow.ail` (Task 3)
- `crates/ail/tests/embed/record_roundtrip.c` (Task 4/5)
- `crates/ail/tests/embed_record_e2e.rs` (Task 4 GREEN +
Task 5 RED-evidence)
## Boss adjudication (2026-05-18 — M2.1-precedent class)
The orchestrator's Task-5 BLOCK was correct and well-reasoned, and
its diagnosis was independently verified by the Boss (not
agent-trusted): built+ran both E2E modes by hand, full
`AILANG_RC_STATS` stderr —
- own: ctx `allocs=1000000 frees=1000000 live=0`; g_rc `allocs=1 frees=1 live=0`; exit 0
- borrow: ctx `allocs=1000000 frees=0 live=1000000`; g_rc `allocs=1 frees=1000001 live=-1000000`; exit 0
Both modes are **globally** leak-free (Σallocs = Σfrees = 1,000,001,
Σ`live` = 0) and value-correct. The defect is purely the spec's
proof *instrument*: §"Testing strategy" items 3/4 + §"Coherent stop"
asserted the single `ailang_ctx_free` ctx readback shows
`allocs == frees`, which is false **by M2's shipped TLS-ctx design**
(ctx bound to `__ail_tls_ctx` only for the synchronous forwarder
call; the host's `make_state`/`dec` run outside it → `g_rc_*`). The
roadmap's own coherent-stop wording is "a record in/out with
**alloc==free** across many calls" = *global*, exactly what holds.
Adjudicated as a **Boss spec-consistency repair, not a brainstorm
bounce** — M2.1-precedent class (genuine spec defect; deliverable +
substantive invariant sound; only verification mechanics
mis-specified; first BLOCKED task in M3, not 2+-in-a-family). The
repair, folded into this partial (M2.1 `c9a84b3` shape):
1. Spec amended (dated repair note + §"Testing strategy" 3/4 +
§"Coherent stop"): proof model → **global leak-freedom**
(Σallocs == Σfrees across *all* `ailang_rc_stats:` lines,
Σ`live` = 0, exit 0) for both modes; the M2-TLS cross-attribution
documented as correct behaviour, not a leak. Strictly stronger
and honest. No fresh grounding-check (removes an over-strong
measurement assumption, adds none about compiler behaviour).
2. Plan amended (Task 4/5 + the harness step): the harness sums
*every* `ailang_rc_stats:` line, not just the first.
3. Code repair applied (mechanical, decided-invariant): the
`build_link_run_embed` parse now sums all stat lines; T4/T5
docstrings + assertion messages corrected to the global model.
Verified GREEN by the Boss: `embed_record_e2e` 2/2 (own+borrow),
gate 10/10, byte-pin 1/1, forwarder 3/3; regression-green set
(`embed_e2e`, `embed_staticlib_cli`, `embed_export_hash_stable`,
`design_schema_drift`, `docs_honesty_pin`) unmodified-green.
Plan-pseudo-code defects the orchestrator self-corrected inline
(`feedback_plan_pseudo_vs_reality` class — `(app Ctor …)` →
`(term-ctor …)` construction, library-`print` non-resolution,
module==file-stem) are noted in Concerns; a planner Step-5 scrub
item is warranted (recorded by the Boss outside this journal).
audit owns the bench + architect milestone-close (spec Testing
items 8/9). This run asserts only the test-suite GREEN state.
## Re-dispatch (2026-05-18 — `task_range:[6,7]`, Tasks 67 GREEN)
Re-dispatched from HEAD `d5c565d` (the committed PARTIAL 5/7 + Boss
spec-defect repair). Tasks 15 NOT re-implemented or re-verified.
- iter embedding-abi-m3.1.6 (DONE): the substantive freeze.
DESIGN.md §"Embedding ABI" — both "provisional until M3" sentences
rewritten to one-way-freeze wording (`:2280-2282` → embedding ABI
accepts `Int`/`Float`/single-ctor record + "**Frozen as of M3**";
`:2293-2295` → "**frozen as of M3** (see "Frozen value layout"
below)") and the new `### Frozen value layout (M3 — one-way
commitment)` SSOT subsection inserted before `## Data model` (byte
table `p-8` header / `p+0` tag / `p+8+i*8` fields, size `8+n*8`,
construction/ownership/free contract). Lockstep `// FROZEN ABI`
pointers added at the three independent encoders: `runtime/rc.c`
layout comment block, `match_lower.rs` `lower_ctor` `:107` size
site, `drop.rs` `:88` tag-load + `:113` field-offset. Three stale
rustdocs corrected — the two plan-named (`codegen/lib.rs:178`,
`ailang-core/ast.rs:211`) PLUS the Concerns carry-forward
`ailang-check/lib.rs:448-450` ("scalar-only…every ADT rejected" →
frozen-as-of-M3, Boss constraint 2). docs_honesty_pin GREEN
baseline AND after the rewrite (5/5 both); the pinned line
("Export parameters are written **bare**…") shifted `:2297`→`:2299`
but stays byte-verbatim+contiguous — the pin asserts content not
line, so pin-safe (planner Step-5 item-6 family honoured). Step-7
enforceability: perturbed `match_lower.rs` `size_bytes` `8`→`16`
(the unanchored sed-class literal hit both `:107` and `:400`; both
feed the byte-pin through `lower_ctor`'s heap path), byte-pin went
RED (`0 passed; 1 failed`, IR emitted `i64 32` not `i64 24`),
restored via `git checkout -- crates/ailang-codegen/src/match_lower.rs`
(working-tree restore only — main HEAD untouched; the single
legitimate git history-touching op of the run), byte-pin GREEN
again (1/1). The freeze is proven enforceable, not aspirational.
- iter embedding-abi-m3.1.7 (DONE): milestone-close verification
gate (no code change). Regression-green-unmodified set GREEN
(`embed_e2e` 1, `embed_rc_accounting_tsan` 2, `embed_swarm_tsan`
1, `embed_staticlib_cli` 2, `embed_staticlib_alloc_guard` 2,
`print_no_leak_pin` 1; `design_schema_drift` 8/8;
`embed_export_hash_stable` 1/1). Full workspace **639 passed, 0
failed**, zero compile errors; M3 net-new present + GREEN
(`embed_record_layout_pin` 1, `embed_export_gate` 10 = orig 6 + 4,
`embed_staticlib_lowering` 3 = orig 2 + 1, `embed_record_e2e` 2),
nothing pre-existing removed/weakened. `round_trip` 2/2 (new
`.ail` fixtures round-trip; no Form-A surface added).
## Concerns (re-dispatch addendum)
- iter embedding-abi-m3.1.7: PLAN TRANSCRIPTION DEFECT (class:
"plan pseudo-code vs reality" — same family as the iter's existing
`(app Ctor)`→`(term-ctor)` / module==file-stem Concerns). Plan
Task-7 Step 1 names `cargo test -p ailang-check --test
embed_export_hash_stable`, but that test target lives in
**`ailang-core`** (`crates/ailang-core/tests/embed_export_hash_
stable.rs`), not `ailang-check`. The substantive requirement
("no schema field added; hashes/drift byte-unchanged") is
unaffected — ran the pin in its actual crate, 1/1 GREEN. Crate
name corrected at execution; observation not correctness. The
recon "named this exact set" claim was wrong for this one target
(recurring `feedback_grounding_check_misses_insource_tests` /
`feedback_plan_pseudo_vs_reality` family — a planner Step-5 scrub
item, recorded here for Boss visibility).
- iter embedding-abi-m3.1.6: the plan places the Step-5 match_lower.rs
`// FROZEN ABI` comment edit and the Step-7 `git checkout --
match_lower.rs` enforceability restore in the SAME task on the SAME
file. The checkout necessarily reverts the uncommitted Step-5
comment along with the perturbation (it restores to HEAD `d5c565d`,
where neither exists). Re-applied the Step-5 comment after the
enforceability demo so Step 5 stays satisfied — this is a plan
step-ordering wrinkle, not a defect: the freeze demo MUST run on a
tree where the only delta is the perturbation, and the comment is
re-addable post-demo. Recorded for planner-scrub awareness.
## Known debt (re-dispatch addendum)
- `llvm_scalar` identifier still slightly narrower than its post-M3
behaviour (carried from the first dispatch's Concerns) — out of
scope for Tasks 6/7; the doc-comment disambiguates. Unchanged.
- Bench + architect milestone-close (spec Testing items 8/9) remain
the `audit` skill's job — NOT this implement run, as the original
account and Boss adjudication both record. audit is the next
pipeline step.
## Files touched (re-dispatch addendum — Tasks 67)
Modified (Task 6; all uncommitted in the working tree):
- `docs/DESIGN.md` (two "provisional until M3" rewrites + the new
"### Frozen value layout" SSOT subsection)
- `runtime/rc.c` (lockstep `// FROZEN ABI` comment in the layout
block)
- `crates/ailang-codegen/src/match_lower.rs` (`lower_ctor` `:107`
lockstep comment)
- `crates/ailang-codegen/src/drop.rs` (`:88` tag-load + `:113`
field-offset lockstep comments)
- `crates/ailang-codegen/src/lib.rs` (`Target::StaticLib` rustdoc
"provisional until M3" → frozen)
- `crates/ailang-core/src/ast.rs` (`FnDef.export` rustdoc
"provisional until M3" → frozen)
- `crates/ailang-check/src/lib.rs` (`ExportNonScalarSignature`
rustdoc "scalar-only…every ADT rejected" → frozen — Concerns
carry-forward)
Task 7: verification only, no files.
## Stats
bench/orchestrator-stats/2026-05-18-iter-embedding-abi-m3.1.json
@@ -1,97 +0,0 @@
# iter embedding-abi-m3.tidy — close the two M3-audit doc-honesty DRIFT items
**Date:** 2026-05-18
**Started from:** 4fc34705528333de765251be9c26f7bb3182e175
**Status:** DONE
**Tasks completed:** 3 of 3
## Summary
Post-audit docs-honesty tidy that reconciles the two DRIFT items the
M3 milestone-close audit (`docs/journals/2026-05-18-audit-embedding-abi-m3.md`,
committed `b8a60b1`) routed here, in the M2.tidy `[medium]+[low]
doc-honesty → tidy` precedent shape. Two surgical, independent text
edits, zero language/checker/codegen behaviour change. (1) DESIGN.md
§"Embedding ABI" `:2300-2301` — replaced *only* the contradicted
M1-era parenthetical (`modes apply only to heap-shaped types, which
the scalar-only rule above forbids at an export boundary anyway`),
which since the M3 frozen-layout SSOT accepts a single-ctor record
export type contradicted both that freeze and the `(own/borrow
(con T))` mode contract at `:2345-2351`; the replacement states the
present-tense truth and points at the frozen-value-layout SSOT. The
`docs_honesty_pin.rs:135`-pinned sentence ("Export parameters are
written **bare**: a scalar type carries no `own`/`borrow` mode",
`norm()`-whitespace-collapsed) shares physical line `:2300` with the
stale clause — the edit kept every pinned word contiguous; the pin
stayed GREEN (5 passed) across the edit, no RED→restore needed.
(2) `crates/ailang-codegen/src/lib.rs:608-610` — comment-only
honesty fix: the forwarder-body comment claimed the gate guarantees
`Int`/`Float`-only, false post-M3 (a gate-guaranteed single-ctor
record lowers to `ptr`); the IR byte-pin + 3 forwarder-IR pins
stayed byte-identically GREEN before and after, the guard that no
generated code moved. Four standing pins at exact recon baseline
(`docs_honesty_pin` 5, `design_schema_drift` 8,
`embed_record_layout_pin` 1, `embed_staticlib_lowering` 3); full
workspace 639/77 byte-unchanged from the M3-DONE baseline. Diff is
exactly the two intended files. This closes the M3 audit's
`[medium]`+`[low]` doc-honesty drift; bench was already carry-on at
the audit (decisively causally exonerated by byte-identical
generated IR — M3 changed no executable-path codegen; NO ratify).
No audit/fieldtest gate after this tidy — the four pins + the
639/77 baseline ARE the regression coverage (M2.tidy precedent).
## Per-task notes
- iter embedding-abi-m3.tidy.1: reconcile the contradicted DESIGN.md
parenthetical (`[medium]`). Replaced only the parenthetical on
`:2300-2301`; pinned bare-scalar sentence kept verbatim+contiguous
(`docs_honesty_pin` 5 passed, `design_schema_drift` 8 passed).
- iter embedding-abi-m3.tidy.2: honesty-fix the stale forwarder-body
comment (`[low]`). Comment-only edit at `lib.rs:608-610`;
`embed_record_layout_pin` 1 + `embed_staticlib_lowering` 3 passed
byte-identically before and after (no IR delta); full codegen
crate green.
- iter embedding-abi-m3.tidy.3: milestone-close verification gate.
Four standing pins at recon baseline; workspace 639/77 unchanged;
diff exactly two files (DESIGN.md, codegen/src/lib.rs). One
plan-vs-actual grep discrepancy recorded under Concerns (Step 1
pattern collides with the plan's own Task 2 replacement text;
substantive intent satisfied via the discriminating fragment).
## Concerns
- Task 3 Step 1's verification grep pattern `guarantees every param`
is a plan defect: it is a substring of the plan's *own* Task 2
prescribed NEW replacement text (`// The check-side export gate
guarantees every param and`), so it cannot return "no output" as
the plan's Step 1 expects without contradicting Task 2. The
substantive intent of Step 1 — the stale `Int`/`Float`-only
comment is gone — was verified via the discriminating fragments
instead: the stale `check-side gate (Task 4)` marker is ABSENT
(grep exit 1) and the stale `guarantees every param` + immediately
following `and the ret are \`Int\`/\`Float\`; map` two-line claim
is ABSENT (Pzo exit 1), while the new correct M3 comment with
`single-constructor` record framing is PRESENT (`:609`). The
implementation followed Task 2's verbatim replacement exactly as
mandated; the retained words "guarantees every param" are correct
(the gate genuinely still guarantees every param — the M3 change
is *what* it guarantees, not *whether*). No edit was made to
dodge the grep; the plan's pattern is the defect, not the code.
## Known debt
(none — the two audit-flagged drift items are both closed; no
deferred follow-up.)
## Blocked detail
(none — DONE.)
## Files touched
- `docs/DESIGN.md` — §"Embedding ABI" parenthetical replaced (`:2300-2301`)
- `crates/ailang-codegen/src/lib.rs` — `Target::StaticLib` forwarder-body comment honesty (`:608-610`)
## Stats
bench/orchestrator-stats/2026-05-18-iter-embedding-abi-m3.tidy.json
@@ -1,74 +0,0 @@
# iter emit-ir-staticlib — `ail emit-ir --emit=staticlib` (M1 fieldtest spec_gap#2)
**Date:** 2026-05-18
**Started from:** 03493c9b316038f6bfe7f86678457bfa223451a5
**Status:** DONE
**Tasks completed:** 4 of 4
## Summary
Resolves M1 fieldtest [spec_gap]#2 ("no public path emits the
staticlib IR though Decision 5 makes IR readability a first-class
LLM affordance") by adding `--emit=staticlib` to `ail emit-ir`,
symmetric with the M1-shipped `ail build --emit=staticlib`. A
one-line codegen convenience `lower_workspace_staticlib(ws)`
(mirroring `lower_workspace`) routes through the M1-audited,
unchanged `Target::StaticLib` forwarder-emission path; the CLI
arm gains an `emit: String` clap field (symmetric with
`Cmd::Build`'s) and a zero-export guard byte-identical to
`build_staticlib`'s. DESIGN.md is *widened* (never narrowed) with
a present-tense affordance sentence in §"Embedding ABI (M1)" and a
synopsis correction that also fixes a pre-existing M1 omission for
`ail build --emit=staticlib`. No new doc pin (architecture
decision): the new E2E file pins the behaviour; the existing
`docs_honesty_pin` (5 tests) confirmed non-regressed.
## Per-task notes
- iter emit-ir-staticlib.1: codegen — added `pub fn
lower_workspace_staticlib` in `ailang-codegen/src/lib.rs` between
`lower_workspace` and `lower_workspace_inner`; delegates to
`lower_workspace_inner(ws, Gc, Target::StaticLib)`. `cargo build
-p ailang-codegen` clean.
- iter emit-ir-staticlib.2: CLI — RED-first E2E file
`crates/ail/tests/emit_ir_staticlib_cli.rs` (3 tests), observed
RED "1 passed; 2 failed" exactly as predicted; added `emit`
clap field to `Cmd::EmitIr`, destructured it in the arm, branched
the lowering call with the byte-identical zero-export guard
(Steps 3+4 honoured as a single compile unit — no cargo between
the E0027 window). GREEN: 3 passed; `embed_staticlib_cli` 2
passed (build-side symmetry, no regression).
- iter emit-ir-staticlib.3: DESIGN.md — added the present-tense
`--emit=staticlib` affordance sentence after the M1 export
snippet; synopsis: both `emit-ir` and `build` lines gained
`[--emit=staticlib]` (the `build` correction discharges a
pre-existing M1 current-state-honesty gap). `docs_honesty_pin`
5/5 green — no new pin, no regression.
- iter emit-ir-staticlib.4: regression gate — `ail` suite 186
passed / 0 failed; workspace `TOTAL_PASSED=626` = pre-iter 623
+ 3 (exactly the new tests, no pre-existing test lost); scope
check clean (only the 4 in-scope paths).
## Concerns
(none)
## Known debt
(none — scoped feature fully closed; no fixture minted, no doc
pin added, all by explicit plan decision.)
## Blocked detail
(none)
## Files touched
- `crates/ailang-codegen/src/lib.rs` — `lower_workspace_staticlib` convenience
- `crates/ail/src/main.rs` — `Cmd::EmitIr` `emit` clap field + arm branch + zero-export guard
- `crates/ail/tests/emit_ir_staticlib_cli.rs` — new, 3 E2E tests (positive forwarder-IR / zero-export guard / default-exe regression)
- `docs/DESIGN.md` — §"Embedding ABI (M1)" affordance sentence + CLI synopsis (both lines)
## Stats
bench/orchestrator-stats/2026-05-18-iter-emit-ir-staticlib.json
@@ -1,73 +0,0 @@
# 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 23). 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
@@ -1,72 +0,0 @@
# iter prose-loop-binders.1 — Form-B `loop` renders binders as a parenthesised init-list on the keyword
**Date:** 2026-05-18
**Started from:** 6533134dab888605c24fb2e50a65fcbf99c1897e
**Status:** DONE
**Tasks completed:** 2 of 2
## Summary
Projection-only single-iteration milestone in `ailang-prose`. The
`Term::Loop` arm of `write_term` (`crates/ailang-prose/src/lib.rs`)
now renders loop binders as a parenthesised init-list on the keyword
line — `loop(acc = 0, i = 1) { … }` — instead of bare
`name = init;` statements inside the body block. The old shape
implied C/Rust re-init-every-iteration semantics; the new shape
mirrors the adjacent (untouched) `Term::Recur` arm's
`enumerate()` + `", "`-separator idiom and renders init expressions
at `level` (they sit on the keyword line, not the indented block).
RED-first: two committed `examples/*.prose.txt` snapshot fixtures
plus two `check_snapshot`-calling `#[test]` fns were added and
verified failing against the old renderer before the arm rewrite
made them pass. Loop/recur AST, Form-A, JSON-AST, typecheck,
codegen and the Form-A↔JSON round-trip invariant are untouched by
construction (single render-arm body rewrite, no signature change,
no caller touched).
## Per-task notes
- iter prose-loop-binders.1.1: RED — created
`examples/loop_sum_to_run.prose.txt` (multi-binder + non-recur
exit) and `examples/loop_forever_build.prose.txt` (single-binder
recur-only) with the exact Option-A bytes from the plan; appended
`snapshot_loop_sum_to_run` + `snapshot_loop_forever_build` after
`snapshot_ordering_match` in `tests/snapshot.rs`. `cargo test -p
ailang-prose snapshot_loop``0 passed; 2 failed; 8 filtered
out` (filter resolved to exactly the two new tests; both panic
with the expected `prose snapshot mismatch` message). RED
confirmed.
- iter prose-loop-binders.1.2: GREEN — rewrote the `Term::Loop`
arm at `lib.rs:938-955` verbatim per the plan (parenthesised
init-list, init at `level`, stale "not yet designed" comment
removed). `cargo test -p ailang-prose snapshot_loop``2
passed; 0 failed`; full `ailang-prose` suite → `10 passed; 0
failed` (no pre-existing snapshot regressed); both `ail prose |
diff` CLI byte-matches → `MATCH`; post-cleanup `git status
--porcelain` is EXACTLY the four expected paths.
## Concerns
(none)
## Known debt
(none — projection-only single-arm rewrite, fully gated by
whole-file byte-equality snapshots + live-CLI diff)
## Files touched
- `crates/ailang-prose/src/lib.rs``Term::Loop` arm of
`write_term` rewritten (single hunk, lines 938-953)
- `crates/ailang-prose/tests/snapshot.rs` — two `#[test]` fns
appended after `snapshot_ordering_match`
- `examples/loop_sum_to_run.prose.txt` — new committed snapshot
- `examples/loop_forever_build.prose.txt` — new committed snapshot
## Blocked detail
(n/a — Status DONE)
## Stats
bench/orchestrator-stats/2026-05-18-iter-prose-loop-binders.1.json

Some files were not shown because too many files have changed in this diff Show More