Skip to content

fix(devx): read NAME.enum.MEMBER so 36 real producers leave the unresolved census - #15736

Merged
baozhoutao merged 1 commit into
mainfrom
claude/issue-15644-error-status-zod-enum-members
Sep 5, 2026
Merged

fix(devx): read NAME.enum.MEMBER so 36 real producers leave the unresolved census#15736
baozhoutao merged 1 commit into
mainfrom
claude/issue-15644-error-status-zod-enum-members

Conversation

@claude

@claude claude Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Fixes #15644

check:error-status-conformance derives the runtime half of the code/status reconciliation out of source, and its header rule is that a declaration it cannot read is REPORTED, never dropped. It was doing exactly that — and 36 of the 50 rows in that census were one mechanical shape: a producer stamping err.code = StandardErrorCode.enum.SOME_CODE beside a status that is already a literal. buildConstantIndex recorded nothing at all for a z.enum([...]) declaration, and lookup() walks at most one dot, so the whole family resolved to undefined.

What changed

  • buildConstantIndex indexes a z.enum([...]) declaration as NAME.enum.MEMBER -> 'MEMBER', one entry per literal — a Zod enum member's value equals its own name by construction, so the array is the value table.
  • lookup() walks one extra segment, for exactly the shape IDENT.enum.MEMBER. Dotted paths in general are still refused: a general walk would resolve members of objects the index never read, which is the guessing this file exists to avoid. A member the array does not list stays absent from the index and its declaration is reported unresolved, never invented.
  • Self-test battery 25 pins the shape in all three directions (member resolves · non-member reported, not invented · .enum. is the only way in). Roster floor 25 -> 26, CASES 47 -> 50, and the green verdict line now names the shape so a run that stopped exercising it cannot print the same sentence.

Before / after — re-measured on today's origin/main

The card's 50/36 were read on the #15633 branch before it landed, so both legs were re-measured here. BEFORE is origin/main 6c0813196 in a detached compare worktree; AFTER is this branch's head 6cc01aa88. pnpm check:error-status-conformance, exit code captured before any pipe.

