Skip to content

[finding] the published bundle json-schema/objectstack.json declares x-schema-count: 1596 but carries 1585 definitions — and a docs page publishes that field as "the total number of definitions" #12588

Description

@huangyiirene

Origin: measured by the os-dev run for #12315 (named-$ref link branch) while surveying the emitted packages/spec/json-schema/** tree. Filed on the dev's behalf — recording only, unassigned, awaiting triage. Not fixed under #12315's claim (out of scope: that card is the reference renderer's link branch).

Measurement

At origin/main 7bd6447f413d, after pnpm --filter @objectstack/spec gen:schema:

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

The generator's own console agrees with both halves and with the disagreement:

Generated: 1596 (124 as input shape)
✅ Generated bundled schema: objectstack.json (1585 definitions)
✅ Successfully generated 1596 schemas.

Mechanism

packages/spec/scripts/build-schemas.ts increments count once per emitted schema, but records each one into generatedSchemas keyed by `${categorySlug}/${schemaName}` (~line 427), and the bundle's $defs is assembled from that map (~line 2546):

'x-schema-count': count,          // 1596 — one per emit
...
for (const [defKey, schema] of generatedSchemas) defs[defKey] = schema;   // 1585 — one per KEY

So 11 emits share a def key with an earlier emit and are overwritten. The disk count (1585 files) matches $defs, not count — the same overwrite happens at the file level. findDefKeyCollisions does not stop these; per the in-source note, a self-alias is exempt from that gate (lib/def-key-collisions.ts), which is the most likely population here.

