Files
AILang/CLAUDE.md
T
Brummel a6fd93adba feat(codegen): pre-resolve the App callee in MIR; delete is_static_callee + type_home_module (mir.2)
Second re-deriver removal of the typed-MIR milestone (spec
docs/specs/0060-typed-mir.md, plan docs/plans/0116-mir.2-callee-static.md).
Static-callee resolution moves out of codegen and into lower_to_mir:
every Term::App callee is classified once, mirroring check's own synth
Term::Var ladder (lib.rs:3409-3582), and emitted as a resolved Callee.
Codegen reads the callee identity off MIR and routes each kind to the
right lowering. The codegen re-derivers is_static_callee and
type_home_module are deleted, and the lower_app <-> is_static_callee
lockstep pair is struck from CLAUDE.md.

Callee reshape (beyond the spec's original {module, fn_name} sketch —
spec 0060's Concrete-code-shapes block is updated in this iteration):

  pub enum Callee {
      Static  { module, fn_name, sig: Type },  // user/prelude fn -> emit_call, module pre-resolved
      Builtin { name, sig: Type },             // operator/intrinsic -> inline opcode lowering
      Indirect(Box<MTerm>),                    // dynamic: fn-pointer / closure / shadowed name
  }

Two forced refinements, both settled in planning:

- sig:Type on each resolved variant. drop::synth_callee_ret_mode reads
  the callee ret_mode, today off Callee::Indirect(inner).ty(). A
  resolved callee has no sub-term, so the sig (= synth_pure(callee),
  mode-preserving) is the lossless replacement. This is NOT a mir.3
  pull-forward: the MArg param-mode / consume_count fields stay at their
  mir.1 defaults. Without it, every statically-resolved call would lose
  its drop signal and the RawBuf / int_to_str RC-stats pins mir.1b fixed
  would regress.

- Builtin variant. Operators and the str-num builtins (+, not,
  str_concat, ...) are lowered inline by name and have no module/fn_name;
  a separate variant is cleaner than a sentinel module. The classifier
  decides Builtin vs Static from CHECK'S OWN resolution (a name in
  env.globals with no owning module is a builtin), never a copy of
  codegen's old is_static_callee allowlist. The recon's divergence audit
  confirmed check's builtin set and codegen's inline-arm set match
  exactly, so the single-engine rule is mechanically satisfiable with no
  env threading gap.

type_home_module is fully deletable: a workspace-wide grep confirmed no
program references a type-scoped T.fn as a value, so its only other
consumer (resolve_top_level_fn) collapses to the module-name fallback
with no behaviour change. (The spec's "x3 mirrors" wording over-counted;
the live surface was the definition plus two call sites.)

Alternatives rejected during planning: sentinel module in Static
(ugly); builtins left as Indirect (breaks — no fn-pointer for an
operator); name-rewrite Static{name,sig} keeping lower_app's by-name
dispatch (blurs builtin/user and still needs sig). The mir.1b
re-synth-strictness trap (post-mono synth_pure re-unify surfacing
mode-strip / bare-vs-qualified / metavar leaks) did NOT fire under the
new producer path — qualify_local_types mode-preservation and the $u
wildcard normalisation from mir.1b held.

