You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix(spec): the generated docs root sidebar lists categories from the same declared page list `meta.json` and the category index already agree on (#11482)
6
+
7
+
`build-docs.ts` writes the docs tree from what should be one answer to "which
8
+
categories/pages exist", but the ROOT `content/docs/references/meta.json` (§3
9
+
— the sidebar's top-level category list) was still answering it a third way:
10
+
11
+
- a category's own `meta.json` (§2) is built from the pages the run
12
+
**emitted**;
13
+
- that category's `index.mdx` card grid (§2.5) reads the SAME declared list
14
+
(#11260) — no longer a second, independently-derived enumeration;
15
+
- the root `meta.json` (§3), until now, filtered on `categoryZodFiles` — the
16
+
`.zod.ts` files found **on disk** — a third, independent enumeration.
17
+
18
+
A category whose published pages all come from plain `.ts` files rather than
19
+
`.zod.ts` ones (the `misc` catch-all class `security/misc` proves is real) has
20
+
zero `.zod.ts` files while still publishing a page, a `meta.json` and an
21
+
`index.mdx`. The old filter would drop such a category from the sidebar even
22
+
though it is fully generated and routed everywhere else — a folder complete on
23
+
disk and unreachable from the nav.
24
+
25
+
**No category is in that state today** — all 14 have at least one `.zod.ts`
26
+
file — so this was a latent defect with no live instance, and the regenerated
27
+
root `meta.json` is byte-identical. The filter now reads `categoryMetaPages`,
28
+
the same map §2.5 already reads, so all three files answer from one list
29
+
instead of three that happen to agree today. The rule moved into
30
+
`scripts/lib/root-meta.ts` (`rootCategoryDirs`), pinned directly with the
31
+
all-`misc`-category shape that has no instance in the repo — the same move
32
+
#11260 made for the category card grid, for the same reason: the defect's
33
+
output is an ABSENT sidebar entry, which `check:docs` cannot see any more than
34
+
it could see an absent card, and the edge that has no live instance cannot be
The #11566`maxLength` narrowing (shipped in 17.x: `z.number().int().min(1)`, refused outside `BOUNDED_STRING_FIELD_TYPES`) is now registered in the ADR-0087 migration ledger (#11950) — the enforcement PR deliberately deferred the entry because the registry file was serialized behind an in-flight change. Following the #8321`scale`/`precision` template, the major-18 semantic entry carries both halves: the mechanical one (delete the key where it was misplaced — inert by construction outside the write-time validator's bounded-string branch) and the judgment one (a malformed value on a bounded-string type WAS consumed by the validator's raw comparison — `maxLength: 0` accepted only empty strings, a negative value refused every write — so only the author knows the bound they meant; the entry tells them to re-declare it). `objectstack migrate meta`, `spec-changes.json` and the upgrade guide surface the entry at the major boundary; no accept/reject behaviour changes in this release.
`FieldSchema.minLength` tightens on both axes (#11949, maintainer ruling 2026-08-25) — `maxLength`'s twin defect pair (#11566), closed with the same template. Shape: the key is now `z.number().int().min(1)`, so `minLength: 0`, negative and non-integer declarations are refused at parse. The lower bound is 1 by ruling: "no minimum" is expressed by omitting the key, not by declaring a vacuous truth — `minLength: 0` can never fail, and a permanently-true declaration is exactly the noise an AI metadata author mass-produces, so it is refused loudly at authoring time. Applicability: the key sat on the base schema and was authorable on every field type; it is now refused on any type that does not store a bounded string, and accepted on exactly the `BOUNDED_STRING_FIELD_TYPES` set — `text`, `textarea`, `email`, `url`, `phone`, `password`, `markdown`, `html`, `richtext`, `code`, `signature`, `qrcode` (twelve members since #11875) — the same set `maxLength` converged on.
6
+
7
+
What newly gets rejected: `minLength: 0` / negative / non-integer on any type, and `minLength` with any value on every non-bounded-string type (`boolean`, `number`, `date`, `select`, `lookup`, `autonumber`, `formula`, `json`, `secret`, …). Both rejections are prescriptive — the message names the legal shape, the legal type set, and the fix. The two authoring forms converge on the same set (`field.form.ts` previously showed the key for three types; `object.form.ts` for nine). Already-legal declarations (a positive-integer `minLength` on a bounded-string type) round-trip byte-identically, and absence stays absence — no default materializes.
Copy file name to clipboardExpand all lines: .github/workflows/docs-drift-check.yml
+41-1Lines changed: 41 additions & 1 deletion
Original file line number
Diff line number
Diff line change
@@ -259,7 +259,47 @@ jobs:
259
259
if (crossCutting.length) limits.push(`**${crossCutting.length}** cross-cutting symbol(s) contributed no route anchor: \`${crossCutting.join('`, `')}\``);
260
260
if (overbroad.length) limits.push(`**${overbroad.length}** anchor(s) matched too much of the corpus to be a work list: \`${overbroad.join('`, `')}\``);
261
261
if (weak.length) limits.push(`**${weak.length}** name(s) were too generic to anchor anything (single lowercase words)`);
262
-
if (bridge && bridge.measured && bridge.unreachable > 0) limits.push(`the SDK route bridge reached **${bridge.reachable}** of **${bridge.clientRows}** client-bound route-ledger rows — the other **${bridge.unreachable}** have no registrar \`path:\` tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run: \`node scripts/docs-audit/affected-docs.mjs --bridge-coverage\``);
262
+
// ── WHY THOSE ROWS ARE UNREACHABLE, NOT JUST HOW MANY (#11867) ────────
263
+
//
264
+
// The line above used to end at the count, and a count is one population.
265
+
// The census says there are three, and the difference is the whole reason
266
+
// the split exists: the auth ledger's `56 of 56` and the rest ledger's
267
+
// `46 of 87` print identically and are NOT the same finding — the first has
268
+
// no in-repo registration site at all, so the discovery widening the second
269
+
// one wants moves it by zero rows. That conflation already aimed one card
270
+
// (#11178) at widening a recognizer that was never the constraint, and this
271
+
// comment is the surface where a human meets the number.
272
+
//
273
+
// ⛔ SAME NAMES, ONE DERIVATION — the rule stated three lines up for the
274
+
// `anchorless` pair, and it binds here for the same reason. `bridge.causes`
275
+
// is a PARTITION of `bridge.unreachable`, computed ONCE inside
276
+
// `bridgeCoverageFrom` and published whole; the parts are READ off the same
277
+
// object as the total and never recomputed here. Re-deriving them from
278
+
// `bridge.ledgers` would be a second derivation that agrees today, drifts
279
+
// silently tomorrow, and renders a breakdown that sums to something the
280
+
// headline beside it denies.
281
+
//
282
+
// And on numbers that do NOT partition that total, the split is WITHHELD and
283
+
// the run says the census is broken, rather than printing three figures
284
+
// beside a fourth they contradict. That state cannot arise from
285
+
// `bridgeCoverageFrom` — `affected-docs.mjs --self-test` pins the partition
286
+
// — which is what makes it a verdict here and not a fallback.
287
+
//
288
+
// `measured: false` keeps its honest arm: a run that supplied no ceiling
289
+
// reports that WHY was not measured. It says so in words rather than
290
+
// rendering three nulls or, worse, three zeroes — "nobody looked" is not
291
+
// "none found", the same distinction `computedOn.dirty`'s null arm draws.
292
+
const causes = (bridge && bridge.causes) || null;
293
+
let bridgeCauses = '';
294
+
if (causes && causes.measured === true) {
295
+
const parts = causes.remediable + causes.structural + causes.undecided;
296
+
bridgeCauses = parts === bridge.unreachable
297
+
? ` Of those **${bridge.unreachable}**: **${causes.remediable}** are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); **${causes.structural}** are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; **${causes.undecided}** are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here).`
298
+
: ` ⛔ Its cause census is BROKEN — **${causes.remediable}** + **${causes.structural}** + **${causes.undecided}** is not the **${bridge.unreachable}** it claims to break down, so the split is withheld.`;
299
+
} else if (causes && causes.measured === false) {
300
+
bridgeCauses = ` ⚠️ Cause NOT measured on this run: ${causes.reason}. No cause may be read into the count above — "nobody looked" is not "none found".`;
301
+
}
302
+
if (bridge && bridge.measured && bridge.unreachable > 0) limits.push(`the SDK route bridge reached **${bridge.reachable}** of **${bridge.clientRows}** client-bound route-ledger rows — the other **${bridge.unreachable}** have no registrar \`path:\` tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run.${bridgeCauses} The rows themselves: \`node scripts/docs-audit/affected-docs.mjs --bridge-coverage\``);
263
303
// ── THE RULE-CARRYING PAGE AN EMITTER DIFF CANNOT REACH (#11434) ──────
264
304
//
265
305
// Every line above is a REPORT about this run: a count this run produced, a
0 commit comments