Filed by the domain:engine execution seat while landing slice 2 of #16159 (ERR_READONLY_FIELD_REJECTED). ⛔ Observation, not a runtime defect — no assignee, no pm:queue; triage grades it. Deliberately not ridden on that PR: the dispatch fenced the docs page out, and the reason it gives is right — the sentence in question becomes followable, not false.
Blocked-by: #16159
The observation
content/docs/kernel/contracts/data-engine.mdx:361 tells readers, of ReadonlyFieldRejectedError:
Catch it by code, not instanceof, and read drops for the per-reason breakdown; the code is stable across reasons deliberately, so adding a reason never adds an error code.
That instruction was already true and is now followable: @objectstack/objectql publishes READONLY_FIELD_REJECTED_CODE as of #16159's slice 2. The page still does not say so, so a reader following its advice writes the string themselves — which is the exact cost #16159 exists to remove, and which acquires a check:error-code-provenance stamp site in the reader's own package.
The same shape will apply to every remaining row of #16159's table as the sweep lands: ERR_DRIVER_CONNECT, ERR_DATASOURCE_UNAVAILABLE, ERR_SUMMARY_RECOMPUTE, ERR_TRANSACTION_UNSUPPORTED, ERR_CROSS_DATASOURCE_TRANSACTION_WRITE. ⇒ this is deliberately one card for the whole sweep rather than one per slice, so it does not need re-filing five more times.
What was measured, on origin/main at 07a43dc74 plus slice 2's branch
| reading |
value |
control |
files in content/docs naming ERR_READONLY_FIELD_REJECTED |
3 (kernel/contracts/data-engine.mdx, references/api/contract.mdx, references/api/error-code-ledger.mdx) |
— |
files in content/docs naming any *_CODE constant of this package |
0 |
firing control @objectstack/objectql = 12 files; negative control (a token that does not exist) = 0 |
files in content/docs naming ReadonlyFieldRejectedError |
1 |
— |
⇒ no page goes stale by the conversions, and none of the three pages is wrong today. This is an affordance the documentation could now point at, not a correction it needs.
Why it is worth a card rather than a shrug
The card that produced it (#16159) argues its whole case from this page: "the published guidance and the published surface disagree today, in the documentation's own words". Landing the constant closes half of that — the surface now offers what the guidance assumes — and leaves the other half in place, because the guidance still does not name what it is now possible to import. A reader who does what the page says still ends up re-spelling the literal.
Suggested shape, for triage rather than a ruling
A clause naming the constant beside the existing instruction, e.g. "…catch it by code — READONLY_FIELD_REJECTED_CODE, exported from @objectstack/objectql — not instanceof…". ⚠️ Two things worth deciding rather than assuming: whether references/api/contract.mdx and references/api/error-code-ledger.mdx are generated projections (in which case the change belongs at their generator, not the page), and whether the right moment is per-row or once at the end of #16159's sweep, when all seven constants exist.
Refs: #16159 · #16260 · content/docs/kernel/contracts/data-engine.mdx
Generated by Claude Code
Filed by the
domain:engineexecution seat while landing slice 2 of #16159 (ERR_READONLY_FIELD_REJECTED). ⛔ Observation, not a runtime defect — no assignee, nopm:queue; triage grades it. Deliberately not ridden on that PR: the dispatch fenced the docs page out, and the reason it gives is right — the sentence in question becomes followable, not false.Blocked-by: #16159
The observation
content/docs/kernel/contracts/data-engine.mdx:361tells readers, ofReadonlyFieldRejectedError:That instruction was already true and is now followable:
@objectstack/objectqlpublishesREADONLY_FIELD_REJECTED_CODEas of #16159's slice 2. The page still does not say so, so a reader following its advice writes the string themselves — which is the exact cost #16159 exists to remove, and which acquires acheck:error-code-provenancestamp site in the reader's own package.The same shape will apply to every remaining row of #16159's table as the sweep lands:
ERR_DRIVER_CONNECT,ERR_DATASOURCE_UNAVAILABLE,ERR_SUMMARY_RECOMPUTE,ERR_TRANSACTION_UNSUPPORTED,ERR_CROSS_DATASOURCE_TRANSACTION_WRITE. ⇒ this is deliberately one card for the whole sweep rather than one per slice, so it does not need re-filing five more times.What was measured, on
origin/mainat07a43dc74plus slice 2's branchcontent/docsnamingERR_READONLY_FIELD_REJECTEDkernel/contracts/data-engine.mdx,references/api/contract.mdx,references/api/error-code-ledger.mdx)content/docsnaming any*_CODEconstant of this package@objectstack/objectql= 12 files; negative control (a token that does not exist) = 0content/docsnamingReadonlyFieldRejectedError⇒ no page goes stale by the conversions, and none of the three pages is wrong today. This is an affordance the documentation could now point at, not a correction it needs.
Why it is worth a card rather than a shrug
The card that produced it (#16159) argues its whole case from this page: "the published guidance and the published surface disagree today, in the documentation's own words". Landing the constant closes half of that — the surface now offers what the guidance assumes — and leaves the other half in place, because the guidance still does not name what it is now possible to import. A reader who does what the page says still ends up re-spelling the literal.
Suggested shape, for triage rather than a ruling
A clause naming the constant beside the existing instruction, e.g. "…catch it by⚠️ Two things worth deciding rather than assuming: whether
code—READONLY_FIELD_REJECTED_CODE, exported from@objectstack/objectql— notinstanceof…".references/api/contract.mdxandreferences/api/error-code-ledger.mdxare generated projections (in which case the change belongs at their generator, not the page), and whether the right moment is per-row or once at the end of #16159's sweep, when all seven constants exist.Refs: #16159 · #16260 ·
content/docs/kernel/contracts/data-engine.mdxGenerated by Claude Code