From 4fc65ccb99643c4c276c42e812a72468d2e6755d Mon Sep 17 00:00:00 2001 From: Brummel Date: Thu, 21 May 2026 12:57:06 +0200 Subject: [PATCH] =?UTF-8?q?iter=20schema-camelcase-fix.1=20(DONE=204/4):?= =?UTF-8?q?=20paramTypes/retType=20=E2=86=92=20param-types/ret-type=20?= =?UTF-8?q?=E2=80=94=20closes=20#30?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `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 --- .../2026-05-21-iter-schema-camelcase-fix.json | 12 +++++ crates/ailang-check/src/lib.rs | 2 +- crates/ailang-core/src/ast.rs | 6 +-- crates/ailang-core/src/workspace.rs | 4 +- .../ailang-core/tests/design_schema_drift.rs | 51 +++++++++++++++++++ crates/ailang-surface/src/parse.rs | 2 +- .../tests/prelude_module_hash_pin.rs | 9 +++- design/contracts/data-model.md | 4 +- ...st_loop_binder_captured_by_lambda.ail.json | 4 +- .../master/examples/fn_with_lambda.ail.json | 4 +- 10 files changed, 84 insertions(+), 14 deletions(-) create mode 100644 bench/orchestrator-stats/2026-05-21-iter-schema-camelcase-fix.json diff --git a/bench/orchestrator-stats/2026-05-21-iter-schema-camelcase-fix.json b/bench/orchestrator-stats/2026-05-21-iter-schema-camelcase-fix.json new file mode 100644 index 0000000..8299b44 --- /dev/null +++ b/bench/orchestrator-stats/2026-05-21-iter-schema-camelcase-fix.json @@ -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 +} diff --git a/crates/ailang-check/src/lib.rs b/crates/ailang-check/src/lib.rs index c34f80d..0e61b9f 100644 --- a/crates/ailang-check/src/lib.rs +++ b/crates/ailang-check/src/lib.rs @@ -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. diff --git a/crates/ailang-core/src/ast.rs b/crates/ailang-core/src/ast.rs index d1e77a0..77ef23a 100644 --- a/crates/ailang-core/src/ast.rs +++ b/crates/ailang-core/src/ast.rs @@ -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, - #[serde(rename = "paramTypes")] + #[serde(rename = "param-types")] param_tys: Vec, - #[serde(rename = "retType")] + #[serde(rename = "ret-type")] ret_ty: Box, #[serde(default)] effects: Vec, diff --git a/crates/ailang-core/src/workspace.rs b/crates/ailang-core/src/workspace.rs index a67ff37..3025840 100644 --- a/crates/ailang-core/src/workspace.rs +++ b/crates/ailang-core/src/workspace.rs @@ -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" } } } diff --git a/crates/ailang-core/tests/design_schema_drift.rs b/crates/ailang-core/tests/design_schema_drift.rs index 3e9f912..b111b64 100644 --- a/crates/ailang-core/tests/design_schema_drift.rs +++ b/crates/ailang-core/tests/design_schema_drift.rs @@ -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::>(), + ); + assert!( + obj.contains_key("ret-type"), + "Term::Lam must emit `ret-type` key; got keys {:?}", + obj.keys().collect::>(), + ); + 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 diff --git a/crates/ailang-surface/src/parse.rs b/crates/ailang-surface/src/parse.rs index 8c0fc36..7d2b56b 100644 --- a/crates/ailang-surface/src/parse.rs +++ b/crates/ailang-surface/src/parse.rs @@ -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 diff --git a/crates/ailang-surface/tests/prelude_module_hash_pin.rs b/crates/ailang-surface/tests/prelude_module_hash_pin.rs index 24d80c7..1ee0cc0 100644 --- a/crates/ailang-surface/tests/prelude_module_hash_pin.rs +++ b/crates/ailang-surface/tests/prelude_module_hash_pin.rs @@ -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." ); diff --git a/design/contracts/data-model.md b/design/contracts/data-model.md index 8dab9b8..d693ea9 100644 --- a/design/contracts/data-model.md +++ b/design/contracts/data-model.md @@ -141,8 +141,8 @@ narrative — defaults, superclasses, diagnostics — lives in // Anonymous fn value; free vars captured from enclosing scope. { "t": "lam", "params": [""...], - "paramTypes": [Type...], - "retType": Type, + "param-types": [Type...], + "ret-type": Type, "effects": [""...], "body": Term } diff --git a/examples/test_loop_binder_captured_by_lambda.ail.json b/examples/test_loop_binder_captured_by_lambda.ail.json index 1b8e296..aad2e6c 100644 --- a/examples/test_loop_binder_captured_by_lambda.ail.json +++ b/examples/test_loop_binder_captured_by_lambda.ail.json @@ -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" } }, diff --git a/experiments/2026-05-12-cross-model-authoring/master/examples/fn_with_lambda.ail.json b/experiments/2026-05-12-cross-model-authoring/master/examples/fn_with_lambda.ail.json index ff2e6d9..f5d426f 100644 --- a/experiments/2026-05-12-cross-model-authoring/master/examples/fn_with_lambda.ail.json +++ b/experiments/2026-05-12-cross-model-authoring/master/examples/fn_with_lambda.ail.json @@ -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",