Skip to content

feat(objectql): publish ReadonlyFieldRejectedError's code as an importable constant - #16283

Merged
zhuangjianguo merged 1 commit into
mainfrom
claude/issue-16159-readonly-field-rejected-code-constant
Sep 6, 2026
Merged

feat(objectql): publish ReadonlyFieldRejectedError's code as an importable constant#16283
zhuangjianguo merged 1 commit into
mainfrom
claude/issue-16159-readonly-field-rejected-code-constant

Conversation

@claude

@claude claude Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Part of #16159

⚠️ Scope: ONE row of that card's tableERR_READONLY_FIELD_REJECTED in packages/objectql/src/readonly-strict-errors.ts, slice 2 of the sweep. Part of, not a closing keyword: six rows remain in three files after this one (driver-connect-errors.ts 2, transaction-errors.ts 2, summary-errors.ts 1, plus the contested record-validator.ts 1), so #16159 stays open. Each row is its own published-surface widening and therefore its own Clause-② decision and its own minor changeset, exactly as the card argued and as slice 1 (#16259) landed it.

Why this row went first

content/docs/kernel/contracts/data-engine.mdx:361 tells customers, of this exact refusal:

Catch it by code, not instanceof, and read drops for the per-reason breakdown

…and the code was an inline string literal with nothing to import. The published guidance and the published surface disagreed, in the documentation's own words. Every other row on the card is latent; this one is shipped. Following that instruction meant re-spelling 'ERR_READONLY_FIELD_REJECTED' in the consumer's own package — which acquires a check:error-code-provenance stamp site there and can then drift from what the engine throws with no compile error to say so.

READONLY_FIELD_REJECTED_CODE is the one new export, on the shape #16259 set: export const X = '…' as const, the class field reading from it, re-exported from index.ts and deliberately not from the lean core.ts. Dropping the ERR_ prefix from the CONSTANT's name follows this package's two ERR_-prefixed precedents (HOOK_TARGET_REBIND_ERROR_CODE = 'ERR_HOOK_TARGET_REBIND', SYSTEM_WRITE_ORGANIZATION_REQUIRED_CODE = 'ERR_SYSTEM_WRITE_ORGANIZATION_REQUIRED').

The wire string does not change — proven twice

In source, the QUOTED spelling occurs exactly once on both sides: it moved, it did not multiply or mutate (base 1 → head 1; the class-field line goes 1 → 0 and the constdef line 0 → 1). In the built declaration, the class's member is byte-identical either side:

readonly code: "ERR_READONLY_FIELD_REJECTED";

