# ctt.1 — Env-overlay shape ratification — Implementation Plan > **Parent spec:** `docs/specs/2026-05-12-ct-tidy.md` > > **For agentic workers:** REQUIRED SUB-SKILL: use `skills/implement` > to run this plan. Steps use `- [ ]` checkboxes for tracking. **Goal:** Anchor the `env.types` / `env.ctor_index` split decision in `docs/DESIGN.md`, pin the `DuplicateCtor` consumer the spec's asymmetry-with-mono-side argument rests on, and close two roadmap todos. **Architecture:** One new behavioural-pin integration test under `crates/ailang-check/tests/duplicate_ctor_pin.rs` exercising `CheckError::DuplicateCtor` via the public `check_workspace` entry point. One DESIGN.md top-level section insertion (`## Env construction`). Two strike-out edits in `docs/roadmap.md`. No production-code edits. **Tech Stack:** `ailang-check` (`check_workspace`, `Diagnostic` shape), `ailang-core` (`Module` / `TypeDef` / `Ctor` / `Workspace` inline construction via `SCHEMA`). **Files this plan creates or modifies:** - Create: `crates/ailang-check/tests/duplicate_ctor_pin.rs` — pinning test for `CheckError::DuplicateCtor` emitted by the per-module overlay rebuild at `crates/ailang-check/src/lib.rs:1366-1376`. - Modify: `docs/DESIGN.md:1957-1959` — insert new top-level section `## Env construction` between `## Convention: qualified cross-module references` and `## Data model`. - Modify: `docs/roadmap.md:79` — strike `[ ]` → `[x]` on the "`types` / `ctor_index` overlay shape question" todo. - Modify: `docs/roadmap.md:183` — strike `[ ]` → `[x]` on the "`check_in_workspace` per-module overlay narrowing" todo. --- ### Task 1: Pinning test for `CheckError::DuplicateCtor` **Files:** - Create: `crates/ailang-check/tests/duplicate_ctor_pin.rs` This task pins existing production behaviour — the `DuplicateCtor` variant is emitted by the per-module rebuild at `crates/ailang-check/src/lib.rs:1366-1376` and surfaced as a `Diagnostic` with `code == "duplicate-ctor"` via the `CheckError::code()` arm at `lib.rs:610`. No production code changes; the test catches a future regression that silently removes the per-module `env.ctor_index` rebuild. The TDD discipline an implementer might apply (write the test first, expect it to fail) does not map cleanly to a behavioural-pin test: the behaviour already exists. The expected outcome on first run is PASS. If the test fails on first run, that is itself a finding — it means either the inline-fixture is malformed or the production-code path has drifted from its documented shape. - [ ] **Step 1: Write the pinning test file** Path: `crates/ailang-check/tests/duplicate_ctor_pin.rs` ```rust //! Pin for `CheckError::DuplicateCtor` — same-named ctors in two //! different ADTs within one module. Ratifies the load-bearing //! consumer of the per-module `env.ctor_index` rebuild in //! `check_in_workspace` that DESIGN.md §"Env construction" rests //! on as the asymmetry-with-mono-side argument. If the //! per-module rebuild is ever removed without an equivalent //! replacement path, this test goes red. use ailang_check::check_workspace; use ailang_core::ast::{Ctor, Def, Module, TypeDef}; use ailang_core::workspace::{Registry, Workspace}; use ailang_core::SCHEMA; use std::collections::BTreeMap; #[test] fn duplicate_ctor_across_two_adts_in_one_module() { let m = Module { schema: SCHEMA.into(), name: "M".into(), imports: vec![], defs: vec![ Def::Type(TypeDef { name: "Cat".into(), vars: vec![], ctors: vec![Ctor { name: "Twins".into(), fields: vec![], }], doc: None, drop_iterative: false, }), Def::Type(TypeDef { name: "Dog".into(), vars: vec![], ctors: vec![Ctor { name: "Twins".into(), fields: vec![], }], doc: None, drop_iterative: false, }), ], }; let mut modules = BTreeMap::new(); modules.insert(m.name.clone(), m.clone()); let ws = Workspace { entry: m.name.clone(), modules, root_dir: std::path::PathBuf::from("."), registry: Registry::default(), }; let diags = check_workspace(&ws); let dup = diags .iter() .find(|d| d.code == "duplicate-ctor") .unwrap_or_else(|| { panic!( "expected at least one `duplicate-ctor` diagnostic, got: {diags:?}" ) }); assert!( dup.message.contains("Twins"), "expected message to name the duplicate ctor `Twins`, got: {}", dup.message ); assert!( dup.message.contains("Cat") && dup.message.contains("Dog"), "expected message to name both ADTs `Cat` and `Dog`, got: {}", dup.message ); } ``` - [ ] **Step 2: Run the test, confirm PASS** Run: `cargo test --workspace -p ailang-check --test duplicate_ctor_pin` Expected: `test duplicate_ctor_across_two_adts_in_one_module ... ok` (1 passed; 0 failed). If the test fails: the production-code path has drifted from its documented shape. Inspect `crates/ailang-check/src/lib.rs:1366-1376`; the in-band `DuplicateCtor` emission is the load-bearing consumer the test pins. Drift here invalidates the spec's asymmetry-with-mono-side argument and is itself a finding — pause the iter and surface it. - [ ] **Step 3: Confirm full workspace test suite stays green** Run: `cargo test --workspace` Expected: all existing tests pass (no regression). The new test adds one passing case; no existing test exercises the two-`Def::Type`-same-ctor shape, so no test changes its outcome. --- ### Task 2: DESIGN.md `## Env construction` section **Files:** - Modify: `docs/DESIGN.md:1957-1959` Inserts a new top-level section between the existing `## Convention: qualified cross-module references` (DESIGN.md:1941) and `## Data model` (DESIGN.md:1959). The initial body is three paragraphs: one naming the split, one stating the rationale (owning-vs-reverse-index roles), one documenting the check-side / mono-side asymmetry as intentional. - [ ] **Step 1: Insert the new section** Apply this exact Edit to `docs/DESIGN.md`: `old_string`: ``` Hash stability: no new AST node, no renamed fields — all previous module hashes stay bit-identical. ## Data model ``` `new_string`: ``` Hash stability: no new AST node, no renamed fields — all previous module hashes stay bit-identical. ## Env construction The check environment carries two parallel per-module overlays for ADT definitions: `env.types: IndexMap` is the **owning index** — it stores the full definition keyed by bare type name. `env.ctor_index: IndexMap` is the **reverse index** — it maps each constructor name back to its owning type, so ctor-by-name lookups run in O(1) rather than searching every type's ctor list. The split is not a refactor wart. The two maps have distinct semantic roles: an owning data index, and a derived lookup accelerator over the same data. Collapsing them into one variant-keyed map (`Map`) would force every type-iterator and every ctor-lookup site to discriminate the variant tag, reversing the optimisation the reverse index exists to provide. The check-side per-module rebuild of both overlays at `crates/ailang-check/src/lib.rs:1342-1384` is intentionally retained: the in-band `DuplicateCtor` diagnostic at the same site consumes the rebuilt per-module `env.ctor_index`. The mono-side overlay was narrowed to types-only at iter ct.3.2 because its consumer is the runtime ctor lookup, which became type-driven post-ct.2.2 — a different consumer story, hence a different overlay shape. The asymmetry between the check side and the mono side is by design and is pinned by `crates/ailang-check/tests/duplicate_ctor_pin.rs`. ## Data model ``` - [ ] **Step 2: Verify section presence and ordering** Run: `grep -n '^## ' docs/DESIGN.md | grep -E 'Convention: qualified|Env construction|Data model'` Expected: three lines, in this order (line numbers approximate; the relative ordering is what matters): ``` 1941:## Convention: qualified cross-module references 1959:## Env construction 1989:## Data model ``` The "Env construction" heading must appear between the other two. --- ### Task 3: roadmap.md strikes **Files:** - Modify: `docs/roadmap.md:79` - Modify: `docs/roadmap.md:183` Closes two P2 `[todo]` lines now superseded by the DESIGN.md anchor in Task 2 and the pinning test in Task 1. The strike follows the roadmap's `[ ]` → `[x]` convention; per `docs/roadmap.md`'s own header guidance, "a finished entry may stay briefly for context, then is removed (with a one-line mirror in `docs/journals/`)". The mirror lands in the per-iter journal at iter close. - [ ] **Step 1: Strike the "overlay shape question" todo** Apply this exact Edit to `docs/roadmap.md`: `old_string`: ``` - [ ] **\[todo\]** `types` / `ctor_index` overlay shape question — decide whether the env's two parallel ctor maps should collapse into one overlay, or stay split. Surfaced during the env-construction unify audit. - context: JOURNAL 2026-05-10 ("Audit close: env-construction unify") ``` `new_string`: ``` - [x] **\[todo\]** `types` / `ctor_index` overlay shape question — decide whether the env's two parallel ctor maps should collapse into one overlay, or stay split. Surfaced during the env-construction unify audit. - context: JOURNAL 2026-05-10 ("Audit close: env-construction unify"); closed by iter ctt.1 — DESIGN.md §"Env construction" anchors the split decision. ``` - [ ] **Step 2: Strike the "check_in_workspace per-module overlay narrowing" todo** Apply this exact Edit to `docs/roadmap.md`: `old_string`: ``` - [ ] **\[todo\]** `check_in_workspace` per-module overlay narrowing — `crates/ailang-check/src/lib.rs:1234` still clears+rebuilds `env.ctor_index` per-module; ct.3.2 narrowed the analogous mono overlay to types-only. The typecheck-side overlay's `env.ctor_index` half serves the duplicate-detection diagnostic at workspace-build time, not the runtime ctor lookup (which is type-driven post-ct.2.2). Narrowing is mechanical but needs a careful read to confirm no other consumer survives. - context: JOURNAL 2026-05-11 ("Iteration ct.4") — milestone close follow-up. ``` `new_string`: ``` - [x] **\[todo\]** `check_in_workspace` per-module overlay narrowing — `crates/ailang-check/src/lib.rs:1234` still clears+rebuilds `env.ctor_index` per-module; ct.3.2 narrowed the analogous mono overlay to types-only. The typecheck-side overlay's `env.ctor_index` half serves the duplicate-detection diagnostic at workspace-build time, not the runtime ctor lookup (which is type-driven post-ct.2.2). Narrowing is mechanical but needs a careful read to confirm no other consumer survives. - context: JOURNAL 2026-05-11 ("Iteration ct.4") — milestone close follow-up; closed by iter ctt.1 — recon confirmed the rebuild is the load-bearing consumer of in-band DuplicateCtor (pinned by `crates/ailang-check/tests/duplicate_ctor_pin.rs`); the asymmetry with the mono side is intentional. ``` - [ ] **Step 3: Verify both strikes** Run: `grep -nE '^- \[x\] \*\*\\\[todo\\\]\*\* (`types`|`check_in_workspace`)' docs/roadmap.md` Expected: two lines, both with `[x]`: ``` 79:- [x] **\[todo\]** `types` / `ctor_index` overlay shape question — 183:- [x] **\[todo\]** `check_in_workspace` per-module overlay narrowing — ``` - [ ] **Step 4: Workspace-wide regression check** Run: `cargo test --workspace` Expected: all tests pass, including the new pinning test from Task 1. The bench scripts (`bench/check.py`, `bench/compile_check.py`, `bench/cross_lang.py`) are not run per-iter at the plan level — they are run by the audit skill at milestone close. ctt.1 makes zero codegen-relevant edits, so no bench movement is plausibly attributable to it; the milestone-close audit confirms. --- ## Acceptance criteria recap (from spec) - `docs/DESIGN.md` carries the new `## Env construction` section with the three-paragraph initial body. ✓ Task 2. - A new pinning test in `crates/ailang-check/tests/` asserts `code == "duplicate-ctor"` for a one-module fixture with two `Def::Type`s declaring same-named ctors, plus message-substring match for the ctor name and both ADT names. ✓ Task 1. - Both relevant roadmap todos struck `[x]` with a one-line forward reference to this iter. ✓ Task 3. - `cargo test --workspace` green. ✓ Task 1 Step 3 and Task 3 Step 4.