Verification (orchestrator, post-implement inspect): diff matches the
plan; grep-clean for is_static_callee + type_home_module across
ailang-codegen and ail; cargo build --workspace green; cargo test
--workspace 700 passed / 0 failed / 3 ignored (no #[ignore] added this
iteration — the 3 are the pre-existing #49 Str-leg pin + two doctests).
Acceptance witnesses green: #53 (mono_new_over_user_adt_builds_and_runs),
#51 (rawbuf_new_int_size_only_builds_and_runs still builds), show_print
cross-module (print_user_adt_runs_end_to_end), the new mir.2 pins
(mir2_resolved_callee_kinds_run_end_to_end, callee_classification_*,
strengthened new_over_user_adt_carries_node_types asserting Callee::Static).
#49 heap-Str loop binder stays #[ignore] (lifts at mir.4).

Two new examples/ fixtures back the new pins (classify_pin.ail for the
producer classification pin, mir2_callee_kinds.ail for the E2E),
ail-parse / round-trip clean.

Forward note (cycle-close architect): crates/ail/tests/codegen_import_map_fallback_pin.rs:14-15
doc prose names lower_app's cross-module arm and synth_with_extras as
failure-mode targets — both now deleted (mir.2 / mir.1b). The pin's
protected invariant is still valid and green; the prose is stale and
wants a doc-honesty reword, left out of this iteration's footprint.
2026-05-31 19:27:56 +02:00

17 KiB

Inherits ~/.claude/CLAUDE.md (chat language, IONOS security) and ~/dev/CLAUDE.md (cross-project dev rules: English in repos, commit convention, issue-tracker vocabulary). This file carries only AILang-specific decisions on top — the language identity, code layout, file-naming convention, the project-calibrated orchestrator discipline (the skill system itself lives in the ~/dev/skills/ plugin and is wired into this project via .claude/dev-cycle-profile.yml), and the LLM-utility feature gate.

AILang — a language for LLM authors

AILang's only author is an LLM, not a human. It is designed for:

  • Machine readability over human readability. The canonical, hashable, content-addressed form is structured data (.ail.json); the authoring projection is Form A (.ail). Authors write .ail; the build derives the JSON-AST in-process via ailang_surface::parse, gated by the round-trip invariant. The two forms are byte-isomorphic — picking either does not change the identity of the module.
  • Local reasoning. Every definition carries its full type and effect set, so a signature can be trusted without reading the body.
  • Provability. Pure core, explicit algebraic effects.
  • Robustness against hallucinations. Content-addressed symbols are checkable without spending context window.

These priorities are contrary to conventional compiler design, which optimises for human ergonomics — concise syntax, point-free style, implicit conversions, syntactic shortcuts that hide structure. AILang keeps none of those. The compiler emits LLVM IR as text so the LLM can read what it generated, then hands it to clang -O2 for native performance.

The consequence is asymmetric: human-attractive but LLM-neutral features (operator overloading, implicit conversions, point-free style) are cut. Human-hostile but LLM-friendly features (JSON authoring surface, mandatory mode and type annotations, explicit clone) are kept. A feature ships only if an LLM reaches for it unprompted AND it measurably improves correctness or removes redundancy.

Code layout

Path Role
crates/ail/ CLI entry point — subcommands include check, build, run, emit-ir, prose, merge-prose, workspace, diff, manifest, render, describe, deps, parse, builtins
crates/ailang-core/ AST, canonicalisation, desugaring, workspace types, hash, pretty
crates/ailang-surface/ Surface syntax — lex, parse, print
crates/ailang-check/ Type and uniqueness/mode analysis, lints, diagnostics
crates/ailang-codegen/ LLVM-IR codegen — RC, drop, lambda lowering, match lowering, escape, synth, subst
crates/ailang-prose/ Form-A ↔ Form-B prose projection
crates/ailang-kernel/ Family-crate hosting the Form-A source of every kernel-tier .ail module, one Rust submodule per module under src/ (today: kernel_stub), each exposing a pub const SOURCE re-exported from src/lib.rs (STUB_AIL). Zero-dependency leaf; parse hops (parse_kernel_stub) live in ailang-surface to keep the graph acyclic (ailang-surface → ailang-kernel → ailang-core). The kernel_stub submodule is injected alongside prelude and is retired in raw-buf.4 once raw_buf becomes the real kernel-tier consumer.
runtime/ C glue around the RC runtime
bench/ Regression harnesses (check.py, compile_check.py, cross_lang.py) and the throughput-and-latency runner (run.sh); bench/reference/ holds the hand-C corpus for cross-language ratios
examples/ AILang fixtures used by tests and benches
design/ The canonical contract ledger — design/INDEX.md (sole addressable spine: a typed Contracts + Models table), design/contracts/ (prose-authoritative test-linked invariants), design/models/ (onboarding whitepapers). Files under contracts/ and models/ follow the counter-prefix convention (see "File-naming convention" below).
docs/ Specs and plans — docs/specs/ (per-milestone design specs), docs/plans/ (per-iteration plans), PROSE_ROUNDTRIP.md. Files under specs/ and plans/ follow the counter-prefix convention (see "File-naming convention" below). Project history lives in git log; the forward queue lives in Gitea issues (see "Roles" section below).
.claude/dev-cycle-profile.yml Project profile consumed by the ~/dev/skills/ plugin — declares paths, commands, vocabulary, naming policy, issue tracker, and pipeline customisations. See ~/dev/skills/docs/profile-schema.md for the slot reference.

File-naming convention

Four directories accumulate files over the life of the project and use a zero-padded counter prefix that reflects creation order:

  • docs/specs/NNNN-slug.md
  • docs/plans/NNNN-slug.md
  • design/contracts/NNNN-slug.md
  • design/models/NNNN-slug.md

The counter is 4-digit, zero-padded, assigned per directory in strict creation order. A new file takes the next-higher counter for its directory. A file's counter is stable for the life of the file — never reassigned, never reused, never compacted. If a file is deleted, its counter is retired; subsequent files do not fill the gap. This is the property that lets cross-references stay literal.

Slugs are the short identity (honesty-rule, skill-system-buildout). Cross-references include the full filename with counter (design/contracts/0007-honesty-rule.md) so they grep cleanly and resolve directly without a glob step. The convention forbids renumbering precisely so refs do not need to chase moving prefixes.

The backfill on 2026-05-28 assigned counters by git-log creation timestamp (alphabetical by original name on ties) and stripped the old YYYY-MM-DD- prefix from docs/specs/ and docs/plans/ — the date is recoverable from git log and the counter carries the ordering.

Skill system

Day-to-day discipline lives in the ~/dev/skills/ plugin — generic SKILL.md files at ~/dev/skills/<name>/SKILL.md, agents under ~/dev/skills/<name>/agents/<agent>.md. The plugin reads project-specific slots (paths, commands, vocabulary, naming, issue tracker, pipeline customisations) from this project's profile at .claude/dev-cycle-profile.yml. See ~/dev/skills/README.md for the skill table and ~/dev/skills/docs/profile-schema.md for the slot schema. Skills are sharper tools, not a replacement for orchestrator judgement.

Specs go to docs/specs/<milestone>.md, plans to docs/plans/<iteration>.md — both per paths.spec_dir / paths.plan_dir in the profile.

Autonomous orchestrator mode — picking the next iter from the Gitea issue backlog and looping until done-state — is gated to the user-invoked /boss skill (~/dev/skills/boss/SKILL.md). Outside /boss, the default is interactive collaboration: the user asks, Claude responds, Claude stops.

My role: orchestrator

I am the orchestrator of this project, not the implementer. The agents under ~/dev/skills/<name>/agents/ are my workers. I direct them, review their output, and integrate it. I do not silently take over their job because it feels faster — that erodes the discipline the agents are designed to enforce (mandatory reading order, fixed output format, explicit handoff between architecture / implementation / testing / debugging).

See ~/dev/skills/README.md for the skill + agent roster.

What this means in practice

  • Plan, design, decide — myself. Architectural choices, scope, invariants, commit bodies, and the contents of the design/ ledger are my work product.
  • Implement, refactor, write tests, diagnose bugs — by default, delegated. implementer for code changes that follow a fixed design, tester for E2E coverage, debugger for diagnostics, architect for read-only drift review.
  • Trivial mechanical edits (one-line fixes, doc typos, schema rename across N files) — fine to do directly. Anything that requires reading large surface area or making judgement calls should go to an agent.
  • Verify the work — agent reports describe intent, not outcome. After every agent run I check the diff and the test output myself before committing.

Commit discipline and main-branch sanctity

Two project-wide rules govern who touches git history and how:

  • Only the Boss (me) commits. No skill agent — implementer, brainstormer, planner, debugger, fieldtester, docwriter, architect, bencher — runs git commit. Every agent writes its output (spec, plan, code, tests, fixtures, rustdoc edits, RED tests, stats, updated baselines; BLOCKED.md on PARTIAL/BLOCKED iter outcomes) into the working tree as unstaged changes. I inspect the result with git status / git diff, decide commit shape (often one cohesive iter-level commit; sometimes a few logical commits when the changes genuinely cover separate concerns), and commit. Per-task or per-phase commits are not a goal in themselves. BLOCKED.md is never committed by convention — Boss removes it on repair or discard.
  • main HEAD is sacrosanct. Nobody (including me) runs git reset or git revert on main. main moves forward only via my commits. The consequence is the working-tree-as-quarantine discipline: nothing half-baked enters main, because nothing can be taken back off. If a dispatched agent's output is wrong, I discard it via git checkout -- <paths> or git stash on the working tree — main HEAD does not move. If something wrong does land on main, the remedy is a forward-fix commit, never a rewind.

These rules supersede earlier mechanics that involved per-iter branches and per-task agent commits. The same rules in plugin form live in ~/dev/skills/README.md and ~/dev/skills/templates/CLAUDE.md.fragment.

Authority over the skill plugin

The skill plugin lives at ~/dev/skills/ and is shared across projects. I am free to add, edit, retire, or replace skill or agent definitions in the plugin whenever the orchestration needs it. Concretely:

  • Adjust an agent's mandatory reading list (in ~/dev/skills/<skill>/agents/<agent>.md, or per-project via standing_reading.by_role.<agent> in this project's profile).
  • Tighten an agent or skill output format if reports are getting verbose.
  • Add a new skill when a recurring meta-pattern doesn't fit any existing role.
  • Add a new agent when a recurring task doesn't fit any existing agent (e.g. a release-cutter).
  • Retire an agent or skill that has become redundant.

