Skip to content

fix(spec): the bundled JSON Schema's x-schema-count counts the definitions it carries (#12588) - #12610

Merged
os-zhuang merged 3 commits into
mainfrom
claude/issue-12588-schema-count-self-description
Aug 26, 2026
Merged

fix(spec): the bundled JSON Schema's x-schema-count counts the definitions it carries (#12588)#12610
os-zhuang merged 3 commits into
mainfrom
claude/issue-12588-schema-count-self-description

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes#12588

json-schema/objectstack.json ships in the npm tarball (json-schema is in the package's files) and content/docs/deployment/troubleshooting.mdx:438 publishes what its x-schema-count field means — "the total number of definitions". It did not report that. The generator took the number from count, a counter incremented once per emitted schema, while the bundle's $defs is assembled from a map keyed by CATEGORY/NAME. Every def key written more than once therefore widened a gap nothing reconciled.

Re-measured on this branch's base (7c0d0c395, which carries #11983's merge — the card's figures were taken at an older ref and are not inherited):

bundle x-schema-count: 1596
bundle $defs actual: 1585
delta: 11
per-schema files on disk (excl. objectstack.json, openapi.json): 1585

The numbers are unchanged from the card. $defs is now assembled before the envelope and x-schema-count is taken from its size, so the artifact describes itself:

x-schema-count : 1585
$defs size : 1585
files on disk : 1585

The disk count already agreed with $defs — the same key collapses the per-schema file writes — so this brings the one disagreeing number into line with the two that already agreed. Emitted key order is unchanged ($defs stays last in the object literal).

The overwritten-emit population — all 11 are benign self-aliases

Triage charter step 2, answered before any code was written. No schema is being silently dropped.

Two independent instruments agree.

1. Structural proof.findDefKeyCollisions runs before the bundle is assembled and process.exit(1)s on any def key claimed by two different schema instances. So a build that produces a bundle at all has only exempt keys by construction — the exemption is identity (bucket.every((e) => e.schema === bucket[0].schema)), not today's byte-equality.

2. Enumeration. A read-only walk of the same namespace exports, bucketed by def key with the guard's own predicate:

zod exports walked : 1620
distinct def keys : 1607
self-aliased def keys : 13
DISTINCT collisions : 0

Reconciling the walk with the real run: 24 exports are skipped as unrepresentable in JSON Schema. Four of those are both halves of system/BatchTask and system/WorkerConfig, so those two keys are never emitted at all; the other 20 each remove a key outright. 1620 - 24 = 1596 emits and 1607 - 22 = 1585 definitions — the arithmetic closes exactly, and 13 - 2 = 11 is the delta.

The 11, now printed by gen:schema itself:

11 emit(s) collapsed into 11 existing def key(s) — all self-aliases (one schema
object reached by two export names), so 1596 emits publish 1585 definitions:
json-schema/api/ApiEndpoint.json <- ApiEndpoint, ApiEndpointSchema
json-schema/api/RestApiConfig.json <- RestApiConfig, RestApiConfigSchema
json-schema/api/RestServerConfig.json <- RestServerConfig, RestServerConfigSchema
json-schema/api/ApiDocumentationConfig.json <- ApiDocumentationConfig, ApiDocumentationConfigSchema
json-schema/api/ApiTestCollection.json <- ApiTestCollection, ApiTestCollectionSchema
json-schema/api/OpenApiSpec.json <- OpenApiSpec, OpenApiSpecSchema
json-schema/api/RestApiPluginConfig.json <- RestApiPluginConfig, RestApiPluginConfigSchema
json-schema/api/RestApiRouteRegistration.json <- RestApiRouteRegistration, RestApiRouteRegistrationSchema
json-schema/system/MiddlewareConfig.json <- MiddlewareConfig, MiddlewareConfigSchema
json-schema/system/QueueConfig.json <- QueueConfig, QueueConfigSchema
json-schema/system/Task.json <- Task, TaskSchema

Each is spelled export const X = Object.assign(XSchema, { … }) in source, which returns the same object — verified by reference equality in the walk above, so the second write provably cannot change what is published. Charter step 3 (the report line) taken up: it names the population rather than only counting it, since the card's third point is that the delta was the only visible trace of these writes.

Docs line: read, deliberately untouched

troubleshooting.mdx:438 says the field "reports the total number of definitions". Once the artifact matches the published semantics that sentence is true as written, so it is not edited. Resolution 2 (rename/redocument the field) was explicitly not taken by triage.

Files

  • packages/spec/scripts/build-schemas.ts — assemble $defs first, count from it; report the exempt population.
  • packages/spec/scripts/lib/def-key-collisions.tsfindSelfAliasedDefKeys and collapsedEmitCount, the complement of the existing guard (same bucketing, same identity predicate, opposite verdict). Declared surface addition beyond the claim: the helper's natural home is beside the guard it complements, and it is what makes the population enumerable instead of implied. findDefKeyCollisions' behaviour is unchanged — the shared bucketing was extracted so the two verdicts cannot disagree about which entries belong to one key.
  • packages/spec/scripts/def-key-collisions.test.ts, packages/spec/scripts/build-schemas-check-mode.test.ts — tests, below.
  • .changeset/schema-count-counts-the-definitions-shipped.md — patch. Not skip-changeset: the diff is scripts-only, but json-schema/ is in the package's files, so a consumer's installed bundle carries a different number. The scripts-only precedent in this repo is mixed and splits on exactly that question.

No schema content changes; packages/spec/json-schema/** is gitignored (.gitignore:61), so it is a build-time artifact and not part of this diff. packages/spec/src/** is untouched — no intersection with in-flight #12606.

Tests

Both halves, following this package's existing convention (a unit pin over the extracted helper plus an end-to-end pin that the caller really routes through it).

Unitscripts/def-key-collisions.test.ts: the two verdicts are complementary; they partition every multiply-written def key; collapsedEmitCount is N-1 per key; and emits - collapsed = definitions, the arithmetic the published field got wrong.

End-to-endscripts/build-schemas-check-mode.test.ts, in the existing sandbox that runs the real generator over the real spec surface (src/ is symlinked, so ~1600 schemas). It reads the emitted bytes, not a helper's return value, and asserts x-schema-count equals its own $defs size, equals the file count on disk, and equals the number the run's own console reports. The invariant is pinned, never today's 1585 — a test that unrelated PRs must edit gets edited without being read. A non-vacuity guard asserts this build still collapses emits, so the case cannot go quietly true.

scripts/def-key-collisions.test.ts + scripts/build-schemas-check-mode.test.ts
Test Files 2 passed (2) Tests 79 passed (79) Duration 334.12s
scripts/json-schema-out-dir · schema-tree-freshness · renamed-defs · sharded-artifacts
+ src/data/field · src/ui/component-reference-rail
Test Files 6 passed (6) Tests 273 passed (273)

Reverse verification. Predicted direction, written before running: RED on the self-description assertion. The fix was committed first, then the deleted limb ('x-schema-count': count) put back. No rebuild step applies — build-schemas.ts runs from source under tsx and the sandbox copies scripts/ and symlinks src/, so no dist/ sits between the mutation and the run. The mutation was confirmed on disk by grepping both spellings and by object hash, not by the editor's exit code:

before: fixed spelling 1 · broken 0 · hash 16f926aa… (= HEAD blob)
after: fixed spelling 0 · broken 1 · hash 1dfbca2e…
result: AssertionError: expected 1596 to be 1585 (both cases RED)
restore: fixed spelling 1 · broken 0 · hash 16f926aa… (= HEAD blob), `git diff HEAD` empty

Restore names HEAD explicitly (a bare git checkout -- path reads the index) and the script carries a trap … EXIT INT TERM with absolute paths.

Gates

Run locally at final head c7aee902b, each result read from the gate's own verdict line with the exit code captured before any pipe:

gateresult
pnpm --filter @objectstack/spec typecheckpass — tsc --noEmit + check:scripts-typecheck + check:test-typecheck
pnpm --filter @objectstack/spec check:authorable-surfacepass (exit 0; the authorable-surface.base.json trailing notice is informational)
pnpm check:nul-bytespass — 6965 files, no raw control bytes
pnpm check:test-source-aliaspass
pnpm check:cross-package-test-inputspass
pnpm check:query-options-erasurepass — ratchet holds, no files added vs 7c0d0c3
check-empty-changeset / check-changeset-no-major / check-adr-0087-registration / check:objectui-changeset / check:changeset-gate-self-testspass

All four edited files were confirmed present in the tsconfig.scripts.json program via tsc --listFiles, so the typecheck green actually covers them — tsconfig.test.json includes only src/**, and reading its green as covering scripts/**/*.test.ts would have been a phantom.

Gate families were re-derived from the real change set with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack rather than from the dispatch list.

Declared narrowing. The full @objectstack/spec suite is 424 test files and does not fit the foreground time budget. Instead: every test file that imports the changed module was run in full (measured — scripts/lib/def-key-collisions.ts has exactly one non-test importer, build-schemas.ts; scripts/lib/zod-graph.ts mentions it in prose only, with no import), plus every test that reads objectstack.json. Repo-wide, x-schema-count is read by no code at all — only the docs page and one audit log — so no consumer can break on the value change. CI runs the farm either way.

Out of scope, filed

#12608 — the same file's prose names "fourteen" self-aliases and a ThemeMode export that no longer exists; measured 13, spanning api and system only. Comment drift, no behaviour affected, left untouched here.


Generated by Claude Code

The bundled objectstack.json took x-schema-count from the per-emit counter
while its $defs is keyed by def key, so every self-aliased key inflated the
published field: 1596 declared, 1585 shipped. Assemble $defs first and count
what the artifact contains.
Also name the exempt population the guard allows through, so the collapsed
emits are reported rather than left implicit in a subtraction.
Unit half: the exempt self-alias population and the emits it absorbs.
End-to-end half: the artifact the generator really writes, cross-checked
against the files on disk and the run's own console.
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs.

What this run could not see
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 0 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 7c0d0c39552b0eb4aa5c324d183f5eb2105e498epackageMentionDocs.

@github-actionsgithub-actionsBot added size/m documentation Improvements or additions to documentation tests tooling labels Aug 26, 2026
@os-zhuang
os-zhuang marked this pull request as ready for review August 26, 2026 19:28
@os-zhuang
os-zhuang added this pull request to the merge queueAug 26, 2026
Merged via the queue into main with commit 146f448Aug 26, 2026
35 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-12588-schema-count-self-description branch August 26, 2026 19:50
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

2 participants

@os-zhuang@claude