feat(spec,objectql): DroppedFieldsEvent.reason names the dispatch-ruled id strip (#6437) - #7125
Conversation
…ed id strip (#6437) `DroppedFieldsEvent.reason` was a closed enum over the two READ-ONLY strips, so the primary-key strip added by #6262 / PR #6433 (multi) and #6435 (by-id) was invisible to `onFieldsDropped` and `strictReadonlyWrites`. Adds `primary_key` and routes both strip sites through `reportDroppedFields`. Because `strictReadonlyWrites` coverage is DERIVED from the reported set (measured: `strictDrops.push` applies no reason-class filter), reporting the new reason also adds a refusal — deliberate, documented, pinned both ways. The refusal message is now composed from `drops` so a `primary_key` rejection never claims the field was read-only; the read-only-only message stays byte-identical. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PiRUoQkTSBBmpyXBY3cVn2
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
…pped-fields-reason-vocabulary # Conflicts: # packages/spec/src/contracts/data-engine.ts
📓 Docs Drift CheckThis PR changes 3 package(s): 109 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
⛔ 7 release-owned page(s) also reference the affected code. These are read-only:
|
|
Base integration + a CI-dispatch incident worth recording (author note). Why this PR briefly showed as closed. I closed and immediately reopened it myself, ~19:00Z. It was not an external close and there is no successor PR — #7125 is the deliverable. Reason: after The actual cause. The PR was The conflict, and how it was resolved.
Those two compose rather than compete, and the merged sentence now says so explicitly — #5503 needed no new arm because Re-verified on the merged base ( CI on
Still a draft — ready-flip and auto-merge remain the PM's step. Generated by Claude Code |
Closes #6437
DroppedFieldsEvent.reasonwas a closed enum over the two READ-ONLY strips, so theprimary-key strip on the update write path was invisible to
onFieldsDroppedand tostrictReadonlyWrites. This widens the enum withprimary_keyand routes both stripsites through the reporting seam.
Direction is the findings-sweep ruling, quoted verbatim and untranslated:
Premise verification (re-located on current
origin/main)mainDroppedFieldsEventSchema.reasonz.enum(['readonly','readonly_when'])atdata-engine.zod.ts~:228engine.tsengine.ts~:7352droppedFieldsapi/batch.zod.ts,api/protocol.zod.tsreportDroppedFieldscall sitesPremise valid, with one correction that widens the work. The card names PR #6433's
multi-branch strip.
maincarries two strips of this class — #6435 added the samestrip to the by-id branch (
engine.ts~:7162), carrying its own copy of thedeliberate "NOT routed through
reportDroppedFields… filed separately as #6437"comment. Both are deleted and both are now reported. That second instance is also the
HOLD comment's own stated restart condition ("promote when a second non-readonly
strip lands on the write path"), which had already been met at dispatch time.
Note on (c): all five
droppedFieldssites derive fromDroppedFieldsEventSchema(
z.array(DroppedFieldsEventSchema), andCrossObjectBatchDroppedFieldsSchemavia.extend()), so they widen transitively and needed no source edit — only regeneratedbaselines. REST's
X-ObjectStack-Dropped-Fieldsheader is generic over the reason(
`${f};reason=${e.reason}`) and needed no change either.Naming rationale — why
primary_keyreadonlyandreadonly_whenboth answer "what about this FIELD caused the strip?".primary_keysits in that same register and stays true regardless of the offendingvalue's shape. Rejected alternatives:
not_a_primary_key— describes the value, and becomes false the day the stripwidens to the same-value truthy-scalar no-op the engine deliberately leaves alone today.
dispatch_ruled— leaks an internal helper's name (resolveEngineUpdateDispatch)into a wire-visible enum.
The house rule this follows is #5503's, applied in the other direction: a new arm is
warranted exactly when no existing arm is truthful. #5503 reported the
implicitly-readonly runtime-owned strip as plain
readonlybecause that was true ofit;
readonlyis not true of anid(a truthy scalaridwrites fine), so this onegets its own value.
strictReadonlyWrites— measured before decidingThe dispatch asked whether strict rejects on ANY reported drop or only readonly-class.
Measured on
main,engine.ts~:6897:Coverage is derived from the reported set, so routing the strip through
reportDroppedFieldsnecessarily adds a refusal. Decision: accept it, deliberately.Excluding
primary_keywould need a consumer-side special case — the lenient-fallbackshape AGENTS.md bans — and would contradict the option's own contract sentence ("covers
every drop
onFieldsDroppedreports"). It is stated in the option's doc, in thechangeset, and pinned both ways.
Consequence owned in the same PR: the refusal message.
ReadonlyFieldRejectedErrorcomposed one sentence asserting the fields "are read-only", remedied by
{ context: { isSystem: true } }— both false for a strippedid. Left alone, thischange would have relocated the exact lie #6433 refused to commit from the event into the
error. The message is now composed from the
dropsbreakdown the error already carried;the read-only-only message is byte-identical to #5126's/#5503's text and the error
codedoes not branch.Reverse verification — direction predicted BEFORE running
The predicted table is committed in the test file header, written before the first run.
Experiment A — delete both
reportDroppedFields(…, 'primary_key')call sites(
git checkout origin/main -- packages/objectql/src/engine.ts):primary_keycase REDprimary_keycasesreadonly/readonly_whencase GREENengine-readonly-strict-writes.test.tsGREENExperiment B — revert only
readonly-strict-errors.ts(
git checkout origin/main -- packages/objectql/src/readonly-strict-errors.ts):primary_keywording pins REDprimary_keywording pinsNo
git stashwas used at any point (sharedrefs/stash); both experiments usedgit checkoutagainst a committed HEAD.Consumer sweep on
reasonservice-automationDROPPED_REASON_LABELRecord< DroppedFieldsEvent['reason'], string >restdroppedFieldsHeaderValuespecapi/batch.zod.ts/api/protocol.zod.tswriteWarningToastobjectui's toast picks wording with
reason === 'readonly_when' ? … : 'Read-only, so it did not take effect'. Itselsearm is "everything that is notreadonly_when", so astripped
idwould be announced to the user as read-only — the same lie, relocated intothe UI. It is not caught by types: the local
byReasonmap is keyed bystring, and aternary is not an exhaustive branch, so the deliberately-narrow spec type re-export
(
data-objectstack/src/index.ts, whose comment says it exists precisely to give tsc thatsignal) stays silent. Sibling repo ⇒ recorded, not fixed: objectui#3935 (already
open,
pm:queue).Gates and verification
All run in this worktree; heavy phases serialized on the shared verify lock.
pnpm lintcheck:error-code-casing/route-envelope/engine-double-contractcheck:doc-authoring/quick-reference-counts/adr-anchors/docs-audit-scope/role-wordcheck:wildcard-fallthrough/meta-type-normalized/stack-collection-mapsspec build(gen:schema+gen:openapi+ dts)spec gen:docsspec check:generated(11 artifacts)spec check:api-surfacespec check:export-origins(new in Type Check)check-nul-bytes.mjs+ control-char self-scantypecheckspec / objectql / service-automation / rest / metadata-protocolobjectql testengine-dropped-fields-primary-key.test.tsspecdata-engine.test.tsengine-readonly-strict-writes.test.ts(regression)service-automation+metadata-protocol+resttestsrest76 files / 1218 tests)Two reds seen mid-run were the fresh-worktree trap, not this change:
resttypecheck andservice-automationtests failed to resolve@objectstack/objectql/@objectstack/service-packagebecause those packages had nodistyet. Building theconsumer set's dependencies (
--filterwith the^...dependency selector) turned bothgreen with no source change.
Generated artifacts
content/docs/references/api/batch.mdx,api/protocol.mdxanddata/data-engine.mdxwere regenerated by
gen:docsand carry the widened enum. These overlap with otherin-flight spec PRs; per the dispatch they are not pre-resolved here — union-keep and
recount on landing (#6526).
Generated by Claude Code