Plugin edits are universal (every project that consumes the plugin sees them). Edits that are only relevant to AILang go in the profile or in this CLAUDE.md, not in the plugin.

Skill and agent definitions are versioned files like any other code — changes go through git in ~/dev/skills/, with a commit message that says why the role shifted. I treat them as part of the toolchain, not as immutable scripture.

When NOT to delegate

  • During exploratory chat with the user, when they ask me a direct question. The user talks to me, not to my agents.
  • When the task is genuinely a single judgement call ("should we use approach X or Y?") — that is orchestrator work.
  • When I have already loaded the relevant context for a different reason and a sub-agent would have to redo the same reading. In that case I do the small change inline and note in the commit body why I bypassed the agent.

Design rationale ≠ implementation effort

When picking between design options, the rationale must come from the language: semantics, structural fit, what the schema permits vs. forbids, compositional clarity, future-proofing. Implementation effort is not a rationale. "Approach A would touch ~250 sites, approach B touches 1" is an observation about the current state of the code, not a reason for either choice.

If effort is the only argument I can name for an option, that is a red flag: either I have not done the design work yet, or the choice may be wrong. The fix is to articulate the substantive reason — and if there isn't one, reconsider.

Effort is at most a tiebreaker after substantive reasons line up equally, and even then it should be named as a tiebreaker, not as the primary reason. The 18a "Type::Fn metadata vs. Type variant" call is the canonical anti-example: the right reason was semantic locality (modes belong to fn-parameter positions, not to types in general), and I retroactively had to add it.

