Spec M53 -- the surface freeze, the deprecation policy and the release canary: the road from 0.16.0 to RC 0.17.0 and to 1.0.0

Written against minicompiler/mc main at 5dd6c90 (newest tag v0.16.0), read-only session except for the extractions of § 1, 2026-09-14. ../plan.md's row is written from this spec and not the other way round.

The owner's direction, verbatim:

Congelar a superfície — RC 0.17.0 — crítico. Política de deprecação (hooks.md § Stability): de 1.0.0 sem rename seco — adiciona novo + alias por um major, remoção = MAJOR. Formatos, mc.toml/mc.lock, nomes <mc/*> e a versão congelados = semver de verdade.

Melhorias + canário — antes de cortar: a release do mc dispara o CI da teko antes de publicar (bloqueante no 1.0.0). Sem credencial — a teko puxa a release pública do mc.

A 1.0.0 sai quando a lista de lacunas da teko chega a zero — ela auto-hospeda nas 5 pernas e publica a teko_std sem pedir gancho novo ao mc.

The one-paragraph version. The measurements move this milestone twice. First, the inventory already exists and nobody reads it as one: scripts/check-docs.sh extracts the whole public surface from src/ on every make check -- 206 symbols, 50 CLI flags, 35 TOML keys, 10 directives -- and then throws it away after asking "is each one documented?". Asking the second question, "was each one here last time?", costs one recorded file and one comparison. Second, the extraction has a hole and the hole is exactly where a freeze would hurt most: val_reg, dst_reg and dst_done are the three names ../reference/machine.md § 3 publishes as the only three names of a machine's internals that are frozen (contract version 3, M24 D2), and they match none of the 17 prefixes and none of the 5 exact names the extraction knows -- 0 of 206. A gate built on today's regex would have frozen 206 names and left the three that were already promised unprotected. So M53 is not a policy document with a script attached: it is one shared extractor (scripts/surface-extract.sh, the single definition both gates read), one recorded golden (tests/golden/surface.txt, 420 lines across seven kinds), one gate (check-freeze), the policy text hooks.md has never had, and a promote job that turns every release into a GitHub pre-release until a public verdict file teko writes says it built. src/ is untouched, stage0/ is untouched, and not one golden moves -- which is what makes a freeze milestone safe to land between a minor and an RC.

0. What this stands on (read, not assumed) #

1. The measurements (step 0), and what they decide #

1.1 The extraction today, counted #

Running check-docs.sh's own four pipelines by hand on this tree:

kindsource of truthcount
symsrc/*.mc, the 17 prefixes + 5 exact names (check-docs.sh:89)206
flagstr_eq/opt_val literals in src/*.mc, plus -o (:112)50
tomltoml_*("literal") + the two walked prefixes (:130, :132)35
dirdir_index() in src/lex.mc (:149)10

The symbol families, largest first: gen_ 46, p_ 25, host_ 20, backend_ 17, sec_ 15, type_ 12, syntax_ 12, sym_ 11, machine_ 9, walk_ 7, intrinsic_ 6, decl_ 6, on_ 4, subcommand_ 2, and the seven that are words on their own or exact names.

1.2 What the extraction does NOT see -- four holes, measured #

a. Three names that were already promised. docs/reference/machine.md § 3 ("The allocator functions a task handler may call", and version 3's changelog entry) publishes val_reg(d, scratch), dst_reg(d) and dst_done(d, reg) as the only three names of a machine's internals that are frozen. docs/reference/hooks.md:546 repeats it. All three are defined in src/machine_arm64.mc (:161, :181, :186), are read by lib/machine_arm64_float.mc, lib/machine_x86_64_float.mc, lib/i128.mc, examples/avx/avx.mc and examples/kernel/machine_riscv64.mc -- and match none of the 17 prefixes and none of the 5 exact names. grep -cx val_reg against the extracted list: 0. This is the found defect, and by the owner's rule it is fixed, not voted: step A adds three exact names and the inventory becomes 209. check-docs stays green without a doc edit, because all three are already written as calls in the reference (machine.md:215, :762, :763).

b. The lock and index row keys are invisible. mc.lock's [[package]] rows and the registry index's [[versions]] rows are read with toml_get(tm_cat(key, "version")) -- a computed path, not a literal -- so check-docs.sh:130's regex cannot see one. Extracted separately from tm_cat(key, "...") in src/pkg.mc, src/tool.mc and src/deps.mc: 14 keys -- bin deps kind lib licence name path permissions sha256 strip tools url version yanked. These are a FORMAT written by one version and read by another and by the registry; the direction names mc.lock explicitly. They become the seventh kind.

c. 448 globals, of which a handful are genuinely read from outside src/. The naive word-boundary grep over lib/, examples/, tools/ and site/gen/ gives 30 candidates, most of them false positives (cp, pe, heap, nodes, locals, sections, symbols are ordinary local names in unrelated files). The ones verified by reading the hit: a64_out (lib/machine_arm64_float.mc:503), x86_shadow/x86_nargreg/x86_argreg (lib/machine_x86_64_float.mc:283-311, lib/i128.mc), x86_tmp (examples/avx/avx.mc), walk_fn_ret (lib/i128.mc:627), nnodes/nodes (lib/pass_demo.mc:5-17), ty_i32 (examples/avr/mc-avr.mc:70), bundle_blob (tools/bundle.mc). Roughly ten of 448. § 3 decides.

d. Diagnostic text. docs/reference/diagnostics.md documents 373 rows across 15 sections. The number a gate actually asserts, counted over scripts/*.sh: 19 distinct mc: … literals, of which check-surface.sh's 18 err_case calls are most. So 95% of the messages are documented and protected by nothing. § 3 decides.

1.3 What the release pipeline does with a candidate today #

Measured by reading, not by cutting a tag: autotag.yml and tag.yml both refuse a - suffix, so the only way to a pre-release version string is a hand-pushed tag, "deliberately outside the automation" (../ci.md § Versioning). release.yml handles such a tag correctly (:649). And a pre-release version would then be skipped by the compiler's own resolver: pkg_newest/pkg_pick take a pre flag that is 0 for mc pkg add NAME and for a plain mc update (CLAUDE.md § State, the 0.15.1 entry: "a candidate is never chosen for you"), and mc upgrade resolves through the same row. So a 1.0.0-rc.1 is a version nobody would install, which is the opposite of what a release candidate is for.

1.4 What that decides #

  1. The inventory is not new work: it is the four pipelines that already run, plus three exact names and one more grep. One extractor, read by both gates -- otherwise § 11.3's drift risk is real and the two regexes part company on the first edit.
  2. The freeze has to cover names the extraction cannot see today (1.2a, 1.2b), and must not try to cover 448 globals or 373 message texts (1.2c, 1.2d) -- see § 3 for the arguments, which are different in each case.
  3. "RC" cannot be a version suffix (1.3). It is a state of the repository between two ordinary tags, and the candidate semantics live in the GitHub release's pre-release flag, where the canary can act on them. § 7.
  4. The canary needs nothing built for it on the mc side that does not exist: a pre-release flag (:649), a post-publish job with no secret (:685), and a public URL to read.

2. What "the surface" is -- decision (design question 1) #

Seven kinds, each with a single source of truth in the tree, none written by hand.

kindissource of truthtoday
syma function a module outside src/ may callsrc/*.mc, surface-extract.sh's prefix + exact-name list209
flaga command-line option mc acceptsthe str_eq/opt_val literals in src/*.mc, plus -o50
tomla key mc.toml may carrythe toml_*("literal") calls and the two walked prefixes35
dira #directivedir_index() in src/lex.mc10
bundlea name #include <…> resolvescolumn 1 of tools/bundle.list101
locka key of an mc.lock or index rowthe tm_cat(key, "…") literals in src/pkg.mc, src/tool.mc, src/deps.mc14
machinethe machine task contract's versionthe Contract version N line of docs/reference/machine.md1

420 lines. Notes on the three that are not simply "what check-docs already extracts":

mc.toml and the registry index row are covered by toml and lock between them. mc --version's output shape is covered by flag (--version) plus § 5's behaviour clause.

3. What is deliberately NOT the surface -- decision #

Three exclusions, each with its own argument. This section exists because a freeze that says "and everything else too" is a freeze nobody can land.

a. File-level globals in src/ are NOT API. 448 of them (§ 0), and freezing the set would make every internal of the compiler permanent: M48 C0's diet -- twelve src/driver.mc globals folded into one arena record so that check-limits stayed under the frozen seed's MAXGLOBALS -- would have been a MAJOR, and the seed's globals row is the tightest in check-limits precisely because that kind of work has to stay possible. The rule instead, and it is already what machine.md § 3 practises: a module reads a compiler internal through a function, and the function is frozen. val_reg/dst_reg/dst_done are the three the contract publishes by name; walk_ret_type() and x86_argreg_at(slot) are the shape for the rest. A consumer that needs a global asks for an accessor, the accessor lands as an additive MINOR, and it is in the inventory from that day.

Two things keep this honest without a new gate. First, the in-repo consumers are the gate: nine modules under lib/ and examples/ read the globals of § 1.2c, and check-float, check-wide, check-surface, check-kernel, check-avr and check-inert are red the moment one is renamed -- a64_out cannot be renamed quietly today. Second, the policy carries one operational sentence for the out-of-tree consumer: a rename or removal of a src/ global goes in the release note, which is what the memory rule (teko, 0.15.12) actually asked for and costs nothing to obey.

b. Diagnostic TEXT is not API; the exit code and the stream are. 373 documented messages, 19 asserted by a gate (§ 1.2d). Freezing 373 English sentences would make every improvement to a message a MAJOR, and messages are the part of a compiler that most deserves improving. What a program may rely on is what a program can act on: the exit code and whether the text went to stderr. Both are already documented per family in diagnostics.md § The four shapes and § 11's exit-code-2 section. The 19 that a gate asserts are protected by that gate -- changing one turns make check red, which is exactly the "you will notice" a freeze needs -- so no new mechanism is bought for them. The policy says: a message's wording is a PATCH; its exit code is a MAJOR.

c. The -opt road, the goldens and the object bytes are not the surface either. They are the determinism promise (../determinism.md), they already have ten gates, and they move by design whenever the compiler improves. A freeze on emitted bytes would forbid the optimizer. Named here only because "freeze the formats" could be misread to include them: an object file's LAYOUT is frozen by its own standard (Mach-O, ELF, COFF) and by check-obj against the frozen seed; its CONTENT is not.

4. The mechanism -- decision (design question 2) #

4.1 One extractor #

scripts/surface-extract.sh prints the inventory to stdout, kind<TAB>name, bytewise-sorted (LC_ALL=C sort). It is the ONLY definition of any of the seven lists. scripts/check-docs.sh is rewritten to call it (surface-extract.sh sym, … flag, … toml, … dir) instead of carrying four grep -hoE pipelines of its own -- it loses about 20 lines and its behaviour, its four ok coverage: lines and its counts are unchanged except that sym goes 206 -> 209. That is the whole answer to § 11.3: two regexes that must agree are a bug waiting; one regex cannot disagree with itself.

4.2 The recorded file #

tests/golden/surface.txt -- under tests/golden/ and not docs/reference/ because it is a recorded artefact compared on every run and rewritten only on purpose, which is that directory's whole meaning, and because a reader looking for the surface should be sent to docs/reference/hooks.md, not to a 420-line list. Format:

# mc public surface -- recorded by scripts/check-freeze.sh --record.
# One line per entry: <kind> <TAB> <name> [<TAB> deprecated <version> -> <replacement>]
# See docs/reference/hooks.md § 8 (Stability) before editing this file by hand. Do not.
bundle	float
bundle	mc/core_min
...
dir	define
flag	--exe
lock	permissions
machine	5
sym	val_reg
toml	project.opt

Sorted by kind then name, bytewise, so a diff is readable and a merge conflict is local. tests/golden/README.md gains a paragraph.

4.3 The gate #

scripts/check-freeze.sh (make check-freeze, inside make check). It re-extracts and compares:

what it seesverdictwhat it prints
identicalokok freeze: 420 entries (209 sym, 50 flag, 35 toml, 10 dir, 101 bundle, 14 lock, 1 machine)
an entry gone, not marked deprecatedFAILthe kind and name of each, then a removal is a MAJOR (docs/reference/hooks.md § 8). Deprecate it first, or re-record deliberately with scripts/check-freeze.sh --record.
an entry gone, marked deprecatedFAIL until re-recordedthe same, minus the first sentence: the removal is legal, the file must be updated in this commit
an entry addedFAIL until re-recordednew: sym foo -- additive, a MINOR. Re-record: scripts/check-freeze.sh --record
machine moved down, or changed with no new version paragraph in machine.mdFAILthe machine contract's version went 5 -> 4 / … -> 6 with no "Version 5 → 6" paragraph in docs/reference/machine.md

Re-recording is a flag, --record, not the goldens' delete-and-rerun. The hash goldens are rewritten by deleting the file because nobody reads their content; this file's content is the review artefact -- the diff is the API change -- so the command that writes it should be nameable in a commit message and the file should never be re-created from nothing by accident.

Every change to the surface therefore costs one extra committed line in the same pull request, which is the point: an addition is cheap and visible, a removal is impossible unless deliberate.

4.4 What the gate does not do #

It does not know what version it is (§ 0: the tree says 0.0.0-dev and a gate asserts it), so it cannot check that a deprecation has aged one major. That check is the release:major label on the pull request, which is a human decision autotag.yml already reads (../ci.md § 2). The gate's job is narrower and complete: a removal cannot happen by accident, and cannot happen at all without a diff a reviewer reads.

5. The policy -- decision (design question 3) #

New section docs/reference/hooks.md § 8, Stability, ~90 lines. One-line pointers to it from cli.md, toml.md, directives.md, packages.md, bundle.md and machine.md, and a row in docs/ci.md § Versioning's label table.

5.1 The rules #

changebumpexample
a message's wording; a fix with no inventory line movedPATCH0.15.37's registry-index fix
a new entry of any kind; a new MTASK_* slot under the null-slot rule; a new accessor for a globalMINORM49's six slots, version 4 → 5
an entry REMOVED; a signature changed; an exit code changed; behaviour a conforming module can observe changed; a machine-contract change that is not append-onlyMAJORnone yet

Conforming carries weight: a module that reads a src/ global, asserts a message's wording, or depends on an emitted byte is not conforming, and § 3 says so in the same page.

5.2 The deprecation lane #

From 1.0.0 there is no dry rename. To replace old with new:

  1. Add new. It is a MINOR and it appears in the inventory.
  2. Keep old as an alias -- for a symbol, a one-line wrapper that calls new; for a flag, a TOML key, a directive or a bundle name, the existing parse site accepting both.
  3. Mark the old entry in tests/golden/surface.txt: sym<TAB>old_name<TAB>deprecated 1.1.0 -> new_name. The version is written by a human, because the tree cannot know its own (§ 0); the reviewer checks it against the tag the pull request will cut.
  4. Remove old no earlier than the next MAJOR, in a pull request labelled release:major.

One major of overlap, exactly as the direction says. The deprecation marker is the only expressible removal, and § 4.3 is what makes that true mechanically.

5.3 The note, specified and priced, NOT built #

A deprecated name should say so. Decided:

M53 ships no deprecation, so it ships no note: the 25 lines land with the first one, and this section is what that pull request implements. Writing them now would be 25 lines of src/ that no test can exercise, which is 25 lines that rot.

5.4 What 0.x may still do, and when the promise starts #

periodwhat the gate doeswhat a removal costs
0.16.x (today)check-freeze does not existnothing -- 0.x semver lets a minor break
0.17.0, the RCthe gate lands, bindingthe deprecation lane, § 5.2; but 0.17.x takes bug fixes only (§ 7), so nothing should be removed at all
1.0.0 onwardunchangedrelease:major, publicly promised

The RC is where the freeze starts -- the gate, the inventory and the policy are all in 0.17.0 -- and 1.0.0 is where it is promised, which is the only difference between the two.

6. The canary -- decision (design question 4) #

Constraint, from the direction: no credential. mc cannot dispatch a workflow in teko's repository (that needs a PAT) and teko cannot receive mc's release webhook (a foreign repository does not get one). Both halves are therefore pull, and the only thing that crosses the boundary is a public URL each side can read anonymously.

6.1 The protocol #

  mc: publish ──► GitHub Release created as a PRE-RELEASE, all assets attached
                          │
  teko: schedule (15 min) │ GET /repos/minicompiler/mc/releases   (public, no token)
                          ▼
                  newer pre-release than its pin? → run the whole recipe against the tarball
                          │
                          ▼  writes with its OWN GITHUB_TOKEN, to its own repo
        raw.githubusercontent.com/teko-org/teko-lang/canary/<version>.json
                          │
  mc: promote ◄───────────┘ polls that URL every 60 s, up to 90 minutes
        ok   → gh release edit "$TAG" --prerelease=false
        fail → job RED, the release stays a pre-release, nothing is unmade
        timeout → advisory before 1.0.0, RED at 1.0.0
                          │
                          ▼
  mc: publish-to-registry  needs: promote   (a pre-release must not become the newest row)

6.2 The five decisions inside it #

  1. Every release is born a pre-release while the canary is armed. release.yml's publish job passes --prerelease whenever vars.MC_CANARY == 'true', not only for a --suffixed version (:649's case stays, OR'd with the variable). The assets, the checksums and the body are identical; the flag is the only difference, and it is the one thing GitHub lets a later job flip.
  2. The verdict is a file at the ROOT of a branch named canary in teko's repository, <version>.json — not canary/<version>.json, which would put a second canary in the path, since the branch already occupies the ref position. Read at https://raw.githubusercontent.com/<owner>/<repo>/canary/<version>.json, which for the consumer registered on 2026-09-14 is teko-org/teko-lang: {"version":"0.17.0","status":"ok","run":"<actions url>","utc":"…"}, with the version bare (no v). A file and not a commit status, because a status is about a teko commit and the canary is about an mc version, and the mapping would have to be invented; a branch and not GitHub Pages, because it needs no Pages setup and is versioned by git. teko writes it with its own GITHUB_TOKEN; mc reads it with curl and no token at all.
  3. promote is a job, not a step, for publish-to-registry's exact reason (release.yml:672): a failed promotion must not be able to unmake a published release, and a rerun of the job alone is the retry. Manual promotion is one command, gh release edit v0.17.0 --prerelease=false, and the release note says so.
  4. Timeout 90 minutes (timeout-minutes: 95), polling every 60 s. The budget: up to 15 minutes for teko's schedule to notice, plus its recipe, which on its own 2026-09-14 dry run was a build, 64 fixtures, a bootstrap fixed point, check-docs and mc limits. A workflow_dispatch on teko's side removes the 15.
  5. "Bloqueante no 1.0.0" is one repository variable. Before 1.0.0 the promote job is continue-on-error: true and a timeout is a ::notice:: with the release promoted anyway -- advisory, so a teko outage cannot hold an mc patch. At 1.0.0 vars.MC_CANARY_REQUIRED is set, continue-on-error becomes false and a timeout leaves the release a pre-release. Same shape as MC_REGISTRY_PUBLISH (§ 0), same reason: a job that is red by design would hide a real failure.

6.3 What the mc side owes and what it does not #

It owes: the pre-release flag, the poll, the flip, needs: promote on the registry job, and a docs/ci.md § The canary that states the contract precisely enough for teko to implement its half without asking -- the URL shape, the JSON keys, the timeout, and that a missing file is neither ok nor fail. It does not owe teko's workflow, which is teko's pull request, and it does not owe a fallback: if teko never publishes a verdict, every release before 1.0.0 is promoted on the timeout and the pipeline is exactly what it is today.

7. RC 0.17.0 and the version string -- decision (design question 5) #

0.17.0 is an ordinary minor tag. "RC" is a STATE of the repository, not a suffix on a version.

Three reasons, all measured in § 1.3:

  1. scripts/next-version.sh:206 and .github/workflows/tag.yml:52-56 refuse a suffix, each with its own error text, and docs/ci.md § Versioning records that as a decision with a reason. Relaxing two refusals for one tag is a change to the release automation with no second user.
  2. A --suffixed version is skipped by the compiler's own resolver (pkg_newest/pkg_pick with pre = 0), so mc upgrade and mc pkg add mc would not offer it. A release candidate nobody can install is not one.
  3. The candidate semantics already have a home where they can be acted on: the GitHub release's pre-release flag, which § 6 flips on a verdict. That flag is orthogonal to the version string, which is what lets 0.17.0 be an ordinary version and still be a candidate.

So:

0.17.0an ordinary release:minor tag, cut by autotag.yml as any other. It contains the freeze gate, the inventory, the policy and the canary in advisory mode
0.17.xbug fixes only. Nothing is added to the inventory and nothing is removed from it; git log --stat -- tests/golden/surface.txt between the two tags should be empty
1.0.0a release:major tag. The only difference in the tree is vars.MC_CANARY_REQUIRED and the policy page's "promised" wording

scripts/set-version.sh and scripts/check-bundle.sh's sentinel are untouched: they already do the right thing for a plain X.Y.Z, and this decision means no suffix ever reaches them.

8. Acceptance (runnable; numbers, not adjectives) #

  1. The inventory: scripts/surface-extract.sh | wc -l is 420, and per kind 209 sym, 50 flag, 35 toml, 10 dir, 101 bundle, 14 lock, 1 machine. tests/golden/surface.txt is that output byte for byte; check-freeze prints the ok freeze: line with those numbers.
  2. The hole is closed: grep -x 'sym val_reg' tests/golden/surface.txt succeeds, and so do dst_reg and dst_done. check-docs is green at 209 symbols with no doc edit (they are already written as calls in machine.md and hooks.md).
  3. One definition: grep -c 'grep -hoE' scripts/check-docs.sh is 0 -- all four pipelines moved into surface-extract.sh, and check-docs's four ok coverage: lines are otherwise unchanged.
  4. The gate has teeth, proved in both directions and in the commit: (a) renaming p_type to p_typ in src/parse.mc makes check-freeze FAIL naming sym p_type and printing the MAJOR sentence; (b) adding void syntax_probe(uptr f) makes it FAIL as new: sym syntax_probe -- additive, a MINOR; (c) marking p_type deprecated and then removing it FAILS until --record is run, and the recorded diff is one deleted line; (d) editing machine.md's version line to 4 FAILS; to 6 with no new paragraph FAILS; to 6 with one passes after --record.
  5. src/ untouched: git diff --stat src/ stage0/ is empty, check-inert is identical on both roads for the 33 objects and all five taught examples, and not one of the ten goldens moves. check-obj 32/32 against the frozen seed, check-limits 17/17 under 90% with globals unchanged.
  6. The canary round trip, measured once in advisory mode against a real teko run: mc cuts a throwaway tag, the release is created as a pre-release, teko's workflow writes canary/<version>.json with "status":"ok", mc's promote reads it and flips the flag. The elapsed time from publish finishing to promote flipping is recorded in this spec's § Implementation notes, and it is under the 90-minute timeout.
  7. A fail verdict leaves the release a pre-release and promote red, with every asset still attached and gh release edit --prerelease=false still available -- asserted once, by writing a "status":"fail" file by hand before a throwaway tag.
  8. publish-to-registry runs after promote, so a pre-release is never announced: read from the workflow's needs: and confirmed by the job graph of the throwaway run.
  9. make check green end to end with check-freeze in it, and make check-linux-host RC 0 over all four cells -- both unchanged in every other count, because nothing the compiler emits moved.
  10. Docs with check-docs green: hooks.md § 8 (new), the six one-line pointers, ci.md § Versioning's label table and § The canary (new), tests/golden/README.md, docs/plan.md's M53 row, CLAUDE.md § State; every ```mc fence compiles; site/check-site green.

9. Steps -- gated commits #

orderstepcontentlines (est.)gate
1A the extractor and the gatescripts/surface-extract.sh (the seven kinds, the three exact names of § 1.2a, the lock grep); check-docs.sh rewritten to call it; scripts/check-freeze.sh; tests/golden/surface.txt recorded; Makefile; tests/golden/README.mdLANDED: surface-extract.sh 81, check-freeze.sh 125, check-docs.sh +19/−13, Makefile +17/−4, surface.txt 420 entries + 3 header lines§ 8.1-8.5
2B the policy ✔ LANDEDhooks.md § 8; the six pointers; ci.md § Versioning's label table; machine.md's pointer to § 8LANDED: hooks.md § 8 +108/-5 (110 lines in place); one-line-or-two pointers in cli.md +3, toml.md +3, packages.md +5, bundle.md +4, objects.md +5, machine.md +5; ci.md § Versioning +18/-6§ 8.10, check-docs green
3C the canary ✔ LANDEDrelease.yml: the MC_CANARY pre-release flag (+ the release note's Canary paragraph), the promote job, needs: [publish, promote] on publish-to-registry; scripts/canary-poll.sh (new, 167 lines, 106 of them code, --test and --url); ci.md § The canaryrelease.yml +97/-8, scripts/ +167 new, docs/ci.md +158/-3§ 8.6-8.8 -- actionlint clean, the four outcomes reasoned from the lines, the poll script run locally in DRY_RUN mode; the first real round trip is the next tag with MC_CANARY armed
--D the note§ 5.3's dep_note, four sites~25 in src/deferred: it lands with the first deprecation

Each step is make check green on its own, and none of them rewrites a golden -- the first milestone since M41 for which that is true, because src/ is not edited until step D, which is not in M53.

10. Files and estimated deltas #

filelineswhat
scripts/surface-extract.sh~70new: the single definition of all seven kinds; surface-extract.sh [KIND] prints one kind, no argument prints all
scripts/check-freeze.sh~90new: compare, classify, --record
scripts/check-docs.sh+12 / −20four pipelines become four calls; sym 206 → 209
tests/golden/surface.txt420new, recorded
tests/golden/README.md+18what this file is, why --record and not delete-and-rerun
Makefile+4check-freeze in check, in .PHONY, and in the Linux and Windows subsets (it needs no compiler -- it is grep over src/ -- so it runs on every host)
.github/workflows/release.yml+45the MC_CANARY flag on publish; the promote job; needs: promote
docs/reference/hooks.md+90§ 8 Stability
docs/ci.md+75§ Versioning's API column; § The canary
docs/reference/{cli,toml,directives,packages,bundle,machine}.md+6one pointer each
docs/plan.md, CLAUDE.md+2the row, the § State entry
src/, stage0/, the ten goldens0§ 8.5

Net new src/ code: 0. That number is the milestone's argument: a freeze that had to change the compiler to describe the compiler would be describing the wrong thing.

11. Risks #

  1. Freezing a name teko needs renamed. Measured at low: teko registers syntax_expr, machine and target (§ 0), the owner stated on 2026-09-08 that the hooks API does not move again, and teko replied the same day that nothing pends. The escape is § 5.2's lane, which costs one MINOR and one alias and is exactly what the direction asks for. The residual is that a bad name gets one major of life -- which is the price of the promise and not a defect of it.
  2. The canary's latency. Bounded at 90 minutes (§ 6.2.4) and advisory until 1.0.0, so the worst case before 1.0.0 is a release that takes an hour and a half to leave pre-release. After 1.0.0 the worst case is a release that stays a pre-release until a person promotes it by hand, which is one command. A workflow_dispatch on teko's side is the fast path and should be documented in the release checklist.
  3. The inventory drifting with check-docs's regex. Closed by construction in § 4.1: there is one regex, in one file, read by both gates. The residual is that the regex itself is a choice about what is public (17 prefixes and now 8 exact names), and widening it later adds entries -- an additive, re-recorded MINOR, which is the right cost for "we noticed something was public all along". § 1.2a is that event happening once, deliberately, before the freeze rather than after it.
  4. A public symbol outside the prefixes stays invisible. The regex is a heuristic and always was. What makes this survivable is that the ONLY names a consumer may rely on are the ones docs/reference/ documents, and check-docs requires a documented name to exist -- so a documented-and-frozen name cannot vanish, and an undocumented one was never promised. § 1.2a found the one case where those two sets had parted (three names documented as frozen and unextracted), and the fix makes the invariant "documented ⊆ inventory" true for the families the regex covers. A sweep of docs/reference/ for call-shaped names outside the inventory is priced at ~20 lines of one-off grep and is worth running once in step A.
  5. A merge conflict in a 420-line sorted file. Sorted by kind then name bytewise (§ 4.2), so two pull requests adding different symbols conflict only if the names are adjacent. Cheaper than any alternative and identical to what mc.toml's [package].files already does.
  6. --record used as a reflex. The gate's message is written to discourage it (§ 4.3 prints the policy sentence before the command), and the recorded diff is in the pull request where a reviewer sees a deleted line. There is no technical defence against a determined re-record, and there should not be: the goldens have the same property and it has held for fifty milestones.
  7. 0.17.x taking a feature. The gate cannot stop it -- an addition is a legal MINOR at any point. § 7's "bug fixes only" is a discipline, and the check is one command in the 1.0.0 checklist: git log --stat v0.17.0..HEAD -- tests/golden/surface.txt must be empty.
  8. The verdict file's owner. mc reads a URL in a repository it does not control. A compromised or mistaken teko could promote a bad mc release -- but only to prerelease: false, which is a visibility flag; nothing about the artefacts changes and publish-to-registry still runs its own sandboxed validation. The blast radius is "a release becomes visible early", which is today's behaviour for every release.
  9. Diagnostics left unfrozen (§ 3b). A consumer that greps a message will break on a PATCH. The answer is written down in the policy rather than engineered around: exit codes are the contract, and mc has 373 messages precisely because it improves them.

12. Decisions #

decisionwhy
D1The surface is seven kinds, each extracted from one source of truth in the tree, never written by hand: sym flag toml dir bundle lock machineit is the rule check-docs.sh already states for four of them; a hand-written list is a second source of truth and would be wrong within a month
D2val_reg, dst_reg and dst_done join the extraction as exact names, 206 → 209they are published BY NAME as frozen in machine.md § 3 and matched no prefix -- a found defect, fixed not voted (§ 1.2a)
D3bundle covers the <mc/*> parts and the libraries in one kind, from tools/bundle.list column 1a part is named exactly as a library is ([compiler].core = "<mc/core_min>"); M52 moved 41 of the 101 out of the blob without changing one name, so the name is the surface and the road is not
D4lock is a seventh kind, 14 keys from tm_cat(key, "…")the direction names mc.lock; a computed TOML path is invisible to check-docs's regex, so the format was covered by nothing
D5machine is ONE line carrying the contract's version, not 37 slot namesthe contract's promise is about order and signatures, which a name list cannot express; machine.md already versions itself append-only with a written changelog
D6src/ globals are NOT API. The migration is an accessor per global a consumer needs, which is then frozen as a sym448 of them; freezing the set would have made M48 C0's globals diet a MAJOR, and that diet is what keeps check-limits under the frozen seed's ceiling. machine.md § 3's three published names are the precedent for the exception
D7A rename or removal of a src/ global goes in the RELEASE NOTE; no gate is added for itthe in-repo modules under lib/ and examples/ already fail make check on such a rename; the out-of-tree consumer needs an announcement, which is what the memory rule asked for and costs nothing
D8Diagnostic TEXT is not API; the exit code and the stream are. Wording is a PATCH, an exit code is a MAJOR373 documented messages against 19 a gate asserts; freezing English sentences would make improving a message a MAJOR, and messages are what most deserves improving. The 19 are protected by the gate that asserts them
D9The inventory lives in tests/golden/surface.txt and is re-recorded with --record, not by deleting the fileit is a recorded artefact compared every run -- the goldens' meaning exactly -- but unlike a hash its CONTENT is the review artefact, so re-creating it from nothing must be a named act
D10check-freeze fails on an addition too, until the file is re-recordedthe goldens' discipline: every surface change costs one committed line in the same pull request, which is what makes the diff the announcement
D11One extractor read by both gates; check-docs.sh loses its four pipelinestwo regexes that must agree are a bug waiting (§ 11.3); one cannot disagree with itself
D12Deprecation = add new + keep the alias + mark the old entry with the version a human writes; remove no earlier than the next MAJORthe direction, verbatim. The version is written by hand because check-bundle.sh guarantees the tree cannot know its own (§ 0)
D13The deprecation note is printed only where a name is parsed from text (flag, TOML key, directive, bundle name), never for a symbol; and it is SPECIFIED AND PRICED, not builta symbol has no parse site, and a wrapper that printed would write on a taught compiler's own stderr. M53 ships no deprecation, so 25 lines that no test can exercise would only rot (§ 5.3)
D14Every release is born a GitHub pre-release while MC_CANARY is armed; a promote job flips the flag on a public verdictthe flag is the one property of a published release a later job can change, and it is already implemented (release.yml:649)
D15The verdict is a JSON file in a canary branch of teko's repository, read anonymously over raw.githubusercontent.comthe direction's "sem credencial", both ways: teko writes with its own token, mc reads with none. A commit status would be about a teko commit, not an mc version
D16promote is a job with needs: publish, and publish-to-registry gets needs: promotepublish-to-registry's own precedent -- a failed follow-up must not unmake a release -- and a pre-release must not become the registry's newest row
D17Advisory before 1.0.0 (continue-on-error: true, a timeout promotes), required at 1.0.0 (vars.MC_CANARY_REQUIRED)"bloqueante no 1.0.0", literally; and a teko outage must not be able to hold an mc patch before then. Same gating shape as MC_REGISTRY_PUBLISH
D180.17.0 is an ordinary minor; "RC" is a STATE of the repository, not a version suffix. No 1.0.0-rc.N tagtwo places refuse a suffix on purpose and docs/ci.md records why; and a pre-release VERSION is skipped by mc upgrade and mc pkg, so nobody could install the candidate (§ 1.3). The candidate semantics live in the release's pre-release flag instead
D190.17.x takes bug fixes only; the check is git log --stat v0.17.0..HEAD -- tests/golden/surface.txt being emptythe RC's whole meaning, and it is checkable in one command rather than asserted
D20src/ is untouched and no golden movesa freeze that had to change the compiler to describe the compiler would be describing the wrong thing

13. The 1.0.0 gate #

The direction: "A 1.0.0 sai quando a lista de lacunas da teko chega a zero — ela auto-hospeda nas 5 pernas e publica a teko_std sem pedir gancho novo ao mc." That list is teko's to keep, and on 2026-09-08 23:40 it was already empty (§ 0). What this side owes at the tag:

here, before 1.0.0M53 steps A, B and C merged and shipped in 0.17.0; 0.17.x carrying bug fixes only (D19, checked by the one command); the canary's round trip measured green at least once in advisory mode (§ 8.6)
here, at the tagrelease:major; vars.MC_CANARY_REQUIRED set; hooks.md § 8's wording moves from "from 0.17.0 this gate is binding" to "promised"; the release note names the seven kinds and their counts
teko, before the tagits own gap list at zero, restated here; its canary branch live and its workflow scheduled; a green verdict for the 0.17.0 line
registry, before the tagMC_VERSION raised, as at every cut (M52.md § 15)

Nothing on this side is waiting for teko: steps A-C land on their own schedule and 0.17.0 can be cut without a single teko change, because the canary is advisory until the major. The gate is on 1.0.0 alone, which is where the promise is.

Implementation notes -- step C (the canary) #

Written while landing § 6, § 8.6-8.8 and § 9 row 3. Every deviation from the text above is here.

  1. The verdict lives at the ROOT of the canary branch, and § 6.2 point 2 has been corrected in place. The prose said "a file in a branch of teko's repository, canary/<version>.json" while the URL beside it read raw.githubusercontent.com/<owner>/teko/canary/<version>.json. Those are two different files: the raw URL is <owner>/<repo>/<REF>/<path>, so the branch already occupies the canary segment and a file inside a canary/ directory would be .../teko-lang/canary/canary/0.17.0.json. The URL was right and the prose was wrong. The ROOT form is also what the consumer was told through the channel on 2026-09-14 and what it is building, so the spec's prose is now the URL's shape and § 6.1's diagram names the real repository.
  1. The consumer is teko-org/teko-lang, read out of the channel file (/Users/schivei/mc-teko/teko-to-mc.md, 2026-09-14) and confirmed against gh repo list teko-org -- not guessed from the milestone's shorthand. It is the DEFAULT of CANARY_REPO in the script and not a literal in the workflow, with vars.MC_CANARY_REPO overriding it, so a move is one variable and no commit. Its canary branch does not exist yet (gh api repos/teko-org/teko-lang/branches/canary -> 404, and the raw URL answers 404), which is exactly the state the poll treats as "not written yet".
  1. Armed is vars.MC_CANARY == 'true' (§ 6.2 point 1's own spelling), not "1" or "required"; "required" is a second variable, MC_CANARY_REQUIRED, because the two questions are independent -- a release can be born a pre-release and promoted advisorily long before the wait blocks. Same gating shape and same reason as MC_REGISTRY_PUBLISH (§ 0).
  1. The timeout promotes before 1.0.0, per § 6.2 point 5 read literally: "a timeout is a ::notice:: with the release promoted anyway -- advisory". It is a ::notice:: and not the ::warning:: the task's brief supposed; the spec is what the script follows. At 1.0.0 (MC_CANARY_REQUIRED = true) the same timeout is an ::error:: and the release stays a pre-release.
  1. continue-on-error is an expression, ${{ vars.MC_CANARY_REQUIRED != 'true' }} -- so the two states of § 6.2 point 5 are one variable and not two workflow edits. A fail VERDICT is exit 1 either way: continue-on-error keeps the RUN green before 1.0.0, but the job renders red, which is what § 8.7 asks of it.
  1. The poll is a script and not thirty lines of YAML. scripts/canary-poll.sh (167 lines, 106 of them neither comment nor blank) can be run off a runner, which is the only way § 8.6's round trip can be reasoned about before a real tag exists, and it is the repository's own precedent (next-version.sh --test, libroot.sh, release-assets.sh). --test is 7 assertions over file:// URLs, no network and no framework: ok promotes, a v-prefixed version field is accepted, fail exits 1 and says so, a verdict naming another version keeps waiting, an advisory timeout promotes with the ::notice::, a required timeout is exit 1 with the ::error::, and the derived URL is the one § 6.2 point 2 writes. --url TAG prints the URL without touching the network, which is how that last case avoids a live request. DRY_RUN prints the gh release edit it would run instead of running it.
  1. The registry job's if had to name a status function. needs: [publish, promote] alone would skip publish-to-registry on every release while MC_CANARY is unset, because a job whose needs is SKIPPED is skipped too. It is !cancelled() && vars.MC_REGISTRY_PUBLISH == 'true' && needs.publish.result == 'success' && needs.promote.result != 'failure'. The load-bearing half is unambiguous: at 1.0.0 continue-on-error is false, a fail or a required timeout makes needs.promote.result equal failure, and the pre-release is not announced. Before 1.0.0 the advisory job's result is reported as success and the registry runs -- which is what "advisory" means, and it is documented rather than relied on silently.
  1. The release note says how to promote by hand, and only while the canary is armed: publish's body composer appends a Canary paragraph with gh release edit <tag> --prerelease=false when vars.MC_CANARY is true. Whoever finds a release still marked pre-release has the one command in front of them without reading docs/ci.md.
  1. Unarmed is byte for byte the pipeline that was there. publish's case "$VERSION" in *-*) is untouched and the new if [ "$CANARY" = "true" ] below it is the only addition; promote is skipped; the registry job's extra conditions are all true. Nothing in src/, stage0/, tests/golden/ or lib/ was touched -- git diff --stat src/ stage0/ tests/golden/ is empty, no golden moves, and mc emits not one different byte.
  1. actionlint is clean. The two findings it reports on release.yml (SC2155 at :527, SC2086 at :547) are byte for byte the ones it reports on the same file from main, in the same steps, and predate this change; shellcheck -s sh scripts/canary-poll.sh is clean.
  1. The four outcomes, from the lines. ok -> promote() -> gh release edit "$TAG" --prerelease=false, exit 0, registry runs. fail -> ::error::, the by-hand line, exit 1; advisory the run is green and the registry still runs, required the run is red and the registry is skipped. timeout -> advisory: ::notice:: + promote() + exit 0; required: ::error:: + exit 1, the release left a pre-release. unarmed -> promote skipped, the release created exactly as before, the registry gated on MC_REGISTRY_PUBLISH alone as it always was.
  1. What is NOT proved here and when it will be. § 8.6 asks for the round trip measured once against a real teko run and § 8.8 for the job graph of a throwaway tag. Neither can be done from a pull request: release.yml fires on a v* tag, the owner cuts tags, and teko's canary branch does not exist yet. The first real round trip is the next tag with MC_CANARY armed -- the elapsed time from publish finishing to promote flipping goes into these notes then, with the run URL.

The first round trip, measured (v0.16.1, 2026-09-14). MC_CANARY=true was armed at 15:42 after the consumer reported its poller live (its 0.16.0.json said fail -- windows/x86_64, and it was true). publish finished 16:23:47; promote polled the raw URL until 17:54:12 -- the 90-minute timeout -- and, advisory, promoted the release (::notice::, job green, prerelease: false, 20 assets, publish-to-registry ran after it). No verdict existed then: the consumer's mc-canary.yml runs on schedule (*/15 * * * *) and GitHub enqueued zero scheduled runs in the first two hours after the workflow was merged (a known behaviour of a freshly added, frequent cron). A manual workflow_dispatch wrote 0.16.1.json {"status":"ok","run":".../runs/34877983682","utc":"2026-09-14T18:02:21Z"} eight minutes later -- the whole recipe green on 0.16.1, the windows/x86_64 fixpoint included. So: the mc half works end to end (armed, waited, timed out, promoted, published), the consumer half needs its cron to fire before MC_CANARY_REQUIRED can be set; until GitHub's scheduler is seen enqueuing it, the consumer dispatches by hand and the 90-minute budget is what absorbs that. Risk 3 (latency) is therefore the live one, and it is the consumer's to close, without a cross-repository token.

