Files
AILang/docs/specs/0010-fieldtest-canonical-type-names.md
T
Brummel 832375f2ac 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.
2026-05-28 13:31:31 +02:00

13 KiB

Fieldtest — canonical-type-names — 2026-05-11

Status: Draft — awaiting orchestrator triage Author: ailang-fieldtester (dispatched by skills/fieldtest)

Scope

The canonical-type-names milestone made cross-module type references load-time validated. Within a .ail.json module, bare Type::Con names refer to the file's own definitions; cross-module references MUST be qualified <owning_module>.<TypeName>; primitives stay bare. Three new workspace-load diagnostics were added: bare-cross-module-type-ref, bad-cross-module-type-ref, qualified-class-name. The prose Form-B printer trims the owning-module qualifier when printing a definition file's own types and keeps cross-module qualifiers verbatim.

Examples

Five fixtures were authored, all .ailx first, parsed via ail parse, then checked via ail check. All live at examples/ top-level rather than examples/fieldtest/ because they need cross-module imports (prelude, std_maybe) which the workspace loader only finds beside the entry module — see Findings for that friction.

examples/ct_1_ordering_signum.ailx — sign-profile of an Int list

Reduces a list of Int to a sequence of -1 / 0 / +1 by pattern- matching the result of compare(n, 0) against prelude.Ordering's three constructors. Drives recursion over a local IntList ADT.

  • Why it fits: canonical happy-path exercise of cross-module type reference (prelude.Ordering) AND local-type reference (IntList) in one file.
  • Outcome: parse OK; check OK ("ok (13 symbols across 2 modules)"); build OK; runs cleanly; stdout matches expected -1 / 0 / 1 / -1 / 1.
  • Form-A round-trip: render → parse → JSON is byte-identical.

examples/ct_2_bare_cross_module.ailx — bare Maybe instead of std_maybe.Maybe

A consumer of std_maybe writes (con Maybe (con Int)) in a fn signature instead of (con std_maybe.Maybe (con Int)). This is what an LLM author who forgot the qualification rule would naturally produce.

  • Why it fits: probes axis 1 (BareCrossModuleTypeRef).

  • Outcome: parse OK; check fails with:

    Error: module ct_2_bare_cross_module contains bare type name Maybe that does not resolve to a local type. AILang's .ail.json requires cross-module type references to be qualified. Candidates from imports: ["std_maybe.Maybe"]. Run ail migrate-canonical-types to fix legacy fixtures.

    Diagnostic code: bare-cross-module-type-ref. JSON ctx includes module, name, candidates. Exit code 1.

examples/ct_3_bad_qualified.ailx — qualified mystery.Widget to unknown module

A consumer references (con mystery.Widget) where the module mystery is not in the workspace.

  • Why it fits: probes axis 2 (BadCrossModuleTypeRef — unknown owner).

  • Outcome: parse OK; check fails with:

    Error: module ct_3_bad_qualified references qualified type mystery.Widget but the owner module is not known or does not declare a type by that name

    Diagnostic code: bad-cross-module-type-ref. Exit code 1.

examples/ct_3b_bad_qualified_known_module.ailx — qualified std_maybe.Widget to known module

A consumer references (con std_maybe.Widget). The module std_maybe exists and is imported, but it has no Widget type def.

  • Why it fits: probes axis 2 (BadCrossModuleTypeRef — known owner, unknown type). This is the second branch of the diagnostic's or clause.
  • Outcome: parse OK; check fails with the identical-shape diagnostic as ct_3, just with std_maybe.Widget substituted. See finding friction: BadCrossModuleTypeRef does not distinguish unknown-owner from unknown-type-in-known-owner.

examples/ct_4_qualified_class.ailx — qualified prelude.Eq in an instance

