Skip to content

feat(scripts): report-only census of ${…} authored on keys nothing evaluates (objectui#7851) - #7868

Merged
os-sam merged 3 commits into
mainfrom
claude/issue-7851-json-fence-nontype-key-sweep
Sep 6, 2026
Merged

os-sam merged 3 commits into
mainfrom
claude/issue-7851-json-fence-nontype-key-sweep

Conversation

@claude

@claude claude Bot commented Sep 6, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #7851

A json fence in content/docs/** can author any key at all on a node and no gate objects. check:doc-types judges the type literal and states the exclusion in its own header — "NOT in scope, deliberately: whether the snippet's OTHER keys are read by the renderer the type resolves to" — and check:doc-snippets covers these pages but type-checks ts/tsx blocks only. So the json fence bodies were read by nothing.

This lands the instrument that reads them, report-only, and the census it produces.

The census, on 59df1dab2

204 json/jsonc fence(s) across 184 file(s) under content/docs
204 parsed, 0 UNPARSED          (1 of them parsed as an object BODY)
359 node(s) with a string `type`
 66 ${…} site(s) on those nodes, 49 of them carried
 17 sites author ${…} on a key nothing evaluates, across 5 pages
page sites keys
content/docs/guide/expressions.md 8 badge.variant x3, badge.text, card.body, card.className, list.items x2
content/docs/guide/schema-rendering.md 5 chart.data, badge.text, badge.variant, alert.variant, alert.message
content/docs/guide/architecture.md 2 badge.text, badge.variant
content/docs/api/schema-reference.md 1 action.condition
content/docs/guide/layout.md 1 page.title

The 8 on expressions.md are an independent reproduction of the number objectui#7418's prototype measured on the same tree — its six sites plus the two list.items occurrences that card deliberately left unclaimed. The other 9, in four further pages, were not in any card's enumeration. ⛔ Nothing outside this PR's four files is touched: the other pages are reported, not repaired.

Fence languages, and the dialect blind spot

Asked for after a sibling seat measured objectui#7440's page: its three teaching blocks are fenced jsonc, not json.

By fence language: json 198 (198 parsed, 0 unparsed); jsonc 6 (6 parsed, 0 unparsed)
✅  Dialect blind spot: none — no fence OUTSIDE json/jsonc parses as a JSON document holding a typed node.

jsonc was already in the scanned set, so content/docs/guide/record-edit-modes.md's four jsonc fences are all counted and judged today (they produce no expression finding, because objectui#7440 is the read-back half, not this one). What was missing was the reading that says so, and it is now printed on every run — per language, both directions — together with a probe that parses every fence outside the scanned set purely to report whether it would have been a JSON document holding a typed node. A missed dialect is now a number rather than something a human finds by re-reading a page. Both are pinned.

⚠️ A hit is a candidate, not a verdict

type is not one vocabulary in these pages — check:doc-types measured at least seven that all spell that key — so a node from another one is judged here against a carriage map that does not govern it. One of today's 17 is exactly that: content/docs/api/schema-reference.md's ActionSchema example authors a ${…} on condition, which the action runner's own gate evaluates, not SchemaRenderer's node channels. ⛔ The obvious fix — classify by enclosing key path — was built and measured by that gate before being rejected as non-convergent, and that ruling is inherited here rather than re-litigated. Report-only is what makes inheriting it safe: a candidate a human reads costs a minute, and nothing is blocked while the corpus is read. The caveat is stated in the script's header and printed under every findings list.

Report-only, deliberately

Three members of this class are open (objectui#7440, objectui#7444, objectui#7838). A blocking gate would go red on all of them the day it landed and turn one card into four, so the 2026-09-06 ruling on this card is that the census is the deliverable and the decision it feeds — clean the corpus first, or build a ledger — is not made here.

Same posture as Changeset Overwrite Report, and the same boundary: report-only means the gate declines to fail on its findings, never that it passes without looking. It exits 1 when the instrument is broken — a derivation that matched nothing, a missing spec artifact, a failed control. There is no continue-on-error on the step, because that would swallow exactly the failure that must be loud.

Both halves of the universe are derived, never typed

half source today's answer
carriage map expressionBindableTextKeysFor out of the built @objectstack/spec@17.2.0 dist/ui/index.mjs — the lookup SchemaRenderer.tsx:1139 itself consumes card -> title, description; badge / progress / input / list -> nothing
evaluated channels SchemaRenderer.tsx's own call sites — evaluator.evaluate(newSchema.X), isConfigBag(newSchema.X), evaluate{Visibility,Enablement}Predicate(newSchema.X, …) content; bags properties, props; 8 condition keys

Read from call sites, never from the surrounding prose: that file's comments name visibleOn, disabled and both bags many times over, and a scan of the raw text would be describing its own docblocks. Either derivation matching nothing is exit 1 — a silently narrowed universe turns correct documentation into findings.

The test cross-checks the condition keys against a different source: the two VISIBILITY_SHOW_KEYS / VISIBILITY_HIDE_KEYS array literals the same file declares. One source read twice proves nothing.

Proving it can see

Built-in controls, run before every census, not once in this PR. The positive fixture is guide/expressions.md's Status Badge block exactly as it stands on main — badge carries nothing, so text and variant are both findings. The negative fixture is a card using only carried channels.

Controls: positive fixture reported [text, variant] (expected text, variant);
          negative fixture reported nothing (expected nothing).
✅  Controls pass: this gate can see the class it is looking for, and does not cry wolf.

The pins sabotage each control too, because a control that cannot fail is not a control: widen the carriage map until the defect disappears and the positive control fails; drop visibleOn from the channels and the negative control fails.

On-disk mutation control, on the real corpus. objectui#7418's repair has not merged yet, so the six sites are still on main — the instrument reports them today, which is the positive direction already live. The remaining direction was measured by applying PR #7847's repair to the Status Badge block on disk and watching the census track it. The implementation was committed before the trap was armed.

HEAD blob   = 03d44a90afc4501555ec181153e2e5746256ffb6
before hash = 03d44a90afc4501555ec181153e2e5746256ffb6   (equal: tree at HEAD before the mutation)
anchor "text": "${status}"   before 1  ->  after 0
anchor "label": "Completed"  before 0  ->  after 1
after hash  = a60c2135800e165264e73408c87706990e38fcc0   (changed: the mutation reached disk)

census before   17 sites in 5 pages
census after    15 sites in 5 pages
diff            -  :353  badge.text      -  :354  badge.variant
                only other change: card.className :404 -> :399, the block shrank 5 lines

Restored under trap … EXIT INT TERM with git checkout HEAD -- "$REPO_ROOT/…", and proven by state, not by an exit code: git status --short empty, git diff HEAD empty, git hash-object back to 03d44a90a, the anchor count back to 1.

The parse surface is printed on every run

0 unparsed is what makes "only 17 sites" mean something, so both directions are printed. Reaching 0 over the whole tree takes four tolerances beyond JSON.parse, all of them removals of non-data or an envelope around it — comments, raw newlines inside ${…} strings, trailing commas, and elision markers in the three spellings the corpus uses — plus one retry for a fence that is an object body rather than an object. None can invent a key, and each is pinned, including the case that must not be treated as an elision ("placeholder": "..." is a real value).

The half this does not answer

This is the expression half — is this key evaluated. The wider half — is this key read back by the renderer — is what objectui#7440 and objectui#7444 are, and it needs the per-renderer read-point contract check:doc-types names as its missing piece. Neither is visible here, and objectui#7838's site lives in docs/ARCHITECTURE.md, outside content/docs/** and so outside this census's population. All three are stated in the script's header so a green census is not misread as "the fences are correct".

Wiring, and the obligation that comes with it

One report-only step in the docs job of ci.yml, and — objectui#3653 pins that table by command — the matching sentence in the docs row of content/docs/guide/ci-cd-pipeline.md, in the same change. ci-cd-pipeline-doc.test.ts passes in both directions. The step inherits the job's should_run condition, so on a PR that changes no content/ or apps/site/ file the census does not run; a PR that edits only the gate is covered by the vitest pins instead. Both consequences are written down at the step.

node scripts/check-governed-queue-guard.mjs --test over all four changed paths, verbatim:

✅ NOT GOVERNED — 4 path(s) checked against 5 governed surface(s); none matched.
An ordinary pull request: the normal review and merge-queue route applies.

Verification, re-run after the final commit at 59df1dab2

Exit codes captured by redirecting first (cmd > log 2>&1; EXIT=$?), never through a pipe.

scripts/__tests__ (vitest)   EXIT=0   105 files, 3089 tests passed (28 of them this gate's)
check:doc-types              EXIT=0   Every documented component type is registered.
check:doc-snippets           EXIT=0   Semantic phase: 456 of 456 block(s) judged, 0 failed.
check:doc-fences             EXIT=0   every TypeScript block in 227 document(s) is fenced ts/tsx/typescript
check:docs-route-closure     EXIT=0
check:doc-example-readers    EXIT=0   OK  80 documented symbol(s), 3944 call site(s)
check:control-bytes          EXIT=0   OK (scanned 6369 tracked text file(s))
check:entry-guard            EXIT=0   67 scripts/ file(s); 61 export bindings, 61 inert on import
check-doc-links.mjs          EXIT=0   Links are valid across 17 scan roots.
type-check:scripts           EXIT=0
lint:root (eslint)           EXIT=0   249 files, 0 errors, 33 pre-existing warnings, 0 from the new files
lint:coverage                EXIT=0   46/46 packages linted

check:doc-snippets first returned EXIT=2, which its own output declares is "I could not run, NOT I ran and found errors" — read as NOT MEASURED. Its scoped build (turbo run build $(node scripts/check-doc-snippet-types.mjs --build-filter) --concurrency=2, 34/34 tasks) then took it to a real green.

node scripts/check-changeset-presence.mjs — EXIT=0, verdict verbatim:

✅ No source or published contract of a released package changed in this range, so no changeset is owed.

Acted on: no changeset added, no label touched.

Not run, said plainly: pnpm lint (turbo run lint across all 46 packages) and the repo-wide test suite. The lint:root scan above is the eslint population that contains these files — measured from eslint's own config at 249 files, with both new files present in it and clean — and this diff adds no file to any package, so no package's lint or test population moves. CI runs the farm regardless.

Session, in prose because a markdown-link footer does not survive an edit: https://claude.ai/code/session_01KbJQ1y1J12nZxYzFWhP8Q3


Generated by Claude Code

…ectui#7851)

A json fence in `content/docs/**` can author any key at all on a node and no
gate objects: `check:doc-types` judges the `type` literal and says so in its own
header, `check:doc-snippets` compiles the ts/tsx blocks only. Four cards of that
shape reached main under green gates (objectui#7418, #7440, #7444, #7838).

`scripts/check-doc-expression-carriage.mjs` answers the expression half — is
this key on a channel `SchemaRenderer` evaluates — deriving both halves of the
universe rather than typing them: the carriage map from the built
`@objectstack/spec` artifact, the four evaluated channels from the renderer's
own call sites. It is REPORT-ONLY by ruling (three members of the class are
open) and exits 1 only when the instrument itself is broken.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KbJQ1y1J12nZxYzFWhP8Q3
… under --list

`0 unparsed` is the sentence that makes "only N sites" mean something, so it is
printed on every run rather than only when there is something to report.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KbJQ1y1J12nZxYzFWhP8Q3
…ialect blind spot

A `jsonc` spelling is what three of `guide/record-edit-modes.md`'s teaching
blocks use, and a gate reading `json` alone cannot see that page at all. Both
languages were already scanned; what was missing was the READING that says so.

Every run now prints parsed/unparsed per language, and parses every fence
OUTSIDE that set purely to report whether it would have been a JSON document
holding a typed node — so a missed dialect is a number, not something a human
finds by re-reading a page.

Also states, in the header and in the output, that a hit is a CANDIDATE:
`type` is at least seven vocabularies in this corpus, and one of today's hits
is an ActionSchema whose `condition` the action runner evaluates.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KbJQ1y1J12nZxYzFWhP8Q3
Merged via the queue into main with commit 0744543 Sep 6, 2026
31 checks passed
baozhoutao pushed a commit that referenced this pull request Sep 6, 2026
…s' surface (objectui#7878)

The report-only census landed (PR #7868) pointed at `content/docs` alone, while
its two sibling doc gates had already been widened onto the per-app docs trees
(#6600) and the root pages (#7115). That is the #7115 geometry rebuilt one gate
over: the root `README.md` fell between two gates' surfaces and taught an
unregistered component type four times, for exactly one reason -- nothing read
the file.

The surface is now imported from `check-doc-component-types.mjs` rather than
copied: `APP_DOCS`, `appDocsDirs` and `ROOT_PAGES` are the same objects, so there
is nothing to drift. `DOCS_ROOT` stays spelled here (that gate declares it
`const`, not `export const`) and is pinned against that file's source text.

Report-only is unchanged: exit 0 on findings, exit 1 only when the instrument is
broken. No predicate, exit-code or ci.yml change.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FhBNJcLRZLe8M87VcUgpKr
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Sep 9, 2026
…s' surface (objectui#7878) (objectstack-ai#8105)

The report-only census landed (PR objectstack-ai#7868) pointed at `content/docs` alone, while
its two sibling doc gates had already been widened onto the per-app docs trees
(objectstack-ai#6600) and the root pages (objectstack-ai#7115). That is the objectstack-ai#7115 geometry rebuilt one gate
over: the root `README.md` fell between two gates' surfaces and taught an
unregistered component type four times, for exactly one reason -- nothing read
the file.

The surface is now imported from `check-doc-component-types.mjs` rather than
copied: `APP_DOCS`, `appDocsDirs` and `ROOT_PAGES` are the same objects, so there
is nothing to drift. `DOCS_ROOT` stays spelled here (that gate declares it
`const`, not `export const`) and is pinned against that file's source text.

Report-only is unchanged: exit 0 on findings, exit 1 only when the instrument is
broken. No predicate, exit-code or ci.yml change.


Claude-Session: https://claude.ai/code/session_01FhBNJcLRZLe8M87VcUgpKr

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

1 participant