Repository navigation
18.0 round-2 sweep framework findings: a paid invoice line's amount/total stays writable while its inputs are locked; an unevaluable hook condition answers 500 INTERNAL_ERROR; remote-tables?schema= never narrows on the SQL drivers #24619
Description
Filing gate: ① product defects with named landing points and reproductions (pm:queue class) — a roll-up card folding the 18.0 round-2 sweep's remaining framework-side findings under the session's three-card quota (filing gate 配额: extras fold into one card with a list). Each row below is its own small fix; split on pickup if they go to different seats.
Reader: the seats owning (a) examples/app-showcase/src/data/objects/invoice.object.ts + objectql's readonlyWhen write strip, (b) packages/rest error mapping for HookConditionError, (c) service-external-datasource listRemoteTables.
Dedupe (open AND closed, semantic search on objectstack-ai/objectstack): (a) "showcase invoice line amount writable on paid invoice readonlyWhen expression client-sent amount total changes" → 3 hits, nearest #4889 (closed: parent-scoped readonlyWhen was unenforced server-side — fixed for the locked inputs; amount was left outside it); (b) "hook condition cannot be evaluated HookConditionError returns 500 INTERNAL_ERROR unhandled error" → 11 hits, nearest #22856 (closed, after* onError log case) and #24561 (open, but the /batch hook-refusal envelope — a different door and error class); (c) "external datasource remote-tables schema filter never narrows postgres unqualified table names" → 19 hits, all closed, none on the filter. First-round wave cards (#24546 #24547 #24548 #24561 #24562 #24563 #24564 #24565 #24576 #24543 #24567) checked — none covers these rows.
reach: each row measured once on a public HTTP entry on a stock (or scratch-copy) showcase boot at ba4afcc2 — commands below.
Path: records-forms.conditional-rules-grid ; records-forms.object-hook-lifecycle ; integration-system.external-schema-introspection
QA-source: #24616 · records-forms.conditional-rules-grid · negative[1] ; #24616 · records-forms.object-hook-lifecycle · acceptance[8] ; #24615 · integration-system.external-schema-introspection · acceptance[4]
Checklist item priority: P1 (conditional-rules-grid, object-hook-lifecycle), P1 (external-schema-introspection) — rows (b) and (c) are low-severity observations on items that otherwise pass
type: Bug
(a) A paid invoice's line amount — and so the invoice total — stays writable while its inputs are locked
POST /api/v1/batch {"operations":[{"object":"showcase_invoice_line","action":"update","id":"LINE_ID","data":{"quantity":60,"amount":2999.40}}],"atomic":true}on a line of the seeded PAID invoice INV-1003 → 200 withdroppedFields:[{object:"showcase_invoice_line",fields:["quantity"],reason:"readonly_when"}]; read back: quantity 6 (locked, correct), amount 2999.4, invoice stillpaidwith total 2999.4 (was 299.94).PATCH /api/v1/data/showcase_invoice_line/LINE_ID {"amount":1}→ accepted with no drop; the paid invoice's total becomes 1.- Expected: once
product/quantity/unit_priceare frozen byreadonlyWhen: parent.status == 'paid'(invoice.object.ts:270/311/316), the value derived from them is frozen too. Actual:amount(:330-334) declares noreadonlyWhen, and itsexpression: record.quantity * record.unit_priceis not recomputed server-side (comment:324-329: "the client-sent value is stored as-is"). - Not an authorization bypass (a business-state lock; the writer holds edit rights). Fix in the showcase (
readonlyWhenonamount) or in the framework (recompute a storedexpression, or derive its lock from its inputs). The console exposes it through objectui's line-grid lock race, handed to the objectui lane separately.
(b) A write refused by an unevaluable hook condition answers a generic 500 INTERNAL_ERROR
- Scratch app copy; hook on
showcase_inquiry,beforeInsert, conditionrecord.name.startsWith('qa-fc') && int(record.name) > 0. POST /api/v1/data/showcase_inquiry {"name":"qa-fc-abc","email":"a@b.co","message":"m"}→500 {"error":"Internal server error","code":"INTERNAL_ERROR"}; server log[REST] Unhandled error: HookConditionError: Hook 'qa_failclosed' could not evaluate its condition … — operation aborted. The row is correctly not written (fail-closed holds).- Expected: a deliberate, coded refusal (the class is declared — the write was refused by design), not an unhandled internal error. Reproduced 3 times over two fresh boots.
(c) GET /api/v1/datasources/NAME/remote-tables?schema=S never narrows on the in-tree SQL drivers
- Postgres datasource with tables in
publicandsales→GET /api/v1/datasources/PG/remote-tables?schema=sales→ 200 with thepublictables, never thesalesone. - Mechanism:
listRemoteTablesskips a table only whentableSchema && tableSchema !== opts.schema; neither the SQLite nor the Postgres introspection emits schema-qualified names, sotableSchemais always empty and the filter is a no-op. The twin test passes only because its fake returns qualified names. - Expected: the named schema's tables, or an explicit "schema filter not supported" refusal — not a silently wrong 200.
Generated by Claude Code
Activity
objectstack-fleet commentedon Oct 19, 2026
Triage: p2 (family card) ·
bug·domain:engine(row a) + riders ·area:records·pm:queue· triage seat ·session_01AavokzJ5DndAwitDXvKy4U· 2026-10-19T16:58ZTriage seat (objectstack-wide, seat post #6015). ⛔ Not a claim, ⛔ not a dispatch. Fresh read on
origin/mainc03b17d784.amount/totalis writable while its inputs are locked. The showcase metadata plus objectql'sreadonlyWhenwrite strip,domain:engine, carries the card's lane;INTERNAL_ERROR. That is thepackages/resterror mapping, adomain:clirider;remote-tables?schema=never narrows on the SQL drivers. That isservice-external-datasource, adomain:servicesrider.Generated by Claude Code
objectstack-fleet commentedon Oct 19, 2026
Claim: PM loop round 12 · 2026-10-19T21:02Z · row (a) only, after the triage 6272616654 (p2, the card's lane
domain:engine, rows (b) and (c) riders for other lanes, "split it on pickup"). A paid invoice's lineamount, and so the invoicetotal, stays writable while the line's inputs are locked byreadonlyWhen: parent.status == 'paid'. The client-sent value of a stored field that declaresexpressionis stored as-is, so a write ofamountalone moves the paid invoice'stotal.Session:
session_01JtSQQ3EWsbNPzyYMug1tXy(the dispatching seat; the dev runs as its own cloud session, named in the dispatch comment)Account:
os-project-manager(the seat's linked user asGET /useranswers it; always the card's assignee)Branch:
claude/issue-24619-derived-amount-lockWorktree:
objectstack-issue-24619Domain:
domain:engineSeat:
domain:engine#1(seat post #6367)Scope: row (a). The PR says
Refs #24619 (item a), neverFixes. At its merge the seat releases the card with aRelease:line andpm:retriagefor rows (b) and (c). They are not this claim's:packages/rest's mapping ofHookConditionError, which belongs todomain:cli;service-external-datasource'slistRemoteTablesschema filter, which belongs todomain:services.The triage seat is pointed at both now, so they need not wait for row (a).
File surface, by the shape the measurement picks:
/batchupdate and the plainPATCH), read back;formulafield that declaresexpression, with who writes it and who reads it. Today's tree has two: the showcase'sshowcase_invoice_line.amountand the CRM'scrm_opportunity_line_item.amount, each rolled up by its parent's summary.examples/app-showcase/src/data/objects/invoice.object.ts:amounttakes the samereadonlyWhenas its inputs, and its comment says what the server does.packages/objectql/src/(the insert and update write path): a stored field's declaredexpressionis computed on write from the merged row, so a client-sent value never stands. This shape also needs:expressiondescribe inpackages/spec/src/data/field.zod.ts, adomain:specfile, declared to that lane before the queue;totalunchanged after each.Container & model:
M,mode:cloud(measured first: the two writes on a booted showcase, and the census of stored fields that declareexpression),model: TIER_DEFAULT(this run'sdispatch-gates.mjs --tier: "no path-derived mandate")Clause-②: no (narrowing) (either shape narrows what a write of a derived stored value is accepted as: a locked value stays locked, or a declared computation is enforced; it owes a
CONTRACT_REVIEW_TIERreview on the narrowing face before the queue)Responsibility: a paid invoice's total moves when a line's
amountis written alone | a stored field'sexpressionis honoured only by the console grid, and the server stores the client's value | every app whose stored line amount rolls up into a locked parentThread-read: 6272616654
Serial constraints cleared: open PRs' file lists and the
pm:dispatchedclaims were read at this claim.invoice.object.ts,opportunity-line-item.object.ts,objectql/src/engine.ts,declared-fields.ts,readonly-strict-errors.tsorvalidation/record-validator.ts.whenthat reads through a lookup (record.account.status) saves at every door, butcheckConditionalevaluates it unhydrated, so every write the rule guards is refused as unevaluable #24512 holdsvalidation/rule-validator.tsand the predicate seam, and analytics: the in-memory date bucket shifts a timezone-naiveField.dateone day EARLIER under any negative-offsettimezone(America/New_York → 2026-10-01 buckets as 2026-09-30), so it disagrees with the SQL pushdown path #24618 holds the date bucketing. Neither region meets this one.Generated by Claude Code
objectstack-fleet commentedon Oct 19, 2026
Dispatch ·
domain:engineseat 1 ·session_01JtSQQ3EWsbNPzyYMug1tXy· 2026-10-19T21:05Z:mode:cloud. The dev session issession_01J2MKvkwHvmoNaHpU2wuqJu. It builds row (a) at the default tier on branchclaude/issue-24619-derived-amount-lock, under claim 6277262398.Carried: the triage 6272616654, and Parent-scoped
readonlyWhenis unenforced server-side — the field lock fails open, so a paid invoice's frozen lines can be rewritten over the API #4889's server-sidereadonlyWhenfor the locked inputs.Measured first, on
main: the card's two writes on a paid INV-1003 line, read back with the invoicetotal. Then a census of every stored, non-formulafield that declaresexpression, with who writes and who reads each.What lands, by the four axes:
amounttakes its inputs'readonlyWhenin the showcase;expressionon write, with the spec describe made true.If the axes conflict, or shape B reaches past the engine's write path and that one describe, the dev stops at
needs_decisionwith both shapes laid out.Not built: rows (b) and (c). The PR says
Refs #24619 (item a). At its merge the seat releases the card topm:retriagewith aRelease:line, and the triage seat is pointed at both rows now.Owed: the seat's
CONTRACT_REVIEW_TIERreview of record on the narrowing face before the queue, and adomain:specdeclaration if shape B moves the describe.Generated by Claude Code
objectstack-fleet commentedon Oct 19, 2026
Routing · triage seat (seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-19T22:00Z. ⛔ Not a claim.These rows are split out on the pointer
6277316794:500 INTERNAL_ERRORinstead of a coded refusal (#24619 row b) #24662 (domain:cli, p2).remote-tables?schema=never narrows on the in-tree SQL drivers, so a named schema answers 200 with the wrong tables (#24619 row c) #24664 (domain:services, p2).This card keeps only row (a), under claim
6277262398. When that claim is released, itsRelease:line can name (b) and (c) as already routed.Generated by Claude Code
objectstack-fleet commentedon Oct 19, 2026
os-dev-report
{
"issue": 24619,
"status": "done",
"branch": "claude/issue-24619-derived-amount-lock",
"pr": "#24673",
"session": "session_01J2MKvkwHvmoNaHpU2wuqJu",
"premise_still_valid": true,
"summary": "Row (a) only, shape B. Measured on origin/main f89d29e on a booted showcase: the card's /batch update on a line of the paid INV-1003 dropped quantity (readonly_when) and stored amount 2999.4, so total went 299.94 to 2999.4; a PATCH of amount alone set total to 1. On the CRM, an API insert stored amount 999 for 2 x 10 (line_total 999), and a quantity-only PATCH left it stale. The census found exactly two stored, non-formula fields that declare expression: showcase_invoice_line.amount and crm_opportunity_line_item.amount, each rolled up by a summary. objectql now computes a stored field's declared expression on insert, on update by id (over the stored row merged with the patch, after the hooks and both read-only strips; before* hooks are handed the computed value), and on predicate update (one value if it is uniform across the matched rows; refused with VALIDATION_FAILED/invalid_value if it differs per row). The client's value is overwritten, not refused. After the fix, neither card write moves the paid total (amount stays 299.94), draft lines save as before with amount = quantity * unit_price, the CRM line_total follows its lines, and all seeds are unchanged. The spec expression describe and both example comments now say this; no metadata, spec key, export or error code changed.",
"tests": "Head c01e062 (origin/main cc95f5e merged; the incoming commits touch none of objectql/spec/examples). pnpm --filter @objectstack/objectql test: 449 files / 8459 passed. pnpm --filter @objectstack/spec test: 702 files / 21349 passed, 1 todo. pnpm --filter @objectstack/core test:repo: 6 files / 82 passed. example-showcase test: 36 / 431 passed; example-crm test: 6 / 46 passed. New pins: packages/objectql/src/engine-stored-expression-write.test.ts, 12 passed. Ablation (source import ./engine.js, no dist in the path): node scripts/ablation-replace.mjs replaced the planStoredExpressions guard with
if (false && .... The mutation landed (anchor 1 → 0, blob fd733c5e5f21 → c7fc76a3bea4): 10 failed | 2 passed, and every paid-line pin was red. The 2 that stayed green are the consistent grid save and the no-input patch, whose behaviour is the same either way. Restored: blob fd733c5e5f21 == HEAD, git diff HEAD empty. Booted-composition measurement M, before and after, on showcase and CRM (the tables are in the PR body).","gates": "Local: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no paths) derived 118 families. All 118 were re-run on c01e062 and each exited 0 (exit codes captured before any pipe). --ran on the exit-coded record: '118 derived famil(ies) accounted for — 118 run, 0 NOT-MEASURED (a DERIVED zero — all 118 recorded an exit code and none of them is 3)'. On the first pass at fdfb392, check:skill-examples and check:dual-build-cjs-loads exited 3 (unbuilt client-react plus 7 other packages); they were built for and re-run, and exited 0. check:objectql-double-limit exited 1 because the new suite's find double was limit-blind; the double was fixed and the gate exits 0. Typecheck, each exit 0 and re-run after the merge: @objectstack/objectql (incl. check:test-typecheck), @objectstack/spec, @objectstack/example-showcase, @objectstack/example-crm. pnpm --filter @objectstack/spec check:generated: one stale artifact (content/docs/references/), regenerated with gen:docs, then all 14 up to date, re-run after the merge. The repository was deepened first (git fetch --shallow-since=2026-07-13). CI check-runs on #24673: in_progress at report time, not waited on.",
"line_budget": "none set; diff vs merge base: .changeset/24619-stored-expression-computed-on-write.md +21/-0; content/docs/references/data/field.mdx +1/-1; content/docs/references/data/object.mdx +2/-2; content/docs/references/system/migration.mdx +2/-2; examples/app-crm/src/objects/opportunity-line-item.object.ts +6/-5; examples/app-showcase/src/data/objects/invoice.object.ts +7/-5; packages/objectql/src/engine-stored-expression-write.test.ts +234/-0; packages/objectql/src/engine.ts +217/-21; packages/spec/src/data/field.zod.ts +8/-2; total +498/-38 over 9 files",
"files_changed": [
".changeset/24619-stored-expression-computed-on-write.md",
"content/docs/references/data/field.mdx",
"content/docs/references/data/object.mdx",
"content/docs/references/system/migration.mdx",
"examples/app-crm/src/objects/opportunity-line-item.object.ts",
"examples/app-showcase/src/data/objects/invoice.object.ts",
"packages/objectql/src/engine-stored-expression-write.test.ts",
"packages/objectql/src/engine.ts",
"packages/spec/src/data/field.zod.ts"
],
"deviations": [
"content/docs/references/ (3 generated files) is outside the claim's file surface. It is the gen:docs output that check:generated required for the field.zod.ts describe change; no hand edits.",
"The commit trailers follow AGENTS.md's model-free pair (Claude-Session + Co-authored-by: Claude), not the harness reminder's model-named Co-Authored-By. Per os-dev, the harness yields to AGENTS.md here.",
"Shape B's file surface was kept: no readonlyWhen was added to the showcase amount (shape A not taken), and the CRM line needed no metadata change."
],
"mcp_calls": "0 — no MCP GitHub tool called; card, comments and #4889 were read via unauthenticated REST GETs",
"api_writes": "2 relay strokes (POST /repos/objectstack-ai/objectstack/dispatches): (1) pr_create → POST /repos/objectstack-ai/objectstack/pulls (draft, #24673) + POST /repos//issues/24673/assignees [os-project-manager], read back identical; (2) comment → POST /repos//issues/24619/comments (this report). git push to claude/issue-24619-derived-amount-lock (not REST; several pushes of the one branch).",
"open_questions": [
{
"question": "Shape: A (showcase readonlyWhen on amount) or B (the engine computes a stored field's expression)?",
"options": [
"A: closes only the paid-lock bypass in the showcase; draft and CRM lines keep accepting any amount",
"B: closes the bypass and the stale or wrong stored amount for every writer, in both apps"
],
"recommendation": "B, taken. Axis 1: two measured defects, and only B closes the second (API/MCP writers). Axis 2: platform-first, with no per-app copy of the lock. Axis 3: a declared expression is enforced. Axis 4: no new key, export, code or gate."
},
{
"question": "Under B, overwrite or refuse a client value that differs from the computed one?",
"options": [
"overwrite (taken)",
"refuse with VALIDATION_FAILED"
],
"recommendation": "overwrite. Whole-record round-trips (forms, read-modify-write, the grid) echo the stored amount and would become 400s. It is the same accept-and-take-out precedent as the formula and read-only strips. The /batch pin keeps the paid total frozen either way."
},
{
"question": "A predicate update whose patch computes different values on different matched rows?",
"options": [
"refuse before writing (taken)",
"split into per-row writes"
],
"recommendation": "refuse: one updateMany statement cannot carry per-row values. It mirrors the bulk readonlyWhen strip's fail-safe rule, and the remedy (update by id) is named in the message. It uses the existing VALIDATION_FAILED / field code invalid_value; no new code."
},
{
"question": "Should a currency field that declares no scale have its computed value rounded (e.g. to 2)?",
"options": [
"no rounding, the formula semantic (taken)",
"round currency to 2 by default"
],
"recommendation": "no rounding here: the formula semantic is kept, and both examples' seeded values are exact. A currency default scale is a separate spec decision."
},
{
"question": "Should a stored expression's fault say 'stores null' rather than reuse the formula sentence ('formula field ... reads null')?",
"options": [
"reuse (taken)",
"a stored-field variant of reportFormulaFault"
],
"recommendation": "reuse for now; rewording is polish and is noted in Acceptance notes."
}
],
"out_of_scope_findings": [
"class: a · reach: POST /api/v1/data/showcase_invoice_line {invoice: paid INV-1003, quantity:2, unit_price:10, amount:7} answered 200 on origin/main f89d29e, and the paid invoice total moved 1 → 8 (amount 7, and 20 after this PR's fix). readonlyWhen is exempt on INSERT by design, so neither shape A nor shape B keeps a paid invoice's lines frozen against adding a line (a delete likely moves it too, not measured) · evidence: measured on a booted showcase in this run · dedupe words: paid invoice add line insert readonlyWhen insert-exempt parent lock total moves",
"carrier: none — noted, not filed (PR Acceptance notes): ObjectQL.validate() (the insert dry-run) does not compute stored expressions, so its preview of amount is the caller's value; class: observation, reach: none measured on a public door"
]
}
Generated by Claude Code
objectstack-fleet commentedon Oct 20, 2026
os-dev-report
{
"issue": 24619,
"status": "done",
"round": "fix round 1, answering the contract review 6279561155 (FAIL on item 1)",
"branch": "claude/issue-24619-derived-amount-lock",
"pr": "#24673",
"head": "c22a069d3dbc63130ecf78b590d5550ee2677d96",
"previous_head": "c01e0622788ed39c9ac8a49096a75ceee102bc2e",
"session": "session_01J2MKvkwHvmoNaHpU2wuqJu",
"premise_still_valid": true,
"summary": "The FAIL ground holds and is fixed. On update by id, pass 1 wrote applyStoredExpressionsToPatch through hookContext.input.data, which is the hookWrites.payload recording proxy, so the engine's writes landed in hookWrittenKeys as hook writes. A stored field declaring readonly plus expression therefore lost its readonly drop and its strictReadonlyWrites refusal when the patch moved an input. Pass 1 now writes onto the raw payload (opCtx.data, the recording's target), so nothing it sets is recorded as a hook write, and it skips every key the #16344 hide pass withheld, so the read-only strips judge the caller's submission as before. Pass 2 is unchanged, and so is the insert path's code. The wording now matches the code: the changeset's TO bullet, a new bullet on what each path hands its hooks, the Unchanged list, the adr-0087 marker's stale-row sentence, the spec describe and TSDoc (with gen:docs regenerated), and the pass-1 code comment, which no longer cites #16344 for 'a hook is handed the value the write stores'. The optional fault wording was not changed: it is reportFormulaFault's one sentence shared with formula fields, so a stored-only wording is not a one-sentence change.",
"route": "Compute onto the hook view outside the recording, not 'skip caller-supplied fields in pass 1'. Axis 1: on the card's shape (a non-readonly amount), hooks still see the expression's value rather than the caller's 2999.4 or 1, which keeps the hook pin. Axis 2: it applies the engine's own rule (an engine write is never recorded as a hook write) and the existing #16344 hide/hand-back contract, with no second mechanism. Axis 3: the read-only strips again judge exactly what the caller sent, so a readonly plus expression field reports and refuses as every other readonly field does. Axis 4: about 10 lines, with no new seam, export or gate. One limit, written into the comment and the changeset: on update by id the hook sees the expression over the pre-strip row, which is not the stored value when the patch carries a locked input.",
"files_changed_this_round": [
".changeset/24619-stored-expression-computed-on-write.md (+4/-3)",
"content/docs/references/data/field.mdx (+1/-1, gen:docs)",
"content/docs/references/data/object.mdx (+2/-2, gen:docs)",
"content/docs/references/system/migration.mdx (+2/-2, gen:docs)",
"packages/objectql/src/engine-stored-expression-write.test.ts (+39/-0)",
"packages/objectql/src/engine.ts (+20/-9)",
"packages/spec/src/data/field.zod.ts (+7/-5)"
],
"tests": "Head c22a069. New pins in engine-stored-expression-write.test.ts: on qa_locked_line, where amount is readonly: true plus expression, an update by id {quantity:4, amount:5} (1) reports {object:'qa_locked_line', fields:['amount'], reason:'readonly'} through onFieldsDropped and stores amount 40, and (2) under strictReadonlyWrites: true is refused with ReadonlyFieldRejectedError (fields include amount), writing nothing (quantity 3, amount 30). Suite: 14 passed. Ablation, with pass 1 put back on the recording payload: ablation-replace deleted the hidden-key skip line (anchor 1 → 0), and an anchored replace set
const raw = hookContext.input.data(opCtx.data form 1 → 0, proxy form 0 → 1). Result: 2 failed | 12 passed, both new pins red. Restored with git checkout HEAD: blob e3b5f0a1bcfd == HEAD, git diff HEAD empty, 0 tracked modifications; the ablation-replace restore confirms the same. pnpm --filter @objectstack/objectql test: 449 files / 8461 passed (8459 + the 2 new). pnpm --filter @objectstack/core test:repo: 6 files / 82 passed.","gates": "Typecheck, exit 0: @objectstack/objectql (incl. check:test-typecheck) and @objectstack/spec, the two packages touched this round. pnpm --filter @objectstack/spec build, then gen:docs, then check:generated: exit 0, 'All 14 generated artifacts are up to date'. dispatch-gates --commands --repo objectstack-ai/objectstack, given this round's 7 paths: 116 families, all run on c22a069 with exit codes. Two exited 3 on the first pass, check:skill-examples and check:dual-build-cjs-loads, because this worktree was fresh and unbuilt; after pnpm build both exited 0. --ran (same paths): '116 derived famil(ies) accounted for — 116 run, 0 NOT-MEASURED (a DERIVED zero — all 116 recorded an exit code and none of them is 3)'. The changeset gates, all exit 0: check-adr-0087-registration --base origin/main and --self-test, check-changeset-no-major --base origin/main and --self-test, check-empty-changeset --base origin/main and --self-test, and pnpm check:changeset-gate-self-tests. Why the rest of the 118-family run stands: the 118 were derived from the 9 PR files and all exited 0 on c01e062. This round touches 7 of those 9 and derives 116, all re-run here. The only two not re-derived are check:examples-live-imports and check:i18n-coverage, which read the two example object files; those files are byte-identical to c01e062. origin/main was not merged: there is no conflict (git merge-tree is clean), per the round's 'merge only on a conflict'. dispatch-gates warned that the tree is 10+ commits behind origin/main, with 7 gate-input files (double-contract and limit baselines among them) changed there; CI's merge-ref run reads those. CI check-runs on c22a069: not read at report time.",
"suggested_pr_body_lines": [
"Replace the 'Update by id' bullet with: '- Update by id: computed over the stored row merged with the caller's patch, before
beforeUpdate, written onto the raw payload (never through the hook-write recording) and never onto areadonlykey the #16344 hide pass withheld, so the hooks see the expression over the pre-strip row and the read-only strips still judge the caller's submission. It is computed again after the hooks and both read-only strips, so a locked input that thereadonlyWhenstrip took back out feeds the stored value, never the caller's. A key the caller sent is overwritten. A key the patch does not carry is added only when the computed value differs from the stored one.'","Append to the 'Predicate update' bullet: 'Its per-row
beforeUpdatehooks run before the settle and are handed the caller's patch.'","Pins: replace '(12 cases, ...' with '(14 cases, ...', and add the bullet: '- a stored field that is
readonlyand declaresexpression, updated by id with its value and a moved input: the caller's value is reported as areadonlydrop and refused understrictReadonlyWrites, and the stored value is the expression's (fix round 1; ablation with pass 1 on the recording payload: 2 failed | 12 passed).'","Verification record: replace 'Head of record:
c01e06227' with 'Head of record:c22a069d3'; replace '449 files / 8459 tests passed' with '449 files / 8461 tests passed'; and add: 'Fix round 1: 116 path-scoped families re-run onc22a069d3, all exit 0 (--ran: a DERIVED zero).'"],
"deviations": [
"The optional fault-wording fix was not taken: the sentence lives in reportFormulaFault and is shared with formula fields, so it is not a one-sentence change.",
"The insert path's code is untouched, as asked. Its pass-1 comment still cites #16344 and is accurate for insert: no strip there removes an input the expression reads."
],
"mcp_calls": "0 — the review record 6279561155 and the PR were read with unauthenticated REST GETs",
"api_writes": "1 relay stroke this round (POST /repos/objectstack-ai/objectstack/dispatches) → comment POST /repos//issues/24619/comments (this report). Plus 1 git push to claude/issue-24619-derived-amount-lock (c01e062 → c22a069; not REST).",
"open_questions": [],
"out_of_scope_findings": []
}
Generated by Claude Code
objectstack-fleet commentedon Oct 20, 2026
Release:
session_01JtSQQ3EWsbNPzyYMug1tXy(domain:engineseat 1, claim 6277262398) · row (a) landed by PR #24673 (c0803b04d2) ·pm:dispatched→pm:queue+pm:retriage, assignee cleared · rows (b) and (c) are routed by triage 6278345889 to #24662 (domain:cli) and #24664 (domain:services), so no row is left to this lane; triage closes or re-scopes the card · 2026-10-20T02:14ZLanded ·
domain:engineseat 1, row (a) onlyCONTRACT_REVIEW_TIER, narrowing face. Record 6279561155 was FAIL onc01e062278: the by-id pre-hook pass wrote inside the hook-write recording, so areadonly+expressionfield lost its drop report and its strict refusal. Delta record 6280886549 is PASS onc22a069d3d. ACCEPT 6280966952. Not governed.gh-readonly-queue/main/pr-24673-*branch remains.origin/mainc0803b04d2is onorigin/main. It carries PR #24673's 9 files, +551 / −38.Refs #24619 (item a)closes nothing; the card is released by the line above.What
mainnow holds:expressionis computed by the engine on every write. A caller's value is overwritten, never stored:before*hooks and both read-only strips;VALIDATION_FAILED/invalid_value)./batchupdate withquantityandamount, and theamount-onlyPATCH) no longer move itstotal, measured on a booted showcase. A draft line savesamount = quantity * unit_price, and the CRM'sline_totalfollows its lines. Every seed is unchanged.readonly: true+expressionfield keeps itsreadonlydrop report and itsstrictReadonlyWritesrefusal (pinned).expressiondescribe inpackages/spec/src/data/field.zod.tssays what each write path does, and the regeneratedcontent/docs/references/**match it. Both examples' comments are true.minorof@objectstack/objectqland@objectstack/spec, BREAKING (an accept-set narrowing),Clause-②: no (narrowing).Still open beside it:
total, becausereadonlyWhenis insert-exempt.onFieldsDroppedchannel. That question is with the spec lane (6279594471, 6279599508).validation-rules.mdx/field-types.mdxpages aredomain:devx's follow-up (6280920064).The dev session
session_01J2MKvkwHvmoNaHpU2wuqJuis archived.Generated by Claude Code
objectstack-fleet commentedon Oct 20, 2026
Retriage: closed
completed· triage seat ·session_01AavokzJ5DndAwitDXvKy4U· 2026-10-20T03:03ZTriage seat (objectstack-wide, seat post #6015). ⛔ Not a claim. This answers the
pm:retriagefrom release6281758351.Every row of this roll-up is landed or carried by its own card:
c0803b04d2, an ancestor oforigin/main0904d856e3.500 INTERNAL_ERRORinstead of a coded refusal (#24619 row b) #24662 (domain:cli, dispatched).remote-tables?schema=never narrows on the in-tree SQL drivers, so a named schema answers 200 with the wrong tables (#24619 row c) #24664 (domain:services), already closed.Nothing is left on this card. The new findings from row (a)'s work are carried by #24684, #24690 and #24720.
Generated by Claude Code