Skip to content

fix(spec): the two remaining shard readers name the file, entry and anchor for a non-string entry (#7076) - #7191

Merged
os-help merged 2 commits into
mainfrom
claude/issue-7076-shard-reader-entry-types
Aug 10, 2026
Merged

fix(spec): the two remaining shard readers name the file, entry and anchor for a non-string entry (#7076)#7191
os-help merged 2 commits into
mainfrom
claude/issue-7076-shard-reader-entry-types

Conversation

@os-help

@os-help os-help commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator

Fixes #7076

aggregateCategoryShards was fixed by #6751 (PR #7075). The same file holds two
more readers that cast a parsed JSON array to string[] after validating only
the container, and neither goes through categoryOfDefKey — so that fix could
not reach them. This adds the missing per-entry check at both, reusing #6751's
message skeleton.

The two sites

aggregateApiSurfaceShardsapi-surface/ is the fourth sharded artifact.
ApiSurfaceShard.exports is declared string[], but readShards gets there by
a JSON.parse cast, so the declaration is a claim about the file rather than a
verified fact. A hand-edited non-string row travelled into the returned
Record< string, string[] > and landed in the breadth diff in
build-api-surface.ts: present in the snapshot, absent from the built surface,
therefore counted as a removed export, ending the run on "a REMOVED export …
is a BREAKING change for third parties — bump @objectstack/spec to a new major".
Red, but it sends the author after an export that never existed.

readShardedKeysAtRev (both the sharded and the legacy branch) — the
historical baseline reader. This one is the reason the card exists: the bad entry
was forwarded into the baseline SET, and the three gates that consume that set
fail three different ways, none of which can name the file the value came from.

Measured: the card's stated consequence is one of three, not the only one

The issue predicts "the deletion gate counts it as a vanished key and demands a
tombstone". Verified against build-schemas.ts, that is right for one consumer
and not the one it reads like. All three were measured:

consumer what a non-string baseline entry does
authorable-surface deletion gate (keys) maps every base entry through entry.replace(RETIRED_MARK, '') and dies on replace is not a function — the same bare-JS-error shape #6751 removed one function up, not a tombstone demand
json-schema.manifest removal check (schemas) the entry is in neither generatedKeys nor RENAMED_DEFS, so it is reported as a schema that left the published set and a RETIRED_DEFS_BY_MAJOR registration is demanded for a def that never existed — this is the card's framing, exactly
compareAnchorKeys (authorable-surface.base.json) reported as a line the committed anchor is missing, i.e. blames the anchor for not mirroring a baseline it mirrors correctly

So the card's framing holds, on the schemas path; the keys path fails earlier
and even more bluntly. Both are loud, neither is a bypass, and all three point at
the wrong thing — which is what makes the reader the right place to say it. The
table is recorded in a comment at the site rather than only here.

The message skeleton, and where it had to differ

One shared helper, nonStringEntryDetail, builds the middle of the message — the
field, the entry index, the JSON type found instead and the offending value, e.g.
keys[1] is a number, not a string (#5837): 12345 — and each reader adds its own
source locator and its own remedy, because those two genuinely differ:

  • aggregateApiSurfaceShards throws, prefixes the shard file
    (api-surface/data.json), and closes with "regenerate rather than reconcile by
    hand";
  • readShardedKeysAtRev returns { error } — its callers in
    build-schemas.ts print that string under the gate's own name and exit, so a
    throw from here would escape that framing and print a baseline problem as a
    problem with the commit under test. It prefixes the path and the revision
    (authorable-surface/ui.json at 3507e516f8ab) and offers no "regenerate"
    advice, because the legacy branch reads an already-merged commit and nobody can
    regenerate history.

So: same diagnosis, two carriers. That was the open question the card flagged and
it did need a judgement rather than a copy-paste.

aggregateCategoryShards is deliberately not refactored onto the helper. It
spells the same middle inline; rewriting a landed, pin-tested message to route it
through a new function would churn a contract for no reader's benefit, and the
region is outside this card. The pin tests assert the shared substring on all
three sites, so a future divergence goes red instead of being trusted.

Scope

Diagnostic quality only. No gate changes its verdict, its exit code or its
timing on any input that was previously accepted — the new refusals fire only on
values the generators never write. Nothing was made quieter.

Tests

Three cases, placed beside #6751's coverage in
packages/spec/scripts/sharded-artifacts.test.ts rather than in a new file,
because what has to stay true is that these readers say the same thing and
that is only reviewable when the assertions sit next to each other.

Reverse verification — the fix removed with git checkout origin/main -- on the
source file only, tests kept — went red in the predicted direction, and the
direction is not the usual one. None of the three failed by throwing a worse
error; all three failed by returning the corrupt value:

× names the shard file, the entry index and the anchor for a non-string export row
  AssertionError: expected the shard reader to reject, but it returned a value

× reports a non-string entry as an error naming the file, the entry and the anchor
  AssertionError: refuses rather than forwarding the bad entry:
    expected { entries: [ 12345, ...(5) ], ...(1) } to have property "error"

× reports a non-string entry in the retired single-file layout the same way
  AssertionError: expected { entries: [ ...(3) ], layout: 'legacy' } to have property "error"

entries: [ 12345, … ] is the card's premise made visible: the number reaching
the baseline set. The other 26 cases in the file — #6751's included — stayed
green throughout, so this adds a verdict without moving an existing one.

Gates run locally, all green: check:api-surface, check:export-origins,
check:generated (11/11 up to date), pnpm --filter @objectstack/spec typecheck,
and the package suite (359 files / 9385 tests).

No changeset: build-tooling diagnostics with no user-visible surface, same
disposition as the sibling 4e6ca32 (#6751 / PR #7075), which also shipped
without one. skip-changeset applied.


Generated by Claude Code

@vercel

vercel Bot commented Aug 10, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 10, 2026 2:16am

Request Review

@os-help os-help added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 10, 2026 — with Claude
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

No hand-written docs reference the 0 changed package(s). ✅

@os-help
os-help marked this pull request as ready for review August 10, 2026 02:36
@os-help
os-help added this pull request to the merge queue Aug 10, 2026
Merged via the queue into main with commit f1ab039 Aug 10, 2026
30 checks passed
@os-help
os-help deleted the claude/issue-7076-shard-reader-entry-types branch August 10, 2026 02:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate tests tooling

Projects

None yet

2 participants