A consumer defines a local Box ADT and writes (instance (class prelude.Eq) (type (con Box)) ...) instead of bare (class Eq). Per the milestone's Out-of-scope note, class names remain bare; a qualified form is a schema violation.

  • Why it fits: probes axis 3 (QualifiedClassName).

  • Outcome: parse OK; check fails with:

    Error: module ct_4_qualified_class contains qualified class name prelude.Eq in field InstanceDef.class. Class names are not module-qualified in this milestone; keep the bare form.

    Diagnostic code: qualified-class-name. JSON ctx includes module, name, field. Exit code 1.

Findings

[working] BareCrossModuleTypeRef diagnostic carries the full fix recipe

Example: ct_2. The diagnostic names the module, the offending bare name, the qualified candidate(s) from imports, the migration tool that automates the rewrite. An LLM author can act on this without re-reading the spec. The ctx field on the JSON diagnostic is structured (module, name, candidates) — machine-actionable.

Recommended downstream action: carry-on.

[working] BadCrossModuleTypeRef catches both Surface and JSON authoring of qualified type refs

Examples: ct_3, ct_3b. The Surface syntax (con mystery.Widget) DOES parse cleanly, which is the right behavior — Surface should be the canonical authoring path, and authoring a deliberately wrong qualifier should not be rejected at the lexer level. The validator catches it later at workspace load. The diagnostic is clear about what's wrong.

Recommended downstream action: carry-on.

[working] QualifiedClassName fires on (instance (class prelude.Eq) ...)

Example: ct_4. The Surface form for an instance with a qualified class is parseable ((class prelude.Eq) produces JSON "class": "prelude.Eq"). The validator rejects it with a clear "keep the bare form" recommendation and names the offending field (InstanceDef.class).

Recommended downstream action: carry-on.

[working] Form-A round-trip is byte-stable across cross-module refs