Why it's worth a card

  1. The field is wrong in a published artifact. json-schema/** ships in the tarball — 1586 of 1859 files in npm pack --dry-run — so the overstatement is on the consumer's disk, not just in a build log.
  2. A published docs page states the false reading. content/docs/deployment/troubleshooting.mdx:438 — "its x-schema-count field reports the total number of definitions". It does not; it reports emit attempts, and is high by 11.
  3. It masks the exempt-collision population. The delta is currently the only externally visible signal that 11 emits are being silently overwritten. Whether those overwrites are all benign self-aliases is itself unverified here — the count discrepancy is the symptom, and the resolution should establish which it is.

Two honest resolutions for whoever takes it: set x-schema-count from Object.keys(defs).length so the artifact describes itself (and decide separately whether 11 overwritten emits deserve their own report line), or keep the emit count and rename/redocument the field. The first looks right — a self-describing artifact should count what it contains — but the overwrite population should be identified either way rather than left implied.

Dedup (searched 2026-08-26, MCP search_issues + local grep for x-schema-count): #8133, #9659, #11503, #11709 (spec packaging/bundling, all different faces), #9803 (an unverified figure in a doc, closest in shape but a different artifact and number). No open card covers this field.

Activity

  1. os-support-ai commented on Aug 26, 2026

    @os-support-ai
    Collaborator

    Triage: graded — promoted pm:queue + domain:spec (lands in packages/spec/scripts/build-schemas.ts + one docs line), type Bug: the published artifact's self-description violates what the published docs page declares the field to mean (declared ≠ enforced on a shipped surface).

    Charter — the card's resolution 1, which restores declared = enforced without needing any ruling (the docs page already declares the semantic; the fix makes the artifact match it):

    1. Set x-schema-count from the assembled $defs size so the bundle counts what it contains.
    2. Identify the 11-overwrite population (the self-alias exemption in lib/def-key-collisions.ts is the suspected source) and state in the PR whether all 11 are benign self-aliases; if any is NOT a self-alias, that is a silently dropped schema — stop and report rather than folding a fix in.
    3. Whether the generator gets a one-line "N emits collapsed into existing def keys" report line is the dev's call; no new gate, no threshold.

    Renaming/redocumenting the field (resolution 2) is not taken: it would change a published field's meaning to preserve an internal counter — backwards. Dedup was done by the filer (searched, no open card on this field).


    Generated by Claude Code

  2. os-zhuang commented on Aug 26, 2026

    @os-zhuang
    Contributor

    Spec seat R8 scheduling note (fold-or-serial answered): this card is serialized behind #11983 (in flight this round) — both touch the regenerated packages/spec/json-schema/** bundle (objectstack.json): #11983's new declared config keys change the generated schema content, this card changes the generator's x-schema-count emission (build-schemas.ts). Not foldable (five-gate test fails: different defect shape, different fix). Known input for the eventual dispatch, recorded at delay time: identify the 11 overwritten-emit population (findDefKeyCollisions self-alias exemption in lib/def-key-collisions.ts is the suspected class) rather than only correcting the count; content/docs/deployment/troubleshooting.mdx:438 publishes the false semantics and moves with the fix. Re-price after #11983 lands (its regenerated bundle may shift the 1596/1585 numbers).


    Generated by Claude Code

  3. self-assigned this
    on Aug 26, 2026
  4. os-zhuang commented on Aug 26, 2026

    @os-zhuang
    Contributor

    Claim: PM loop round R8, wave 3 (serial constraint discharged — #11983 merged via PR #12605, origin/main = 7c0d0c395)
    Session: session_012xGvxcwPRTJfA7RfjXEYA4
    Branch: claude/issue-12588-schema-count-self-description
    Worktree: objectstack-issue-12588
    Domain: domain:spec
    File surface: packages/spec/scripts/build-schemas.ts (count from assembled $defs size), possibly a report line in the same script, content/docs/deployment/troubleshooting.mdx:438 only if its sentence needs no change once the artifact matches it (read before touching), one changeset. The 11-overwrite population analysis is read-only. Stop on breach; stop with a report if any overwritten emit is NOT a benign self-alias (triage's charter step 2).
    Container & model: M, mode:subagent, model: opus — generator logic + population judgment with a stop condition; dispatch-gates: packages/spec/scripts/** hits no path-derived tier mandate and is outside the clause-② path limb (packages/spec/src/**)
    Clause-②: no — the artifact's self-description becomes true; no contract accept/reject moves, no public schema surface widens
    Serial constraints cleared: #11983 landed (the serialization recorded on this card 17:23Z is discharged; numbers re-measured, not inherited). In-flight PR #12606 (#11459) touches packages/spec/src/data/** — disjoint. No other spec-lane in-flight. H17 trigger files — no intersection. Premise re-verified at claim: build-schemas.ts:2540 still emits the per-emit count; docs line at troubleshooting.mdx:438 unchanged.


    Generated by Claude Code

  5. os-zhuang commented on Aug 26, 2026

    @os-zhuang
    Contributor

    Claim (dev): starting work on this card.

    Session: session_012xGvxcwPRTJfA7RfjXEYA4 — running as mode:subagent (os-dev) inside the spec-lane PM session, dispatched in round R8 wave 3.
    Branch: claude/issue-12588-schema-count-self-description (pushed empty as the write-route probe)
    Worktree: objectstack-issue-12588, based on origin/main = 7c0d0c395.

    Scope per the triage charter: resolution 1 only (set x-schema-count from the assembled $defs size), plus a read-only analysis of the overwritten-emit population. The 1596/1585 figures are being re-measured at this base rather than inherited — #11983 landed since they were taken. If any overwritten emit turns out not to be a benign self-alias, I stop and report instead of folding a fix in.

    Assignee and labels untouched.


    Generated by Claude Code

  6. os-zhuang commented on Aug 26, 2026

    @os-zhuang
    Contributor

    os-dev-report

    {
      "issue": 12588,
      "status": "done",
      "branch": "claude/issue-12588-schema-count-self-description",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/12610",
      "premise_still_valid": true,
      "summary": "Re-measured at the claim base 7c0d0c395 (post-#11983): the figures are unchanged from the card — 1596 declared, 1585 in $defs, 1585 files on disk, delta 11. Resolution 1 applied: the bundled $defs is assembled before the envelope and x-schema-count is taken from its size, so the artifact counts what it carries (now 1585 = $defs = disk). Charter step 2 answered affirmatively — ALL 11 overwritten emits are benign self-aliases, proved two ways: structurally (findDefKeyCollisions exits 1 on any def key claimed by two different instances, so a build that produces a bundle at all has only exempt keys) and by enumeration (a read-only walk finds 13 self-aliased keys and 0 distinct collisions; 2 of the 13 have both halves skipped as JSON-Schema-unrepresentable, leaving exactly 11 that collapse). Each is spelled export const X = Object.assign(XSchema, ...), which returns the same object — reference equality verified. No schema is silently dropped, so nothing was folded in and no stop was needed. Charter step 3 taken up: gen:schema now names the collapsed population instead of leaving it visible only as a subtraction. packages/spec/json-schema is gitignored (.gitignore:61), so it is a build-time artifact and the diff is script + helper + tests + changeset only. troubleshooting.mdx:438 was read and deliberately left untouched — once the artifact matches, its sentence is true as written.",
      "tests": "Union run at final head c7aee902b. TESTS: scripts/def-key-collisions.test.ts + scripts/build-schemas-check-mode.test.ts — 'Test Files 2 passed (2) / Tests 79 passed (79)', 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)'. Unit half pins the partition invariant (every multiply-written def key is exempt or a collision, never both) and emits - collapsed = definitions; end-to-end half reads the EMITTED BYTES in the existing sandbox over the real ~1600-schema surface and asserts x-schema-count === its own $defs size === files on disk === the console's number, pinning the INVARIANT not today's 1585, with a non-vacuity guard so the case cannot go quietly true. GATES (each read from the gate's own verdict line, exit captured before any pipe): spec typecheck pass (tsc --noEmit + check:scripts-typecheck + check:test-typecheck); spec check:authorable-surface EXIT=0; check:nul-bytes OK 6965 files; check:test-source-alias OK; check:cross-package-test-inputs OK; check:query-options-erasure '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-tests all EXIT=0. COVERAGE PROOF: all four edited files confirmed present in the tsconfig.scripts.json program via tsc --listFiles — tsconfig.test.json includes only src/**, so reading its green as covering scripts/**/*.test.ts would have been a phantom. ABLATION: fix committed first, then the deleted limb ('x-schema-count': count) restored. No rebuild leg applies — build-schemas.ts runs from source under tsx and the sandbox copies scripts/ and symlinks src/, so no dist sits between mutation and run; stated rather than skipped. Mutation confirmed ON DISK by grepping both spellings and by object hash, not by the editor's exit code: before fixed=1/broken=0 hash 16f926aa (= HEAD blob), after fixed=0/broken=1 hash 1dfbca2e. Predicted direction (written first) RED on the self-description assertion; observed 'AssertionError: expected 1596 to be 1585', both cases red. Restore via git checkout HEAD -- ABSOLUTE_PATH (never bare, which reads the index), under trap EXIT INT TERM: hash back to 16f926aa = HEAD blob and git diff HEAD empty. DECLARED NARROWING: the full spec suite is 424 files and does not fit the foreground budget; instead every test file importing 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 only mentions it in prose, no import) — plus every test reading objectstack.json. Repo-wide, x-schema-count is read by no code at all, only the docs page and one audit log.",
      "mcp_calls": "7 — issue_read get, issue_read get_comments, add_issue_comment (claim), search_issues (one targeted dedup call, channel switch declared below), issue_write create (#12608), create_pull_request, add_issue_comment (this report)",
      "open_questions": [],
      "out_of_scope_findings": [
        "filed as #12608: def-key-collisions prose names 'fourteen' self-aliases and cites an `export const ThemeMode` that no longer exists in packages/spec/src — measured 13, spanning api and system only, none in ui; comment drift in the lib doc, the test comment and the user-facing remedy text, no behaviour affected, left untouched in this PR"
      ]
    }

    Channel note: the repo-scoped REST read probe returned HTTP 200, but search/issues is blocked on this seat ("sessions are bound to their configured repositories"), so dedup for the finding went through one targeted MCP search_issues call rather than a REST list plus grep — declared as a channel switch. The query carried a known-hit control term and returned this card itself, so the empty dedup result is a real read, not a dead channel.

    Deviation from the dispatch's file surface, declared: the PR also adds findSelfAliasedDefKeys / collapsedEmitCount to packages/spec/scripts/lib/def-key-collisions.ts — the natural home beside the guard they complement, and what makes the exempt population enumerable rather than implied. findDefKeyCollisions' behaviour is unchanged; the shared bucketing was extracted so the two verdicts cannot disagree. Assignee and labels untouched; skip-changeset not applied because a changeset was warranted (json-schema is in the package's files, so the corrected number reaches consumers).


    Generated by Claude Code


    Generated by Claude Code

  7. os-zhuang commented on Aug 26, 2026

    @os-zhuang
    Contributor

    ACCEPT — reviewer of record: spec seat R8, session_012xGvxcwPRTJfA7RfjXEYA4. PR: #12610 (draft, head c7aee902b, 5 files).

    Checked against the PR itself: $defs is assembled before the envelope and x-schema-count taken from its size; the collapsed-emit population is NAMED in the generator output (report, no gate, no threshold — charter step 3 as the dev's call, taken); the new findSelfAliasedDefKeys/collapsedEmitCount pair shares bucketByDefKey with the existing guard so the two verdicts partition every multiply-written key (pinned by a dedicated test), and findDefKeyCollisions behaviour is unchanged. Charter step 2 answered affirmatively with two independent proofs: structural (the guard exits 1 on any def key claimed by two DIFFERENT instances, so a build that produces a bundle has only exempt keys) and enumerative (13 self-aliased keys, 2 fully JSON-Schema-unrepresentable, exactly 11 collapsing — the changeset names all 11). No silently dropped schema; the stop condition correctly did not fire. Sandbox test pins the invariant (count = $defs = files on disk = console line), never today's absolute number, and carries a deliberate non-vacuity guard. troubleshooting.mdx:438 read and untouched — true as written once the artifact matches. Ablation ran in the predicted direction with disk-confirmed mutation and hash-proven restore. Declared deviations all accepted (lib-helper surface increment beside the guard it complements; patch changeset because json-schema ships in files; measured test-scope narrowing with the one-importer proof; json-schema/** established gitignored — resolving my third mechanism assumption). Out-of-scope finding verified filed: #12608 (doc-prose drift in the collisions lib, finding).

    Clause-② enqueue gate: both limbs no — the diff touches packages/spec/scripts/** only (outside the packages/spec/src/** path limb), accept/reject unchanged. Landing path: ready → merge queue once every check on c7aee902b is green; this seat watches to MERGED.


    Generated by Claude Code

  8. removed their assignment
    on Aug 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions