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-zhuang os-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)

anchor card said found on main verdict
(a) DroppedFieldsEventSchema.reason z.enum(['readonly','readonly_when']) at data-engine.zod.ts ~:228 verbatim, line 228 VALID
(b) PR #6433 strip block + "not reporting this one" comment multi branch of engine.ts present, engine.ts ~:7352 VALID
(c) protocol consumers of droppedFields api/batch.zod.ts, api/protocol.zod.ts present — 5 sites VALID, see note
(d) reportDroppedFields call sites by-id and multi present VALID

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 filter
  return;
}

Coverage is derived from the reported set, so routing the strip through
reportDroppedFields necessarily 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):

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

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

predicted measured
only the primary_key wording pins RED 2 failed — exactly the two primary_key wording pins
byte-identity pin GREEN GREEN
event/refusal cases GREEN GREEN (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

consumer shape outcome
service-automation DROPPED_REASON_LABEL Record< DroppedFieldsEvent['reason'], string > tsc-enforced — demanded the new arm; worded
rest droppedFieldsHeaderValue generic over reason no change needed
spec api/batch.zod.ts / api/protocol.zod.ts derive from the schema widen transitively
objectui writeWarningToast binary ternary mislabels — 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 / suite result
pnpm lint pass
check:error-code-casing / route-envelope / engine-double-contract pass
check:doc-authoring / quick-reference-counts / adr-anchors / docs-audit-scope / role-word pass
check:wildcard-fallthrough / meta-type-normalized / stack-collection-maps pass
spec build (gen:schema + gen:openapi + dts) pass
spec gen:docs pass — 231 files
spec check:generated (11 artifacts) all 11 up to date
spec check:api-surface unchanged
spec check:export-origins (new in Type Check) pass — 4991 exports / 16 entry points
check-nul-bytes.mjs + control-char self-scan clean
typecheck spec / objectql / service-automation / rest / metadata-protocol pass
objectql test 164 files / 2809 tests passed
new engine-dropped-fields-primary-key.test.ts 16/16 passed
spec data-engine.test.ts 84/84 passed
engine-readonly-strict-writes.test.ts (regression) 12/12 passed
service-automation + metadata-protocol + rest tests pass (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

vercel Bot commented Aug 9, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 9, 2026 7:16pm

Request Review

@os-zhuang os-zhuang closed this Aug 9, 2026
@os-zhuang os-zhuang reopened this Aug 9, 2026
…pped-fields-reason-vocabulary

# Conflicts:
#	packages/spec/src/contracts/data-engine.ts
@github-actions github-actions Bot added the size/l label Aug 9, 2026
@github-actions

github-actions Bot commented Aug 9, 2026

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-actions github-actions Bot added documentation Improvements or additions to documentation protocol:data tests tooling labels Aug 9, 2026

Copy link
Copy Markdown
Contributor Author

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 landed main, 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.ts 28/28, spec data-engine.test.ts 84/84.

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

job conclusion
TypeScript Type Check (carries check:generated, check:export-origins) success
ESLint (carries the family gates) success
CI success

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


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 protocol:data size/l tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

2 participants