832375f2ac
All 176 files in the four accumulating directories now use a zero-padded 4-digit counter prefix that reflects creation order (`NNNN-slug.md`). The counter is assigned per directory in strict git-log creation order; ties broken alphabetically by original name. The old `YYYY-MM-DD-` prefix on docs/specs/ and docs/plans/ files is dropped — the date is recoverable from git log and the counter carries the ordering. A file's counter is stable for the life of the file: never reassigned, never reused, never compacted. Deleted files retire their counter; subsequent files do not fill the gap. This is the property that lets cross-references stay literal — refs use the full filename including the counter (`design/contracts/0007-honesty-rule.md`) so they grep cleanly and resolve directly without a glob step. 313 cross-references updated across .md/.rs/.toml/.c/.json files (test pins, include_str! paths, design-INDEX entries, baseline notes, runtime C comments, inter-contract markdown links incl. bare basename and `../models/foo.md` forms). CLAUDE.md gets a new "File-naming convention" section spelling out the rule and rationale. skills/brainstorm/SKILL.md and skills/planner/SKILL.md updated so new spec/plan creation produces counter-prefixed names from the start. The full test suite (cargo test --workspace) passes.
461 lines
16 KiB
Markdown
461 lines
16 KiB
Markdown
# schema-camelcase-fix — Implementation Plan
|
|
|
|
> **Parent spec:** `docs/specs/0051-schema-camelcase-fix.md`
|
|
> (commit `55ce6d0`, amended after plan-recon)
|
|
>
|
|
> **For agentic workers:** REQUIRED SUB-SKILL: use `skills/implement`
|
|
> to run this plan. Steps use `- [ ]` checkboxes for tracking.
|
|
|
|
**Goal:** Rename the two camelCase JSON tags `paramTypes` / `retType`
|
|
on `Term::Lam` to the kebab-case `param-types` / `ret-type`,
|
|
migrate every checked-in code/contract reference in the same
|
|
iteration, and pin the new schema shape with a `design_schema_drift`
|
|
assertion. After this iteration the AILang canonical-JSON schema
|
|
has zero camelCase tags. Closes Gitea #30.
|
|
|
|
**Architecture:** Single-iteration milestone. Four layers
|
|
touched: (1) `#[serde(rename)]` strings on `Term::Lam` in
|
|
`crates/ailang-core/src/ast.rs`; (2) one in-source test JSON-literal
|
|
in `crates/ailang-core/src/workspace.rs`; (3) two `.ail.json` fixtures
|
|
+ one canonical-schema contract document
|
|
(`design/contracts/0002-data-model.md`); (4) three rustdoc strings that
|
|
describe present-state schema vocabulary. The Form-A surface
|
|
(`crates/ailang-surface/`) is structurally untouched — Form-A uses
|
|
`(params (typed ...)) (ret ...)`, not the renamed tags. No hash-pin
|
|
refresh is required (no `.ail` module currently pinned in
|
|
`hash_pin.rs` contains a `(lam ...)` term — verified per-file).
|
|
|
|
**Tech Stack:** `serde_json` rename attributes, AILang `Term::Lam`
|
|
schema, `design_schema_drift.rs` anchor walker, `anchor_in_jsonc_block`
|
|
helper.
|
|
|
|
---
|
|
|
|
## Files this plan creates or modifies
|
|
|
|
- Modify: `crates/ailang-core/tests/design_schema_drift.rs` — add
|
|
one new `#[test] fn` (the schema-shape pin) and extend the
|
|
existing `anchor_in_jsonc_block` walk to require the new tag
|
|
spellings inside the `Term::Lam` fenced JSON block of
|
|
`data-model.md`.
|
|
- Modify: `crates/ailang-core/src/ast.rs:492,494` — two
|
|
`#[serde(rename)]` string-literal swaps.
|
|
- Modify: `crates/ailang-core/src/ast.rs:8` — module-level `//!`
|
|
rustdoc enumerates rename targets.
|
|
- Modify: `crates/ailang-core/src/workspace.rs:1925-1926` — in-source
|
|
JSON literal inside `#[test] fn ct1_validator_walks_lam_embedded_types`
|
|
(function at line 1911).
|
|
- Modify: `crates/ailang-surface/src/parse.rs:81` — module-level
|
|
`//!` rustdoc mentions `lam`'s `paramTypes` carry.
|
|
- Modify: `crates/ailang-check/src/lib.rs:1707` — `///` rustdoc on
|
|
the InstanceMethod routing helper.
|
|
- Modify: `design/contracts/0002-data-model.md:142-147` — fenced JSON
|
|
block describing the `Term::Lam` schema.
|
|
- Modify: `examples/test_loop_binder_captured_by_lambda.ail.json`
|
|
— two key renames, one per line (currently lines 32-33; verify
|
|
by grep before editing).
|
|
- Modify: `experiments/2026-05-12-cross-model-authoring/master/examples/fn_with_lambda.ail.json`
|
|
— two key renames, one per line (currently lines 25-26).
|
|
|
|
**Forbidden touches** (per spec acceptance criterion 7):
|
|
- `docs/plans/2026-05-10-23.3-*.md`, `docs/plans/0003-22b3-monomorphisation.md`,
|
|
`docs/plans/0005-22c-user-class-e2e.md`, `docs/plans/0056-24.2.md`
|
|
- `experiments/2026-05-12-cross-model-authoring/master/spec.md`
|
|
- `experiments/2026-05-12-cross-model-authoring/rendered/*.md`
|
|
- `experiments/2026-05-12-cross-model-authoring/runs/**`
|
|
|
|
These embed the old tag names but are frozen historical artefacts.
|
|
|
|
---
|
|
|
|
## Task 1: RED — pin the new schema shape
|
|
|
|
**Files:**
|
|
- Modify: `crates/ailang-core/tests/design_schema_drift.rs`
|
|
|
|
The pin lives in `design_schema_drift.rs` (not `schema_coverage.rs`)
|
|
because that file already constructs `Term::Lam` exemplars at
|
|
L121-129, already includes `data-model.md` via `include_str!`
|
|
(L22, constant name `DATA_MODEL`), and already calls the
|
|
`anchor_in_jsonc_block` helper (L34-47, called at L184) for fenced-
|
|
block walks. The new pin and the data-model anchor extension belong
|
|
in the same test file as the rest of the schema-drift family.
|
|
|
|
- [ ] **Step 1: Read the existing pin family**
|
|
|
|
Read `crates/ailang-core/tests/design_schema_drift.rs`. Note:
|
|
- the `Term::Lam` exemplar around L121-129
|
|
- the `anchor_in_jsonc_block` helper at L34-47
|
|
- the existing `design_md_anchors_every_term_variant` test (where
|
|
the new tag anchors will land)
|
|
|
|
Step exists to ground the new test in the file's prevailing shape;
|
|
no edit.
|
|
|
|
- [ ] **Step 2: Add the schema-shape pin**
|
|
|
|
Append at end of `crates/ailang-core/tests/design_schema_drift.rs`
|
|
(or co-locate with the other `Term::Lam`-touching tests at L121-129
|
|
— pick the location with the closest sibling test):
|
|
|
|
```rust
|
|
/// 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() {
|
|
use ailang_core::ast::*;
|
|
|
|
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",
|
|
);
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 3: Extend the data-model.md anchor walk**
|
|
|
|
Find the existing test that uses `anchor_in_jsonc_block` to assert
|
|
fenced-block content (recon names `design_md_anchors_every_term_variant`
|
|
at the test name level, and `anchor_in_jsonc_block` calls at L184).
|
|
Read the existing anchor list, identify where the `lam` block
|
|
anchors live, and add two new anchor assertions for the same `lam`
|
|
fenced JSON block:
|
|
|
|
```rust
|
|
assert!(
|
|
anchor_in_jsonc_block(DATA_MODEL, "lam", "\"param-types\""),
|
|
"data-model.md `lam` fenced block must list `param-types` key",
|
|
);
|
|
assert!(
|
|
anchor_in_jsonc_block(DATA_MODEL, "lam", "\"ret-type\""),
|
|
"data-model.md `lam` fenced block must list `ret-type` key",
|
|
);
|
|
```
|
|
|
|
If the existing test asserts anchors via a `for (variant, anchor) in &[…]` table, add the two rows there instead of two
|
|
standalone `assert!`s — match the local shape. The implementer
|
|
reads the file to confirm the local shape before editing.
|
|
|
|
- [ ] **Step 4: Run the new pin and verify it FAILS**
|
|
|
|
Run: `cargo test -p ailang-core --test design_schema_drift lam_serialises_with_kebab_keys 2>&1 | tail -30`
|
|
|
|
Expected: FAIL. The current `ast.rs:492,494` emit `paramTypes` /
|
|
`retType`, so `obj.contains_key("param-types")` is false. The
|
|
data-model anchor extension also FAILs because lines 142-147 still
|
|
carry camelCase. Both RED conditions confirm we are pinning a real
|
|
property the milestone is about to make true.
|
|
|
|
If the test unexpectedly passes, STOP — the assumption "current
|
|
state emits camelCase" is wrong; re-read `ast.rs:490-499` before
|
|
proceeding.
|
|
|
|
---
|
|
|
|
## Task 2: GREEN-1 — atomic schema-coupled swap
|
|
|
|
**Files:**
|
|
- Modify: `crates/ailang-core/src/ast.rs:492` (line `#[serde(rename = "paramTypes")]`)
|
|
- Modify: `crates/ailang-core/src/ast.rs:494` (line `#[serde(rename = "retType")]`)
|
|
- Modify: `crates/ailang-core/src/workspace.rs:1925-1926`
|
|
- Modify: `examples/test_loop_binder_captured_by_lambda.ail.json`
|
|
- Modify: `experiments/2026-05-12-cross-model-authoring/master/examples/fn_with_lambda.ail.json`
|
|
- Modify: `design/contracts/0002-data-model.md:142-147`
|
|
|
|
These files MUST move together. Changing only `ast.rs` makes any
|
|
JSON literal containing the old tags undeserialisable: serde
|
|
`Term::Lam` has no `#[serde(default)]` on `param_tys` / `ret_ty`,
|
|
so an absent (renamed-away) key is a hard deserialise error. The
|
|
workspace.rs test, the two `.ail.json` fixtures, and the
|
|
data-model.md fenced JSON block all participate in the same
|
|
serialiser/deserialiser contract — they migrate atomically, in a
|
|
single task, with the full `cargo test` gate at the end.
|
|
|
|
`data-model.md` is included by `data-model.md` consumer
|
|
`design_schema_drift.rs` via `include_str!`; the constant is
|
|
`DATA_MODEL`. After Task 1's anchor extension, leaving the fenced
|
|
JSON block on camelCase would keep the test RED.
|
|
|
|
- [ ] **Step 1: Edit `crates/ailang-core/src/ast.rs:492`**
|
|
|
|
Locate line 492:
|
|
|
|
```rust
|
|
#[serde(rename = "paramTypes")]
|
|
```
|
|
|
|
Replace with:
|
|
|
|
```rust
|
|
#[serde(rename = "param-types")]
|
|
```
|
|
|
|
- [ ] **Step 2: Edit `crates/ailang-core/src/ast.rs:494`**
|
|
|
|
Locate line 494:
|
|
|
|
```rust
|
|
#[serde(rename = "retType")]
|
|
```
|
|
|
|
Replace with:
|
|
|
|
```rust
|
|
#[serde(rename = "ret-type")]
|
|
```
|
|
|
|
- [ ] **Step 3: Edit `crates/ailang-core/src/workspace.rs:1925-1926`**
|
|
|
|
The two lines live inside the test function
|
|
`ct1_validator_walks_lam_embedded_types` (function attribute `#[test]`
|
|
at L1911). Find the literal lines:
|
|
|
|
```rust
|
|
"paramTypes": [{ "k": "con", "name": "Ordering" }],
|
|
"retType": { "k": "con", "name": "Unit" },
|
|
```
|
|
|
|
Replace with (preserve exact indentation and surrounding context):
|
|
|
|
```rust
|
|
"param-types": [{ "k": "con", "name": "Ordering" }],
|
|
"ret-type": { "k": "con", "name": "Unit" },
|
|
```
|
|
|
|
- [ ] **Step 4: Edit `examples/test_loop_binder_captured_by_lambda.ail.json`**
|
|
|
|
Grep first to confirm exact occurrences:
|
|
|
|
Run: `grep -n '"paramTypes"\|"retType"' examples/test_loop_binder_captured_by_lambda.ail.json`
|
|
|
|
Expected: 2 hits, one with `"paramTypes"`, one with `"retType"`,
|
|
on consecutive lines (currently 32-33; verify).
|
|
|
|
For each line, replace `"paramTypes"` → `"param-types"` and
|
|
`"retType"` → `"ret-type"`. No other content changes.
|
|
|
|
- [ ] **Step 5: Edit `experiments/2026-05-12-cross-model-authoring/master/examples/fn_with_lambda.ail.json`**
|
|
|
|
Grep first:
|
|
|
|
Run: `grep -n '"paramTypes"\|"retType"' experiments/2026-05-12-cross-model-authoring/master/examples/fn_with_lambda.ail.json`
|
|
|
|
Expected: 2 hits on consecutive lines (currently 25-26; verify).
|
|
|
|
Same rename as Step 4.
|
|
|
|
- [ ] **Step 6: Edit `design/contracts/0002-data-model.md:142-147`**
|
|
|
|
The fenced JSON block currently reads (lines 142-147):
|
|
|
|
```
|
|
{ "t": "lam",
|
|
"params": ["<id>"...],
|
|
"paramTypes": [Type...],
|
|
"retType": Type,
|
|
"effects": ["<id>"...],
|
|
"body": Term }
|
|
```
|
|
|
|
Replace lines 144-145 with:
|
|
|
|
```
|
|
"param-types": [Type...],
|
|
"ret-type": Type,
|
|
```
|
|
|
|
The surrounding `{ "t": "lam", ...` opener, `"params"` line,
|
|
`"effects"` line, and `"body"` line are untouched. Verify the
|
|
fenced-block boundaries (` ``` ` markers) above and below remain
|
|
intact.
|
|
|
|
- [ ] **Step 7: Build the workspace to catch any deserialisation regression**
|
|
|
|
Run: `cargo build --workspace 2>&1 | tail -20`
|
|
|
|
Expected: clean build, no new warnings.
|
|
|
|
- [ ] **Step 8: Run the schema-shape pin (now expected GREEN)**
|
|
|
|
Run: `cargo test -p ailang-core --test design_schema_drift lam_serialises_with_kebab_keys 2>&1 | tail -10`
|
|
|
|
Expected: PASS (Task 1's assertion now holds).
|
|
|
|
- [ ] **Step 9: Run the extended data-model anchor walk**
|
|
|
|
Run: `cargo test -p ailang-core --test design_schema_drift design_md_anchors_every_term_variant 2>&1 | tail -10`
|
|
|
|
Expected: PASS (the data-model.md fenced JSON block now carries
|
|
the new tag anchors).
|
|
|
|
- [ ] **Step 10: Run the workspace ct1 test**
|
|
|
|
Run: `cargo test -p ailang-core ct1_validator_walks_lam_embedded_types 2>&1 | tail -10`
|
|
|
|
Expected: PASS. This is the in-source test that owns the JSON
|
|
literal edited in Step 3; if Step 3 missed a key, this test fails
|
|
with a `serde_json::from_value` error mentioning the missing field.
|
|
|
|
---
|
|
|
|
## Task 3: GREEN-2 — rustdoc honesty pass
|
|
|
|
**Files:**
|
|
- Modify: `crates/ailang-core/src/ast.rs:8`
|
|
- Modify: `crates/ailang-surface/src/parse.rs:81`
|
|
- Modify: `crates/ailang-check/src/lib.rs:1707`
|
|
|
|
These are pure prose edits — no compile or test consequence. They
|
|
exist solely to keep the rustdoc honest after the rename. Per the
|
|
Honesty Rule (`design/contracts/0007-honesty-rule.md`), production
|
|
rustdoc describes present-state vocabulary, not history.
|
|
|
|
- [ ] **Step 1: Edit `crates/ailang-core/src/ast.rs:8`**
|
|
|
|
Locate line 8 in the module-level `//!` block. Current text:
|
|
|
|
```rust
|
|
//! `fn`, `paramTypes`, `retType`), enum tags (`kind`, `t`, `k`, `p`),
|
|
```
|
|
|
|
Replace with:
|
|
|
|
```rust
|
|
//! `fn`, `param-types`, `ret-type`), enum tags (`kind`, `t`, `k`, `p`),
|
|
```
|
|
|
|
The surrounding `//!` lines are untouched.
|
|
|
|
- [ ] **Step 2: Edit `crates/ailang-surface/src/parse.rs:81`**
|
|
|
|
Locate line 81 in the module-level `//!` block. Current text:
|
|
|
|
```rust
|
|
//! - The `lam` form carries `paramTypes`, a `ret` type, and an
|
|
```
|
|
|
|
Replace with:
|
|
|
|
```rust
|
|
//! - The `lam` form carries `param-types`, a `ret` type, and an
|
|
```
|
|
|
|
If the line wraps differently in the local file (the carrier word
|
|
`paramTypes` may sit on its own line or break across lines after
|
|
the comma), edit the contiguous `paramTypes` occurrence; do NOT
|
|
soft-wrap split the new `param-types` across two lines.
|
|
|
|
- [ ] **Step 3: Edit `crates/ailang-check/src/lib.rs:1707`**
|
|
|
|
Locate line 1707. Current text:
|
|
|
|
```rust
|
|
/// convention a `Term::Lam` carrying its own `paramTypes` / `retType`
|
|
```
|
|
|
|
Replace with:
|
|
|
|
```rust
|
|
/// convention a `Term::Lam` carrying its own `param-types` / `ret-type`
|
|
```
|
|
|
|
- [ ] **Step 4: Run a doc-build to catch broken intra-doc links**
|
|
|
|
Run: `cargo doc --workspace --no-deps 2>&1 | grep -E "warning|error" | head -20`
|
|
|
|
Expected: no NEW warnings (the workspace already carries a
|
|
known-stable warning count; flag any new diagnostic that mentions
|
|
`paramTypes` / `retType` / `param-types` / `ret-type`).
|
|
|
|
---
|
|
|
|
## Task 4: Final verification
|
|
|
|
**Files:** none modified — verification only.
|
|
|
|
- [ ] **Step 1: Exhaustive grep — nothing left on the old tags**
|
|
|
|
Run: `grep -rn '"paramTypes"\|"retType"\|paramTypes\|retType' crates/ examples/ design/ runtime/ --include='*.rs' --include='*.json' --include='*.md' 2>/dev/null`
|
|
|
|
Expected output (and only this output):
|
|
- `docs/plans/*.md` matches in the historic plan files listed under "Forbidden touches" above.
|
|
- `experiments/2026-05-12-cross-model-authoring/master/spec.md` matches.
|
|
- `experiments/2026-05-12-cross-model-authoring/rendered/*.md` matches.
|
|
- `experiments/2026-05-12-cross-model-authoring/runs/**` matches.
|
|
- ZERO matches under `crates/`, `examples/`, `runtime/`, or `design/`.
|
|
|
|
Any match outside the expected set is a missed touch-point; resolve
|
|
it by adding the rename, then re-run the full test suite (Step 2).
|
|
|
|
- [ ] **Step 2: Run the workspace test suite**
|
|
|
|
Run: `cargo test --workspace 2>&1 | tail -25`
|
|
|
|
Expected: every crate's test summary shows `0 failed`. The total
|
|
test-group count must equal the pre-iter baseline of 90
|
|
(unchanged — Task 1 adds one new test inside an existing test crate,
|
|
which does not change the test-group count).
|
|
|
|
- [ ] **Step 3: Round-trip invariance check**
|
|
|
|
Run: `cargo test -p ailang-surface --test round_trip 2>&1 | tail -10`
|
|
|
|
Expected: PASS. This is the Form-A-unchanged gate; if it fails
|
|
the spec's Layer-2 claim ("Form-A never used these tag names") was
|
|
wrong and the iteration scope was mis-sized.
|
|
|
|
- [ ] **Step 4: Confirm no forbidden files were touched**
|
|
|
|
Run: `git diff --name-only HEAD 2>&1 | grep -E "docs/plans/|experiments/.*/(master/spec\.md|rendered/|runs/)" | head -20`
|
|
|
|
Expected: empty output. If any line appears, the implementer
|
|
mutated a forbidden file — `git checkout -- <path>` the change.
|
|
|
|
- [ ] **Step 5: Sanity check the new pin would actually go RED**
|
|
|
|
Mental check (no `git revert` — main is sacrosanct): the pin in
|
|
Task 1 was confirmed RED in Task 1 Step 4 before the swap. Step 8
|
|
of Task 2 confirmed it GREEN after the swap. Therefore the pin is
|
|
a real RED/GREEN bisector. No additional action — this step exists
|
|
to mark the property as audited.
|
|
|
|
- [ ] **Step 6: Status summary for Boss commit**
|
|
|
|
Print a one-line summary of the resulting `git status`:
|
|
|
|
Run: `git status --short`
|
|
|
|
Expected: nine modified files (the eight listed in "Files this
|
|
plan creates or modifies" plus the new test slot in
|
|
`design_schema_drift.rs`), no untracked files of interest. The
|
|
Boss will inspect the diff and assemble the iter-level commit.
|