Feature acceptance: LLM utility

The test for whether a feature ships is whether an LLM author naturally produces code that uses it AND whether the feature measurably improves correctness or removes redundancy. Aesthetic appeal does not count; neither does human ergonomics. Full criterion lives in design/contracts/0004-feature-acceptance.md and is applied as a gate by the plugin's brainstorm skill during spec writing — see ~/dev/skills/brainstorm/SKILL.md Step 4.

Bug fixes — TDD, always

Bug fixes are RED-first, autonomous, no orchestrator gate. See ~/dev/skills/debug/SKILL.md (trigger + handoff) and ~/dev/skills/debug/agents/debugger.md (Iron Law, four phases, Phase 4.5 architecture trigger).

Milestone cycle

Work clusters into milestones, each subdivided into iterations. Pipeline (brainstorm → plan → implement → audit → fieldtest), skipping rules, and exit-code gating live in ~/dev/skills/README.md and the per-skill SKILL.md files; this project's pipeline configuration is in .claude/dev-cycle-profile.yml under pipeline:.

Vocabulary note: pre-2026-05-09 git history uses "iter" / "family"; current vocabulary is "iteration" / "milestone". Old commits are not retroactively renamed. The plugin uses "cycle" / "iteration" as generic vocabulary; this project's profile maps cycle: milestone.

