Repository navigation
fix(showcase): a paid invoice's lines can no longer be added, removed or moved, so its total stays what was paid - #24731
Conversation
…nd moving Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FmarW2qJYp6PYJJz9cpvK5
…ne lock Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FmarW2qJYp6PYJJz9cpvK5
…id-header-line-lock
Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FmarW2qJYp6PYJJz9cpvK5
Contract reviewServed-tier: Read on the narrowing face, adversarially, from the card #24684 (body and all six comments: triage 6280795779, claim 6280841440, dispatch 6280866009, needs_decision 6281203874, ruling 6281609466, done 6282206376), #4889, #19853, the PR body and file list, the net diff against the merge-base ① Derived judgmentsThe three locks — right on every door that reaches
The exemptions — each as wide as stated, with one width the dev understates.
The header read — right, and the "fail closed" claim holds in the way the prelude states it.
The draft control — right. The move guard's condition is false on an update that leaves The pins — teeth confirmed from the code. The five refusal pins assert The comments — true about where each lock lives; one stale line beside them. ② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Seat note ·
|
…id-header-line-lock # Conflicts: # docs/qa/platform-checklist/areas/records-forms.json
…stem writers pass the paid-line guards Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FmarW2qJYp6PYJJz9cpvK5
Seat delta note ·
|
席位核对 ·
|
| Item | Reading |
|---|---|
| Shape | Draft PR → main. The body opens Fixes #24684. 5 files, +386/−8. Not governed: check-governed-merges matched 0 of 5 paths. |
| Scope | Three lifecycle hooks on showcase_invoice_line refuse, each with a sentence (400 on /data), when the invoice is paid: adding a line (beforeInsert), moving a line onto or off the invoice (beforeUpdate, gated on record.invoice != previous.invoice), and removing a line (beforeDelete). Each reads the header under runAs: 'system'. A system write and the header's own cascade delete pass. Measured on a booted showcase: before, an insert moved INV-1003's total 299.94 → 306.94, a delete moved it to 7, and a repoint moved it both ways. After, each is refused and the total stays 299.94. Draft lines insert, edit, move and delete as before. No platform change (ruling 6281609466). |
| Seat's own read | Checked on the base head (seat note on 6282567187): the action envelope's isSystem elevation, the engine-private cascade marker, the three guards' gates, and the cookie jar's absence from the tree. The fix round was read in-seat (delta 6283767171): behaviour is byte-identical, the checklist is renumbered to revision 8 with #24662's revision 7 kept, and the two nits are true. The PR body's read-only-strip sentence was patched. |
| Review | CONTRACT_REVIEW_TIER, narrowing face. Record 6282567187 is PASS on 0aa9da1b31; the seat's delta 6283767171 carries it to this head. |
| Declaration | One changeset, @objectstack/example-showcase patch (a private package; privatePackages.version), with Clause-②: no (narrowing) at a line start and adr-0087: not-required (unpublished). No export from a published package, no error code, no spec key, and no file under packages/**. |
| Cross-lane | domain:devx, for docs/qa/platform-checklist/areas/records-forms.json (roster-only, no clause moved): 6282575431 (#6023) and 6282580043 (#20163), naming the serial order behind PR #24714, which is now kept. No objection on the card or the seat posts. |
| Gates | Dev at aaec016144: 58 derived, 58 run, all exit 0. check:platform-checklist: OK. Showcase suite 443 passed; core test:repo 82 passed. Ablation (base round): the five paid pins red, restored to HEAD. |
| CI | At this read on aaec016144: ci-failure GREEN, all 39 check-runs completed, none failed. |
Disposition:
open_questions: both taken as the dev settled them, per record 6282567187 ③: the system-write exemption passes, and the cascade delete passes. The exemption's width is now stated in the hook header.deviations: all accepted (record ③ and delta 6283767171).- The checklist file was outside the fence, and is declared to its lane.
- The pins assert the error envelope; the wire status was measured on a boot.
- The cookie jar
cj-38921held a dead--freshlocalhost session. It is absent from the net tree, and the queue lands a squash. - The static-strip wording in the code comment.
out_of_scope_findings:- A sandboxed hook's refusal carries no
codeon the wire. That is ADR-0112's declared shape (record ③); not filed. - The lint trap, where a
parent-readingvalidations[]rule refuses every write, is lint: avalidations[]rule whose condition readsparentpassesos validate, but novalidations[]rule bindsparent, so every write under it is refused as unevaluable #24720 (nowdomain:spec).
- A sandboxed hook's refusal carries no
Landing: ordinary queue (pr_ready + automerge_enable through the relay). Fixes #24684 then closes the card.
Generated by Claude Code
Fixes #24684
Clause-②: no (narrowing) (the showcase refuses two writes it accepted; if the measurement shows the platform half would relax a refusal a faulting rule makes today, the dev stops and reports before building it, and the seat re-declares)
Built under the seat ruling 6281609466 on #24684 (route C, so B now), after the dev's
needs_decision6281203874. Noobjectql, lint or spec change: the lint trap measured on the way is #24720.What changes
A paid invoice's lines are now locked against being added, removed and moved, not only edited, so its
totalstays what was paid.readonlyWhen: parent.status == 'paid'on a line'sproduct,quantityandunit_pricestill locks edits. It is insert-exempt by design (Parent-scopedreadonlyWhenis unenforced server-side — the field lock fails open, so a paid invoice's frozen lines can be rewritten over the API #4889) and judges field values, not whether a line exists.showcase_invoice_line(examples/app-showcase/src/data/hooks/index.ts) hold the rest. Each reads the header invoice underrunAs: 'system'and refuses with a sentence:showcase_guard_paid_invoice_line_add(beforeInsert): a new line under a paid invoice.showcase_guard_paid_invoice_line_move(beforeUpdate, gated byrecord.invoice != previous.invoice): a line moved onto or off a paid invoice. The repoint was measured first and is admitted onmain(below), so the ruling's guard is owed.showcase_guard_paid_invoice_line_remove(beforeDelete): a line removed from a paid invoice, by id or by a bulk predicate.session.isSystem), because the seed loads INV-1003's line under a header that is already paid. The platform's staticreadonlystrip exempts system writes the same way (readonlyWhenexempts no one). The exemption is every system writer, not only the seed: a script action'sctx.apiand arunAs: 'system'flow pass too. None of them writesshowcase_invoice_linein the showcase today.ctx.referentialCascadeDelete), because that is the header itself being deleted, not a line removed from a header that stays.invoice.object.ts'sproductcomment now says where each lock lives. Theamountcomment PR fix(objectql)!: a stored field's declared expression is computed on every write, so a paid invoice's line amount and total stay frozen #24673 edited is untouched; that PR landed first and is merged in.docs/qa/platform-checklist/areas/records-forms.jsonrecords-forms.object-hook-lifecycle, rev 7: fixture roster only, no clause moved. Three stated facts the new hooks made false ("the five seeded showcase hooks", "beforeDelete has NO seeded showcase hook", "the one seeded hook that throws") now name them. This file is outside the claim's file surface; it is here because the change made its text false.Why hooks, not a
validations[]rule: measured onmain, novalidations[]rule bindsparenton any write. A rule readingparent.statusfaultedUnknown variable: parentand refused every insert and update, the draft header's and the seed's included. Andvalidations[]never runs on delete.ValidationRuleSchema.eventsprescribes lifecycle hooks for that case.Verification record
Measured on a booted showcase (
pnpm dev -- --fresh -p PORT, seeded admin).Before, at
main8e14ce0 and 54159fd:invoiceto INV-1003invoiceto a draftAfter, at this head
0aa9da1b3:error: "This invoice is paid, so its lines are locked: a line cannot be added to it."quantityon INV-1003's linereadonlyWhenas beforeThe boot logs no ERROR line: the seed's paid line still loads.
Pins.
examples/app-showcase/test/paid-invoice-line-lock.test.ts, 12 tests, on the production harness the reopen-guard pin uses (realObjectQL,SqlDriver,QuickJSScriptRunner, the app's real objects and hooks).SandboxErrorwhoseinnerMessageis the exact sentence (the field the/datadoor serves as the 400 above), plus the invoice's line count andtotalread back unchanged.Ablation.
node scripts/ablation-replace.mjsdeleted the three hooks fromallHooks(anchor 1 to 0, blobeeb196a2115dtofb951071b9d5).eeb196a2115d, equal toHEAD, andgit diff HEADempty.Local runs.
origin/maind20bf9b: rebuilt withturbo run build --filter=!@objectstack/docs --concurrency=2, 76/76.pnpm --filter @objectstack/example-showcase typecheck: exit 0.tsc --listFilesincludes the new test and the hook module.pnpm --filter @objectstack/example-showcase test: 37 files, 443 tests passed.pnpm --filter @objectstack/core test:repo: 6 files, 82 tests passed.os validate(showcase): exit 0.Gates.
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, no paths, at0aa9da1b3: 58 derived lines, each run with its exit code captured before any pipe, all exit 0.--rananswers: "58 derived famil(ies) accounted for — 58 run, 0 NOT-MEASURED (a DERIVED zero — all 58 recorded an exit code and none of them is 3)".Changeset.
.changeset/24684-showcase-paid-invoice-line-lock.md:'@objectstack/example-showcase': patch.private: true. It is still versioned (privatePackages.version: true), and showcase-only fixes carry apatchchangeset by precedent.check-changeset-no-majorpasses the level axis on the declaredno (narrowing).check-adr-0087-registrationpasses withnot-required (unpublished).Acceptance notes
/dataas400with the sentence inerrorand nocodefield. That is the platform's mapping for every hook refusal, the reopen guard's too. These hooks throw a plain sentence, the showcase's established shape, so no error code is invented here.--freshlocalhost server, so a dead credential) was committed in 6b26999 and removed in 0aa9da1. A history rewrite was not done.Generated by Claude Code