waxwing

Engineering guidance and actual enforcement

AGENTS.md is normative. CLAUDE.md is a pointer, so rules are not duplicated. The verification command is npm run verify. scripts/build.mjs removes the output of every workspace module before building, so each build recompiles them and a promoted strict warning fails every build (F004 fixed; dependency output stays cached). scripts/strict-rebuild.mjs, run by verify, proves it. Never obtain green results by suppressing a gate, weakening an assertion, or adding a bypass allowlist.

Automated now

The CST ban on all anonymous lambdas is deliberately stronger than MileAhead’s manual ‘name what you pass’ rule. The Maybe/Either rule is stronger than its pass-through-only automated eliminator gate, following explicit user steering. Named where helpers and maybe’ preserve laziness for computed fallbacks.

Review conventions, not full automated lint yet

Types/instances, constants, public functions, private functions; descriptive names; named constants except 0/1/2 and test data; 30 declaration lines, 3 nested decisions, 8 active branches, 8 where bindings. Branch expressions should call a named helper when they hold computation. The CST gate currently catches branch do blocks, not every complicated operator/record expression. Comments explain why. These conventions were inspected in MileAhead and apply here; E001 tracks extending automation after the first slice.

Loops whose length grows with the input (characters, tokens, declarations) use tailRecM or a self tail call, and accumulate with Format.Stack or indexing rather than Array.snoc/uncons/drop per item, which copy the array. This is a review convention; only test/large-source.test.mjs (megabyte padding, 20,000 declarations) checks it, and only for the paths it drives.

The FX008 probe gate (test/fx008-probes.test.mjs, data in test/fx008-probes/, table in test/fx008-probe-table*.mjs) pins the outcome of every review probe (code, text, span, Go SHA-256, stdout, stderr, status). A task may change an entry only where the FX008 plan lists that probe for that task; any other change stops the work, and a hash of the table without flipped (test/fx008-probe-hash.mjs) catches accidental edits only, so the reviewer’s diff of the table is the real control.

No coverage percentage is claimed. Properties use deterministic generators and explicit edge cases with no discarded inputs. Parsing tests compare with independent generated trees; executable comparisons use a BigInt interpreter. The unifier (Features.Check.Unify, P001 Task 3) is compared with an independent union-find oracle (test/unify-oracle.mjs) and checked for soundness, acyclicity and most-generality over generated pairs. Row unification (Features.Check.UnifyRow, FX001 Task 3) is compared with an independent recursive Leijen oracle (test/row-oracle.mjs) over generated rows with repeated keys, shared tails and rigid tails, and checked for soundness (scoped-label equality), symmetric acceptance and acyclicity; its side-condition test runs in a timeout-guarded child process. These solved properties remain in test/unify-row.test.mjs. The separate deferred-row properties (test/row-properties.test.mjs) distinguish pending acceptance from solved equality: an independent transactional oracle models rollback, postponement and settling, with deferred keys, same-family payloads, arrow label arguments and nonempty starting substitutions. Pending constraints are retried after the body’s ordinary constraints; they are not equality witnesses. groundErased is used only at Resolve/Seeds monomorphism boundaries; strict ground still checks both type variables and row tails.

Measurement and checking tools are committed, not ad hoc (T003); neither runs in verify. scripts/differential.mjs compares two built compilers phase by phase (lex, parse, resolve, check, specialize, Go emission, full compile) over the sources the test suite parses (harvested by scripts/differential-hook.mjs into .build), token soups, generated nested-match programs and mutations, and exits 1 on any difference: run it against an isolated build of the previous commit for every behavior-preserving refactor or performance change. Its digests carry normalisations for fields an IR gained, each dropped only where a baseline without it agrees: a Plain token’s kind (Text v1), an empty records in the monomorphic IR (R001 Task 7) and a checked function’s sorts, a copy of resolve-phase data (R001 Task 8). scripts/ladder-profile.mjs times the match-lift ladder cold and per phase, alternating with a baseline build. Both are review conventions. The differential loads compilers and harvests sources through scripts/differential-load.mjs.

FX020’s corpus migration used one-off tools (a resolver-driven rewriter and a paired differential of each original program with its migrated twin). They were removed in FX020 Task 7; git history keeps them.