Roles of the design/ ledger, git log, Gitea issues, docs/specs/, docs/plans/

  • The design/ ledger is the canonical specification. It describes what AILang is: schema, semantics, invariants, runtime contracts. design/INDEX.md is the sole addressable spine — a typed two-table ledger; design/contracts/ holds the prose-authoritative, test-linked invariants; design/models/ holds the onboarding whitepapers. Every new feature must justify itself against the relevant contract before it can ship; if the feature requires changes to a contract, those changes are part of the same iteration. The design/ ledger is also the artefact the architect agent checks the code against during drift review. A contract describes only the actual present state; forward intent goes to the Gitea backlog, history and rationale to git log (see design/contracts/0007-honesty-rule.md).

  • git log is the project history. Iter and audit commit bodies carry the why — alternatives considered and rejected, verification steps, ratify statements, lessons. The Boss writes these bodies at commit time; they are the durable record. Read recent state with git log -5 --format=full; chronological scan with git log --oneline -30; per-milestone scope with git log <prev-milestone-close>..HEAD --format=full.

  • docs/specs/<milestone>.md (since 2026-05-09): per-milestone design spec produced by the brainstorm skill. Hard-gate before any plan or code work for the milestone.

  • docs/plans/<iteration>.md (since 2026-05-09): per-iteration bite-sized executable plan produced by the planner skill, consumed by the implement skill.

Together these answer three questions: "what is the language right now?" (the design/ ledger), "how did we get here?" (git log), and "what's next?" (Gitea backlog).

Lockstep-invariant pairs

Two cross-file pairings must move together. A new arm in one without the matching update in the other ships silently broken. The architect agent walks these pairs during audit drift review, and plan-recon consults this section when computing the cross-references column of a file-map.

Pair Failure mode
Pattern::Lit::* typecheck rejects in crates/ailang-check/src/lib.rs ↔ pre-desugar walkers in crates/ailang-check/src/pre_desugar_validation.rs A reject arm placed AFTER desugar_module in the call chain is unreachable through the public check API if the desugar pass rewrites that pattern shape first (e.g. desugar::build_eq rewrites Pattern::Lit::Float into (== scrut lit) before typecheck runs). New rejects on a Pattern::Lit::* shape MUST live in pre_desugar_validation.rs, or be paired with a written guarantee that no desugar pass eats the shape.
INTERCEPTS entries in crates/ailang-codegen/src/intercepts.rs(intrinsic) markers (Term::Intrinsic bodies) in the kernel-tier sources (examples/prelude.ail, crates/ailang-kernel/src/kernel_stub/source.ail) A registry entry without a marker is a compiler-supplied body no source declares; a marker without an entry is an (intrinsic) fn codegen cannot emit. The bijection (modulo the optimisation-only allowlist — registry entries that intercept the monomorphised __Int specialisation of a real-bodied polyfn) is enforced by intercepts::tests::intercepts_bijection_with_intrinsic_markers. Every new intrinsic-backed intercept needs both an INTERCEPTS entry and an (intrinsic) marker; every new optimisation-only intercept needs an OPTIMISATION_ONLY allowlist entry.

Walk procedure: for each milestone-scope commit-range arm landed in these files (use git diff <prev-close>..HEAD -- on each file in the pair), open both files in the pair and confirm the matching update is present. Flag any unpaired arm as drift.