Example: ct_1. ail render → ail parse → JSON reproduces the input file byte-for-byte. The prose Form-B printer trims the owning-module qualifier for local types correctly (IntList renders bare in ct_1's prose, since IntList is a local def). Cross-module ctors elide the type-name qualifier in prose (std_either_demo.prose shows Right(42) not std_either.Either::Right(42)), but the canonical JSON underneath keeps the qualified form. This matches the spec's "prose drops qualifiers for surface readability while JSON stays canonical" intent.

Recommended downstream action: carry-on.

[friction] BadCrossModuleTypeRef does not distinguish unknown-owner from unknown-type-in-known-owner

Examples: ct_3 (unknown owner mystery) and ct_3b (known owner std_maybe, unknown type Widget) produce identical-shape diagnostics:

references qualified type `<owner>.<name>` but the owner module is
not known or does not declare a type by that name

The two cases suggest different fixes:

  • Unknown owner: add an (import <owner>) clause, or fix the typo in the owner prefix.
  • Known owner, unknown type: fix the typo in the type name, or check what types the owner actually exports.

The merged diagnostic leaves the LLM author guessing. When the owner IS known, the diagnostic could list available type defs in the owner's module the way bare-cross-module-type-ref lists candidates from imports.

Recommended downstream action: plan (tidy iteration to split the two cases and, in the known-owner branch, list available type names as candidates).

[friction] Workspace search does not look beyond the entry-module's directory

Encountered while placing fixtures. The agent spec convention says fieldtest fixtures go under examples/fieldtest/. But every fixture that imports prelude or std_maybe had to be moved to examples/ top-level because the workspace loader only finds sibling .ail.json files in the same directory as the entry module. There is no --workspace-root flag on ail check / ail build, and the diagnostic

module prelude not found (expected at /prelude.ail.json)

names the precise lookup path but offers no remediation. The result is that subdirectories are effectively forbidden for any consumer of stdlib or prelude — an LLM author hoping to organise example files into subdirectories has to either (a) keep everything flat or (b) duplicate stdlib .ail.json files into each subdir.

This is orthogonal to canonical-type-names per se (it predates the milestone) but it surfaced naturally during the field test and shapes how the fixtures could be placed.

Recommended downstream action: plan (a small iteration to add either a --workspace-root flag, or upward-search from the entry module's directory; or ratify the current behavior in DESIGN.md if the flat-workspace assumption is intentional).

[friction] ail check on a .ailx source fails with a misleading JSON-parse error

Encountered immediately on the first invocation: ail check foo.ailx produced:

Error: schema/parse error in foo.ailx: json: expected value at line 1 column 1

The diagnostic blames foo.ailx for not being valid JSON, when the real situation is that ail check only accepts .ail.json. The LLM author has to learn (from ail --help) that ail parse does the surface→JSON step, and then chain ail parse foo.ailx > foo.ail.json && ail check foo.ail.json.

The clean fix would be either (a) ail check recognises .ailx and parses internally, or (b) the diagnostic special-cases the .ailx extension and points to ail parse. Today, neither holds, and an LLM author's natural first command produces a misleading diagnostic.

This is orthogonal to canonical-type-names per se. Orthogonal but load-bearing — every fieldtest invocation hits this.

Recommended downstream action: plan (a small iteration to teach ail check and ail build to accept .ailx, or to detect the extension and emit a "did you mean ail parse foo.ailx?" hint).

[friction] (import prelude) is rejected as reserved

Encountered while writing ct_1. The LLM-natural reach when writing a cross-module program that consumes prelude is to write (import prelude) in the module header. This is rejected:

Error: module name prelude is reserved (auto-injected by the loader)

The diagnostic is clear and short. But the fact that prelude is auto-injected and must NOT be explicitly imported is not documented in any .ailx corpus example I can read (no example uses (import prelude), but also no comment explains why). DESIGN.md mentions prelude as the autodiscovered module but does not say "do not write (import prelude)". An author has to discover this by trying and being told.

This is mild — it's a one-trip teaching diagnostic, and the message is plain. But worth recording.

Recommended downstream action: carry-on (the diagnostic itself is clear; the deeper fix is a DESIGN.md or per-module-comment hint, not a code change).

[spec_gap] No .ailx counterpart for compare_primitives_smoke.ail.json

Observation: the canonical happy-path example shipped with this milestone (examples/compare_primitives_smoke.ail.json) exists only as JSON. There is no .ailx counterpart. Surface IS the LLM-author surface per Decision 6, so a milestone whose "happy path" demonstrates cross-module type usage SHOULD have a .ailx exhibit — otherwise the LLM author has to read raw JSON to understand the pattern. My ct_1_ordering_signum.ailx partly fills this gap (it exercises prelude.Ordering pattern-matching from a Surface program) but it is not bit-identical to compare_primitives_smoke.

Recommended downstream action: plan (tidy iteration: author examples/compare_primitives_smoke.ailx to round-trip into the existing JSON, OR retire compare_primitives_smoke.ail.json in favour of ct_1_ordering_signum.{ailx,ail.json} as the new canonical happy-path exhibit).

Recommendation summary

Finding Class Action
BareCrossModuleTypeRef carries full fix recipe working carry-on
BadCrossModuleTypeRef catches Surface-authored qualified refs working carry-on
QualifiedClassName fires on (class prelude.Eq) working carry-on
Form-A round-trip byte-stable working carry-on
BadCrossModuleTypeRef merges two distinguishable cases friction plan
Workspace search confined to entry dir friction plan or ratify
ail check on .ailx produces misleading JSON-parse error friction plan
(import prelude) rejected — diagnostic clear, doc-gap mild friction carry-on
No .ailx counterpart for compare_primitives_smoke.ail.json spec_gap plan

The four milestone-scoped findings are all working. The three friction items orthogonal to canonical-type-names are pre-existing UX gaps surfaced by the field test. The two canonical-type-names-internal friction items (BadCrossModuleTypeRef case-merging; missing .ailx exhibit) are recommendable tidy iterations.