The second round trip (v0.17.0, 2026-09-14) closed in four minutes: publish 19:49:41, the consumer's manual dispatch wrote 0.17.0.json ok at 19:53:28, promote flipped the flag at 19:53:49, publish-to-registry ran after it. One CDN fact came out of it, reported by the consumer: raw.githubusercontent.com had answered 404 for minutes AFTER the push -- a negative cache from a poll made before the file existed -- while the contents API already served it. scripts/canary-poll.sh now busts that cache with a query on the raw URL and, when raw still answers nothing, asks api.github.com/repos/<repo>/contents/<version>.json?ref=<branch> (public; the job's token only lifts the unauthenticated 60/h ceiling that ninety one-minute polls would hit). The self-test's file:// fixtures still pass 7/7.

Implementation notes -- step A #

Written after the step landed. Everything here was measured on this tree.

  1. The counts are the spec's, to the line. sh scripts/surface-extract.sh | wc -l is 420: 209 sym, 50 flag, 35 toml, 10 dir, 101 bundle, 14 lock, 1 machine, and tests/golden/surface.txt is that output with three # header lines above it. check-freeze prints ok freeze: 420 entries (209 sym, 50 flag, 35 toml, 10 dir, 101 bundle, 14 lock, 1 machine).
  1. The hole of § 1.2a is closed and it cost no documentation. val_reg, dst_reg and dst_done join the extraction as exact names (206 -> 209) and check-docs is green with no edit to docs/reference/, because all three are already written as calls in machine.md and hooks.md -- which is what § 1.2a predicted. grep -x 'sym val_reg' tests/golden/surface.txt succeeds, and so do the other two.
  1. One extractor, and check-docs.sh has no regex left. grep -c 'grep -hoE' scripts/check-docs.sh is 0: its four pipelines became four sh scripts/surface-extract.sh <kind> calls (+19/−13, most of the added lines being the comment that says where the families now live). Its four ok coverage: lines are unchanged except the first: 206 -> 209 symbols, 50 flags, 35 TOML keys, 10 directives; samples 52 and links 543 are untouched.
  1. lock is keyed on the variable, not on the shape. The 14 keys come from tm_cat(key, "...") with key spelled literally -- every one of the 14 call sites in src/pkg.mc, src/tool.mc and src/deps.mc names that variable. A regex over any tm_cat(<ident>, "<word>") picks up tm_cat(dir, "x") and tm_cat(ldst, "x") as a fifteenth key x and misses sha256 unless digits are allowed -- both measured before the narrower form was chosen.
  1. machine gets its own verdict and is excluded from the add/remove diff. A version bump is neither a removal nor an addition: it is one line whose value moved, and the rule is about the direction. Leaving it in the generic diff would have printed removed: machine 5 under the MAJOR sentence for an append-only bump, which is false. All four cases of § 4.3's table were measured: -> 4 FAILS with the machine contract's version went 5 -> 4; -> 6 with no paragraph FAILS naming the Version 5 → 6 paragraph it wants; -> 6 WITH such a paragraph FAILS as machine: the contract version went 5 -> 6 / re-record; and after --record it passes. The paragraph is matched as Version <old> (->|→|-->) <new>, which is the shape machine.md's own four changelog entries use (they spell it with the UTF-8 arrow).
  1. The deprecation marker is implemented, and nothing is deprecated. A removed entry whose golden line carries a third column deprecated <version> -> <replacement> fails with the shorter message (the removal is deprecated and legal; re-record it in this commit) instead of the MAJOR sentence -- measured by marking bundle float and dropping its tools/bundle.list row. § 5.3's compiler-side NOTE (D13) is a different thing and is not built.
  1. The policy pointer is to this spec, not to hooks.md § 8. That section is step B's, so check-freeze's messages, tests/golden/README.md and the new two-line hooks.md § 8 all point at § 5 here and say where the full text will be. Step B replaces the string in one place (policy in scripts/check-freeze.sh).
  1. Teeth, measured in both directions, each with the tree restored afterwards: renaming p_type to p_typ in src/parse.mc gives removed: sym p_type + the MAJOR sentence AND new: sym p_typ -- additive, a MINOR, exit 1; deleting tools/bundle.list's float row gives removed: bundle float + the MAJOR sentence, exit 1; the pristine tree is ok freeze: 420 entries, exit 0.
  1. check-freeze runs on every host and is in all three check: lists -- it is grep over src/, tools/bundle.list and one line of machine.md, and needs no compiler. On macOS it is placed right after check-docs, the other gate that reads the same extractor.
  1. src/ and stage0/ are untouched and no golden moved: git diff --stat src/ stage0/ tests/golden/*.sha256 is empty, and the only new file under tests/golden/ is surface.txt itself.

Implementation notes -- step B #

Written after the step landed. Docs only -- git diff --stat src/ stage0/ tests/golden/ is empty, exactly as step A left it, because this step writes no code and freezes no new name.

  1. hooks.md § 8 came in at 110 lines, not ~90. The four blocks § 5 asks for (what the surface is, what is not, the rules table, the deprecation lane) fit the estimate; two more things earned their own subsections because leaving them as prose inside another one would have buried the one fact a reader comes here for: the note is specified and priced, not built (§ 5.3/D13, its own heading with the exact stderr line and the four parse sites it would need), and the RC/1.0.0 table (§ 5.4), which is what a reviewer actually checks against when a label is picked. Both are short, and both are load-bearing enough that a pointer alone (as the six other pages get) would have made this page say less than the spec asked it to.
  2. check-freeze.sh's policy string still names the spec, unchanged, on purpose. § 4.3 of the design and the header comment of check-freeze.sh (M53 step A note 7) both said the string would move to hooks.md § 8 once this section existed. It stays pointing at docs/specs/M53.md § 5 instead: the SPEC is the decision record with the "why" (§ 3's two arguments, the risks, the twenty decisions), and hooks.md § 8 is the policy STATED for a reader who already accepts it -- a gate's failure message is read by someone about to make a change, and the spec is where the reasoning that change has to survive actually lives. Moving the string would have made the message shorter and the reader's next step longer. hooks.md § 8 opens by pointing at tests/golden/README.md, which is itself a pointer back to this section, so a reader following either road reaches the same page; nothing in step A's acceptance (§ 8.1-8.5) named the string's exact target, only that check-docs stay green, which it does.
  3. The six pointers are cli.md, toml.md, packages.md, bundle.md, objects.md and machine.md -- objects.md in place of the directives.md § 5's own prose names. directives.md documents the ten entries of the dir kind, which is the smallest of the seven and already fully enumerated on its own first page (#include through #embed, ten names, one line each) with nothing further to say about which of them may move. objects.md documents the gen_*/backend_*/machine_* API -- the single largest concentration of the sym kind -- and it is the one page on this list that ALREADY drew the byte/name distinction this section needs (docs/determinism.md's promise is about emitted bytes, which move by design; § 8's promise is about names, which do not) without a line added for it: the pointer there states the difference explicitly (M53 § 3c) rather than assuming a reader carries it over from hooks.md. A pointer was not added to directives.md: its own page needs no correction and none of its ten rows has ever been ambiguous about whether it was public.
  4. packages.md's pointer sits at § 4, not at the top of the page, beside the mc.lock sample the lock kind actually describes -- the top of the page is § 1's trust paragraph, and a stability pointer there would answer a question nobody asked yet. bundle.md's sits right after the intro paragraph and before the worked #include <sys> sample, which is where a reader first meets the word <mc/*> and can be told, in the same breath, that the name is frozen and the road that answers it (blob or tree, M52) is not.
  5. docs/ci.md § Versioning gained a fourth column on the label table and one paragraph, not a new section. The task's own two asks -- "what the policy allows" per label, and "RC 0.17.0 is a state, not a suffix" -- were both already ANSWERED elsewhere on this page before this step: the canary section (step C, landed first) already explains the pre-release-flag mechanism in full, and the paragraph right above the label table already argued the state-vs-suffix point in general terms (docs/specs/M53.md § 7). What was missing was the label table SAYING it, in the one place a human actually clicks a checkbox, and the milestone number named plainly rather than left to a cross-reference -- both closed in four lines, with the general argument left where it was and not duplicated.
  6. No golden moves, check-docs reports the seven counts unchanged, and check-freeze reports the same 420 entries. Measured: sh scripts/check-docs.sh -> ok coverage: 209 symbols, 50 flags, 35 TOML keys, 10 directives, samples and links counted separately and unaffected by a change with no fenced ```mc block in it; sh scripts/check-freeze.sh -> ok freeze: 420 entries (209 sym, 50 flag, 35 toml, 10 dir, 101 bundle, 14 lock, 1 machine), byte for byte what step A recorded -- a policy page names no symbol surface-extract.sh's regex could see, and a Contract version 5 line untouched by a pointer sentence placed above the blockquote that carries it.
  7. make check-docs green after every edit, checked per file rather than once at the end, so a broken relative link would be caught at its own source: every one of the six new inbound pointers to hooks.md resolves, along with the two self-references inside hooks.md § 8 -- one to docs/ci.md § The baked version, one to tests/golden/README.md -- and the link count in check-docs's own report moves by exactly that many. One escape, found while writing this very paragraph: check-docs's link scan (grep -oE '\]\([^)]+\)') reads the whole tree, not markdown structure, so it cannot tell a real link from one merely QUOTED to illustrate its shape -- naming the pointer's markup literally, bracket and parenthesis together, made the gate try to resolve a file relative to docs/specs/ where it does not live, and fail as unresolved links. Fixed by describing the pointer in prose, with no live bracket-paren pair next to it.

Implementation notes -- the first freeze regression (#92) and the arity column #

Written after the fix landed. The gate this milestone built caught nothing here, and the reason is worth the paragraph.

  1. What broke. PR #92 ("unsigned comparisons", machine contract v6) needed to ask the comparison table for its unsigned half, and gave the EXISTING function the parameter: cmp_cond(op) became cmp_cond(op, uns). The consumer's teko_typeof.tk:318 is if (cmp_cond(op) >= 0) return TY_I64; -- one argument -- so every taught compiler built on 0.17.3/0.17.4 died with wrong number of arguments before it compiled a line of its own language. The canary held both as pre-releases; this is the first regression the canary caught that the gates did not, which is exactly the division of labour § 6 describes.
  2. Two holes, not one. cmp_cond was not in surface.txt at all -- the sym regex reaches a name through one of 17 prefixes or an exact-name list, and cmp_ is neither -- and the inventory recorded NAMES, so even a listed name could have grown a parameter silently. Both are closed here: cmp_cond and cmp_cond_of join the exact-name list (211 syms, 423 entries), and a sym line is now sym<TAB>name<TAB>arity, the parameter count read from the definition's own (...) in src/*.mc (0 for (), otherwise commas + 1).
  3. The arity is a THIRD verdict, beside removed and added, and the only one whose remedy is not "re-record": changed: sym cmp_cond 1->2 fails with a public function's parameter list is frozen: add a new name instead. Measured both ways -- with #92's two-parameter cmp_cond the gate prints exactly that line and exits 1; with the restored one it prints ok freeze: 423 entries.
  4. The fix is the deprecation lane's step 1 and 2 without step 3: the new behaviour got a new name, cmp_cond_of(op, uns), and cmp_cond(op) is a one-line wrapper over it (cmp_cond_of(op, 0)) -- not a deprecated alias but the function it always was, so no marker and no version are written. Nothing about v6 changed: gen_binary still asks cmp_cond_of(op, cmp_unsigned(...)) and res_binary still asks cmp_cond(op), which is the signed half by definition.
  5. The consumer's shape is a gate now, not a report: lib/user_cmpcond.mc is teko_typeof.tk:318 transliterated, and scripts/check-surface.sh builds a taught compiler from it and RUNS its pass, which asserts the signed condition, the -1 for a non-comparison and the unsigned twin under the new name. Against origin/main's compiler the same file is lib/user_cmpcond.mc:16: wrong number of arguments, exit 1.
  6. The arity column changed no other line of the inventory. The 209 pre-existing syms all carry one arity apiece -- no name in src/ is defined with two different parameter counts, and a prototype and its definition agree -- so the re-record is 209 lines gaining a column plus the two new entries. check-docs.sh needed no edit: it asks the extractor for a single kind, and that road prints the name column alone.

Edit this page