The FX001 differential corpus (Task 10) is a test-side reference interpreter (test/fx-oracle.mjs, written from the design, no compiler code) and a seeded generator of well-typed programs over two user effects, two failure families and Console traces (test/fx-programs.mjs, fx-gen-.mjs). test/fx-differential.serial.test.mjs (serial phase, ruling F11) builds the programs in go-batches of at most 100 and compares Go stdout, stderr and exit status with the interpreter exactly; the census (test/fx-census.mjs counts what the interpreter observed, not what the generator intended) must meet its minimums (50 programs per scoped-handler scenario, 25 per cleanup outcome) or the run fails, and so does a generated program the compiler rejects, with its seed. Knobs, outside verify: WAXWING_FX_SEED (default 20261010; program i has seed SEED+i, reproduced with generate(SEED + i, i)) and WAXWING_FX_PROGRAMS (default 500; the minimums need about 400, so a smaller count fails by design; larger counts build in more batches). A differing program’s source, both outcomes and a shrunk source (test/fx-shrink.mjs) are written to .build/fx001-differential/. The 25 rejection variants of strict defer are generated from the same seed, and 25 more reach a failing handler (FX008: directly, through a forwarding clause, through a clause calling an ambient callback). Since FX020 Task 5 the interpreter reads qualified operations, signature aliases and imports, with Console and Fail as effects (test/fx-oracle-names.mjs; tests in test/fx020-oracle.test.mjs), and the generator spells each function’s operations bare, qualified (some through an alias) or through imports of its own row (test/fx-gen-names.mjs), from a stream of its own so a seed keeps its program shape. Each performed operation reports its spelling, and the census requires programs that perform one qualified (400), imported (150), through an alias (50) and bare (75, until Task 7). Since R001 Task 9 the interpreter models records (test/fx-oracle-records.mjs; hand traces in test/fx-oracle-records.test.mjs): fields and update replacements evaluate once in written order, values compare and print in sorted label order (Text quoted as nested data), and a record payload’s Fail family is its label set. A record frame (test/fx-gen-records.mjs, in 70% of programs) draws a label universe with one type per label, writes literals and types in shuffled order, and calls generic functions over open records (test/fx-gen-record-fns.mjs). The census requires 250 programs each running a record literal, access, update, generic record function, comparison, print and Fail payload. test/fx-oracle.test.mjs validates the interpreter against the hand-derived traces of Tasks 2, 4, 7 and 8 (imported from their program modules), the shrinker, and a sensitivity check (clauses run in the operation’s context must change some of 50 programs). Both run in verify; the differential test is in the serial phase.

Random clauses may fail (FX008 Task 11): a clause is fail-free, fails with a family handled outside its with (parameter w, reserved for generated failing clauses; the census counts the aborts of such clauses as failing random clause, at least 25 programs), or forwards to an outer operation whose handler may fail. A failing clause is reachable: the with may sit in its own handle, and its body ends with the operation. Designed failing frames (test/fx-gen-failing.mjs) add the shapes lexical, forward, callee, value and ambient; they are extras outside the focus rotation, and the random path alone meets the census row. The generator tracks the user labels whose innermost handler may fail (ctx.failing): constrained cleanup performs none and no function whose row names one is called under it, so every program compiles. The one-way soundness differential (test/fx008-soundness.serial.test.mjs, serial phase) generates 300 programs generate(SEED + 100000 + i, i, { unconstrained: true }) whose cleanup ignores those frames. A rejected program must carry an FX008 headline at a defer or at a call, or a strict defer text at a defer; a witnessed escape must be rejected by an FX008 headline. An accepted program must never make the interpreter report typed abort escaped cleanup and must agree with Go exactly. Minimums: 50 accepted programs whose cleanup performs a user operation, 50 rejected by an FX008 headline, 25 oracle-witnessed escapes (oracle error: typed abort escaped cleanup), every one rejected, and at least 5 witnessed escapes in each shape (lexical, forward, callee-plain, callee-nofail, value-passed, value-returned, ambient, random). Failures are saved to .build/fx008-soundness/. Sensitivity (ruling R1), run on commit ff0c6d1 (Task 8, no mark check) with this corpus at the default seed: 110 of the 120 witnessed escapes are accepted, every shape except value-returned (its 10 hit the strict Fail text), and the test fails.

Definition of done and handoff

Acceptance criteria must run through the actual CLI/backend, with typed rejections and regression sensitivity. Check new instructions from a clean output directory, because incremental zero warnings do not prove a rebuild. Update plan, progress, findings, ADRs, provenance, and backlog together. Record known failures with concrete next actions; deferred language features are scope decisions, not hidden failures. No external issue/commit/push rule from a reference project is inherited.

Generated output/output caches and lockfiles are excluded from line limits. Waxwing fixture files and generated Go are data; maintained source or test code cannot be moved into those trees to evade checks. The checker tests test the checker itself. Structural guarantees are bounded by direct imports and known library APIs, not a proof of arbitrary dependency purity.

Installed workflow material

Superpowers is project-local, revision-pinned third-party guidance, preserved unmodified under .agents/skills with its MIT notice. It is distinct from maintained Waxwing source/tests/tooling; project gates still cover their existing roots without weakened checks. docs/planning-skills.md records instruction precedence and adoption. Migrated plans preserve historical evidence without asserting retroactive TDD/commit records. Execution uses the installed plan/task/review helpers and real commit ranges.

Audit review findings (2026-10-07)

The audit moved computed Maybe fallbacks to named maybe’ functions, contextual traversal callbacks into where, independent resolver bindings into where, and branch calculations into helpers. Private comma-list parsing functions now have an explicit export boundary. No language semantics or gate was relaxed. Numeric limits in maintained checking/mutation scripts are named constants.

The 30-line/3-decision/8-branch/8-binding budgets are review targets. The flat exhaustive mappings in Format.Diagnostic (codeName: twelve ErrorCode arms; code and message: fifteen Problem arms each) are retained together for readability; E003 records this specific target exception and its next review. All maintained code remains below 250 lines; automatic counts do not prove full naming/complexity compliance. No new source behavior or unification is claimed. Existing executable/snapshot/diagnostic tests validate this refactor.

Known gaps in enforcement (A001)

Review conventions only: the layer names describe reasons to change, which no gate checks; the gate checks import direction, purity and IR access. F005: the nil-guard probe’s temp-directory cleanup is not asserted.