# prep.2 — Term::New construct — Implementation Plan

> **Parent spec:** `docs/specs/0052-kernel-extension-mechanics.md` (Iteration prep.2 section, lines 232-356)
> **Gitea issue:** refs #32 (the implementer-side iter-completion commit closes via `closes #32`)
>
> **For agentic workers:** REQUIRED SUB-SKILL: use `skills/implement`
> to run this plan. Steps use `- [ ]` checkboxes for tracking.

**Goal:** Ship the `new` Form-A keyword + `Term::New { type_name, args: Vec<NewArg> }` AST variant + `NewArg::Type|Value` enum end-to-end through schema/surface/checker, with two new diagnostics, a workspace-wide bare→qualified rewrite extension (piggybacking on prep.1's `qualify_workspace_term`), and drift/spec anchors. Codegen is deliberately out of scope — the construct is verified at `ail check` cleanness.

**Architecture:** A new Term variant Term::New holds an unqualified type-name plus a heterogeneous arg vector (`NewArg::Type` or `NewArg::Value`). The prep.1 pre-pass `qualify_workspace_term` gains a `Term::New` arm that rewrites bare `type_name` to qualified `<home>.<type>` form, mirroring the existing `Term::Ctor` arm — so the synth pass sees qualified Types. The checker elaborates `Term::New` via type-scoped lookup → home module → find `new` def → arg-count + arg-kind check → forall binding. The variant addition is compile-forced across 24 files (44+ exhaustive `match … { Term::Lit | … }` sites); Task 1 walks all of them with a small set of mechanical arm patterns. Tasks 2-4 layer surface, checker, and schema-anchor work on top.

**Tech stack:** `crates/ailang-core` (AST + serde + drift tests), `crates/ailang-surface` (lex/parse/print + round-trip), `crates/ailang-check` (synth elaboration + diagnostics + pre-pass arm + 21+ analysis-walker arms), `crates/ailang-codegen` (error arms — codegen is out of scope), `crates/ailang-prose`, `crates/ail/src/main.rs`, and the spec/contract anchors `design/contracts/0002-data-model.md` + `crates/ailang-core/specs/form_a.md`.

**Out of scope (deferred per spec §50-55, §327-330, §687-700):**

- Codegen lowering of `Term::New` — codegen arms return `CodegenError::Internal(...)`. Real codegen lands in the `raw-buf` milestone.
- `param-in` enforcement on the type-arg of `Term::New` — prep.3.
- Kernel-tier module auto-import — prep.3.
- Hash-pin refreshes — additive variant, no existing fixture emits `"t": "new"`.

**Files this plan creates or modifies:**

- Modify: `crates/ailang-core/src/ast.rs` — add `Term::New { type_name, args }` to the Term enum (line 405); add `pub enum NewArg { Type(Type), Value(Term) }` with tagged serde repr (after line 565).
- Modify: `crates/ailang-core/src/desugar.rs` — 9 exhaustive Term matches gain `Term::New` arms (Pattern A or B per site).
- Modify: `crates/ailang-core/src/workspace.rs` — 2 walker sites gain `Term::New` arms (Pattern A).
- Modify: `crates/ailang-check/src/lib.rs` — `substitute_rigids_in_term` (line 181, Pattern B), `verify_tail_positions` (line 2749, Pattern A), `verify_loop_body` (line 2862, Pattern A), `synth` Term-match (line ~3005, Pattern E stub in Task 1 → real elaboration in Task 3), `synth`'s "what was here" descriptor (line ~3886, Pattern F), `qualify_workspace_term` (line 4155, Pattern C).
- Modify: `crates/ailang-check/src/lib.rs` — add `CheckError::NewTypeNotConstructible` + `CheckError::NewArgKindMismatch` variants (Task 3, after `TypeScopedReceiverNotAType` at line ~705); add to `code()` (line ~759), `ctx()` (line ~837), and message-format arms.
- Modify: `crates/ailang-check/src/lift.rs` — 3 exhaustive Term matches gain `Term::New` arms (Pattern A).
- Modify: `crates/ailang-check/src/linearity.rs` — 5 Term matches (Pattern A or specific per arm).
- Modify: `crates/ailang-check/src/mono.rs` — 2 sites (Pattern A walker + Pattern B rewrite).
- Modify: `crates/ailang-check/src/pre_desugar_validation.rs` — 1 site (Pattern A).
- Modify: `crates/ailang-check/src/reuse_shape.rs` — 2 sites (Pattern A).
- Modify: `crates/ailang-check/src/uniqueness.rs` — 2 sites (Pattern A).
- Modify: `crates/ailang-codegen/src/lib.rs` — `lower_term` + `synth_with_extras` (Pattern D — error).
- Modify: `crates/ailang-codegen/src/escape.rs` — 3 sites (Pattern A).
- Modify: `crates/ailang-codegen/src/lambda.rs` — 1 site (Pattern A).
- Modify: `crates/ailang-surface/src/parse.rs` — head-keyword dispatcher (~line 1227), `is_term_head_kw` (~line 1610), new `parse_new` method (Task 2).
- Modify: `crates/ailang-surface/src/print.rs` — `write_term` (line 420-597) gains `Term::New` arm (Task 2).
- Modify: `crates/ailang-prose/src/lib.rs` — 3 sites (Pattern A — walker / printer per site).
- Modify: `crates/ail/src/main.rs` — `walk_term` + rewrite_term inner (Pattern A).
- Modify: `crates/ailang-core/tests/design_schema_drift.rs` — exhaustive Term match arm (Task 1, Pattern F) + new `term_new_round_trips` test (Task 4).
- Modify: `crates/ailang-core/tests/schema_coverage.rs` — `VariantTag::TermNew` enum entry; `EXPECTED_TAGS` array gains the entry; `visit_term` arm.
- Modify: `crates/ailang-core/tests/spec_drift.rs` — exhaustive Term match arm + exemplar row + form_a.md anchor assertion (Task 4 covers the anchor add).
- Modify: `crates/ail/tests/codegen_import_map_fallback_pin.rs` — `contains_xmod_show_var` Term match (Pattern A).
- Modify: `crates/ailang-surface/tests/round_trip.rs` — Task 2: add `(new T <type-arg> <value-arg>)` fixture.
- Modify: `design/contracts/0002-data-model.md` — Task 4: add `{ "t": "new", "type": "<id>", "args": [NewArg...] }` to the JSON-tag block (lines 107-178 area) + add the `NewArg` shape definition.
- Modify: `crates/ailang-core/specs/form_a.md` — Task 4: add `(new TYPE-NAME ARG*)` to the surface-keyword cheatsheet (lines 289-298 area).
- Test (in-source `#[cfg(test)] mod tests` in `crates/ailang-check/src/lib.rs`): three new tests — `new_resolves_via_type_scope`, `new_type_not_constructible`, `new_arg_kind_mismatch_value_where_type` (Task 3).

---

## Task 1: AST variant + NewArg + workspace-wide compile-forced arms

**Files:** see "Files this plan creates or modifies" above — every entry except the surface-parse/print sites (Task 2), the synth real-logic arm (Task 3), and the spec/contract anchors (Task 4). Roughly 20 production files + 4 test files.

**Six arm patterns** (each new `Term::New` arm matches one of these):

```rust
// Pattern A — walker that recurses into sub-terms. Most analysis
// passes use this. NewArg::Type carries no value and contributes
// nothing.
Term::New { args, .. } => {
    for arg in args {
        match arg {
            NewArg::Value(v) => /* the function's recursion call on v */,
            NewArg::Type(_) => {}
        }
    }
}
```

```rust
// Pattern B — Term-reconstruction (substitution, desugar map).
// NewArg::Type may carry a Type that itself needs rewriting.
Term::New { type_name, args } => Term::New {
    type_name: type_name.clone(),
    args: args.iter().map(|arg| match arg {
        NewArg::Value(v) => NewArg::Value(/* recursive rewrite of v */),
        NewArg::Type(t)  => NewArg::Type(/* recursive rewrite of t — or t.clone() if the fn doesn't touch Types */),
    }).collect(),
}
```

```rust
// Pattern C — bare→qualified rewrite. Used by qualify_workspace_term
// ONLY. Mirrors the existing Term::Ctor arm at lib.rs:4187.
Term::New { type_name, args } => {
    if !type_name.contains('.')
        && !ailang_core::primitives::is_primitive_name(type_name)
        && !own_local_types.contains_key(type_name)
    {
        if let Some(home) = module_types
            .iter()
            .find_map(|(m, types)| if types.contains_key(type_name) { Some(m.clone()) } else { None })
        {
            *type_name = format!("{home}.{type_name}");
        }
    }
    for arg in args {
        match arg {
            NewArg::Value(v) => qualify_workspace_term(v, own_local_types, module_types),
            NewArg::Type(t)  => *t = qualify_workspace_types(t, own_local_types, module_types),
        }
    }
}
```

```rust
// Pattern D — codegen error stub. Real codegen for Term::New lands
// in the raw-buf milestone.
Term::New { .. } => Err(CodegenError::Internal(
    "Term::New requires the type's `new` def to be desugared first; \
     codegen does not yet support Term::New directly — milestone raw-buf".into()
))
```

```rust
// Pattern E — synth elaboration STUB (Task 1 only). Task 3 replaces
// this with the real per-spec elaboration logic.
Term::New { .. } => Err(CheckError::Internal(
    "Term::New elaboration not yet implemented — prep.2 Task 3".into()
))
```

```rust
// Pattern F — small descriptor (e.g. "what was here" in synth's
// inner error, or the exhaustive name-match in design_schema_drift.rs's
// test).
Term::New { .. } => "new"
```

**Per-site assignment table:**

| File | Line / fn name | Pattern | Notes |
|---|---|---|---|
| `crates/ailang-core/src/ast.rs` | Term enum body (~line 405) | — | ADD variant: `New { type_name: String, args: Vec<NewArg> }`. Carries `#[serde(rename = "type")]` on `type_name` per the spec's `"type": "<id>"` byte shape; no rename on `args` (default snake_case matches `"args"`). |
| `crates/ailang-core/src/ast.rs` | (after Term, before LoopBinder) | — | ADD `pub enum NewArg { Type(Type), Value(Term) }` with `#[derive(Debug, Clone, Serialize, Deserialize)] #[serde(tag = "kind", content = "value", rename_all = "lowercase")]`. The `tag = "kind", content = "value"` lands the spec's `{"kind": "type", "value": ...}` shape exactly. |
| `crates/ailang-core/src/ast.rs` | in-source round-trip tests (~line 920+, ~line 953+) | A + B (test helpers) | ADD `Term::New` round-trip test using the standard pattern (build a Term::New, serialise to JSON, assert deserialise round-trips). The exhaustive matches inside test helpers also need arms. |
| `crates/ailang-core/src/desugar.rs` | `collect_used_in_term` (~line 283) | A | recurse over NewArg::Value with the function's collector |
| `crates/ailang-core/src/desugar.rs` | `Desugarer::desugar_term` (~line 467) | B | reconstruct Term::New; desugar each NewArg::Value |
| `crates/ailang-core/src/desugar.rs` | `free_vars_in_term` (~line 1136) | A | union free vars over NewArg::Value |
| `crates/ailang-core/src/desugar.rs` | `subst_var` (~line 1263) | B | substitute through each NewArg::Value |
| `crates/ailang-core/src/desugar.rs` | peel walker (~line 1414) | A | walker semantics — recurse over NewArg::Value |
| `crates/ailang-core/src/desugar.rs` | `subst_call_with_extras` (~line 1432) | B | substitute, reconstruct |
| `crates/ailang-core/src/desugar.rs` | `find_non_callee_use` (~line 1557) | A | binder-use walker — recurse over NewArg::Value |
| `crates/ailang-core/src/desugar.rs` | `any_nested_ctor` (~line 1599) | A | recurse, return `false` for NewArg::Type |
| `crates/ailang-core/src/desugar.rs` | `any_let_rec` (~line 1657) | A | recurse |
| `crates/ailang-core/src/desugar.rs` | `any_lit_pattern` or similar (~line 2783) | A | recurse |
| `crates/ailang-core/src/workspace.rs` | `walk_term_embedded_types` (~line 1205) | A | walk each NewArg::Type via `walk_type`; recurse into NewArg::Value |
| `crates/ailang-core/src/workspace.rs` | `walk_term` (~line 1337) | A | recurse over NewArg::Value |
| `crates/ailang-check/src/lib.rs` | `substitute_rigids_in_term` (~line 181) | B | substitute through NewArg::Value; apply `substitute_rigids` to NewArg::Type |
| `crates/ailang-check/src/lib.rs` | `verify_tail_positions` (~line 2749) | A | recurse over NewArg::Value with non-tail semantics |
| `crates/ailang-check/src/lib.rs` | `verify_loop_body` (~line 2862) | A | recurse with `in_loop_tail=false` for arg subterms |
| `crates/ailang-check/src/lib.rs` | `synth` Term match (~line 3005) | E (stub) | Task 3 replaces with real logic |
| `crates/ailang-check/src/lib.rs` | "what was here" descriptor (~line 3886) | F | `Term::New { .. } => "new"` |
| `crates/ailang-check/src/lib.rs` | `qualify_workspace_term` (line 4155) | C | THE prep.1 pre-pass partner — full bare→qualified arm |
| `crates/ailang-check/src/lift.rs` | 3 exhaustive matches | A | recurse over NewArg::Value |
| `crates/ailang-check/src/linearity.rs` | `walk` (~line 427) | A | walk(NewArg::Value, Position::Consume) for each value arg |
| `crates/ailang-check/src/linearity.rs` | `any_sub_binder_consumed_for` (~line 880) | A | recurse |
| `crates/ailang-check/src/linearity.rs` | `ctor_uses_any_binder` (~line 1046) | A | recurse |
| `crates/ailang-check/src/linearity.rs` | `term_mentions_any_binder` (~line 1098) | A | recurse |
| `crates/ailang-check/src/linearity.rs` | joined pattern at ~line 1102 | A — separate arm | If the existing joined pattern (`Term::Ctor | Term::Do | Term::Recur`) covers Term::New's semantics, add to the joined arm; otherwise separate. Default: separate arm with recurse-over-args. |
| `crates/ailang-check/src/mono.rs` | walker at ~line 1123 | A | recurse |
| `crates/ailang-check/src/mono.rs` | rewrite at ~line 1520 | B | reconstruct |
| `crates/ailang-check/src/pre_desugar_validation.rs` | exhaustive match (~line 71) | A | recurse |
| `crates/ailang-check/src/reuse_shape.rs` | walker (~line 127) | A | recurse |
| `crates/ailang-check/src/reuse_shape.rs` | rewrite (~line 466) | B | reconstruct |
| `crates/ailang-check/src/uniqueness.rs` | walker (~line 153) | A | recurse over NewArg::Value with App-like ownership semantics |
| `crates/ailang-check/src/uniqueness.rs` | walker (~line 233) | A | recurse |
| `crates/ailang-codegen/src/lib.rs` | `lower_term` (~line 1525-2068) | D | error |
| `crates/ailang-codegen/src/lib.rs` | `synth_with_extras` (~line 3415-3546) | D | error |
| `crates/ailang-codegen/src/escape.rs` | 3 sites | A | recurse over NewArg::Value (escape analysis) |
| `crates/ailang-codegen/src/lambda.rs` | 1 site (~line 460-510) | A | recurse |
| `crates/ailang-prose/src/lib.rs` | walker / printer (~line 927) | A or Form-B printer | walker arms recurse; Form-B printer emits `new T(args...)` |
| `crates/ailang-prose/src/lib.rs` | (~line 1127) | A | recurse |
| `crates/ailang-prose/src/lib.rs` | (~line 1274) | A | recurse |
| `crates/ail/src/main.rs` | `walk_term` (~line 1430) | A | recurse |
| `crates/ail/src/main.rs` | rewrite_term inner (~line 2676) | A | recurse |
| `crates/ail/tests/codegen_import_map_fallback_pin.rs` | `contains_xmod_show_var` (~line 62) | A | recurse |
| `crates/ailang-core/tests/design_schema_drift.rs` | exhaustive Term match arm (~line 166) | F | `Term::New { .. } => "new"` |
| `crates/ailang-core/tests/schema_coverage.rs` | `VariantTag` enum (~line 29) | — | ADD `TermNew` discriminant |
| `crates/ailang-core/tests/schema_coverage.rs` | `EXPECTED_TAGS` array (~line 84-98) | — | ADD `VariantTag::TermNew` |
| `crates/ailang-core/tests/schema_coverage.rs` | `visit_term` exhaustive match | A — variant | `Term::New { args, .. } => { tags.insert(VariantTag::TermNew); for a in args { match a { NewArg::Type(t) => visit_type(t, tags), NewArg::Value(v) => visit_term(v, tags) } } }` |
| `crates/ailang-core/tests/spec_drift.rs` | exemplar list (~line 90-158) | — | ADD exemplar row for Term::New (asserting `"(new"` anchor in form_a.md) |
| `crates/ailang-core/tests/spec_drift.rs` | exhaustive Term match (~line 135-151) | F | `Term::New { .. } => /* anchor literal */` |

- [ ] **Step 1: Add the `Term::New` variant and `NewArg` enum**

In `crates/ailang-core/src/ast.rs`, add the variant inside the `Term` enum (insert before the closing `}` of the enum at the end; or alphabetically near `Recur` — Boss judgement on ordering, but past-convention is "last-added is at the end"):

```rust
    /// Functional construction: `(new T arg+)` calls the `new` def in
    /// `T`'s home module with the supplied args. Each arg is either a
    /// Type or a Value (see [`NewArg`]). Resolution: type-scoped
    /// lookup of `type_name` → home module → find `new` def → check
    /// arg-count and arg-kind. See prep.2 of the
    /// kernel-extension-mechanics milestone.
    New {
        #[serde(rename = "type")]
        type_name: String,
        args: Vec<NewArg>,
    },
```

After the `Term` enum closing brace, before the next enum/struct (or near `LoopBinder` at ~line 586), add:

```rust
/// One positional arg to a `(new T args...)` call. Either a `Type`
/// (e.g. `(new Series (con Float) 3)` — first arg is `NewArg::Type`)
/// or a `Value` (e.g. the literal `3` in the same example, a
/// `Term`). Disambiguation at parse-time is by syntactic form: a
/// Type starts with one of the Type-production heads (`con`,
/// `fn-type`, `borrow`, `own`); anything else is a Term.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "kind", content = "value", rename_all = "lowercase")]
pub enum NewArg {
    Type(Type),
    Value(Term),
}
```

- [ ] **Step 2: Run the workspace compile-gate**

Run: `cargo build --workspace 2>&1 | tail -60`
Expected: a cluster of `error[E0004]: non-exhaustive patterns: \`Term::New { .. }\` not covered` errors across the per-site table above. This is the EXPECTED RED state for Task 1.

- [ ] **Step 3: Add the `Term::New` arm to every exhaustive Term match (per-site table)**

For each entry in the per-site table above, edit the file and add the listed Pattern's arm. The patterns are inline in this task; the per-site table assigns one to each. Notes:

- Apply Pattern E (stub) to `synth`'s big Term-match in `lib.rs` — Task 3 replaces with the real elaboration.
- Apply Pattern C (qualify-workspace-term) ONLY to `qualify_workspace_term`. Other sites use Pattern A or B.
- For test files (`schema_coverage.rs`, `spec_drift.rs`), update both the enum/exemplar data AND the exhaustive match arm — they go together.

- [ ] **Step 4: Run the workspace compile-gate again**

Run: `cargo build --workspace 2>&1 | tail -15`
Expected: 0 errors. The variant addition + per-site arms compile cleanly.

- [ ] **Step 5: Run the workspace test suite for regressions**

Run: `cargo test --workspace 2>&1 | tail -30`
Expected: All tests pass. The variant is additive — no existing fixture emits `"t": "new"`, so byte-shapes are unchanged. `schema_coverage.rs` passes because EXPECTED_TAGS gained the new entry AND `visit_term` emits it.

If any test fails, it's because a Pattern was applied incorrectly (e.g. uniqueness.rs arm not properly recursing into NewArg::Value, causing a test that exercises ownership analysis to break). Fix per-site, re-run.

---

## Task 2: Form-A surface — lex/parse + print + round-trip fixture

**Files:**

- Modify: `crates/ailang-surface/src/parse.rs:1227-1257` — head-keyword dispatcher
- Modify: `crates/ailang-surface/src/parse.rs:1610-1631` — `is_term_head_kw` helper
- Modify: `crates/ailang-surface/src/parse.rs` — new `parse_new` method (after `parse_recur` at line ~1687)
- Modify: `crates/ailang-surface/src/print.rs:420-597` — `write_term` Term::New arm
- Modify: `crates/ailang-surface/tests/round_trip.rs` — new fixture exercising `(new T <type> <value>)`

- [ ] **Step 1: Write the failing round-trip test**

Add to `crates/ailang-surface/tests/round_trip.rs` (find the existing fixture pattern; one new fn following the convention):

```rust
#[test]
fn round_trip_term_new_mixed_args() {
    // (new T (con Float) 3) — mixed type-arg + value-arg.
    let form_a = "(module new_demo\n  (data Counter (ctor MkCounter (con Int)))\n  (fn new (type (fn-type (params (con Int)) (ret (con Counter)))) (params n) (body (term-ctor Counter MkCounter n)))\n  (fn main (type (fn-type (params) (ret (con Counter)))) (params) (body (new Counter 42))))\n";
    let module = ailang_surface::parse_module(form_a).expect("parse OK");
    let printed = ailang_surface::print_module(&module);
    assert_eq!(printed.trim(), form_a.trim(),
        "round-trip Form-A → JSON → Form-A must be bit-identical");
}
```

- [ ] **Step 2: Run the test to verify it fails**

Run: `cargo test --workspace -p ailang-surface round_trip_term_new`
Expected: FAIL — `parse_module` returns a `ParseError` because `new` is not a known head keyword.

- [ ] **Step 3: Add `"new"` to the head-keyword dispatcher in parse.rs**

At `crates/ailang-surface/src/parse.rs:1227-1257` (the match block dispatching on the head keyword of a `(<kw> …)` form), add an arm:

```rust
            "new" => self.parse_new()?,
```

between the existing `"recur"` arm and the `other =>` fallthrough. Update the error-message enumeration in the fallthrough at line 1249-1252 to include `new` in the list of known keywords.

- [ ] **Step 4: Add `"new"` to `is_term_head_kw` helper**

At `crates/ailang-surface/src/parse.rs:1610-1631`, the `is_term_head_kw` helper enumerates head keywords that introduce a Term form (used to disambiguate Term vs. Type vs. Pat in contexts like loop binders). Add `"new"` to the matches set:

```rust
fn is_term_head_kw(kw: &str) -> bool {
    matches!(kw,
        "app" | "let" | "letrec" | "if" | "do" | "term-ctor" |
        "match" | "lam" | "seq" | "clone" | "reuse-as" | "loop" |
        "recur" | "new"
    )
}
```

(Adjust the existing list to match — this template assumes the exact set; verify against the function body before editing.)

- [ ] **Step 5: Add the `parse_new` method**

After `parse_recur` (line ~1687), add:

```rust
fn parse_new(&mut self) -> Result<Term, ParseError> {
    // Caller has consumed `(` and `new`. Read the type-name ident,
    // then one-or-more args. Each arg is a Type if its head ident
    // matches the Type-production keyword set (`con`, `fn-type`,
    // `borrow`, `own`); otherwise a Term.
    let type_name = self.expect_ident_for_type_name()?;
    let mut args: Vec<NewArg> = Vec::new();
    loop {
        // Closing paren = end of args.
        if self.peek_close_paren() {
            self.expect_close_paren()?;
            break;
        }
        // Look at the next token: if `(`, peek the head ident to
        // disambiguate Type vs Term. Otherwise (bare token), it's
        // a Term (a literal or var).
        if self.peek_open_paren() {
            // Save state, peek the head ident, restore.
            let head = self.peek_head_ident_after_lparen();
            match head.as_deref() {
                Some("con") | Some("fn-type") | Some("borrow") | Some("own") => {
                    let t = self.parse_type()?;
                    args.push(NewArg::Type(t));
                }
                _ => {
                    let v = self.parse_term()?;
                    args.push(NewArg::Value(v));
                }
            }
        } else {
            // Bare token — must be a Term (literal or var).
            let v = self.parse_term()?;
            args.push(NewArg::Value(v));
        }
    }
    if args.is_empty() {
        return Err(self.error_at_cursor("`(new T ...)` requires at least one arg"));
    }
    Ok(Term::New { type_name, args })
}
```

(The helper names `expect_ident_for_type_name`, `peek_close_paren`, `peek_open_paren`, `peek_head_ident_after_lparen` may not exist verbatim; use the existing parser conventions in `parse.rs`. The implementer adjusts the spelling.)

- [ ] **Step 6: Add the `write_term` arm in print.rs**

At `crates/ailang-surface/src/print.rs:420-597` (`fn write_term`), add an arm before the closing `}`:

```rust
        Term::New { type_name, args } => {
            buf.push_str("(new ");
            buf.push_str(type_name);
            for arg in args {
                buf.push(' ');
                match arg {
                    NewArg::Type(t) => write_type(buf, t),
                    NewArg::Value(v) => write_term(buf, v),
                }
            }
            buf.push(')');
        }
```

- [ ] **Step 7: Run the round-trip test to verify it passes**

Run: `cargo test --workspace -p ailang-surface round_trip_term_new`
Expected: PASS.

- [ ] **Step 8: Run the full surface test suite for regressions**

Run: `cargo test --workspace -p ailang-surface 2>&1 | tail -15`
Expected: 0 failures. The `new` keyword does not collide with any existing keyword (per spec §351 verification).

---

## Task 3: Checker — synth Term::New + 2 new diagnostics + 3 in-source tests

**Files:**

- Modify: `crates/ailang-check/src/lib.rs:~705` — add `CheckError::NewTypeNotConstructible` + `NewArgKindMismatch` variants (alongside prep.1's two type-scoped variants)
- Modify: `crates/ailang-check/src/lib.rs:~759` — `code()` arms
- Modify: `crates/ailang-check/src/lib.rs:~837` — `ctx()` arms (note: this match has a `_ =>` catch-all so adding arms is for explicit structured diagnostics, not compile-forced)
- Modify: `crates/ailang-check/src/lib.rs:~3005` — replace Task 1's Pattern-E stub in `synth`'s Term match with the real elaboration
- Test (in-source): `crates/ailang-check/src/lib.rs` `#[cfg(test)] mod tests` — three new tests

- [ ] **Step 1: Write the three failing tests**

Append to the in-source `#[cfg(test)] mod tests` block in `crates/ailang-check/src/lib.rs`:

```rust
#[test]
fn new_resolves_via_type_scope() {
    // The prep.2 spec's worked example. (module new_demo
    //   (data Counter (ctor MkCounter (con Int)))
    //   (fn new ... (body (term-ctor Counter MkCounter n)))
    //   (fn main ... (body (let c (new Counter 42) ...))))
    let module = serde_json::from_value::<ailang_core::ast::Module>(serde_json::json!({
        "schema": ailang_core::SCHEMA,
        "name": "new_demo",
        "imports": [],
        "defs": [
            {"kind": "data", "name": "Counter", "vars": [], "ctors": [
                {"name": "MkCounter", "args": [{"k": "con", "name": "Int"}]}
            ]},
            {"kind": "fn", "name": "new",
             "type": {"k": "fn", "params": [{"k": "con", "name": "Int"}],
                      "ret": {"k": "con", "name": "Counter"}, "effects": []},
             "params": ["n"],
             "body": {"t": "term-ctor", "type": "Counter", "ctor": "MkCounter",
                      "args": [{"t": "var", "name": "n"}]}},
            {"kind": "fn", "name": "main",
             "type": {"k": "fn", "params": [],
                      "ret": {"k": "con", "name": "Counter"}, "effects": []},
             "params": [],
             "body": {"t": "new", "type": "Counter",
                      "args": [{"kind": "value", "value": {"t": "lit", "lit": {"kind": "int", "value": 42}}}]}}
        ]
    })).unwrap();
    let mut modules = std::collections::BTreeMap::new();
    modules.insert("new_demo".to_string(), module);
    let workspace = ailang_core::workspace::build_workspace(&modules, &["prelude"])
        .expect("workspace builds");
    let res = check_workspace(&workspace);
    assert!(res.is_empty(), "(new Counter 42) must check cleanly; got {res:?}");
}

#[test]
fn new_type_not_constructible() {
    // (new T 42) where T has NO `new` def in its home module.
    let module = serde_json::from_value::<ailang_core::ast::Module>(serde_json::json!({
        "schema": ailang_core::SCHEMA,
        "name": "no_new",
        "imports": [],
        "defs": [
            {"kind": "data", "name": "T", "vars": [], "ctors": [
                {"name": "MkT", "args": []}
            ]},
            // No `new` def for T.
            {"kind": "fn", "name": "main",
             "type": {"k": "fn", "params": [], "ret": {"k": "con", "name": "T"}, "effects": []},
             "params": [],
             "body": {"t": "new", "type": "T",
                      "args": [{"kind": "value", "value": {"t": "lit", "lit": {"kind": "int", "value": 42}}}]}}
        ]
    })).unwrap();
    let mut modules = std::collections::BTreeMap::new();
    modules.insert("no_new".to_string(), module);
    let workspace = ailang_core::workspace::build_workspace(&modules, &["prelude"])
        .expect("workspace builds");
    let diagnostics = check_workspace(&workspace);
    assert!(diagnostics.iter().any(|d| d.code == "new-type-not-constructible"),
        "expected new-type-not-constructible diagnostic; got {diagnostics:?}");
}

#[test]
fn new_arg_kind_mismatch_value_where_type() {
    // (new T arg) where `new` expects a Type arg in position 0 but
    // a Value was supplied (or vice versa).
    // Construct T with a `new` fn whose first param is a Type (i.e.
    // a forall-bound type variable supplied as a Type at the new-site).
    // Provide a Value where the Type was expected.
    let module = serde_json::from_value::<ailang_core::ast::Module>(serde_json::json!({
        "schema": ailang_core::SCHEMA,
        "name": "kind_mismatch",
        "imports": [],
        "defs": [
            {"kind": "data", "name": "T", "vars": ["a"], "ctors": [
                {"name": "MkT", "args": [{"k": "var", "name": "a"}]}
            ]},
            // `new` declared as `forall a. (a) -> T a`. The new-site
            // supplies the type-arg via a NewArg::Type, then the value-
            // arg via a NewArg::Value. Below the new-site mistakenly
            // supplies NewArg::Value for both positions.
            {"kind": "fn", "name": "new",
             "type": {"k": "forall", "vars": ["a"], "constraints": [], "body":
                 {"k": "fn", "params": [{"k": "var", "name": "a"}],
                  "ret": {"k": "con", "name": "T", "args": [{"k": "var", "name": "a"}]},
                  "effects": []}},
             "params": ["x"],
             "body": {"t": "term-ctor", "type": "T", "ctor": "MkT",
                      "args": [{"t": "var", "name": "x"}]}},
            {"kind": "fn", "name": "main",
             "type": {"k": "fn", "params": [], "ret": {"k": "con", "name": "T",
                      "args": [{"k": "con", "name": "Int"}]}, "effects": []},
             "params": [],
             "body": {"t": "new", "type": "T", "args": [
                 // WRONG: should be `{kind: "type", value: <Int type>}` for position 0
                 {"kind": "value", "value": {"t": "lit", "lit": {"kind": "int", "value": 42}}}
             ]}}
        ]
    })).unwrap();
    let mut modules = std::collections::BTreeMap::new();
    modules.insert("kind_mismatch".to_string(), module);
    let workspace = ailang_core::workspace::build_workspace(&modules, &["prelude"])
        .expect("workspace builds");
    let diagnostics = check_workspace(&workspace);
    assert!(diagnostics.iter().any(|d| d.code == "new-arg-kind-mismatch"),
        "expected new-arg-kind-mismatch diagnostic; got {diagnostics:?}");
}
```

- [ ] **Step 2: Run the three tests to verify they fail**

Run: `cargo test --workspace -p ailang-check new_resolves_via_type_scope new_type_not_constructible new_arg_kind_mismatch`
Expected: FAIL — 1st test fails because Task 1's stub returns `CheckError::Internal`; 2nd + 3rd fail because the variants `NewTypeNotConstructible` / `NewArgKindMismatch` don't exist yet.

- [ ] **Step 3: Add the two new `CheckError` variants**

In `crates/ailang-check/src/lib.rs`, after `TypeScopedReceiverNotAType { name: String }` at ~line 705 (Task 1's prep.1 variants), add:

```rust
    /// prep.2 (kernel-extension-mechanics): a `(new T args...)` call
    /// where `T`'s home module has no `new` def. Code:
    /// `new-type-not-constructible`.
    #[error("type `{type_name}` is not constructible: home module `{home_module}` declares no `new` def")]
    NewTypeNotConstructible {
        type_name: String,
        home_module: String,
    },

    /// prep.2 (kernel-extension-mechanics): a `(new T args...)` call
    /// where the arg at `position` has kind `got` (Type / Value) but
    /// the `new` def's signature expects `expected`. Code:
    /// `new-arg-kind-mismatch`.
    #[error("`(new {type_name} …)` arg at position {position}: expected {expected}, got {got}")]
    NewArgKindMismatch {
        type_name: String,
        position: usize,
        expected: &'static str,
        got: &'static str,
    },
```

Add the two arms to `CheckError::code()` (the match at ~line 759), right after prep.1's two new arms:

```rust
            CheckError::NewTypeNotConstructible { .. } => "new-type-not-constructible",
            CheckError::NewArgKindMismatch { .. } => "new-arg-kind-mismatch",
```

Add the two arms to `CheckError::ctx()` (the match at ~line 837, which has a `_ =>` catch-all — add explicit arms for richer structured diagnostics):

```rust
            CheckError::NewTypeNotConstructible { type_name, home_module } => {
                serde_json::json!({"type": type_name, "home_module": home_module})
            }
            CheckError::NewArgKindMismatch { type_name, position, expected, got } => {
                serde_json::json!({"type": type_name, "position": position,
                                   "expected": expected, "actual": got})
            }
```

- [ ] **Step 4: Implement the real `synth` Term::New arm**

In `crates/ailang-check/src/lib.rs`, replace the Pattern-E stub in `synth`'s Term match (at ~line 3005, the arm added in Task 1) with the real elaboration per spec §318-330. Insert before the existing `Term::Recur` arm:

```rust
        Term::New { type_name, args } => {
            // prep.2 §318-330. After prep.1's pre-pass, `type_name` is
            // already qualified to `<home>.<Type>` form (or stays
            // local if `T` is defined in this module). Resolve to
            // home module:
            let (home_module, bare_type) = if let Some((m, t)) = type_name.split_once('.') {
                (m.to_string(), t.to_string())
            } else {
                // Local — find own home.
                let owning = env.module_name.clone();
                (owning, type_name.clone())
            };
            // Step 2: find `new` def in home module's globals.
            let home_globals = env.module_globals.get(&home_module).ok_or_else(|| {
                CheckError::TypeScopedReceiverNotAType { name: type_name.clone() }
            })?;
            let new_ty = home_globals.get("new").cloned().ok_or_else(|| {
                CheckError::NewTypeNotConstructible {
                    type_name: bare_type.clone(),
                    home_module: home_module.clone(),
                }
            })?;
            // Qualify any bare type-cons in `new`'s signature against
            // the home module's owner-types (mirror the existing
            // module-as-receiver path).
            let owner_types = env
                .module_types
                .get(&home_module)
                .cloned()
                .unwrap_or_default();
            let qualified = qualify_local_types(&new_ty, &home_module, &owner_types);
            // Step 3: arg count. If `new`'s sig is Forall, first peel
            // it; the params/ret on the inner Fn type is what we count
            // against, with the type-vars filled by Type-positional args.
            let (vars, param_tys, ret_ty, effects) = match &qualified {
                Type::Forall { vars, body, .. } => match body.as_ref() {
                    Type::Fn { params, ret, effects, .. } => {
                        (vars.clone(), params.clone(), (**ret).clone(), effects.clone())
                    }
                    _ => return Err(CheckError::FnTypeRequired("new".into())),
                },
                Type::Fn { params, ret, effects, .. } => {
                    (Vec::new(), params.clone(), (**ret).clone(), effects.clone())
                }
                _ => return Err(CheckError::FnTypeRequired("new".into())),
            };
            // Count Type-positional args (these instantiate `vars`)
            // and Value-positional args (these are checked against
            // `param_tys`).
            let type_arg_count = args.iter().filter(|a| matches!(a, NewArg::Type(_))).count();
            let value_arg_count = args.iter().filter(|a| matches!(a, NewArg::Value(_))).count();
            if type_arg_count != vars.len() {
                return Err(CheckError::NewArgKindMismatch {
                    type_name: bare_type.clone(),
                    position: 0,
                    expected: if vars.is_empty() { "value-arg" } else { "type-arg" },
                    got: if type_arg_count > vars.len() { "type-arg" } else { "value-arg" },
                });
            }
            if value_arg_count != param_tys.len() {
                return Err(CheckError::ArityMismatch {
                    callee: format!("new {bare_type}"),
                    expected: param_tys.len(),
                    got: value_arg_count,
                });
            }
            // Step 4: build the var-binding map from Type-args (in order).
            let mut var_mapping: BTreeMap<String, Type> = BTreeMap::new();
            let mut value_args_iter: Vec<&Term> = Vec::new();
            let mut type_vars_iter = vars.iter();
            for (i, a) in args.iter().enumerate() {
                match a {
                    NewArg::Type(t) => {
                        let var = type_vars_iter.next().ok_or_else(|| {
                            CheckError::NewArgKindMismatch {
                                type_name: bare_type.clone(),
                                position: i,
                                expected: "value-arg",
                                got: "type-arg",
                            }
                        })?;
                        var_mapping.insert(var.clone(), t.clone());
                    }
                    NewArg::Value(v) => value_args_iter.push(v),
                }
            }
            // Step 5: substitute the type-vars into param_tys + ret_ty,
            // then synth each value-arg against the corresponding
            // (substituted) param-type.
            let mut substituted_params: Vec<Type> = param_tys
                .iter()
                .map(|p| substitute_rigids(p, &var_mapping))
                .collect();
            let substituted_ret = substitute_rigids(&ret_ty, &var_mapping);
            for (i, (arg, expected)) in value_args_iter.iter().zip(substituted_params.iter()).enumerate() {
                let arg_ty = synth(arg, env, counter, residuals)?;
                unify(&arg_ty, expected).map_err(|_| CheckError::TypeMismatch {
                    expected: expected.clone(),
                    got: arg_ty,
                })?;
            }
            // Effects are inherited from `new`'s signature.
            for e in &effects {
                if !env.declared_effects.contains(e) {
                    return Err(CheckError::UndeclaredEffect(e.clone()));
                }
            }
            Ok(substituted_ret)
        }
```

(The implementer reconciles any helper-name spellings — `unify` may be `unify_types`; `env.module_name` may be `env.owning_module`; `Diagnostic` field shapes follow the existing convention. Adjust to compile.)

- [ ] **Step 5: Run the three tests to verify they pass**

Run: `cargo test --workspace -p ailang-check new_resolves_via_type_scope new_type_not_constructible new_arg_kind_mismatch`
Expected: PASS, all three.

- [ ] **Step 6: Run the full `ailang-check` test suite for regressions**

Run: `cargo test --workspace -p ailang-check 2>&1 | tail -20`
Expected: 0 new failures. No existing fixture uses `Term::New`, so the synth elaboration cannot break pre-existing paths.

- [ ] **Step 7: Run the full workspace test suite**

Run: `cargo test --workspace 2>&1 | tail -25`
Expected: 0 new failures.

---

## Task 4: Schema drift + spec / contract anchors

**Files:**

- Modify: `crates/ailang-core/tests/design_schema_drift.rs:130-189` — add the `term_new_round_trips` test pin AND the exemplar row (the match arm was already added in Task 1)
- Modify: `crates/ailang-core/tests/spec_drift.rs` — add the exemplar row asserting the `(new` anchor in form_a.md
- Modify: `crates/ailang-core/specs/form_a.md` — add `(new TYPE-NAME ARG*)` to the surface-keyword cheatsheet
- Modify: `design/contracts/0002-data-model.md` — add `{ "t": "new", "type": "<id>", "args": [NewArg...] }` to the JSON-tag block + add the `NewArg` shape definition

- [ ] **Step 1: Add the `term_new_round_trips` schema-drift test**

In `crates/ailang-core/tests/design_schema_drift.rs`, add a new test fn (place near the other Term-round-trip pins, e.g. after `term_recur_round_trips` if it exists, or alongside the exemplars list):

```rust
#[test]
fn term_new_round_trips() {
    // Pin Term::New's JSON shape: `t: "new"`, `type: "<TypeName>"`,
    // `args: [{kind, value}]`. Each NewArg uses tag = "kind",
    // content = "value", lowercase ("type" / "value").
    let t = Term::New {
        type_name: "Counter".into(),
        args: vec![
            NewArg::Value(Term::Lit { lit: Literal::Int { value: 42 } }),
        ],
    };
    let json = serde_json::to_value(&t).expect("serialise");
    assert_eq!(json["t"], "new");
    assert_eq!(json["type"], "Counter");
    let args = json["args"].as_array().expect("args is array");
    assert_eq!(args.len(), 1);
    assert_eq!(args[0]["kind"], "value");
    assert!(args[0]["value"].is_object(), "value is the inlined Term JSON");
    // Round-trip back through Deserialize.
    let recovered: Term = serde_json::from_value(json).expect("deserialise");
    assert!(matches!(recovered, Term::New { type_name, .. } if type_name == "Counter"));
}

#[test]
fn term_new_type_arg_round_trips() {
    // Pin: NewArg::Type serializes with kind: "type", value: <Type JSON>.
    let t = Term::New {
        type_name: "Series".into(),
        args: vec![
            NewArg::Type(Type::Con { name: "Float".into(), args: vec![] }),
            NewArg::Value(Term::Lit { lit: Literal::Int { value: 3 } }),
        ],
    };
    let json = serde_json::to_value(&t).expect("serialise");
    let args = json["args"].as_array().expect("args is array");
    assert_eq!(args[0]["kind"], "type");
    assert_eq!(args[0]["value"]["k"], "con");
    assert_eq!(args[0]["value"]["name"], "Float");
    assert_eq!(args[1]["kind"], "value");
}
```

- [ ] **Step 2: Add the form_a.md anchor for the `new` keyword**

In `crates/ailang-core/specs/form_a.md`, locate the surface-keyword cheatsheet (around lines 289-298 per recon; verify by `grep -n "term-ctor" specs/form_a.md`). Add a new line in the keyword-list block:

```
- `(new TYPE-NAME ARG*)` — functional construction; calls `TYPE-NAME`'s
  home-module `new` def with the supplied args. Each arg is a Type
  (e.g. `(con Float)`) or a Term. Disambiguation is by syntactic form.
```

- [ ] **Step 3: Add the `Term::New` row to spec_drift.rs's exemplars list**

In `crates/ailang-core/tests/spec_drift.rs`, locate the exemplars list (around lines 90-158 per recon). Add a row asserting the `(new` literal appears in `form_a.md`:

```rust
    (
        Term::New {
            type_name: "T".into(),
            args: vec![NewArg::Value(Term::Lit { lit: Literal::Int { value: 0 } })],
        },
        "(new",  // anchor expected in form_a.md per Step 2
    ),
```

(The exact tuple shape depends on the existing exemplars-list format; mirror the prior rows.)

- [ ] **Step 4: Add the JSON-tag block to data-model.md**

In `design/contracts/0002-data-model.md`, locate the JSON-tag block listing existing Term variants (around lines 107-178 per recon; the block contains entries like `lit`, `var`, `app`, `let`, …). Add a new entry for `new` plus a `NewArg` shape:

```markdown
- `{ "t": "new", "type": "<TypeName>", "args": [NewArg, ...] }` — functional
  construction `(new T args...)`. Resolution: the `type` is looked up
  via type-scoped resolution to its home module; the home module's
  `new` def is invoked with the args. Each arg is a `NewArg`. See
  prep.2 of the kernel-extension-mechanics milestone.

`NewArg` shape:

- `{ "kind": "type", "value": <Type JSON> }` — a Type-positional arg.
- `{ "kind": "value", "value": <Term JSON> }` — a Value-positional arg.
```

- [ ] **Step 5: Run the schema-drift + spec-drift tests**

Run: `cargo test --workspace -p ailang-core --test design_schema_drift && cargo test --workspace -p ailang-core --test spec_drift`
Expected: PASS — both new pin functions land green, plus the existing pin tests stay green (no byte-shift on existing fixtures).

- [ ] **Step 6: Run the full workspace test suite**

Run: `cargo test --workspace 2>&1 | tail -25`
Expected: 0 new failures. The iter is complete.

---

## Self-review (planner Step 5)

1. **Spec coverage:** every prep.2 spec subsection has a task —
   - AST schema (Term::New + NewArg) → Task 1
   - Surface (lex/parse/print + round-trip) → Task 2
   - Checker (synth elaboration + 2 diagnostics) → Task 3
   - Drift / round-trip / spec anchors → Task 4
   - Out-of-scope (codegen): Task 1 ships Pattern-D error arms; no real codegen logic.
   - Pre-pass extension (prep.1 partner) → Task 1, Pattern C arm in `qualify_workspace_term`

2. **Placeholder scan:** grepped for "TBD", "TODO", "implement later", "similar to Task", "add appropriate error handling". Pattern E (the synth stub) explicitly names "Task 3 replaces with the real logic" which is a structured forward-reference within the plan, not a placeholder. The 44+ arm-site table uses "Pattern A/B/…" shorthand but the patterns are inlined in the same task body. No literal placeholder strings.

3. **Type consistency:** `Term::New { type_name, args }` is spelled identically across the AST addition (Task 1 Step 1), the 44+ arm-templates (Task 1 Step 3), the Form-A printer arm (Task 2), the synth arm (Task 3), the drift pin (Task 4), and the in-source tests (Task 3). `NewArg::Type(Type)` and `NewArg::Value(Term)` similarly. `NewTypeNotConstructible { type_name, home_module }` and `NewArgKindMismatch { type_name, position, expected, got }` use the same field names everywhere.

4. **Step granularity:** Task 1's per-site sweep is the longest single step (~30-45 minutes across 44 sites with mechanical patterns). It's mechanical — each site takes 1-2 minutes. The OTHER steps are 2-5 minutes each. The Task-1 sweep is intentionally one step rather than 44 steps because each site is so mechanical that splitting would obscure the rhythm; the explicit per-site table keeps it tractable.

5. **No commit steps:** zero `git commit` instructions appear. The implementer leaves work in the working tree; the Boss commits at iter close.

6. **Pin/replacement substring contiguity:** N/A — this plan does not contain `contains("...")` assertions paired with verbatim text replacements. The drift-test pins (`assert_eq!(json["t"], "new")` etc.) are checked against the JSON-tag derivation, not against a separate verbatim file. Spec anchor strings (`"(new"` in form_a.md) are short identifiers; no line-wrap risk.

7. **Compile-gate vs. deferred-caller ordering:** Task 1's variant addition is the canonical signature change that makes the WHOLE workspace fail to compile until all 44+ callers are updated. Task 1's Step 3 explicitly walks every caller in the same step; the compile-gate at Step 4 verifies `cargo build --workspace 2>&1 | tail -15` shows 0 errors. The synth arm uses Pattern-E STUB in Task 1 (compiles cleanly because `CheckError::Internal(String)` exists); Task 3 replaces with real logic. No caller-threading is deferred past Task 1's compile gate.

8. **Verification-command filter strings:**
   - `cargo test --workspace -p ailang-surface round_trip_term_new` — Task 2 Steps 2/7 — filter resolves to the new test `round_trip_term_new_mixed_args` (the `round_trip_term_new` prefix matches it).
   - `cargo test --workspace -p ailang-check new_resolves_via_type_scope new_type_not_constructible new_arg_kind_mismatch` — Task 3 Steps 2/5 — three filters; each matches a NEW test added in Step 1. After Step 1 lands, all three resolve.
   - `cargo test --workspace -p ailang-core --test design_schema_drift` — Task 4 Step 5 — `--test design_schema_drift` resolves to `crates/ailang-core/tests/design_schema_drift.rs` (verified file exists).
   - `cargo test --workspace -p ailang-core --test spec_drift` — Task 4 Step 5 — `--test spec_drift` resolves to `crates/ailang-core/tests/spec_drift.rs` (verified file exists).
   - `cargo build --workspace 2>&1 | tail -15` — Task 1 Steps 2/4 — unfiltered build; count-based assessment via tail.
   - `cargo test --workspace 2>&1 | tail -25` (and `-30`) — multiple steps — unfiltered suite; count-based assessment.

All filter strings resolve to ≥1 named target in the current tree post-Step-1 additions, or to unfiltered count-based outputs.

## Handoff

Plan sits in the working tree at `docs/plans/0102-prep.2-term-new-construct.md`. Hand off to `skills/implement` with: path to this plan; no task-range focus (run all 4 tasks in sequence). Iter-completion commit body closes #32.
