Skip to content

feat(spec,objectql): DroppedFieldsEvent.reason names the dispatch-ruled id strip (#6437) - #7125

Merged
os-zhuang merged 2 commits into
mainfrom
claude/issue-6437-dropped-fields-reason-vocabulary
Aug 9, 2026
Merged

feat(spec,objectql): DroppedFieldsEvent.reason names the dispatch-ruled id strip (#6437)#7125
os-zhuang merged 2 commits into
mainfrom
claude/issue-6437-dropped-fields-reason-vocabulary

Conversation

@os-zhuang

@os-zhuangos-zhuang commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Closes#6437

DroppedFieldsEvent.reason was a closed enum over the two READ-ONLY strips, so the
primary-key strip on the update write path was invisible to onFieldsDropped and to
strictReadonlyWrites. This widens the enum with primary_key and routes both strip
sites through the reporting seam.

Direction is the findings-sweep ruling, quoted verbatim and untranslated:

Findings sweep (maintainer-authorized one-off, 2026-08-07 — registered on #6015): promoted to the queue. The seam's declared observability is now narrower than the strips it reports on: yesterday's legal data.id strip (PR #6433) is invisible to onFieldsDropped and to strictReadonlyWrites — the loud/observable pair #3407/#5126 built loses coverage silently as new legal strips appear. Widen the enum (acceptance-surface change ⇒ spec seat). findingpm:queue + domain:spec.

Premise verification (re-located on current origin/main)

anchorcard saidfound on mainverdict
(a) DroppedFieldsEventSchema.reasonz.enum(['readonly','readonly_when']) at data-engine.zod.ts ~:228verbatim, line 228VALID
(b) PR #6433 strip block + "not reporting this one" commentmulti branch of engine.tspresent, engine.ts ~:7352VALID
(c) protocol consumers of droppedFieldsapi/batch.zod.ts, api/protocol.zod.tspresent — 5 sitesVALID, see note
(d) reportDroppedFields call sitesby-id and multipresentVALID

Premise valid, with one correction that widens the work. The card names PR #6433's
multi-branch strip. main carries two strips of this class — #6435 added the same
strip to the by-id branch (engine.ts ~:7162), carrying its own copy of the
deliberate "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 droppedFields sites derive from DroppedFieldsEventSchema
(z.array(DroppedFieldsEventSchema), and CrossObjectBatchDroppedFieldsSchema via
.extend()), so they widen transitively and needed no source edit — only regenerated
baselines. REST's X-ObjectStack-Dropped-Fields header is generic over the reason
(`${f};reason=${e.reason}`) and needed no change either.

Naming rationale — why primary_key

readonly and readonly_when both answer "what about this FIELD caused the strip?".
primary_key sits in that same register and stays true regardless of the offending
value's shape. Rejected alternatives:

  • not_a_primary_key — describes the value, and becomes false the day the strip
    widens 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 readonly because that was true of
it; readonly is not true of an id (a truthy scalar id writes fine), so this one
gets its own value.

strictReadonlyWrites — measured before deciding

The dispatch asked whether strict rejects on ANY reported drop or only readonly-class.
Measured on main, engine.ts ~:6897:

if(strictReadonlyWrites){strictDrops.push({ object, fields, reason });// no reason-class filterreturn;}

Coverage is derived from the reported set, so routing the strip through
reportDroppedFieldsnecessarily adds a refusal. Decision: accept it, deliberately.
Excluding primary_key would need a consumer-side special case — the lenient-fallback
shape AGENTS.md bans — and would contradict the option's own contract sentence ("covers
every drop onFieldsDropped reports"). It is stated in the option's doc, in the
changeset, and pinned both ways.

Consequence owned in the same PR: the refusal message. ReadonlyFieldRejectedError
composed one sentence asserting the fields "are read-only", remedied by
{ context: { isSystem: true } } — both false for a stripped id. Left alone, this
change would have relocated the exact lie #6433 refused to commit from the event into the
error. The message is now composed from the drops breakdown the error already carried;
the read-only-only message is byte-identical to #5126's/#5503's text and the error
code does 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):

predictedmeasured
every primary_key case RED10 failed — exactly the primary_key cases
every readonly/readonly_when case GREENGREEN (6 passed in the new file)
byte-identity pin GREENGREEN
pre-existing engine-readonly-strict-writes.test.ts GREEN12/12 GREEN

Experiment B — revert only readonly-strict-errors.ts
(git checkout origin/main -- packages/objectql/src/readonly-strict-errors.ts):

predictedmeasured
only the primary_keywording pins RED2 failed — exactly the two primary_key wording pins
byte-identity pin GREENGREEN
event/refusal cases GREENGREEN (26 passed; strict-writes 12/12)

No git stash was used at any point (shared refs/stash); both experiments used
git checkout against a committed HEAD.

Consumer sweep on reason

consumershapeoutcome
service-automationDROPPED_REASON_LABELRecord< DroppedFieldsEvent['reason'], string >tsc-enforced — demanded the new arm; worded
restdroppedFieldsHeaderValuegeneric over reasonno change needed
specapi/batch.zod.ts / api/protocol.zod.tsderive from the schemawiden transitively
objectui writeWarningToastbinary ternarymislabels — filed, not fixed here

objectui's toast picks wording with reason === 'readonly_when' ? … : 'Read-only, so it did not take effect'. Its else arm is "everything that is not readonly_when", so a
stripped id would be announced to the user as read-only — the same lie, relocated into
the UI. It is not caught by types: the local byReason map is keyed by string, and a
ternary 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 that
signal) 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.

gate / suiteresult
pnpm lintpass
check:error-code-casing / route-envelope / engine-double-contractpass
check:doc-authoring / quick-reference-counts / adr-anchors / docs-audit-scope / role-wordpass
check:wildcard-fallthrough / meta-type-normalized / stack-collection-mapspass
spec build (gen:schema + gen:openapi + dts)pass
spec gen:docspass — 231 files
spec check:generated (11 artifacts)all 11 up to date
spec check:api-surfaceunchanged
spec check:export-origins (new in Type Check)pass — 4991 exports / 16 entry points
check-nul-bytes.mjs + control-char self-scanclean
typecheck spec / objectql / service-automation / rest / metadata-protocolpass
objectql test164 files / 2809 tests passed
new engine-dropped-fields-primary-key.test.ts16/16 passed
specdata-engine.test.ts84/84 passed
engine-readonly-strict-writes.test.ts (regression)12/12 passed
service-automation + metadata-protocol + rest testspass (rest 76 files / 1218 tests)

Two reds seen mid-run were the fresh-worktree trap, not this change: rest typecheck and
service-automation tests failed to resolve @objectstack/objectql /
@objectstack/service-package because those packages had no dist yet. Building the
consumer set's dependencies (--filter with the ^... dependency selector) turned both
green with no source change.

Generated artifacts

content/docs/references/api/batch.mdx, api/protocol.mdx and data/data-engine.mdx
were regenerated by gen:docs and carry the widened enum. These overlap with other
in-flight spec PRs; per the dispatch they are not pre-resolved here — union-keep and
recount on landing (#6526).


Generated by Claude Code

…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
@vercel

vercelBot commented Aug 9, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectstackIgnoredIgnoredAug 9, 2026 7:16pm

Request Review

@os-zhuangos-zhuang reopened this Aug 9, 2026
…pped-fields-reason-vocabulary
# Conflicts:
#	packages/spec/src/contracts/data-engine.ts
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/objectql, @objectstack/service-automation, @objectstack/spec.

109 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/agents.mdx(via @objectstack/spec)
  • content/docs/ai/skills-reference.mdx(via @objectstack/spec)
  • content/docs/ai/skills.mdx(via @objectstack/spec)
  • content/docs/api/client-sdk.mdx(via @objectstack/spec)
  • content/docs/api/environment-routing.mdx(via @objectstack/spec)
  • content/docs/api/error-catalog.mdx(via @objectstack/spec)
  • content/docs/api/error-handling-client.mdx(via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx(via @objectstack/spec)
  • content/docs/api/index.mdx(via @objectstack/spec)
  • content/docs/automation/approvals.mdx(via @objectstack/spec)
  • content/docs/automation/connectors.mdx(via @objectstack/spec)
  • content/docs/automation/flows.mdx(via @objectstack/service-automation, @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx(via packages/spec)
  • content/docs/automation/hooks.mdx(via @objectstack/spec)
  • content/docs/automation/index.mdx(via @objectstack/spec)
  • content/docs/automation/webhooks.mdx(via @objectstack/spec)
  • content/docs/automation/workflows.mdx(via @objectstack/spec)
  • content/docs/concepts/architecture.mdx(via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx(via packages/spec)
  • content/docs/concepts/index.mdx(via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx(via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx(via @objectstack/objectql, packages/spec)
  • content/docs/concepts/north-star.mdx(via @objectstack/spec)
  • content/docs/data-modeling/analytics.mdx(via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx(via @objectstack/spec)
  • content/docs/data-modeling/external-datasources.mdx(via @objectstack/spec)
  • content/docs/data-modeling/field-types.mdx(via @objectstack/spec)
  • content/docs/data-modeling/fields.mdx(via @objectstack/spec)
  • content/docs/data-modeling/formulas.mdx(via packages/objectql, @objectstack/spec)
  • content/docs/data-modeling/index.mdx(via @objectstack/spec)
  • content/docs/data-modeling/objects.mdx(via @objectstack/spec)
  • content/docs/data-modeling/queries.mdx(via @objectstack/spec)
  • content/docs/data-modeling/schema-design.mdx(via @objectstack/spec)
  • content/docs/data-modeling/seed-data.mdx(via @objectstack/spec)
  • content/docs/data-modeling/validation-rules.mdx(via @objectstack/spec)
  • content/docs/data-modeling/validation.mdx(via @objectstack/spec)
  • content/docs/deployment/cli.mdx(via @objectstack/spec)
  • content/docs/deployment/migration-from-objectql.mdx(via @objectstack/objectql)
  • content/docs/deployment/tenancy-modes.mdx(via @objectstack/spec)
  • content/docs/deployment/troubleshooting.mdx(via @objectstack/spec)
  • content/docs/deployment/validating-metadata.mdx(via @objectstack/spec)
  • content/docs/deployment/vercel.mdx(via @objectstack/objectql)
  • content/docs/getting-started/build-with-claude-code.mdx(via @objectstack/spec)
  • content/docs/getting-started/common-patterns.mdx(via @objectstack/spec)
  • content/docs/getting-started/examples.mdx(via @objectstack/spec)
  • content/docs/getting-started/quick-reference.mdx(via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx(via @objectstack/spec)
  • content/docs/getting-started/your-first-project.mdx(via @objectstack/spec)
  • content/docs/kernel/cluster.mdx(via @objectstack/spec)
  • content/docs/kernel/contracts/auth-service.mdx(via packages/spec)
  • content/docs/kernel/contracts/cache-service.mdx(via packages/spec)
  • content/docs/kernel/contracts/data-engine.mdx(via @objectstack/spec)
  • content/docs/kernel/contracts/index.mdx(via @objectstack/spec)
  • content/docs/kernel/contracts/metadata-service.mdx(via packages/spec)
  • content/docs/kernel/contracts/storage-service.mdx(via @objectstack/spec)
  • content/docs/kernel/index.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/data-service.mdx(via @objectstack/spec)
  • content/docs/kernel/runtime-services/email-service.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/examples.mdx(via packages/objectql, @objectstack/spec)
  • content/docs/kernel/runtime-services/index.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/sharing-service.mdx(via @objectstack/spec)
  • content/docs/kernel/runtime-services/sms-service.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx(via @objectstack/spec)
  • content/docs/kernel/services-checklist.mdx(via @objectstack/objectql, @objectstack/service-automation, @objectstack/spec)
  • content/docs/kernel/services.mdx(via @objectstack/objectql, @objectstack/spec)
  • content/docs/permissions/authentication.mdx(via @objectstack/objectql)
  • content/docs/permissions/authorization.mdx(via @objectstack/spec)
  • content/docs/permissions/permission-sets.mdx(via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx(via @objectstack/spec)
  • content/docs/permissions/positions.mdx(via @objectstack/spec)
  • content/docs/permissions/rls.mdx(via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx(via @objectstack/spec)
  • content/docs/permissions/system-context.mdx(via packages/objectql, packages/spec)
  • content/docs/plugins/adding-a-metadata-type.mdx(via @objectstack/spec)
  • content/docs/plugins/development.mdx(via @objectstack/spec)
  • content/docs/plugins/index.mdx(via @objectstack/objectql, @objectstack/spec)
  • content/docs/plugins/packages.mdx(via @objectstack/objectql, @objectstack/service-automation, @objectstack/spec)
  • content/docs/protocol/backward-compatibility.mdx(via @objectstack/spec)
  • content/docs/protocol/diagram.mdx(via packages/spec)
  • content/docs/protocol/kernel/config-resolution.mdx(via @objectstack/spec)
  • content/docs/protocol/kernel/http-protocol.mdx(via @objectstack/spec)
  • content/docs/protocol/kernel/i18n-standard.mdx(via @objectstack/spec)
  • content/docs/protocol/kernel/index.mdx(via @objectstack/objectql, @objectstack/spec)
  • content/docs/protocol/kernel/lifecycle.mdx(via @objectstack/spec)
  • content/docs/protocol/kernel/plugin-spec.mdx(via @objectstack/spec)
  • content/docs/protocol/knowledge.mdx(via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx(via @objectstack/spec)
  • content/docs/protocol/objectql/query-syntax.mdx(via packages/objectql, @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx(via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx(via packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx(via @objectstack/objectql, @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/record-alert.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx(via @objectstack/spec)
  • content/docs/ui/actions.mdx(via @objectstack/spec)
  • content/docs/ui/apps.mdx(via @objectstack/spec)
  • content/docs/ui/create-vs-edit-form.mdx(via @objectstack/spec)
  • content/docs/ui/dashboards.mdx(via @objectstack/spec)
  • content/docs/ui/field-grouping-and-order.mdx(via @objectstack/spec)
  • content/docs/ui/forms.mdx(via @objectstack/spec)
  • content/docs/ui/index.mdx(via @objectstack/spec)
  • content/docs/ui/public-data-collection.mdx(via @objectstack/spec)
  • content/docs/ui/setup-app.mdx(via @objectstack/spec)
  • content/docs/ui/translations.mdx(via @objectstack/spec)
  • content/docs/ui/views.mdx(via @objectstack/spec)

7 release-owned page(s) also reference the affected code. These are read-only:

  • content/docs/releases/implementation-status.mdx(via @objectstack/objectql, @objectstack/service-automation, @objectstack/spec)
  • content/docs/releases/index.mdx(via @objectstack/spec)
  • content/docs/releases/v12.mdx(via @objectstack/spec)
  • content/docs/releases/v13.mdx(via @objectstack/spec)
  • content/docs/releases/v16.mdx(via @objectstack/spec)
  • content/docs/releases/v17.mdx(via @objectstack/spec)
  • content/docs/releases/v9.mdx(via @objectstack/service-automation, @objectstack/spec)

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.

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation protocol:data tests tooling labels Aug 9, 2026
@os-zhuangClaude

Copy link
Copy Markdown
ContributorAuthor

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 opened, zero workflow runs ever attached (only Vercel Preview Comments), while PRs created after mine (#7126, 18:44Z) picked up runs normally, and draft PRs such as #7114 had full run sets — so draft status was not the cause. Close/reopen was an attempt to re-fire the pull_request event; it produced only Cross-repo Issue Closer, so it was not the fix either.

The actual cause. The PR was mergeable_state: dirty — a real merge conflict with main. GitHub cannot build refs/pull/N/merge for a conflicted PR, so the pull_request-triggered workflows never dispatched at all. The zero-runs symptom was the conflict, not a CI outage.

The conflict, and how it was resolved.main moved from f5a9bc2f3 to 3f8817ae3 while this was in flight. The only conflicting file was packages/spec/src/contracts/data-engine.ts, against #7064 / PR #7109 (docs(spec): strictReadonlyWrites contract no longer claims INSERT ignores it) — two TSDoc edits to the same two paragraphs. Resolved by keeping both facts, not by taking a side:

Those two compose rather than compete, and the merged sentence now says so explicitly — #5503 needed no new arm because readonly was true of its strip, whereas readonly is not true of an id. That is the same reasoning the naming section already gave, now anchored to #7109's own wording. This is integration of landedmain, not pre-resolution of another in-flight PR's generated artifacts; the union-keep note for those still stands.

Re-verified on the merged base (b392043), not assumed: spec build + gen:docs + check:generated (11/11 up to date, and regeneration produced no new deltas), typecheck for spec/objectql/service-automation, engine-dropped-fields-primary-key.test.ts + engine-readonly-strict-writes.test.ts28/28, spec data-engine.test.ts84/84.

CI on b392043 — all 9 workflows green, with the two required gate jobs read individually rather than off the aggregate:

jobconclusion
TypeScript Type Check (carries check:generated, check:export-origins)success
ESLint (carries the family gates)success
CIsuccess

Still a draft — ready-flip and auto-merge remain the PM's step.


Generated by Claude Code

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

Labels

documentationImprovements or additions to documentationprotocol:datasize/lteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

DroppedFieldsEvent.reason 只有 readonly / readonly_when 两值,写路径上新增的合法剥离对 onFieldsDropped / strictReadonlyWrites 不可见

2 participants

@os-zhuang@claude