iter schema-camelcase-fix.1 (DONE 4/4): paramTypes/retType → param-types/ret-type — closes #30

`Term::Lam`'s two camelCase JSON tags become kebab-case, matching
the convention every other compound-key tag in the AST schema
already follows. After this iter the canonical JSON has zero
camelCase outliers.

## Schema swap (atomic, Task 2)

- `crates/ailang-core/src/ast.rs:492,494` — two
  `#[serde(rename)]` strings: `"paramTypes" → "param-types"`,
  `"retType" → "ret-type"`. Rust field names unchanged.
- `crates/ailang-core/src/workspace.rs:1925-1926` — in-source
  JSON literal in the `ct1_validator_walks_lam_embedded_types`
  test.
- `design/contracts/data-model.md:142-147` — fenced JSON block
  in the canonical data-model contract. Honesty-Rule touch-point:
  the contract document must describe the actual present-state
  schema.
- `examples/test_loop_binder_captured_by_lambda.ail.json` and
  `experiments/2026-05-12-cross-model-authoring/master/examples/fn_with_lambda.ail.json`
  — the two `.ail.json` fixtures whose canonical-JSON embeds
  the renamed tags.

The five files moved together within one task because `Term::Lam`
has no `#[serde(default)]` on `param_tys` / `ret_ty` — a
renamed-away key is a hard deserialise error. Splitting the swap
would have left the workspace untestable mid-step.

## RED → GREEN pin (Tasks 1, 2)

`crates/ailang-core/tests/design_schema_drift.rs` gains
`lam_serialises_with_kebab_keys`: pins that `serde_json::to_value(&Term::Lam{...})`
emits `"param-types"` / `"ret-type"` AND does not emit the old
camelCase keys. Companion: the existing
`design_md_anchors_every_term_variant` test now also asserts the
kebab anchors appear inside `data-model.md`'s `lam` fenced block.
Both confirmed RED on entry, GREEN after Task 2.

## Rustdoc honesty pass (Task 3)