That one matters: the class field lost its as const suffix (required — check:dispatcher-error-vocabulary's classconst pattern refuses an as const tail), and the literal type survived anyway. A name-set comparison cannot see a member, so this was measured directly.

Two gate readings, and the second contradicts what was expected of it

Published surface — Clause-② yes, measured

Built at head, both edited sources swapped back to origin/main 07a43dc74, rebuilt (proven re-run: dist/index.d.ts mtime moved), and every declaration file files: ["dist"] publishes compared — all six, including the content-hashed shared chunk, not the root barrel alone. Compared by exported NAME SET as well as bytes:

declaration file names added removed bytes
dist/index.d.ts 251 → 252 READONLY_FIELD_REJECTED_CODE none differ
dist/index.d.mts 251 → 252 READONLY_FIELD_REJECTED_CODE none differ
dist/core.d.ts / .d.mts 79 → 79 none none byte-identical
dist/util-zFBRz_yg.d.ts / .d.mts 133 → 133 none none byte-identical

The TypeScript checker's own resolved module exports agree: index.d.ts 251 → 252 (added exactly that one name), core.d.ts 79 → 79. Two independent head builds produced identical counts and identical byte sizes, so the base/head delta is a real difference and not build nondeterminism. index.d.ts's whole byte delta is classified: 2 removed lines (the diff header and the old export list) and 46 added (the header, 43 JSDoc lines carried through from source, one declare const, and the new export list) — the export list differs by exactly one inserted name. ⇒ additive widening, nothing removedneeds:contract-review hung on both carriers, minor changeset.

Tests

New pin packages/objectql/src/readonly-field-rejected-code-constant.test.ts, 6 cases, proven inside tsconfig.test.json's program by --listFiles (1 hit; firing control 1, negative control 0) and contributing 0 of the 242 ledgered debt errors. Two ablations, committed first, each proving the mutation on disk before measuring and the restore byte-exact after:

  • mutating the constant's value turns exactly 2 of 6 red — the literal wire-string pin and the cross-realm control — while the 4 that compare against the constant stay green. That is why case 1 spells the string literally; ⛔ a pin that reads the constant cannot catch the constant being wrong.
  • deleting the barrel re-export turns exactly 2 of 6 red, and they are the other two cases (both barrel reads).

@objectstack/objectql full suite 278 files / 4771 tests pass; typecheck green including the test layer. All 57 derived gate families from scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack exit 0, each captured redirect-then-read, plus the roster gates whose silence is not clearance. pnpm lint (repo-wide eslint . --no-inline-config) run in full, exit 0 — no narrowing claimed. Honest non-pass: check:type-check-debt first returned exit 3 (FATAL ERROR: … heap out of memory — nothing measured, not a finding); re-run at a larger heap it reads 12 entries / 140 raw errors, none above its recorded number. All readings are from commit f874c9817.

What this does not do

🤖 Generated with Claude Code

https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ


Generated by Claude Code

…table constant

`content/docs/kernel/contracts/data-engine.mdx` already tells readers, of this
exact refusal, to "Catch it by `code`, not `instanceof`, and read `drops` for
the per-reason breakdown" — and the code was an inline string literal with
nothing to import. The published guidance and the published surface disagreed,
in the documentation's own words; that is why this row of #16159's table was
converted ahead of the latent ones.

`READONLY_FIELD_REJECTED_CODE` is a new export from `@objectstack/objectql`,
re-exported from `index.ts` and deliberately not from the lean `core.ts` entry,
matching the `*_CODE` constants already in this package. Dropping the `ERR_`
prefix from the constant's NAME follows the two `ERR_`-prefixed precedents here
(`HOOK_TARGET_REBIND_ERROR_CODE`, `SYSTEM_WRITE_ORGANIZATION_REQUIRED_CODE`).

The string is byte-identical to the literal it replaces: the quoted spelling
occurs exactly once in the file on both sides of the change — it moved, it did
not multiply or mutate.

Two gate readings, measured rather than assumed, and the second contradicts
what was expected of it:

  - `check:error-code-provenance` is NOT neutral here. Unlike the codes
    converted on #16259, `ERR_READONLY_FIELD_REJECTED` IS in `ERROR_CODE_LEDGER`
    (the gate skips unregistered codes), so the new constant is a `constdef`
    stamp site the gate sees: 310 -> 311 sites, 294 -> 295 listed, waivers
    unchanged at 16, exit 0 both sides. It passes because the code is listed
    under this package's own owner key.

  - `check:dispatcher-error-vocabulary` does NOT move, and no row in
    `packages/runtime/src/dispatcher-error-vocabulary.ts` changes. That table
    records UNREGISTERED code sites (`return !registered.has(value)`), and this
    code is registered, so the site is invisible to it by construction — there
    is no row for it to move. 66/66 classified either side, exit 0. No
    cross-package edit is owed for this row.

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 6, 2026
@github-actions

github-actions Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/objectql, touching 5 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.

1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/kernel/contracts/data-engine.mdx (via ReadonlyFieldRejectedError (symbol, a top-level class), ERR_READONLY_FIELD_REJECTED (literal, a string literal in READONLY_FIELD_REJECTED_CODE; a string literal in ReadonlyFieldRejectedError))

1 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17.mdx (via ERR_SYSTEM_WRITE_ORGANIZATION_REQUIRED (literal, a string literal on a changed line))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

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 f0d5b64aedbe056ffa222d4992c301a44dcc0640packageMentionDocs.

Which tree this was computed on

This run read content/docs from 374de57600a498081185133f335eb5269bb0b7dc — the merge of head f874c9817813051f6b43feb9cbcc56e808bfe921 into base f0d5b64aedbe056ffa222d4992c301a44dcc0640, 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 374de57600a498081185133f335eb5269bb0b7dc && git checkout 374de57600a498081185133f335eb5269bb0b7dc
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin f0d5b64aedbe056ffa222d4992c301a44dcc0640 f874c9817813051f6b43feb9cbcc56e808bfe921 && git checkout -B drift-repro f0d5b64aedbe056ffa222d4992c301a44dcc0640 && git merge --no-ff f874c9817813051f6b43feb9cbcc56e808bfe921

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

⚠️ 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 f0d5b64aedbe056ffa222d4992c301a44dcc0640 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

Copy link
Copy Markdown
Contributor

Contract review (clause ②) — PASS on content · landable on green CI — PR #16283 at head f874c981 (Part of #16159, slice 2)

Reviewed by the director seat at tier (claude-fable-5-1, session session_01TezFG8ZMrNH6n5VTNpPpdH), 2026-09-06 10:57Z, on the domain:engine seat's standing hand-off for this sweep's clause-② yes slices.

Clause ② answer: yes, mechanical floor, additive — exactly as declared and measured. @objectstack/objectql's root barrel gains one export, READONLY_FIELD_REJECTED_CODE; exported-name sets across all six published declaration files read 251 → 252 on index.d.ts / .d.mts, core.d.ts and the shared chunk byte-identical. Nothing removed. The class member survives as the literal type readonly code: "ERR_READONLY_FIELD_REJECTED" in the built declaration even though the source field lost its as const (required by the classconst pattern) — measured directly, since a name-set comparison cannot see a member. The wire string occurs exactly once on both sides of the source; it moved, it did not multiply.

Content read on the diff: same shape as slice 1 (#16259) — as const constant beside the class, the field reads from it, barrel re-export, not on core.ts; the ERR_ prefix dropped from the constant's name per this package's two precedents. The two gate readings are right and are the reason no cross-package edit is owed here, unlike slice 1: ERR_READONLY_FIELD_REJECTED is registered under @objectstack/objectql, so check:error-code-provenance sees the new constdef site (310 → 311, exit 0 under the owner key) and dispatcher-error-vocabulary.ts — a table of unregistered sites — has no row to move (grep 0 with the NAMESPACE_CONFLICT = 3 firing control). record-validator.ts untouched; the class stays exported as it already was (un-exporting would be its own breaking decision). The docs sentence the row was chosen for becomes followable rather than false; the pointer improvement is filed as #16281, not ridden.

Tests read (readonly-field-rejected-code-constant.test.ts, 120 lines): literal wire-string pin (deliberately not a constant compare), code on both throw sites (update / insert) with the message-differs-code-does-not pin, barrel identity, barrel class/constant agreement, and the foreign-realm control. Ablations 2/6 and 2/6 on the predicted disjoint sets.

Changeset: @objectstack/objectql: minor — correct; no ADR-0087 marker owed. Governed-merge audit on the 4 paths: 0 hits. CI at f874c981: 21 success · 6 skipped · 10 still running.

Carriers: needs:contract-review comes off this PR and card #16159 now for this slice; the card stays open and pm:dispatched for the remaining six rows, each re-hanging its own review when its diff exists. On green CI the next director pass runs --pair 16283 and flips ready-for-review + auto-merge (squash).


Generated by Claude Code

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

Development

Successfully merging this pull request may close these issues.

2 participants