Uh oh!
There was an error while loading. Please reload this page.
Gate the form-section name convention in content/docs TS fences - #11890
Conversation
os-steve
commented
Aug 24, 2026
ACCEPT — PM review, ⭐⭐ You found the card's fourth instrument error — the one all three earlier passes sharedThe card's whole argument was its own history: the same defect hand-counted three times, three different numbers, each pass correcting its predecessor's instrument. #10579's scan was single-line; #10709's was multi-line but required All three were also wrong in the same way, and none of them noticed — they all scoped to I verified this rather than taking it: Zero. The real corpus is 22 literals across 8 pages, not 16 across 5, and the 6 live nameless ones were all outside the window every previous pass looked through. PR #10827 was correct about what it measured; it simply never looked outside That is a different and nastier error class than the three the card documents: those were matcher errors, each caught by the next pass because the next pass re-derived. This one is a scope error, invisible to re-derivation because every pass inherited the same scope. A card whose thesis is "hand-counting keeps producing different answers" turned out to be understating itself — the three answers also shared a common blind spot. ⭐ Zone 2 falsified in the direction that matteredI flagged that the population was plausibly zero since #10827 swept it, and said the judged-population line would therefore be load-bearing. It is not zero — it is 6 — so the fail-before/pass-after is real: And the anti-vacuity machinery is still there and still bites, which is what makes the eventual zero mean something. The census anchor is the sharp instrument, and Leg B proves itexportconstCENSUS_ANCHORS=[`${DOCS_ROOT}/ui/forms.mdx`,`${DOCS_ROOT}/ui/views.mdx`,`${DOCS_ROOT}/concepts/architecture.mdx`,←outsideui/];Pinning a file outside And the file states why the anchors carry weight the floor cannot: "deleting every example from one page still clears a count floor and still fails its anchor." Two evasions, two instruments. Zone 1: all three rulings held, checked by diff
That last one is the good kind of not-triggered: only 1 of the 8 affected pages carries an Second premise corrected without drama:
|
Uh oh!
There was an error while loading. Please reload this page.
Fixes#10830
Adds
scripts/check-docs-section-name.mjs— a standalone docs gate requiringnameonevery form-section object literal in a TypeScript fence under
content/docs/**— wires itinto
lint.yml, and sweeps the live population the gate found.Zone 2 first: the card's own assumption is FALSIFIED, and the direction matters
The dispatch asked me to re-derive the 8-of-16 census on current
main, warning the livepopulation was "plausibly zero" because PR #10827 swept it. It is not zero, and the
reason is the interesting part.
All three hand-passes scoped to
content/docs/ui/**. PR #10827 did sweep thatdirectory clean — 16 of 16 named there, exactly as it reported. But
sections: [objectliterals also live in TS fences under
content/docs/concepts/andcontent/docs/protocol/objectui/, and every one of those six was nameless:content/docs/concepts/architecture.mdx:265Company Informationcontent/docs/concepts/architecture.mdx:269Contactcontent/docs/protocol/objectui/concept.mdx:477Infocontent/docs/protocol/objectui/index.mdx:237Basic Infocontent/docs/protocol/objectui/index.mdx:238Datescontent/docs/protocol/objectui/index.mdx:239Team⭐ So the card's history has a fourth instrument error, and it is the one none of the
three passes noticed because they all shared it: #10579's scan was single-line, #10709's
required
{to start a fresh line, PR #10827 fixed the matcher — and all three took thescope for granted. A hand-derived scope is defeated the same way a hand-derived
matcher is. That is a stronger argument for a mechanical check than the one the card
made, and it makes the anti-vacuity work load-bearing rather than decorative.
CENSUS_ANCHORSpinscontent/docs/concepts/architecture.mdxfor exactly this reason:re-narrowing the gate's scope back to
content/docs/ui/**makes it REFUSE, not pass.The card's other assumption holds, with one correction:
nameis still.optional(), butat
packages/spec/src/ui/component.zod.ts:853, not:825— the line drifted, the fact didnot. An
os:checkfence still type-checks clean without it.Zone 1 compliance
packages/spec/**. Bracket-matching, per theruling — line-matching is what defeated both earlier passes.
check:skill-examplesstrictness-overlay shape: the population lives in plain TS fences that no
os:checkmarker covers (only one of the eight pages carries one), so an overlay on the
type-checked subset would have reached a smaller corpus than a standalone sweep, not
a larger one.
packages/spec/scripts/**is untouched.namestays.optional(). No contract file is in this diff. The gate's headerand its failure text both say so explicitly, because "make it required" is the wrong
fix a future reader will reach for first.
What the gate asserts, and what it deliberately does not
Presence of a
namekey at the literal's own top level. Not casing, not non-emptiness— zero occurrences of either were measured, and a rule with no population is a rule whose
first red lands on work that is not the defect.
Bracket-matched, not line-matched. Comment and string spans come from
scripts/js-comment-mask.mjs— this tree's one answer to "code or prose" — rather than aprivate
stripComments, so a[inside a string and asections: [inside a docblockare both handled by a masker already diffed against a real TS parser over 4,739 files.
Quoted keys (
'sections':,'name':) are recognised through the scan's flags, sinceblanking literal content would otherwise erase a quoted key's spelling and hand anyone a
silent escape.
⛔ Its zero is a measurement, not a silence
This gate's own subject is a defect that survived three counts because each count's
silence read as a clean bill of health. So:
run()REFUSES (exit 2, never 0) on a broken selector wearing a pass: missing docsroot, zero files, zero TS fences, counts below their floors, or a census anchor that
stopped contributing.
--self-testdrives the real sweep over a real temp tree on disk.The out-of-scope population is declared and counted, not silently skipped. YAML fences
carrying
sections:are a different parser, not a bigger regex. That gap holds a realpopulation — 21 nameless section mappings across 16 fences — filed as #11887. (#11887 is
not addressed here.)
Non-vacuity
Fail-before / pass-after, same tree, same script:
Planted control on a real page. A nameless section injected into
content/docs/ui/forms.mdx, the gate run, then a byte-identical restore:The reported line is the line the plant actually landed on.
Ablation — three legs, each mutated on disk under
trap '<restore>' EXIT INT TERM,each
node --checked before running so a red cannot be a load error, each restoredbyte-identically:
namedalways true)content/docs/uia clean corpus passes — got 2 (REFUSED), want 0an inline sections: [{ is judged (the #10709 miss)Every leg reported
anchor occurrences AFTER: 0,replacement occurrences AFTER: 1,sha CHANGED: YES, andBYTE-IDENTICAL RESTORE: YES. Leg B is the sharp one: it provesthe anchor guarding against re-narrowing actually bites.
TypeError, not by assertion —findings[0].fileon an empty array. That is a red that reads exactly like proof whileproving only that the harness crashed. Fixed before the run above (
NONE/at()fallbacks), and the reason is recorded in the file next to them, since the next person to
add a case will reach for the same unguarded index.
Verification
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack(no paths — itderives its own change set) named 35 families over 5 paths. 34 run green at
4fbb12069, the final commit. Exit codes captured before any pipe(
cmd > file 2>&1; e=$?) — never a bare$?after atail:check:doc-formula-expressionsandcheck:doc-security-posturefailed on first run withPREREQUISITE NOT MET — @objectstack/lint is not built. That is the unbuilt-worktree trap,not this change:
--filter '@objectstack/lint^...'builds the closure but not thepackage itself. After
--filter '@objectstack/lint...'both are green, the formerreporting
22 record-scoped formula example(s) across 421 files / 1448 TS blocks judged clean.Two DECLARED narrowings — each a measurement, not a skip
1.
pnpm check:type-check-debt— its--re-measureleg refused; not run. The gateitself refuses without a built dependency closure ("52 workspace dependencies … have no
built type entry point … measuring now would not fail, it would silently measure a
DIFFERENT WORLD"). Three pieces of evidence, all from the tool's own output:
✓ --self-test — 47 semantic + 59 observation + 29 re-measure + 28 built-closure + 19 auto-lowering case(s) hold, thencheck-type-check-coverage: OK — 65/78 workspace packages type-checked, 13 in the DEBT ledger (436 frozen raw errors), 1 exempt.--re-measure, and only it.packages/**. This diff's change set containszero such paths —
git diff --name-only $(git merge-base HEAD origin/main)...HEAD | grep -c '^packages/'→0. No ledgered number can move.2.
pnpm lint(repo-wideeslint . --no-inline-config) — narrowed to the diff. Threepieces of evidence:
five changed paths: the three
.mdxfiles andlint.ymleach come backFile ignored because no matching configuration was supplied.Exactly one file in this diff is ineslint's population.
--format json: 1 file linted,errorCount=0,warningCount=0.eslint.config.mjs:327states it in its own words —"this repo runs one
eslint.config.mjs, which never enables type-aware linting (noparserOptions.project, no typed@typescript-eslintrules) for ANY file". Per-fileverdicts are therefore independent, so this diff cannot move any untouched file's result.
CI runs the full farm regardless; these two narrowings are declared so a reader can tell
them apart from a gate that was simply not run.
Control characters
grep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]'over all five changed files → exit 1, nomatches.
check:nul-bytesgreen over 6,615 files.Wiring
One step in
lint.yml'sLint & Repo Gatesjob, placed directly after theReact pages honour the useAdapter() query and result contractsstep — its closestsibling (same
content/docs/**corpus, same census-anchor discipline) and a region clearof PR #11864's (~line 988) and #10778's in-flight edits.
Invoked as a direct
node scripts/…step rather than a rootpackage.jsoncheck:*alias, matching the precedent of the
check-doc-frontmatterandcheck-aggregator-rostersteps in the same job: that alias sits in declared territory of the @changesets/cli v3
migration lane (#9465) while it runs. Root
package.jsonis untouched.Naming convention followed
Read off the 16 already-named sections in this corpus, not invented: snake_case derived
from the label, placed first in the literal, ahead of
label. Single-word labelstransliterate directly (
Contact→contact,Info→info,Dates→dates,Team→team), matchingcreate-vs-edit-form.mdx:69andfield-grouping-and-order.mdx:62.Noun phrases transliterate whole (
Company Information→company_information), matchingviews.mdx:352'sbasic_informationforBasic Information.Basic Info→basic_infohas a stronger precedent than convention:
content/docs/protocol/kernel/i18n-standard.mdx:185already carries
_sections: { basic_info: … }— the i18n anchor spelling itself.skip-changesetConfirmed for the files actually touched, not defaulted.
content/docs/**is consumed onlyby
apps/docs, whosepackage.jsonis"private": true;scripts/**and.github/workflows/**publish nothing. No package source is in this diff.Generated by Claude Code
Generated by Claude Code