Three pure-prose edits keep production rustdoc consistent with
the new schema vocabulary: `crates/ailang-core/src/ast.rs:8` (the
module-level rename enumeration), `crates/ailang-surface/src/parse.rs:81`
(prose mention of `lam`'s carry), `crates/ailang-check/src/lib.rs:1707`
(InstanceMethod routing helper rustdoc). No compile or test
consequence — Honesty-Rule maintenance.

## Plan-pseudo-vs-reality finding: under-audited hash blast radius

The brainstorm spec audited `crates/ailang-core/tests/hash_pin.rs`
exhaustively (5 pinned modules, zero `(lam ...)` occurrences,
"no hash refresh required"). It missed
`crates/ailang-surface/tests/prelude_module_hash_pin.rs` — a
separate hash-pin file in a different crate that pins `prelude.ail`,
which contains 11 `(lam ...)` forms. The prelude module hash
drifted (`6d0577ff0d4e50ac` → `562a03fc57e7e017`); the implementer
refreshed it with a Honesty-Rule provenance comment matching the
precedent set in commit `26fb345`. Form-A is unchanged — only the
canonical-JSON byte stream differs, which is exactly what the
rename targets. The spec-side learning is: schema-rename
brainstorms must walk *every* test crate for hash-pin files, not
just the home crate of the AST.

## Why this shape, and not the alternatives

- *`params-ty` / `ret-ty` (Rust-field-name vibe)* — rejected. The
  AST JSON schema follows its own kebab/single-word convention,
  not Rust field names. The other multi-word tag (`reuse-as`) is
  kebab; abbreviating `params-ty` mixes singular+plural and has
  no precedent.
- *Inline typed params as a sub-tag* (e.g. `params: [{name, type}, ...]`)
  — rejected. Stronger semantic locality, but it changes schema
  topology rather than tag spelling. Out of scope for a camelCase
  correction; would have re-pinned far more than this milestone.
- *Bundle with #27 (arith-rename)* — rejected. #27 carries four
  open brainstorm questions (mod vs rem, neg vs sub 0, Num class,
  deprecation window) and is structurally a separate milestone.
  Each gets one re-pin wave with its own rationale; no churn
  saving from bundling.

## Honest call on the feature-acceptance gate

Clause 1 (LLM author naturally uses the new form) is the weakest
of the three. Direct empirical evidence is absent — the 2026-05-21
naming-A/B run measured a different axis. The argument is
indirect: a future LLM author generalises from the rest of the
schema ("compound keys are kebab"), and the two camelCase
outliers are precisely the sites where that generalisation
diverged from reality. Clause 2 (redundancy reduction) and
Clause 3 (no semantic surface touched) are direct.

## Forbidden touches verified untouched

`docs/plans/*.md` historic plans (which embed old tag names in
example JSON), `experiments/2026-05-12-cross-model-authoring/master/spec.md`,
`experiments/.../rendered/*.md`, and `experiments/.../runs/**` —
all left as frozen historical artefacts per Honesty-Rule
analogue (describe state at time of writing, not present state).
Verified via `git diff --name-only HEAD` (Task 4 Step 4).

## Verification

- `cargo test --workspace`: all test groups OK, 0 failed,
  2 ignored (pre-existing).
- `cargo test -p ailang-surface --test round_trip`: GREEN
  (Form-A unchanged invariant).
- New schema-shape pin and the extended data-model anchor walk:
  GREEN after Task 2.
- Negative grep across `crates/`, `examples/`, `design/`,
  `runtime/`: the only remaining `paramTypes` / `retType`
  occurrences in checked-in code are the load-bearing
  negative-assertion strings inside the new pin (intentional —
  they are what makes the pin RED-able on regression).
- Stats: `bench/orchestrator-stats/2026-05-21-iter-schema-camelcase-fix.json`
  — 4/4 tasks DONE, no re-loops, no review-loops.

closes #30
This commit is contained in:
2026-05-21 12:57:06 +02:00
parent 90977663ff
commit 4fc65ccb99
10 changed files with 84 additions and 14 deletions
@@ -0,0 +1,12 @@
{
"iter_id": "schema-camelcase-fix.1",
"date": "2026-05-21",
"mode": "standard",
"outcome": "DONE",
"tasks_total": 4,
"tasks_completed": 4,
"reloops_per_task": { "1": 0, "2": 0, "3": 0, "4": 0 },
"review_loops_spec": 0,
"review_loops_quality": 0,
"blocked_reason": null
}
+1 -1
View File
@@ -1704,7 +1704,7 @@ fn check_def(
/// Route each `InstanceMethod` body through `check_fn` by wrapping
/// it in a synthetic `FnDef`. The InstanceMethod body is by
/// convention a `Term::Lam` carrying its own `paramTypes` / `retType`
/// convention a `Term::Lam` carrying its own `param-types` / `ret-type`
/// (the canonical class-form shape); we extract its `param_tys` /
/// `ret_ty` / `params` / `body` into an `FnDef` shape and reuse the
/// existing fn-body-check machinery.
+3 -3
View File
@@ -5,7 +5,7 @@
//! `crates/ailang-core/tests/design_schema_drift.rs` fires.
//!
//! The serde attributes carry the schema: field renames (`as`, `type`,
//! `fn`, `paramTypes`, `retType`), enum tags (`kind`, `t`, `k`, `p`),
//! `fn`, `param-types`, `ret-type`), enum tags (`kind`, `t`, `k`, `p`),
//! and `skip_serializing_if` predicates that keep the canonical-JSON
//! representation backwards compatible across schema extensions.
//!
@@ -489,9 +489,9 @@ pub enum Term {
/// the inferred shape.
Lam {
params: Vec<String>,
#[serde(rename = "paramTypes")]
#[serde(rename = "param-types")]
param_tys: Vec<Type>,
#[serde(rename = "retType")]
#[serde(rename = "ret-type")]
ret_ty: Box<Type>,
#[serde(default)]
effects: Vec<String>,
+2 -2
View File
@@ -1922,8 +1922,8 @@ mod tests {
"body": {
"t": "lam",
"params": ["x"],
"paramTypes": [{ "k": "con", "name": "Ordering" }],
"retType": { "k": "con", "name": "Unit" },
"param-types": [{ "k": "con", "name": "Ordering" }],
"ret-type": { "k": "con", "name": "Unit" },
"effects": [],
"body": { "t": "lit", "lit": { "kind": "unit" } }
}
@@ -186,6 +186,18 @@ fn design_md_anchors_every_term_variant() {
add it to design/contracts/data-model.md"
);
}
// The two Term::Lam compound-key tags are kebab-case (closes #30).
// Pin the spellings inside data-model.md's fenced jsonc blocks so a
// future edit cannot silently revert the contract document to
// camelCase while ast.rs ships kebab.
for anchor in [r#""param-types""#, r#""ret-type""#] {
assert!(
anchor_in_jsonc_block(DATA_MODEL, anchor),
"design/contracts/data-model.md is missing Term::Lam kebab-key anchor `{anchor}` — \
check the `lam` fenced JSON block"
);
}
}
/// Every `Pattern` variant must have its JSON-schema anchor present in
@@ -451,6 +463,45 @@ End.
);
}
/// Schema-shape pin: the JSON serialisation of `Term::Lam` must
/// emit kebab-case tags `"param-types"` and `"ret-type"`, and
/// must NOT emit the old camelCase `"paramTypes"` / `"retType"`.
/// Pinned because the closes-#30 milestone made the rename a
/// schema-stability invariant: a future regression that flips
/// the `#[serde(rename)]` string back to camelCase must fire RED
/// here, not slip through.
#[test]
fn lam_serialises_with_kebab_keys() {
let lam = Term::Lam {
params: vec!["x".into()],
param_tys: vec![Type::int()],
ret_ty: Box::new(Type::int()),
effects: vec![],
body: Box::new(Term::Var { name: "x".into() }),
};
let v = serde_json::to_value(&lam).expect("Term::Lam serialises");
let obj = v.as_object().expect("Term::Lam serialises as object");
assert!(
obj.contains_key("param-types"),
"Term::Lam must emit `param-types` key; got keys {:?}",
obj.keys().collect::<Vec<_>>(),
);
assert!(
obj.contains_key("ret-type"),
"Term::Lam must emit `ret-type` key; got keys {:?}",
obj.keys().collect::<Vec<_>>(),
);
assert!(
!obj.contains_key("paramTypes"),
"Term::Lam must NOT emit old camelCase `paramTypes` key",
);
assert!(
!obj.contains_key("retType"),
"Term::Lam must NOT emit old camelCase `retType` key",
);
}
/// Nested struct key anchors must be present in design/contracts/data-model.md.
/// These keys appear inside `Suppress`, `ClassMethod`, `InstanceMethod`,
/// and `Type::Forall` — they are not discriminators but they ARE part
+1 -1
View File
@@ -78,7 +78,7 @@
//! Notes on the form (the binding constraints are in
//! design/contracts/authoring-surface.md):
//!
//! - The `lam` form carries `paramTypes`, a `ret` type, and an
//! - The `lam` form carries `param-types`, a `ret` type, and an
//! optional `effects` clause. The AST stores them per-lambda and
//! so the form must round-trip them.
//! - The `import` form admits an optional `as` alias to round-trip
@@ -16,8 +16,15 @@ fn prelude_parse_yields_canonical_hash() {
// 3abe0d3fa3c11c99): Eq Int/Bool/Str bodies flip to placeholder
// `false`; Eq Unit added; six float_* fns added; class-Eq doc
// rewritten away from the "P2 follow-up" comment.
// 2026-05-21 schema-camelcase-fix re-pinned (prior:
// 6d0577ff0d4e50ac): `Term::Lam`'s JSON tags renamed from
// `paramTypes`/`retType` to `param-types`/`ret-type`. The
// prelude contains 11 `(lam ...)` forms, so each lambda's
// canonical-JSON bytes change and the module hash drifts.
// Form-A surface unchanged — only the canonical-JSON byte
// stream differs, which is what the rename targets.
assert_eq!(
h, "6d0577ff0d4e50ac",
h, "562a03fc57e7e017",
"prelude module hash drifted; if intentional, capture the new \
hex below + record the why in the commit body."
);
+2 -2
View File
@@ -141,8 +141,8 @@ narrative — defaults, superclasses, diagnostics — lives in
// Anonymous fn value; free vars captured from enclosing scope.
{ "t": "lam",
"params": ["<id>"...],
"paramTypes": [Type...],
"retType": Type,
"param-types": [Type...],
"ret-type": Type,
"effects": ["<id>"...],
"body": Term }
@@ -29,8 +29,8 @@
"value": {
"t": "lam",
"params": [],
"paramTypes": [],
"retType": { "k": "con", "name": "Int" },
"param-types": [],
"ret-type": { "k": "con", "name": "Int" },
"effects": [],
"body": { "t": "var", "name": "acc" }
},
@@ -22,8 +22,8 @@
"body": {
"t": "lam",
"params": ["y"],
"paramTypes": [ { "k": "con", "name": "Int" } ],
"retType": { "k": "con", "name": "Int" },
"param-types": [ { "k": "con", "name": "Int" } ],
"ret-type": { "k": "con", "name": "Int" },
"effects": [],
"body": {
"t": "app",