Skip to content

feat(spec,objectql): report a caller value a stored expression replaced, as DroppedFieldsEvent reason stored_expression - #24778

Merged
objectstack-fleet[bot] merged 8 commits into
mainfrom
claude/issue-24690-dropped-fields-recomputed
Oct 20, 2026
Merged

objectstack-fleet[bot] merged 8 commits into
mainfrom
claude/issue-24690-dropped-fields-recomputed

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Closes #24690
Clause-②: yes (widening)

What

PR #24673 (c0803b04d2) made the engine compute a stored (non-formula) field's declared expression on every write, so a value the caller sends for such a field never stands. Before this PR nothing said so. onFieldsDropped stayed silent, so the PATCH and /batch doors answered with no droppedFields, and a flow's update_record reported success over a row that holds the computed value.

Scope follows the maintainer's ruling 「甲」 (#6015 comment 6292005854, applied on decision card #24854): in 18.x only the loud half ships. That half is the new reason value, its emit, and its report. The strictReadonlyWrites refusal is runtime behaviour that no conversion can carry, so it waits for 19.0.

  • spec (packages/spec/src/data/data-engine.zod.ts): DroppedFieldsEventSchema.reason gains stored_expression, with its describe. The field IS written, with the computed value in place of the caller's, so computed (a formula with no column, whose key is dropped) would be untrue of it. The name follows the existing arms, which name the field's role (primary_key, computed). The fields describe and the schema describe now also cover a field written with the computed value. In contracts/data-engine.ts, the strictReadonlyWrites TSDoc names stored_expression as the one arm outside its coverage in 18.x.
  • objectql (engine.ts): replacedStoredExpressionFields reports a field when three things hold: the caller sent it, no other strip took it, and the value stored differs from the caller's. It runs on insert (single, batch, and insertMany's per-row droppedFields), on update by id and on predicate update, in every context, isSystem included. A client that sends the computed value (the console grid's consistent save) or no value is told nothing. A value a static readonly strip already took is reported once, as readonly.
  • strictReadonlyWrites is unchanged. The update path sends the report through a listener-only channel (notifyDrop). On insert, the strict check judges only the strips' own drops. Both strict checks sit exactly where they sit on main, before the computation. So a strict write whose only drop is a stored_expression replacement completes with the computed value and is reported to onFieldsDropped. A strict write that also carries a refusable drop is refused for that drop alone, writes nothing, and fires no listener. readonly-strict-errors.ts gains only the REASON_PHRASE row its exhaustive key type requires. No refusal carries that row in 18.x.
  • service-automation (crud-nodes.ts): DROPPED_REASON_LABEL is an exhaustive Record over the reason union, so it needs the new row. For this reason the step warning says the requested value(s) were NOT stored, and that the write succeeded with the computed value(s). The other reasons keep their exact text.
  • Generated: content/docs/references/** regenerated with check:generated --fix.
  • Changeset: Clause-②: yes (widening). @objectstack/spec and @objectstack/objectql are minor, @objectstack/service-automation is patch. Not breaking. It records that the strict refusal is deferred to 19.0 per 「甲」.

Landing order: the objectui pin must move first

objectui's STRIPPED_LINE (packages/app-shell/src/providers/writeWarningToast.ts) is a Record keyed by DroppedFieldsEvent['reason']. app-shell builds with tsc, and scripts/build-console.sh installs this tree's @objectstack/* into the pinned objectui. The pin on main today is 616df6598b. It does not contain objectui PR 13233 (objectstack-ai/objectui#13178), which adds the stored_expression row ahead of the spec pin: merge commit 8803ca54ac269643101f98dd0cc24ab25feb55a0, which is 36 commits ahead of 616df6598b and is on objectui main. Landing this PR at today's pin would turn the full-battery Console Pin Gate red.

The pin is not bumped here. docs/releases-maintenance.md says the pin moves by hand, in its own PR, with scripts/bump-objectui.sh, to a revision chosen in an objectstack issue. This PR must not land before that pin PR does. The pin PR must:

  1. Run scripts/bump-objectui.sh 8803ca54ac269643101f98dd0cc24ab25feb55a0 (or a later objectui main commit). It writes .objectui-sha and the @objectstack/console changeset for 616df6598b..NEW.
  2. Regenerate the committed manifest in the same PR: pnpm objectui:build, then node scripts/gen-sdui-manifest-node.mjs. That rewrites sdui.manifest.json and scripts/sdui-manifest.record.json, and scripts/check-sdui-manifest.mjs must be green.
  3. Land before this PR. The objectui row is keyed by the spec union plus stored_expression, so the pinned objectui compiles against main's spec both before and after this PR.

Tests (on the head named in the latest report)

  • packages/objectql/src/engine-stored-expression-dropped-fields.test.ts covers by-id update (the PATCH door and each by-id /batch op), insert single, batch and insertMany, and predicate update. It has controls for an equal value and an unsent value, and checks the single report when the readonly strip took the value. Under strictReadonlyWrites it checks three things. By id, on insert and on a predicate update, the write completes and is reported. A write that also has a readonly_when drop is refused for that drop alone, writes nothing and fires no listener. An insert with a refusable drop is refused before the computation, carrying no stored_expression drop.
  • packages/rest/src/rest-stored-expression-dropped-fields.test.ts runs through the composed stack (RestServer, then the protocol, then ObjectQL, then a real SqlDriver on better-sqlite3). PATCH answers droppedFields and the X-ObjectStack-Dropped-Fields header. /batch tags the op index. Each has a consistent-value control.
  • crud-dropped-fields.test.ts (service-automation) pins the new warning. data-engine.test.ts (spec) pins the five-value enum.
  • Two ablations, one-time and with no test file left behind, via scripts/ablation-replace.mjs. The tool restored and verified the file each time (blob equal to HEAD, git diff HEAD empty).
    • The reporter's guard was forced to return [], and 12 of 30 objectql tests failed (first round).
    • The by-id notifyDrop was routed back through the strict-or-listener emitDrop, and the strict by-id report test failed (1 of 17).
  • Reverse type check (first round): with the stored_expression row commented out of DROPPED_REASON_LABEL, service-automation tsc failed with TS2741.

Acceptance notes

  • Object.is is the comparison, as in the compute passes, over caller values normalized before the snapshot. Take a stored expression whose computed value has a different JS type from an equal value the caller sends, such as a date expression computing a Date against an ISO string: it would be reported. This was not measured.
  • The ImportRowResult.droppedFields (api/export), /batch and protocol droppedFields describes still say "caller-supplied fields the engine LEGALLY strips". They carry the fifth reason through the shared enum, and their prose was left alone because it is outside this card's file surface.
  • The 19.0 half, refusing a stored_expression replacement under strictReadonlyWrites, needs its own card, filed by the seat.

Generated by Claude Code

…der DroppedFieldsEvent reason stored_expression (WIP)

Claude-Session: https://claude.ai/code/session_01D6F9axfoPzR7t8G1J7Ytzz
Co-authored-by: Claude <noreply@anthropic.com>
…hrough the composed stack (WIP)

Claude-Session: https://claude.ai/code/session_01D6F9axfoPzR7t8G1J7Ytzz
Co-authored-by: Claude <noreply@anthropic.com>
… stored, and add the changeset

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D6F9axfoPzR7t8G1J7Ytzz
…eldsEvent reason

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D6F9axfoPzR7t8G1J7Ytzz
…her than casting to any

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D6F9axfoPzR7t8G1J7Ytzz
@github-actions

github-actions Bot commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

2 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 WriteObservabilityOptions (symbol, a top-level interface), primary_key (literal, a string literal in DroppedFieldsEventSchema), readonly_when (literal, a string literal in DroppedFieldsEventSchema))
  • content/docs/protocol/objectql/security.mdx (via primary_key (literal, a string literal in DroppedFieldsEventSchema), readonly_when (literal, a string literal in DroppedFieldsEventSchema))
What this run could not see
  • 2 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • 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.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 144 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 a7199ef9744edf0c3ef0e381bb95a328545a3da9 → packageMentionDocs.

Which tree this was computed on

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

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

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

This was referenced Oct 20, 2026
…donlyWrites instead of refusing it, deferring the refusal to 19.0

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D6F9axfoPzR7t8G1J7Ytzz
@objectstack-fleet objectstack-fleet Bot changed the title feat(spec,objectql)!: report a caller value a stored expression replaced, as DroppedFieldsEvent reason stored_expression feat(spec,objectql): report a caller value a stored expression replaced, as DroppedFieldsEvent reason stored_expression Oct 20, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Pin moved, for this PR's seat · domain:devx seat 1 · os-warren · session_01FM29ZqLTrnPNXxijioZwMp · 2026-10-20T22:19Z. ⛔ Not a claim; a reply is owed only on an objection.

This PR's landing is the domain:spec seat's.


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 56cac787cbe56defaa3853fd178e7865bf6ea889
Local-runs: none
Scope: packages/spec/src/contracts/data-engine.ts, packages/spec/src/data/data-engine.zod.ts + packages/objectql/src/engine.ts, packages/objectql/src/readonly-strict-errors.ts, packages/services/service-automation/src/builtin/crud-nodes.ts, .changeset/24690-stored-expression-dropped-fields.md, packages/objectql/src/engine-stored-expression-dropped-fields.test.ts, packages/spec/src/data/data-engine.test.ts

Read: card #24690 (body and every comment, claim 6282617789 through 6294902176), decision card #24854 and its ruling-applied comment 6292049030, the standing ruling 「甲」 (#6015 6292005854), the PR body and file list, the three-dot diff against origin/main at this head, the head's check-runs, origin/main:.objectui-sha and the objectui compare API. Adversarial by design: the dispatching seat's own conclusions were not adopted, each claim below was re-derived from the diff.

① Derived judgments

  1. DroppedFieldsEvent.reason gains stored_expression (widening, right). data-engine.zod.ts closes reason over five values in the stated order; the spec pin test asserts the exact five-tuple and still rejects an unknown string. The fields and schema describes now cover a field written with the computed value, which is what distinguishes it from computed (key dropped, nothing written). The name follows the field-role register of primary_key / computed, as the describe argues. Right.
  2. Emit on insert, batch, insertMany per-row, update by id, predicate update, only when the caller's value differs (right). replacedStoredExpressionFields requires three facts: the key is on the entry snapshot (suppliedPerRow / suppliedValues, both pre-existing on main, taken from opCtx.data before hooks), the key is still on the payload the compute pass reads (so a value the readonly / readonly_when strip took is reported once under that reason, confirmed by the test at line 191), and the stored value differs by Object.is on at least one row. Insert: computed per live row, filtered against insertDroppedPerRow, carried into the batch union and the per-row droppedFields of insertMany. By id: compared against the one computed row. Predicate: compared against every matched row merged with the settled patch, so a patch that moves no input (column left alone) still reports a differing caller value, and the equal-value control reports nothing. Tests cover each path with an equal-value and an unsent-value control. Right.
  3. Not refused under strictReadonlyWrites in 18.x (right, per 「甲」). Insert: the strict throw now judges refusableInsertDrops (computed + readonly only) at the exact point it stood on main, before the computation; the listener then fires for the full insertDrops, which under strict can only hold stored_expression. Update paths: a new listener-only notifyDrop carries the report, and emitDrop (strict-or-listener) is untouched for the four refusable reasons. In the head file assertNoStrictDrops() sits at 18096 and the by-id notifyDrop at 18109; on the predicate path 18380 then 18397. So a refused write never fires the listener, and the test at line 281 pins it (refused for readonly_when alone, nothing written, no events). readonly-strict-errors.ts gains only the REASON_PHRASE row its exhaustive key type demands; no refusal is built with it. The WriteObservabilityOptions TSDoc names stored_expression as the one arm outside strict coverage for 18.x, which keeps the "derived coverage" contract honest instead of silently contradicting it. Right.
  4. Exhaustive consumers over reason (all covered). At the head, git grep finds two Record tables keyed by the union: objectql REASON_PHRASE (row added) and service-automation DROPPED_REASON_LABEL (row added, and droppedFieldsWarning says "value(s) were NOT stored ... succeeded with the computed value(s)" for this reason instead of the "NOT written ... without them" sentence that would be false of a written field; pinned by crud-dropped-fields.test.ts). READONLY_CLASS_REASONS (a ReadonlySet) and protocol.ts's byKey map are non-exhaustive and need no row. objectui STRIPPED_LINE at the pin cf153eb2e535 is keyed by DroppedFieldsEvent['reason'] | 'stored_expression' and carries the stored_expression row (writeWarningToast.ts:145, :173), so it type-checks against the spec both before and after this PR. TypeScript Type Check and Type Check · consumer gates are success on the head.
  5. Generated references (content/docs/references/**): regenerated text only, in line with the new describes. The PR body's acceptance notes name the two things this review also sees and that are outside the card's surface: an Object.is type mismatch (a Date against an ISO string) would be reported, and the /batch, protocol and ImportRowResult "LEGALLY strips" prose is now one reason short. Neither blocks: the first is a report, not a refusal, and the second is describe drift.

② Semver level

Changeset .changeset/24690-stored-expression-dropped-fields.md: @objectstack/spec minor, @objectstack/objectql minor, @objectstack/service-automation patch. A new value on a published payload enum is an additive widening, so spec minor is right; objectql publishes a new event value on onFieldsDropped / droppedFields, minor is right; service-automation changes only a label row and a warning sentence, patch is right. No BREAKING marker and no ADR-0087 marker, which matches the re-scoped head (the strict refusal is out). The PR body and the changeset agree on the line, and Check Changeset, Governed Surface Queue Guard and changeset-no-major are success on the head.
Clause-②: yes (widening)

③ Boundary flags

Implemented-by: claude/issue-24690-dropped-fields-recomputed
Reviewed-by: session_01D6F9axfoPzR7t8G1J7Ytzz

VERDICT: PASS

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 20, 2026 22:31
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 20, 2026 22:32
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 20, 2026
Merged via the queue into main with commit 7532ea3 Oct 20, 2026
50 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-24690-dropped-fields-recomputed branch October 20, 2026 23:05
This was referenced Oct 20, 2026
baozhoutao pushed a commit to objectstack-ai/objectui that referenced this pull request Oct 21, 2026
…in with stored_expression subtracted (objectui#13309)

objectstack-ai/objectstack#24778 added a fifth arm, `stored_expression`, to
`DroppedFieldsEventSchema.reason`. `_ReasonIsTheEnum` stated the union as
exactly four members, so the Spec Shape Gate (objectui main against the spec
built from objectstack main) reds on TS2344. Apply the objectui#11206 shape:
subtract the new arm, a no-op at the pin; drop it at the pin bump that carries
the arm.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XVp19E1MYUTFM3dnkwz8gB
github-merge-queue Bot pushed a commit that referenced this pull request Oct 21, 2026
…red expression replaces is refused, and nothing is written (#25086)

Closes #25000
Clause-②: no (narrowing: under `strictReadonlyWrites`, a write whose
value a stored expression replaces is refused instead of reported; it
ships now under the amended 「甲」, #6015 `6303865025`)

## What changes

`strictReadonlyWrites` refuses every drop that `onFieldsDropped`
reports. Since the `stored_expression` reason landed (#24690, PR #24778
`7532ea3c08`), one reported drop was left out of the refusal: a caller
value for a stored (non-`formula`) field that declares an `expression`,
which the engine replaces with the computed value. Under strict such a
write completed and was only reported. The amended ruling 「甲」 (#6015
`6303865025`) ships the refusal in 18.x as a BREAKING minor, and triage
re-timed this card to 18.x (`6305854988`).

- **Insert (single, batch, `insertMany`).** The strict check in
`engine.insert` now judges `insertDrops`, the one event list that
includes the stored-expression replacement, after the compute pass.
Before, it judged only the strips' drops, ahead of that pass. A replaced
value throws `ReadonlyFieldRejectedError` (`operation: 'insert'`) before
validation and before any driver dispatch, and no listener fires.
`insertMany` delegates to `insert`, so it refuses the whole call, as it
does for the other strict drops.
- **Update by id and predicate update.** The `stored_expression` report
now goes through `emitDrop`, the channel every strip reports through,
and no longer calls `notifyDrop` directly. On both branches the one
`assertNoStrictDrops()` call now comes after the stored-expression pass.
So under strict the write is refused before the statement and nothing is
written. An object that declares no stored expression skips that pass,
so its refusal happens exactly where it did before.
- **One refusal names everything.** A payload that carries another
strip's drop as well as a replaced value is now refused once, naming
both, with `stored_expression` last. Before, it was refused for the
other drop alone.
- **Wording.** `REASON_PHRASE.stored_expression` (written by #24690
ahead of this refusal) is reused. The per-reason message gains a
`stored_expression` remedy (leave the field out, or send the computed
value). That remedy is spoken only when the reason is present, so every
other refusal's message is unchanged byte for byte, by the same rule the
`computed` remedy follows.
- **Docs.** The TSDoc in `packages/spec/src/contracts/data-engine.ts`
(`strictReadonlyWrites`) and `packages/spec/src/data/data-engine.zod.ts`
(`DroppedFieldsEvent.reason`) no longer names `stored_expression` as the
arm strict does not refuse. The objectql comments in `engine.ts` and
`readonly-strict-errors.ts` follow. No `.describe()` text moved:
`check:generated` reports all 14 artifacts current, so the generated
reference docs have nothing to regenerate.

**Unchanged:** a strict write that sends the computed value, or no
value, is not refused. A stored expression field that is also `readonly`
is still refused once, as `readonly`. Without strict, the write
completes with the computed value and is reported. The error code is
unchanged.

## Tests


`packages/objectql/src/engine-stored-expression-dropped-fields.test.ts`:
the strict block changes from "reported, not refused" to "refused,
nothing written". It covers update by id, insert single, batch,
`insertMany` and predicate update. Each refusal asserts the `code`,
`drops` (and `operation`/`fields` where they matter), that the store is
unchanged, and that the listener stayed silent. A non-strict control
sits beside each refusal (the write succeeds and is reported), together
with the strict controls for "computed value sent / no value". The union
case is also covered (`readonly_when` + `stored_expression` in one
refusal), and so is the `readonly`-only insert refusal.

All runs were taken on the final code commit `a8e9d3611c`:

- `pnpm --filter @objectstack/objectql test`: 455 files / 8569 tests
passed. `pnpm --filter @objectstack/objectql typecheck`: `tsc --noEmit`
clean, and `check:test-typecheck` OK (ledger held).
- `pnpm --filter @objectstack/spec build && check:generated`: all 14
generated artifacts up to date. `spec test`: 705 files / 21424 passed (1
todo). `spec typecheck`: OK. `spec test:repo`: 72 files / 1237 passed.
- `node scripts/pm/dispatch-gates.mjs --commands` derived 94 commands
from merge base `688052e14`, and all 94 exited 0. On the first pass,
`check:dual-build-cjs-loads` and `check:type-check-debt` printed
PREREQUISITE NOT MET (exit 3, nothing measured). I re-ran
`check:type-check-debt` under the verify lock: its closure build ran and
it re-measured OK, `none above its recorded number`. That full build
also produced the missing `dist/` trees, and
`check:dual-build-cjs-loads` then passed (112 entry points across 69
packages load). `dispatch-gates --ran` reconciles 94 derived, 94 run, 0
NOT-MEASURED, 0 UNRUN. The changeset gates are among the 94:
`check-adr-0087-registration`, `check-changeset-no-major`,
`check-empty-changeset`, `check:changeset-gate-self-tests`.

**Ablation.** The fix was committed first. Each leg went through
`scripts/ablation-replace.mjs`, with an anchor hit of 1 to 0 and the
blob proven back to the HEAD blob `3e8c0ec2bd1a` with `git diff HEAD`
empty. The suite imports `./engine.js` from source, so no build is
involved.

- A, update paths: `emitDrop` lets `stored_expression` past strict.
Three tests go red (update by id, predicate update, the union case), and
the other 22 stay green.
- B, insert: the strict check ignores a `stored_expression`-only list.
Three tests go red (insert single, batch, `insertMany`), and the other
22 stay green.

## Producers measured

No non-test caller in this repository passes `strictReadonlyWrites`: the
examples, `packages/qa` dogfood and the runtime and REST doors pass
none. The option is also not on the serializable options bag, so no wire
caller can set it. No in-repo producer moves. In the objectql suite, the
in-package strict tests on stored-expression objects
(`engine-stored-expression-write.test.ts`,
`engine-write-formula-hydration.test.ts`,
`engine-formula-write-strip.test.ts`) stay green unchanged.

## Changeset

`.changeset/25000-strict-stored-expression-refusal.md`:
`@objectstack/objectql` `minor`, with a BREAKING section that maps FROM
(completed, reported) TO (refused, nothing written), gives the remedy,
and carries the claim's `Clause-②` line and an ADR-0087 `not-required
(no-migration-prescription)` marker (the precedent shape of
`bb4f5cc005`). `@objectstack/spec` is not listed: only TSDoc comments
moved there, and no `.describe()` text or export did.

## Acceptance notes

- The release-owned 18.x upgrade checklist (`content/docs/releases/`) is
not edited here. The changeset carries the text that release compilation
reads.
- The pending `.changeset/24690-stored-expression-dropped-fields.md` (in
the open Version Packages PR) says strict "is unchanged" and that the
refusal is deferred to 19.0. That file is outside this card's claimed
file surface, so this PR leaves it alone. The new changeset says it
supersedes that bullet. The report names the one-line amendment for the
seat to decide.
- `content/docs/data-modeling/validation-rules.mdx` (the `expression` on
a stored field paragraph) still says a sent value is "not reported as
dropped", which has been false since #24690. That is a hand-written doc
outside this card's surface, and it is noted in the report.

Open PRs touching the same files were checked: #25072 and #24893 edit
`engine.ts` in disjoint hunks (lines 4628–6300 and 9043–12250). No open
PR touches `data-engine.zod.ts`, `contracts/data-engine.ts` or
`readonly-strict-errors.ts`.

---
_Generated by [Claude
Code](https://claude.ai/code/session_01D6F9axfoPzR7t8G1J7Ytzz)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
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

2 participants