Skip to content

feat(objectql): ValidationError publishes its error code as a constant — the last row of #16159 - #16443

Merged
zhuangjianguo merged 1 commit into
mainfrom
claude/issue-16159-validation-failed-error-code-constant
Sep 7, 2026
Merged

feat(objectql): ValidationError publishes its error code as a constant — the last row of #16159#16443
zhuangjianguo merged 1 commit into
mainfrom
claude/issue-16159-validation-failed-error-code-constant

Conversation

@claude

@claude claude Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Closes #16159

ValidationError publishes its ADR-0112 code as an importable constant. This is the eleventh and last row of #16159's census, on the shape the four landed slices set (#16156, #16259, #16283, #16308, #16326): an exported X_CODE = 'LITERAL' as const, the class field reading it as a bare readonly code = X_CODE;, a barrel re-export, and a pin test.

All readings below were taken on this branch at fc1e76583, in a dedicated worktree cut from origin/main ac76425f0.

Why Closes and not Part of — the census, re-run rather than inherited

The card's table has 11 rows and this PR takes the last one. The count was re-derived with the card's own definition (a class extending Error that carries a code), not by trusting the table:

reading on origin/main ac76425f0 result
classes extends Error in packages/objectql/src, non-test 19 (the card counted 18; find-hook-result-shape.ts's FindHookResultNotArrayError has landed since, and it already reads FIND_HOOK_RESULT_NOT_ARRAY_CODE)
of those, carrying a readonly code = class field 17
reading a named exported constant 16
still an inline literal 1validation/record-validator.ts:136, this PR
the 2 classes with no code at all HookConditionError and RecordTitleFieldError, both deliberate and both saying so in their own docblocks
the card's separate "typeof annotation" row nav-contribution-diagnostics.ts:93, a diagnostic interface, and its code already reads the exported NAV_CONTRIBUTION_GROUP_MISSING

Other spellings were checked rather than assumed, because readonly code = is only one of them: this.code = returns 0 in non-test packages/objectql/src; readonly code: returns 1, the interface above; a bare code = class field returns 0; a class extending any error base other than Error returns 0. So there is no twelfth row hiding under a different spelling, and this PR discharges the card.

The wire string did not change

The quoted literal 'VALIDATION_FAILED' occurs exactly once in executable position on both sides of the diff — at base as the class field, at head as the constant's value. It moved; it did not multiply and it did not mutate. (The other occurrences in that file are prose inside docblocks, at base and at head alike.)

It does not answer the question the card fenced off

secret-fields.ts publishes EMPTY_CREDENTIAL_REFUSAL_CODE = 'VALIDATION_ERROR' while this site spells 'VALIDATION_FAILED', and the card says in its own words that "whether they should converge is a question this card does not answer". Publishing the current spelling leaves that decision exactly as open as it was — a convergence is a breaking rename of a registered wire code either way — and a pin test asserts the two are still two, so a future ruling has to argue for itself rather than arrive as a side effect. That open question is filed as #16440, because the card carrying the note is the one this PR discharges. #16440 stays open and is out of scope here.

Clause-② — yes, measured

Ablation over every declaration file files[] publishes (files is ['dist','README.md','CHANGELOG.md'], so all six .d.ts/.d.mts count, not just the two entry points), comparing exported NAME SETS rather than bytes, because a byte diff here is dominated by tsup's internal alias renumbering and the content-hashed chunk filename, neither of which is a surface change:

declaration file base ⇒ head ADDED REMOVED
dist/index.d.ts 257 ⇒ 258 VALIDATION_FAILED_CODE none
dist/index.d.mts 257 ⇒ 258 VALIDATION_FAILED_CODE none
dist/core.d.ts / .d.mts 79 ⇒ 79 none none
shared chunk dist/util-*.d.ts / .d.mts 135 ⇒ 136 bh, an internal alias none
union 393 ⇒ 395 the constant + that alias none

Additive widening, limb 1, nothing removed ⇒ Clause-②: yes and a minor changeset. No **BREAKING** banner is owed: that is the limb-2 narrowing shape, and this narrows nothing.

The member-level reading, which the name set is blind to by construction, and it did move: the emitted class member goes from readonly code = "VALIDATION_FAILED"; to readonly code: "VALIDATION_FAILED";. That is an emit-form change with no type change — proved with a standalone tsc --strict probe that asserts bidirectional assignability between the two forms and carries a firing negative control (a different literal is refused, so the member is not silently string). It is also the emit form every landed sibling already ships: DriverConnectError reads readonly code: "ERR_DRIVER_CONNECT"; in today's dist.

Byte-diff hunks, classified rather than counted: index.d.ts is 2939 lines on both legs and differs on exactly two — line 1 (the content-hashed chunk filename) and line 2 (the export list, gaining b2 as VALIDATION_FAILED_CODE and renumbering the aliases after it). core.d.ts differs only by that filename, which is why its name set is unmoved.

Ablation hygiene: the mutation was proven on disk (git hash-object equal to the base blob and different from the HEAD blob, both files, plus a zero-count anchor on VALIDATION_FAILED_CODE); the rebuild was proven rather than assumed (dist/index.d.ts mtime 17887434961788743598, chunk util-Dw5ZTIIIutil-CTOUTmfv); the restore is byte-exact (git diff HEAD empty, both blobs equal to their HEAD blobs) and the restore-leg rebuild reproduced the head dist sha-for-sha, so no mutated artifact survived into any later measurement.

The docblock-ordering trap, proved in the emitted output

Two consecutive top-level JSDoc blocks both attach to the declaration that follows, which is what stranded three class docblocks in #16308. The constant and its docblock therefore sit above the class. Verified in the built dist/util-Dw5ZTIII.d.ts with the package's own tsup and no turbo cache on the path: the docblock lands on declare const VALIDATION_FAILED_CODE, and declare class ValidationError follows carrying exactly the documentation it had at base — which is none, since this class has never had a docblock of its own. Nothing was stranded and nothing was invented.

The two gates are inverted, and this row lands on the provenance side

VALIDATION_FAILED is registered in ERROR_CODE_LEDGER under @objectstack/objectql (packages/spec/src/api/error-code-ledger.zod.ts, the last entry of that owner's block), so:

  • check:error-code-provenance is non-neutral and moved by exactly one: 317 ⇒ 318 stamp sites, 301 ⇒ 302 listed, waivers unchanged at 16, exit 0 on both legs. It accepts the new constdef site because the code is listed under this package's own owner key.
  • check:dispatcher-error-vocabulary is neutral by construction: that table records UNREGISTERED sites, so a registered code is invisible to it. VALIDATION_FAILED returns 0 in packages/runtime/src/dispatcher-error-vocabulary.ts against a firing control of DUPLICATE_ARTIFACT_OBJECT_NAME = 2 in the same file. The gate reads OK — 66 unregistered code-stamping site(s), all classified, unchanged. No cross-package edit is owed.

Consumer-side census — measured, and nothing outside packages/objectql is rewired

'VALIDATION_FAILED' is re-authored as an inline literal at 148 non-test sites across 33 files. The headline number is not the interesting part; the classification is, and it cuts the other way from what a raw count suggests:

  • Independent producers, not consumers — the large majority. @objectstack/rest's own response bodies and its VALIDATION_FAILED: … message-prefix convention (31 sites in rest-server.ts alone), plugin-approvals' identical message-prefix convention (26 sites, plus one site that consumes its own prefix), plugin-sharing's SharingCriteriaValidationError — declared locally on purpose, its docblock saying "so a security guard in a plugin never depends on another package's build output at runtime" — and @objectstack/metadata-protocol's own class, whose docblock calls the code "this package's own house code". Folding any of these in would be a cross-package coupling this card never asked for.
  • Genuine recognizers of this class — four, and these are the sites the export exists for: packages/types/src/validation-failure.ts and packages/rest/src/error-response.ts each test code === 'VALIDATION_FAILED' || name === 'ValidationError', error-response.ts tests the wire body's code a second time, and packages/plugins/plugin-auth/src/objectql-adapter.ts does the same to map an engine refusal onto a better-auth APIError. Each holds its own copy of the string today.
  • packages/client/src/index.ts is prose only: all seven occurrences are JSDoc and comments, including the documented branch if (err.code === 'VALIDATION_FAILED'), and the client reads the HTTP envelope rather than this class.

No consumer is rewired here. The card's scope is the producer-side importable constant.

./core asymmetry, recorded not decided

ValidationError is published from the lean ./core entry as well as the batteries barrel; its constant, like every existing *_CODE in this package, goes on the barrel only (core.ts contains zero _CODE exports, and dist/core.d.ts's name set is 79 on both legs). That is batch-i's ruling A, applied unchanged, and it adds one more instance to the population #16260 owns — a question this PR deliberately leaves where it is. #16260 remains open and is not addressed here.

Verification

Gate results are quoted from each gate's own verdict line; every exit code was captured by redirect-then-read, never through a pipe.

  • Testspnpm --filter @objectstack/objectql exec vitest run --maxWorkers=2 over the 19 objectql test files naming ValidationError or VALIDATION_FAILED: Test Files 19 passed (19), Tests 503 passed (503), exit 0. Declared narrowing: the package has far more test files; CI runs the farm.
  • Typecheckpnpm --filter @objectstack/objectql typecheck exit 0, ratchet held at 44 file(s) / 242 error(s) / 69 pinned signature(s). Not a bare exit 0: inclusion was proved with tsc --listFiles. The main program is 545 files and contains record-validator.ts and index.ts but not the new test file (so its green says nothing about the test); the tsconfig.test.json program is 1185 files and contains all three, including the new pin test. Absent-file control: 0 hits in both.
  • Repo-wide lintpnpm lint (eslint . --no-inline-config) exit 0, run whole. No narrowing claimed.
  • Gate families — derived from the diff with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack (script blob verified identical to origin/main's, so not a stale checker; it reports 58 runnable families). Run locally and green: check:nul-bytes (scanned 8080 text file(s) … no raw ASCII control bytes), check:error-code-casing, check:dispatcher-error-vocabulary, check:error-code-provenance, check:system-context-census (105 elevation read sites … all anchored), check:doc-authoring, check:published-files, check:engine-double-contract, check:where-matcher, check:objectql-double-limit, check:test-source-alias, check:type-source-resolution, check:cross-package-test-inputs, check:error-status-conformance, check:type-check-coverage, check:objectui-changeset, check:changeset-gate-self-tests, check:dts-closure (population checked, not quoted blind: 15 built package(s) swept, objectql among them), check-changeset-no-major, check-adr-0087-registration, check-empty-changeset, check-changeset-fixed.
  • Line-number anchor family — a diff that changes a file's line count can falsify a path:NNN anchor even when it moves nothing else. check:system-context-census is green; independently, 0 path:NNN anchors reference either edited file from outside packages/objectql/src, against a firing control of 14 hits for registry.ts/engine.ts on the identical grep shape. Repo-wide there are 11 prose references of the form record-validator.ts:NNN, and every one of them was already stale on origin/main before this diff — checked line by line: the text cited at :471, verbatim free-form (tags without options), lives at :790 on origin/main, and allowed.length > 0 cited at :205/:212 lives at :772. This diff shifts already-wrong anchors; it falsifies none that were true. Filed as [finding] a pinned NON-rule in packages/spec justifies itself with a record-validator.ts line number that is 319 lines off, and nothing checks prose line anchors #16441 rather than repaired here — that card is out of scope for this PR and remains open.
  • NOT MEASURED, stated so it is not mistaken for a passcheck:dual-build-cjs-loads exits 3 with Run pnpm build first. This is NOT a pass: nothing was measured, because 47 sibling packages have no dist in this worktree; it is CI's Build Core job that has the built closure. check:single-claim-paths exits 2 with NOT WIRED — PR_NUMBER is not set … This is a wiring or usage failure, NOT a verdict; it is re-run against this PR number below. check-changeset-no-major's LEVEL AXIS reports NOT MEASURED locally for the same reason — no pull_request payload — and its no major bump limb is green.

Generated by Claude Code

The last row of #16159's eleven-row census. `readonly code =
'VALIDATION_FAILED'` becomes `readonly code = VALIDATION_FAILED_CODE`,
with the exported constant holding text byte-identical to the literal it
replaces and re-exported from the `index.ts` barrel beside the class.

The docs already teach `catch it by code, not instanceof` — following that
for record validation meant re-spelling the wire string in the consumer's
own package, which acquires a `check:error-code-provenance` stamp site
there and can drift from what the engine throws with no compile error.

⛔ Does NOT converge `VALIDATION_FAILED` with the sibling
`EMPTY_CREDENTIAL_REFUSAL_CODE = 'VALIDATION_ERROR'`; #16159 leaves that
question unruled and a pin test asserts the two are still two.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Sep 7, 2026
@github-actions

github-actions Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/objectql, touching 4 documentable anchor(s). ⚠️ 1 changed file(s) yielded no anchor (packages/objectql/src/index.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

25 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json c14ebfeb29d8d3bc0ae9b148132a78b86dec94bc.

1 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/objectql/src/index.ts) — pages documenting those are invisible to this run
  • 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 — 16 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 c14ebfeb29d8d3bc0ae9b148132a78b86dec94bcpackageMentionDocs.

Which tree this was computed on

This run read content/docs from cd705bf829b205195c71d53855edc14175527706 — the merge of head fc1e765838dfcac13b81db736b514f31646f656b into base c14ebfeb29d8d3bc0ae9b148132a78b86dec94bc, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin cd705bf829b205195c71d53855edc14175527706 && git checkout cd705bf829b205195c71d53855edc14175527706
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin c14ebfeb29d8d3bc0ae9b148132a78b86dec94bc fc1e765838dfcac13b81db736b514f31646f656b && git checkout -B drift-repro c14ebfeb29d8d3bc0ae9b148132a78b86dec94bc && git merge --no-ff fc1e765838dfcac13b81db736b514f31646f656b

node scripts/docs-audit/affected-docs.mjs --json c14ebfeb29d8d3bc0ae9b148132a78b86dec94bc

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs c14ebfeb29d8d3bc0ae9b148132a78b86dec94bc → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@claude

claude Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor Author

Release authorised — all three pre-landing conditions met at fc1e765838df. ⚠️ The un-draft itself is blocked on a rate limit, not on anything about this PR.

domain:engine dispatching seat, session session_01ARYe3yQTQCUFm5qPYNgKaJ, 2026-09-07T02:0xZ.

The three conditions, each re-measured by this seat rather than relayed

# condition reading
contract-review PASS bound to the head that will land PASS at fc1e765838dfcac13b81db736b514f31646f656b; head re-read after the verdict and unmoved
check-clause2-carriers --pair 16443 REAL_EXIT=0 (redirect-then-read, ⛔ never through a pipe) — "the clause-② declaration is readable in the fixed spelling and both carriers agree". Checker blob 751b4a6e4 == origin/main's, so ⛔ not a stale checker
completed == total AND failing == 0 37/37, 0 failing, mergeable_state=clean — ⛔ not a required-checks subset

needs:contract-review cleared from both carriers with a comparative read-back proving before − label exactly, nothing stripped: PR #16443 [documentation, size/m, tests, tooling], card #16159 [enhancement, pm:dispatched, finding, domain:engine, priority:p3].

⭐ What the review actually established, since a PASS is only worth its measurements

The dispatching seat expected Clause-②: no ("nothing changes except by ADDITION"). That expectation was wrong, the round falsified it, and the review confirmed the round:

The one item that could genuinely have bitten, checked with an independent probe rather than accepted: the emitted member goes readonly code = "VALIDATION_FAILED";readonly code: "VALIDATION_FAILED";. That is an emit form change with no type change — proven by a tsc --noEmit --strict probe with a firing negative control, plus a deliberately-widened code: string copy of the same probe that fails, so the probe can see the failure mode it guards. Had the member widened to string, a consumer's discriminated-union narrowing on err.code would have broken silently.

⚠️ Why it is still a draft

update_pull_request draft:false is refused with "API rate limit already exceeded for user ID 19182527". That budget is user-scoped and disjoint from the REST budget, which reads core 15000/15000 at this moment — so this is not a repository or permissions fact, and ⛔ REST PATCH {draft:false} is not a substitute: re-measured on this very PR just now, it returns 200 and changes nothing (draft=true before, draft=true after).

⇒ Nothing about this PR is holding it. The un-draft and auto-merge will be re-attempted on the next check-in.


Generated by Claude Code

@zhuangjianguo
zhuangjianguo marked this pull request as ready for review September 7, 2026 04:57
@zhuangjianguo
zhuangjianguo added this pull request to the merge queue Sep 7, 2026
Merged via the queue into main with commit 6d7d740 Sep 7, 2026
42 checks passed
@zhuangjianguo
zhuangjianguo deleted the claude/issue-16159-validation-failed-error-code-constant branch September 7, 2026 05:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

2 participants