reading (the gate's own lines) BEFORE 6c0813196 AFTER 6cc01aa88
exit code EXIT=0 EXIT=0
self-test 47 cases pass 50 cases pass
scope: producer sites derived 279 producer site(s) derived 315 producer site(s) derived
scope: codes reconciled 52 code(s) reconciled = 50 StandardErrorCode member(s) + 2 ledger code(s) identical
reconciled: 26 code(s) with a derived producer, 27 (code, status) pair(s) matched identical
unpinned: 26 documented code(s) with no derivable producer (baselined: 26) identical
unresolved: 50 declaration(s) the deriver could not read 14 declaration(s) the deriver could not read
of those, StandardErrorCode.enum.* 36 0
final verdict every derivable runtime status is documented, and every documented status is reachable same

+36 producer sites is exactly the 36 rows that left unresolved — proven set-wise, not inferred from the delta: running buildConstantIndex/deriveRuntimeStatuses from both revisions over the same 2244 sources gives declarations that MOVED out of unresolved: 36, all of them .enum.: true, and LOST pairs: [].

The anticipated baseline shrink is EMPTY — measured, not skipped

The card and the triage both flagged that this moves a ratchet and that removing the nowPinned rows by hand is part of the change. On today's main it moves none, and that is a reading rather than an omission. scripts/error-status-unpinned-baseline.json is untouched — no row deleted, no --update run — for two independently measured reasons:

  1. The 36 sites introduce zero NEW (code, status) pairs. Same probe as above: pairs BEFORE 106, pairs AFTER 106, NEW (code,status) pairs introduced: []. A baseline row can only become nowPinned by gaining a first producer; none does. The gate's own reconciled: and unpinned: lines agree — both unchanged across the change.
  2. The six codes the 36 sites name are disjoint from the baseline. They are VALIDATION_ERROR, INVALID_FIELD, INVALID_FILTER, INVALID_QUERY, NOT_IMPLEMENTED, DATABASE_ERROR (36 sites across driver-sql, driver-mongodb, driver-turso, driver-memory, objectql, formula, service-analytics); intersecting that set with the 26 baseline rows is empty. Each already had a derived producer through another rule, so these sites corroborate pairs that were already reconciled.

The control leg the ruling asked for is this run. Indexer change with the baseline left alone is the delivered state: EXIT=0, zero nowPinned findings printed. Had the shrink set been non-empty, this run would have been the EXIT=1 that names it. So the removed-row table is empty by measurement — there is no row to point a producer site at, and none was deleted "to be safe". #15708 touches this same baseline file; this PR does not touch it at all, so there is nothing to merge there.

Ablation — the .enum. walk is load-bearing

Trap-guarded, absolute paths, mutation and restore both proven on disk. No build leg applies: the gate is a plain .mjs run from source (scripts/, sibling relative imports), nothing resolves through a dist/.

  • Mutation = delete the .enum. alternation from lookup(). Landed on disk: anchor count 1 -> 0, MUT_BLOB=e9be4543… vs HEAD_BLOB=fdd1600a….
  • Ablated: self-test ✗ … 2/50 case(s) failedx self-test: 25 a z.enum member resolves to its own name and x self-test: 25c the member is addressable through .enum. and nowhere else. Real run falls back to 279 producer site(s) / unresolved: 50 with the .enum. subset back at 36. (Case 25b stays green under ablation by construction: it asserts a declaration is reported, which an ablated walk also does. 25 and 25c are the legs that can only pass with the walk present.)
  • Restored: REST_BLOB equals HEAD_BLOB byte for byte, git diff HEAD empty (0 bytes), anchor back at 1, self-test 50 cases pass, real run 315 producer site(s) / unresolved: 14 / .enum. subset 0.

Gates — the derived family on the final head 6cc01aa88

node scripts/pm/dispatch-gates.mjs --changed --commands --repo objectstack-ai/objectstack (change set derived by the tool from git, not hand-fed): 29 commands, all re-run on this head after the rebase, exit code captured before any pipe. All 29 exit 0, including:

  • pnpm check:error-status-conformanceEXIT=0, verdict quoted above.
  • pnpm check:ratchet-remedy-authorityEXIT=0: OK check-ratchet-remedy-authority: 205 scripts swept …; 14 mark the expanding remedy ⛔ MAINTAINER-ONLY, 6 turn it down outright …. The maintainer-only marker on the baseline-EXPANDING remedy is untouched.
  • pnpm check:nul-bytesEXIT=0: check-nul-bytes: OK (scanned 7594 text file(s) …; no raw ASCII control bytes). Plus a direct scan of the edited file for control bytes: no match.
  • pnpm check:pm-dispatch-gatesEXIT=0, 1445 cases pass.

Six roster gates whose roster lives under scripts/ were run beyond the derived family (their silence is not a clearance): check-published-list-mirrors and its self-test, check:console-injection, check:engine-double-contract, check:i18n-stale-fill — all EXIT=0.

NOT MEASURED, by name:

  • pnpm check:published-readme-exportsEXIT=3, check-published-readme-exports: PREREQUISITE NOT MET (45 packages' built type entries absent in this worktree). Its own header states this is neither a pass nor a finding. It is outside the derived family for this change set, and this diff touches no README and no package.
  • Three families in the derived list take a value from the workflow and have no argv outside a CI run: check-cross-package-test-inputs --union-into "$RUNNER_TEMP/…", check-shard-attestation --shard ${{ matrix.shard }}, check-test-completeness "$RUNNER_TEMP/…".
  • The always-runs tail (workflows with no path filter) is CI's, not measured locally.

No changeset: scripts/ publishes nothing, so this carries skip-changeset.

Deliberately out of scope

The other 14 unresolved rows are different shapes and stay reported, per the card's own scope:

  • 7 carry a bare code identifier opaque at the site — code=code status=status, code=code status=422, code=code status=403 (core/artifact-packages.ts:147, mcp/stdio-data-bridge.ts:192, metadata-protocol/protocol.ts:2769, metadata-protocol/sys-metadata-repository.ts:1601, plugin-sharing/share-link-service.ts:353, runtime/artifact-collections.ts:184, service-storage/attachment-access-hooks.ts:91).
  • 6 read both halves off a runtime value — raw.code/res.status, reject.code/reject.status, refusal.code/refusal.status (x2), info.code/info.status, p.code/p.status.
  • 1 is a scanner artifact rather than a producer at all: R6's \s*=\s* matches the FIRST = of ===, so if (typeof e.code === 'string') return e.code; at packages/create-objectstack/src/detect-package-manager.ts:104 is read as an assignment. It is inert today (neither half resolves, so it can only ever be reported, never fabricated into a producer) but it is noise in the census.

None is the mechanical shape this card names, and each plausibly has its own reason to stay unresolvable. Not folded in and no row invented for them; they are handed back in the dev report for the PM to file or drop.

🤖 Generated with Claude Code

https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk


Generated by Claude Code

…esolved census

`check:error-status-conformance` derives the runtime side of the code/status
reconciliation out of source, and REPORTS every declaration it cannot read
rather than dropping it. 36 of the 50 declarations in that census were one
mechanical shape: a producer stamping `err.code = StandardErrorCode.enum.X`
beside a literal status. `buildConstantIndex` recorded nothing at all for a
`z.enum([...])` declaration, and `lookup()` walks at most one dot, so the whole
family resolved to `undefined` — 36 genuine producers, every one of them with
its status already a literal, sat outside the derivation.

Index a `z.enum([...])` declaration as `NAME.enum.MEMBER -> 'MEMBER'`, one entry
per literal (a Zod enum member's value equals its own name by construction), and
let `lookup()` walk that ONE extra segment. A name the array does not list stays
absent from the index and its declaration is reported unresolved, never invented;
dotted paths in general are still refused, deliberately.

Self-test battery 25 pins the shape in all three directions (member resolves,
non-member reported, `.enum.` is the only way in); the roster floor and case
count move with it: 47 cases -> 50, floor 25 -> 26.

Measured on this tree, before -> after:
  producer sites derived   279 -> 315   (+36, exactly the 36 `.enum.` rows)
  unresolved declarations   50 -> 14    (0 of them `.enum.` now)
  reconciled            25 codes / 26 pairs  -> unchanged
  unpinned census         26 (baselined 26) -> unchanged

The baseline shrink the card anticipated is EMPTY on today's main, and that is a
reading rather than an omission: the 36 sites introduce zero NEW (code, status)
pairs. All six codes they name (VALIDATION_ERROR, INVALID_FIELD, INVALID_FILTER,
INVALID_QUERY, NOT_IMPLEMENTED, DATABASE_ERROR) already had a derived producer
via another rule, and none of the six appears in
`scripts/error-status-unpinned-baseline.json`. No row becomes `nowPinned`, so the
baseline is untouched.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk
@claude claude Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 5, 2026
@github-actions github-actions Bot added the size/s label Sep 5, 2026
@claude

claude Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor Author

ACCEPT — PM seat domain:devx @ objectstack (#6023), session session_012zGPuVVX3deAx9LdjK8jCk.

Reviewed against the three-dot diff at 6cc01aa88 (1 file, +85/−8), not the report. Closing keyword: Fixes #15644 only.

What the diff does: buildConstantIndex gains a ZOD_ENUM reader — (export) const NAME = z.enum([ 'A', "B", … ]) — and indexes each literal as NAME.enum.MEMBER → 'MEMBER'; lookup() admits exactly one extra shape, IDENT.enum.MEMBER (a second regex alternative beside the existing one-dot form), so arbitrary dotted paths stay refused and a member the array does not list stays absent from the index (reported, not invented). Self-test battery 25 (3 cases: member resolves; non-member is REPORTED; .enum. is the only way in), roster floor 25→26, CASES 47→50. The baseline is UNTOUCHED — the shrink the card anticipated measured EMPTY on today's main (the 36 sites add 0 new (code,status) pairs; their six codes already had producers by other rules and none is a baseline row), which is the ruling's control leg answered by measurement rather than skipped, and it means zero overlap with #15708.

Measured by this seat on the PR head and on origin/main (plain node, detached worktrees; readings copied from the run output):

PR head:      node scripts/check-error-status-conformance.mjs --self-test   EXIT=0   50 cases pass
              node scripts/check-error-status-conformance.mjs               EXIT=0
                scope: … 2244 source files scanned; 315 producer site(s) derived …
                reconciled: 26 code(s) with a derived producer, 27 (code, status) pair(s) matched against the docs.
                unpinned: 26 documented code(s) with no derivable producer (baselined: 26).
                unresolved: 14 declaration(s) the deriver could not read —      (0 of them `StandardErrorCode.enum.*`)
                ✓ every derivable runtime status is documented, and every documented status is reachable.
origin/main:  279 producer site(s); unresolved: 50 declaration(s) — 36 of them `StandardErrorCode.enum.*`

So the 36 mechanical rows left the census (+36 sites), reconciled/unpinned lines byte-identical, baseline unchanged. The dev's set-wise proof (pairs 106 before and after, NEW [], LOST []) and ablation (drop the .enum. alternation → cases 25/25c red, census back to 50/36; restore blob-proven) match these readings.

Fences held: one file; check-governed-merges.mjs --test0 of 1 path(s) hit the register; no other open PR holds it; git merge-tree origin/main clean; skip-changeset (nothing published); check:ratchet-remedy-authority intact per the dev (205 swept, 14 maintainer-only markers).

Recorded for triage, not filed (the dev's two observations, handed back on the card): the remaining 14 unresolved rows are three other shapes (7 opaque bare code identifiers, 6 runtime-value pairs, 1 R6 precision defect — \s*=\s* matches the first = of === at create-objectstack/src/detect-package-manager.ts:104, inert but noise). Flip + arm once both required jobs read success (Lint & Repo Gates in progress at review time).


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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

Projects

None yet

2 participants