convention: counter-prefix file naming across docs/specs/, docs/plans/, design/contracts/, design/models/
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.
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
# Roundtrip Invariant
|
||||
|
||||
## Roundtrip Invariant
|
||||
|
||||
Every well-formed AILang module has a canonical `.ail.json`
|
||||
representation (the hashable, content-addressed
|
||||
[JSON-AST](0002-data-model.md)) and a hand-authored `.ail` representation
|
||||
(Form A, the [authoring projection](0001-authoring-surface.md)). The
|
||||
JSON-AST is derived from `.ail` in-process by consumers that need
|
||||
it; the round-trip is now the *property* of that derivation, not
|
||||
the byte-level agreement of two on-disk forms.
|
||||
|
||||
Concretely:
|
||||
|
||||
1. **Parse-determinism.** For every well-formed `.ail` text `t`,
|
||||
`ailang_surface::parse(t)` produces a unique AST. The parser is
|
||||
a pure function of input — no randomness, no time dependence,
|
||||
no environment leak. Hashing `canonical::to_bytes(parse(t))` is
|
||||
therefore well-defined for any `.ail` source.
|
||||
|
||||
2. **Idempotency under print.** For every well-formed `.ail` text
|
||||
`t`, `canonical(parse(t)) == canonical(parse(print(parse(t))))`.
|
||||
The printer is a left-inverse of the parser modulo canonical
|
||||
form: print-then-parse is a no-op on the canonical bytes.
|
||||
|
||||
3. **CLI-pipeline idempotency.** For every `.ail` fixture, the
|
||||
public CLI pipeline `ail parse | ail render | ail parse` is
|
||||
byte-identical to direct `ail parse` of the source `.ail`.
|
||||
Pins drift that crate-internal tests cannot see.
|
||||
|
||||
4. **Carve-out anchor.** Seven `.ail.json`-only fixtures
|
||||
(subject-matter rejection tests) survive in the corpus by
|
||||
structural necessity. They participate in their own dedicated
|
||||
rejection-shape tests, not in the round-trip gate. (The prelude is
|
||||
embedded as `examples/prelude.ail` in `ailang-surface` and parsed
|
||||
at compile time via `ailang_surface::parse_prelude`.)
|
||||
|
||||
Hashing is the consequence the language depends on: BLAKE3 of the
|
||||
canonical bytes is well-defined for any `.ail` source via parse-
|
||||
determinism. An LLM author writes `.ail`; the build derives the
|
||||
canonical hash without ambiguity.
|
||||
|
||||
### Float literals are inside the invariant
|
||||
|
||||
Float literals carry an IEEE-754 bit pattern, not a decimal
|
||||
approximation. The canonical encoding is
|
||||
`{"kind":"float","bits":"<16-lowercase-hex>"}` and the surface
|
||||
emits the same bits-hex string. NaN, ±Inf, signed zero, and subnormals all
|
||||
round-trip exactly because the JSON-number path is bypassed (see
|
||||
[Float semantics](0005-float-semantics.md), "Form-A serialisation").
|
||||
Floats are not an exception to the invariant — the bits-hex
|
||||
encoding is the mechanism that keeps them *inside* it.
|
||||
|
||||
### Enforcement
|
||||
|
||||
The invariant is workspace-CI-enforced by five tests, each
|
||||
operating on the `examples/` corpus via dynamic `read_dir`
|
||||
collection (no hardcoded fixture list, so newly added fixtures
|
||||
inherit the gate automatically):
|
||||
|
||||
- `crates/ailang-surface/tests/round_trip.rs::parse_is_deterministic_over_every_ail_fixture`
|
||||
— for every `.ail`, parse twice and assert canonical-byte
|
||||
equality between the two ASTs. Direction 1 above.
|
||||
- `crates/ailang-surface/tests/round_trip.rs::parse_then_print_then_parse_is_idempotent_on_every_ail_fixture`
|
||||
— for every `.ail` text `t`, `parse(t)` and `parse(print(parse(t)))`
|
||||
produce canonical-byte-equal AST. Direction 2 above.
|
||||
- `crates/ail/tests/roundtrip_cli.rs::cli_parse_then_render_then_parse_is_idempotent`
|
||||
— for every `.ail`, the public CLI pipeline `ail parse | ail render | ail parse`
|
||||
reproduces canonical bytes byte-identical to direct `ail parse`.
|
||||
Pins drift internal tests cannot see.
|
||||
- `crates/ailang-core/tests/schema_coverage.rs::every_ast_variant_is_observed_in_the_fixture_corpus`
|
||||
— every variant of `Def`, `Term`, `Pattern`, `Literal`, `Type`,
|
||||
and `ParamMode` appears in at least one `.ail` fixture. New AST
|
||||
variants fail compile until the visitor and corpus are extended
|
||||
in lockstep.
|
||||
- `crates/ailang-core/tests/carve_out_inventory.rs::examples_ail_json_inventory_matches_carve_outs`
|
||||
— exactly the seven named carve-out files exist under
|
||||
`examples/*.ail.json` at any commit. A new `.ail.json` or a
|
||||
missing carve-out fails the test.
|
||||
|
||||
A new fixture or a new AST variant that violates the invariant
|
||||
fails one of these tests; the fix is in render or parse code,
|
||||
never by relaxing the test.
|
||||
|
||||
### Why this is anchored at top level
|
||||
|
||||
The invariant is a property of the language identity, not of any
|
||||
one surface-design choice. The [authoring surface](0001-authoring-surface.md)
|
||||
introduces the `.ail` surface and lists round-trip-as-property as
|
||||
one of its constraints, but the property is load-bearing for every
|
||||
downstream concern that treats the two forms as exchangeable:
|
||||
content-addressed hashing, the LLM-author's choice of authoring
|
||||
form, the integrity of fixture cross-references, and the
|
||||
prerequisite for empirical cross-model authoring-form studies.
|
||||
Lifting it out of the authoring-surface contract makes the property
|
||||
quotable on its own and reviewable by the architect agent at every
|
||||
milestone close.
|
||||
|
||||
Ratified by: `crates/ailang-surface/tests/round_trip.rs`.
|
||||
Reference in New Issue
Block a user