@@ -1395,10 +1395,19 @@ rest at #5107.
13951395 in the same change (` SortNodeSchema ` + ` normalizeSortNodes ` in
13961396 ` metadata-protocol ` ), per finding 6's asymmetry.
13971397
1398- - ** ` ui/app.zod.ts ` 's ` BaseNavItemSchema ` ** is the base that the strict
1398+ - ~~ ** ` ui/app.zod.ts ` 's ` BaseNavItemSchema ` ** is the base that the strict
13991399 discriminated-union members ` .extend() ` . Finding 16 is the warning: closing
14001400 a base closes every extension of it, including any that is deliberately a
1401- wire shape. ** Still open.**
1401+ wire shape.~~ ** SETTLED at #5249 (maintainer ruling 2026-08-06, option A) —
1402+ and the bullet's own premise was wrong** , which is why it is struck rather
1403+ than deleted. The members do ** not** ` .extend() ` this base, they spread
1404+ ` ...BaseNavItemSchema.shape ` , and that is the whole of finding 16: a spread
1405+ lands the keys in a fresh ` z.object ` whose posture is its own, so the base
1406+ is inert and closing it is a guaranteed no-op. The row got the ninth
1407+ verdict, ` covered ` , rather than being rounded onto ` no door ` , whose
1408+ prescribed ADR-0049 retirement would have deleted nine branches' shared
1409+ keys. Evidence and the mechanical spread-vs-extend test are in the
1410+ ` app.zod.ts ` row of the ` ui/ ` remaining-strip map above.
14021411
14031412Done in the registered-types batch: ` strictObject ` (` shared/strict-object.ts ` )
14041413replaced the four-part wiring recipe, and ` seed ` + ` doc ` became the first two
@@ -1486,6 +1495,226 @@ apps (23 + 6 objects, 20 + 1 pages, 4 + 1 datasets, 3 + 1 dashboards) report
14861495** zero** unknown keys. No finding this time — worth recording precisely because
14871496the app step's ` ACCOUNT_APP.defaultOpen ` came from exactly this class of check.
14881497
1498+ ## Campaign closing record — the terminal re-measure (2026-08-16)
1499+
1500+ This section is the ** terminal state** of the #4001 campaign: the census re-run
1501+ on the final tree, the confirmation of the three closing claims, the one entry
1502+ left deliberately open, and what holds the posture once no batch is scheduled.
1503+
1504+ Read it as the answer to * "is there authorable work left in this ledger?"* — no.
1505+ Read it as * "is this ledger finished?"* — also no, and deliberately: the
1506+ remaining-strip map still carries ** 22 file rows covering 123 strip sites, 122
1507+ of them non-authorable** , and they are kept so the arithmetic is complete and so
1508+ nobody re-triages them from scratch. The ** forced scope** of the 2026-08-03
1509+ ruling is what closed.
1510+
1511+ ### The measurement, and why the start column had to be re-taken
1512+
1513+ ** The campaign's opening numbers and its closing numbers were never comparable,
1514+ because the instrument changed underneath them.** #4001 's opening post recorded
1515+ ` .strict()=31 / .passthrough()=20 / 默认 strip=1885 ` — taken with the ** textual**
1516+ method, which ` scripts/lib/strictness-ledger.ts ` later documented as wrong in
1517+ both directions at once (it counted ` z.object( ` inside JSDoc prose, and missed
1518+ the prettier-wrapped ` z\n.object( ` call). Comparing that triple against today's
1519+ AST reading would publish an instrument change as a campaign result, which is
1520+ this file's own subject matter.
1521+
1522+ So the terminal re-measure runs ** today's AST instrument over the campaign's
1523+ start tree as well** — ` d6bfb3d0a ` , ` main ` 's tip when #4001 was filed
1524+ (2026-07-30) — and both columns below come from that one instrument.
1525+
1526+ What that alone shows, before any campaign work is counted: on the start tree
1527+ the AST reads ** 15 strict / 20 passthrough / 2 catchall / 1903 strip over 1940
1528+ sites** , where the opening post recorded 31 / 20 / 1885. The headline "31
1529+ strict" was an over-read of ** 16** , and the opening post's per-directory table
1530+ (a strip-count table) under-read ` ui/ ` 177→183, ` data/ ` 149→157, ` system/ `
1531+ 383→389, ` ai/ ` 72→74, ` automation/ ` 80→81. The consequence worth recording:
1532+ the campaign's forced scope was scoped in that post as ** "≈453 authorable
1533+ sites"** , and the AST says the five directories held ** 484** . The plan was
1534+ drawn against a number that was never measured — the estimate was low by 31
1535+ sites, which is more than the whole of ` studio/ ` .
1536+
1537+ ### Start → final, the five triaged directories (one instrument)
1538+
1539+ | | Sites | strict | passthrough | catchall | ** strip** |
1540+ | ---| ---| ---| ---| ---| ---|
1541+ | ** start** (` d6bfb3d0a ` , 2026-07-30) | 484 | 12 | 4 | 0 | ** 468** |
1542+ | ** final** (2026-08-16) | 439 | 310 | 6 | 0 | ** 123** |
1543+
1544+ Per directory:
1545+
1546+ | Dir | Sites start → final | strict start → final | ** strip start → final** |
1547+ | ---| ---| ---| ---|
1548+ | ` ui/ ` | 193 → 174 | 7 → 163 | ** 183 → 6** |
1549+ | ` data/ ` | 163 → 153 | 5 → 71 | ** 157 → 81** |
1550+ | ` automation/ ` | 81 → 65 | 0 → 42 | ** 81 → 23** |
1551+ | ` security/ ` | 20 → 20 | 0 → 7 | ** 20 → 13** |
1552+ | ` studio/ ` | 27 → 27 | 0 → 27 | ** 27 → 0** |
1553+
1554+ ** Read the site totals, not only the strip column.** The five directories LOST
1555+ 45 sites over the campaign — surface that was retired under ADR-0049 rather than
1556+ closed, because a batch's per-schema read kept coming back "nobody writes this"
1557+ (` no door ` ) or "nothing parses it". ` strip 468 → 123 ` is therefore two movements
1558+ summed, and the ledger's rows say which is which per file. That is the campaign's
1559+ least-expected result: ** the single most common outcome of reading a shape
1560+ carefully was not tightening it.**
1561+
1562+ Whole-spec context (fourteen directories, out of the ruling's forced scope, same
1563+ instrument): 1940 → 1722 sites, strict 15 → 361, strip 1903 → 1338. The untriaged
1564+ directories are coarse-classified in the section above and are not this campaign's
1565+ debt; ` api/ ` , ` system/ ` , ` kernel/ ` and ` cloud/ ` are wire surface by construction.
1566+
1567+ ### The three closing claims, confirmed
1568+
1569+ 1 . ** Global authorable strip = exactly 1.** The generated bucket split reads
1570+ `authorable 1 · unresolved 0 · wire/open 118 · no door 3 · no gate 0 ·
1571+ covered 1` — 123. Only ` ui/` contributes to the authorable bucket; ` data/`,
1572+ ` automation/ ` , ` security/ ` and ` studio/ ` all read 0. The single site is
1573+ ` ui/view.zod.ts ` 's ` FormFieldBaseSchema ` , and the other five ` ui/ ` strip
1574+ sites resolve elsewhere, per their rows:
1575+
1576+ | Site (line as of this record) | Schema | Class |
1577+ | ---| ---| ---|
1578+ | ` view.zod.ts:1888 ` | ` FormFieldBaseSchema ` | ** authorable — the one parked entry** |
1579+ | ` view.zod.ts:3011 ` / ` :3015 ` | ` ViewItemWireSchema ` (both arms) | wire by design (#5074 's authoring/wire split) |
1580+ | ` app.zod.ts:295 ` | ` BaseNavItemSchema ` | covered (#5249 ) |
1581+ | ` widget.zod.ts:89 ` | ` FieldWidgetPropsSchema ` | no door (#5055 ) |
1582+ | ` action-params.zod.ts:321 ` | ` ActionSessionSchema ` | wire (#5697 ) |
1583+
1584+ 2 . ** ` data/ ` 's forced scope is discharged.** The directory's authorable bucket
1585+ is 0. Its 81 remaining strip sites are all wire/open/no-door, each with a row
1586+ and a per-schema verdict; the last three ` mixed (p) ` rows were discharged at
1587+ batch D and the last firm authorable site (` IndexSchema ` ) closed at 批 20
1588+ site 14.
1589+
1590+ 3 . ** ` data/object.zod.ts ` is closed.** 批 20's unit was the file's ** 14 inner
1591+ blocks — 13 at 批 20 and the held 14th (` IndexSchema ` ) on 2026-08-16** ; the
1592+ AST unit is the file's ** 20 object sites, all 20 strict, 0 strip** . Both
1593+ readings are in this record because the two counts are different units of the
1594+ same fact, and a future reader comparing "14/14" against a generated "20"
1595+ should not have to re-derive that.
1596+
1597+ ### The batches
1598+
1599+ The campaign did not advance as one sweep and its waves are not a tidy numbered
1600+ run — they are named for the surface each took, and the ** evidence for every one
1601+ of them is in the rows above, not here** . This list exists so the roll-call is
1602+ readable in one place:
1603+
1604+ - the ** Tier-A slice** (` security/permission.zod.ts ` , ` automation/flow.zod.ts ` ,
1605+ ` ui/action.zod.ts ` re-homed onto the shared factory), the ** registered-types
1606+ batch** (` strictObject ` itself, ` seed ` + ` doc ` ), then the ** security** ,
1607+ ** app** (PR A tombstones, PR B strict) and ** data** steps;
1608+ - the ` automation/ ` waves — 批 9 through 批 12 — each resolving a ` (p) ` by
1609+ finding a door the prose had missed;
1610+ - the ` ui/ ` waves — 批 13 through 批 19 — which produced the campaign's
1611+ vocabulary as much as its closures: ` no door ` (批 13), ` no gate ` (批 15),
1612+ the per-schema split (批 14), the largest single reclassification
1613+ (` component.zod.ts ` , 批 17), the authoring/wire split (#5074 ), and ` covered `
1614+ (#5249 , ruled at 批 19's request rather than guessed);
1615+ - the ` data/ ` waves — batch A, batch B, batch D and 批 20 — ending with
1616+ ` object.zod.ts ` site 14 once its cross-repo hold (#5247 → objectui #4772 ) was
1617+ spent.
1618+
1619+ ** The method that survived all of them** : verify who writes the input * before*
1620+ tightening, per schema and never per file, with a positive control in the same
1621+ run. It changed the verdict often enough that it, and not the closure count, is
1622+ what this campaign should be remembered for.
1623+
1624+ ### The one parked entry — ` ui/view.zod.ts ` 's ` FormFieldBaseSchema `
1625+
1626+ ** Standing rationale.** It is a module-private base (` const ` , not exported) with
1627+ zero ` .parse() ` of its own. Its sole consumer is the door:
1628+ ` FormFieldSchema = FormFieldBaseSchema.extend({ fields }).strict().transform(…) `
1629+ — so an undeclared form-field key is already ** rejected** , with the ADR-0089
1630+ visibility error map (` VISIBILITY_STRICT_OPTIONS ` + ` strictObjectError ` since
1631+ #6619 ) carrying the prescription. Closing the base changes no parse.
1632+
1633+ ** Why it is ` authorable ` and not ` covered ` .** The two verdicts are separated by
1634+ one mechanical test, not by a judgement (#5249 ): ` covered ` requires the keys to
1635+ reach consumers by a ` ...X.shape ` ** spread** , which lands them in a fresh
1636+ ` z.object ` with its own posture and makes the base inert. This consumer uses
1637+ ` .extend() ` , which ** inherits** posture — so the base is a real door whose
1638+ posture simply happens to be overridden downstream. Calling it ` covered ` would
1639+ be recording the wrong mechanism, and the mechanism is what the next reader acts
1640+ on.
1641+
1642+ ** Why it still counts as a strip site.** The site deliberately keeps its literal
1643+ ` z.object( ` spelling so this instrument keeps counting it. A conversion would
1644+ remove it from the map, and the map is what makes the parked state visible.
1645+
1646+ ** Restart condition — and it is mechanically gated, not remembered.** This entry
1647+ becomes real work the moment either holds:
1648+
1649+ - ** the consumer stops applying its own ` .strict() ` ** (relying on inheritance
1650+ from a strip base would open the door silently), or
1651+ - ** a second consumer of the base appears** that does not close its own clone.
1652+
1653+ The first is pinned: ` view-strictness-batch18.test.ts ` 's
1654+ * "` FormFieldBaseSchema ` stays a bare ` z.object ` : its ONE consumer already
1655+ ` .strict() ` s it"* asserts through the real door (` FormViewSchema ` ) that an
1656+ undeclared field key is refused, and goes red if the ` .strict() ` is dropped. The
1657+ sibling case pins that the ADR-0089 visibility pair still resolves through its
1658+ own error map — the reason the base was not converted in the first place.
1659+ ` check:strictness-ledger ` holds the second half: the site cannot leave the map
1660+ quietly, and its row cannot be deleted while it is strip.
1661+
1662+ ### What now enforces the posture, with no batch scheduled
1663+
1664+ The campaign ends without a standing worklist, so the posture has to be held by
1665+ mechanism. Four, and each has been shown to go red:
1666+
1667+ 1 . ** The strictness-ledger gate** (` check:strictness-ledger ` , wired into the
1668+ Spec Liveness Check workflow) — the generated counts must be byte-fresh
1669+ against the AST, every sited file in a triaged directory must have a row, and
1670+ every ` Class ` cell must parse. A new ` *.zod.ts ` in ` ui/ ` / ` data/ ` /
1671+ ` automation/ ` / ` security/ ` / ` studio/ ` is ** undeclared surface** and fails
1672+ the gate until someone classifies it. This is what makes the closing state
1673+ above a ratchet rather than a snapshot.
1674+ 2 . ** The reverse pin at zero** — a remaining-strip row whose file reaches zero
1675+ strip sites ** fails** . A worklist that can outlive its work will, and this
1676+ ledger has the scar; it is why ` object.zod.ts ` , ` analytics.zod.ts ` and
1677+ ` driver/memory.zod.ts ` are absent from the map above rather than sitting in
1678+ it at 0.
1679+ 3 . ** The strict-template idiom** — ` strictObject ` (` shared/strict-object.ts ` )
1680+ is one call, reads its key list from the shape at the call site, and carries
1681+ ` surface ` / ` history ` / ` aliases ` / ` guidance ` . The four-part hand-wiring it
1682+ replaced is what made each closure expensive enough to defer; the next
1683+ authorable schema is closed by writing ` strictObject ` instead of ` z.object ` ,
1684+ which is the campaign's durable output.
1685+ 4 . ** The unknown-key warning layer** — ` lintUnknownAuthoringKeys ` /
1686+ ` lintUnknownStackKeys ` , wired into ` defineStack() ` , ` os validate ` and
1687+ ` os compile ` , descending nested objects/arrays/records with the same posture
1688+ rules. It reports what the ratchet has not reached, which is how a
1689+ still-strip authorable site would announce itself between campaigns.
1690+
1691+ ** The standing question in "Next steps" §1 is answered, as that step asks.** It
1692+ was * "has ` lintUnknownAuthoringKeys ` reported an unknown key on any surface
1693+ outside this repo yet?"* , with the honest third outcome flagged as the one to
1694+ check for: * zero findings because nothing is reporting back* . That is still the
1695+ answer — this remains a pre-1.0 product with no third-party authors, so no
1696+ outside-repo report exists, and none is pending. The wait was discharged by the
1697+ maintainer's decision to proceed on mechanical, self-prescribing rejections
1698+ rather than by data arriving, and the campaign closed on that basis. ** Recording
1699+ "still nothing" is the point** : a wait nobody re-examines is indistinguishable
1700+ from an abandoned one.
1701+
1702+ ### What this record does NOT close
1703+
1704+ - ** The anchor issue.** Closing #4001 is a maintainer/PM decision taken after
1705+ this record lands, not by it — #8687 carries a ` Blocked-by: ` on the anchor and
1706+ needs re-pricing at closure time. #8687 's gate is strict propagation at the
1707+ ** top-level stack surface** (` ObjectStackDefinitionSchema ` 's 43 keys), which
1708+ no slice of this campaign delivered.
1709+ - ** The 122 non-authorable strip sites** , in the map's 22 file rows (the
1710+ ` view.zod.ts ` row is the one that spans both, ` 1 authorable, 2 wire ` ). Wire,
1711+ open, ` no door ` and ` covered ` rows stay in the map by design. The ` no door `
1712+ ones carry the only follow-up in the set, and it is a different ratchet:
1713+ ADR-0049 removal, tracked at #8562 for ` field.zod.ts ` 's two.
1714+ - ** The nine untriaged directories.** They were never in the ruling's forced
1715+ scope and are classified coarsely; a future campaign that wants them starts by
1716+ giving them per-file rows, at which point this gate begins holding them too.
1717+
14891718## This file is now machine-checked
14901719
14911720` pnpm --filter @objectstack/spec check:strictness-ledger ` (wired into the Spec
0 commit comments