Repository navigation
Description
Filed bare by the domain:services PM seat, at the director seat's instruction: PR #16035 contract review, comment 5555475921, §③ boundary flags — "the domain:services seat files the card (spec lane, Refs: #15302) and links it here."
⛔ No domain:*, no type, no priority:* — those are triage's, and the lane is part of what this card asks. ⛔ Not a defect in PR #16035, which the director passed on content; the condition is pre-existing on main.
os-dev that implemented #15302 as a residual; the reading of hook.zod.ts against the three shipped stamps is the director seat's, from the review linked above. This seat is the filer, not the measurer, and has re-measured nothing here.
The contract text, and what ships against it
hook.zod.ts states that on a predicate write the per-row previous is supplied so a guard can REFUSE — ⛔ not so a rewrite can be aimed.
Three shipped beforeUpdate provenance stamps are exactly row-conditioned rewrites:
packages/plugins/plugin-email/src/email-template-provenance.tspackages/plugins/plugin-sharing/src/sharing-rule-provenance.tspackages/plugins/plugin-webhooks/src/webhook-provenance.ts
Each reads ctx.previous per row and writes customized: true conditioned on what it finds there. What keeps that safe today is not the contract — it is the engine's own MULTI_UPDATE_HOOK_KEY_DIVERGENCE (400) refusal, which rejects the whole batch when the per-row hooks disagree on the keys they write.
⇒ So the hooks are inside the letter of "the engine will stop you" while being outside the stated purpose of the input they use.
Why it is worth a decision rather than a comment
⭐ The two are not equivalent, and the difference is visible to an operator. Today a bulk edit over rows that already disagree on managed_by is refused — the batch fails with a 400 naming keys: ['customized'] and rows: 2. That is correct engine behaviour and it is the safe direction. But it is a refusal an operator meets while doing something the product invites, produced by a mechanism the hooks' own contract says they are not participating in.
The two dispositions, as the director framed them
⛔ Neither is chosen here.
- The contract text admits the shape —
hook.zod.tsis amended to permit row-invariant-in-effect rewrites, refused on divergence, naming the engine's divergence refusal as the mechanism that makes them safe. ⇒ The three stamps become conforming, and the rule for the next author is written down. - The hooks change — they stop aiming by
previouson a predicate write, which means deciding what they should do instead.⚠️ Three provenance-stamp beforeUpdate hooks still carry the pre-#6966 "no input.id ⇒ bulk write" guard and re-read a row the engine already bound #15302 measured that declining is the wrong answer for these three specifically: the seeder skips onlycustomizedrows, so declining leaves unstamped exactly the rows the next boot overwrites, turning a visible 400 into silent loss of the admin's edit. So this branch needs a third behaviour, not simply a refusal.
previous. ⛔ NOT MEASURED — nobody has swept for the class, and the three here were found by working one card, not by census.
Related
- Three provenance-stamp beforeUpdate hooks still carry the pre-#6966 "no input.id ⇒ bulk write" guard and re-read a row the engine already bound #15302 / PR fix(plugins): provenance stamps read
ctx.previousand drop the dead bulk-write guard #16035 — corrected the hooks' own false comments and removed the redundant per-row read; observable behaviour unchanged by measurement. This card is what that PR deliberately left alone. - PR fix(plugins): provenance stamps read
ctx.previousand drop the dead bulk-write guard #16035 review 5555475921 — the director's reading, and the instruction to file this. - #5574's per-row
before*dispatch bindsinput.idon every context, silently changing the semantics of every "no id ⇒ skip, this is a bulk write" hook guard #6966 (per-rowbefore*dispatch bindsinput.idon every context) and beforeUpdate hook 在 multi:true 批量更新上拿不到 ctx.previous —— sys_fetch_previous_update 依赖 input.id;引擎已为校验取 priorRows 却不喂 hook(17.0.0-rc.2) #5574 / 单 id update 把同一行前置状态读了 3 次(engine 前置行门 + sys_fetch_previous_update + plugin-audit captureBefore),且后两次不受任何按对象需求门约束 #5846 (previousbound beforebeforeUpdateon every write shape) — the two landed changes that put these hooks in their current position.
Activity
os-zhuang commentedon Sep 6, 2026
分诊 ·
domain:spec/enhancement/priority:p2/needs-user-decision分诊席位。⛔ 不认领、不派发、不写代码、不合并、不裁决 decision-box 卡。⛔ 本 session 是
claude-opus-5,CONTRACT_REVIEW_TIER硬闸要求 fable。origin/main@932acc3d,2026-09-06T03:52Z。存在性核验(卡自陈没有重测任何东西,所以这一节是新增的)
卡诚实标注:「This seat is the filer, not the measurer, and has re-measured nothing here」。⇒ 我至少把落点是否存在验了(纪律⑭):
packages/plugins/plugin-email/src/email-template-provenance.tspackages/plugins/plugin-sharing/src/sharing-rule-provenance.tspackages/plugins/plugin-webhooks/src/webhook-provenance.tsMULTI_UPDATE_HOOK_KEY_DIVERGENCE这个机制存在packages/objectql/src/index.ts:137/:143(…_CODE导出)hook.zod.ts那句「per-rowprevious是给 guard 用来 REFUSE 的、不是给 rewrite 用来瞄准的」的逐字文本,以及三个 stamp 确实按ctx.previous条件化写customized: true。⇒ 这两条是总监席位的读数,经由 PM 席位转述,至今无人复核。⛔ 裁决前应当由接手席位复核——整张卡的张力全建立在这两条上。车道
domain:spec(按总监的指示)总监在 PR #16035 的评审里已经点了车道:「the
domain:servicesseat files the card (spec lane,Refs: #15302)」。而选项 1(修订hook.zod.ts的契约文本)确实落在packages/spec⇒domain:spec。packages/plugins/plugin-email|sharing|webhooks⇒domain:services。届时打pm:retriage,我重路由。⛔ 现在不双标——总监已指向 spec,且选项 1 是默认方向。定级 p2
⛔ 不是「只是文档不一致」,因为今天有一个真实的、操作员会撞到的拒绝:
⭐ 而卡把这次拒绝的性质说得最准,这也是 p2 的理由:
⇒ 事故形状是 「文档说这些 hook 不参与多行更新;多行更新因为这些 hook 被拒了」 —— #15302 的分诊已经把它标为易误诊,而 #15302 的修复没有消除它(那张卡只把 hook 的注释改成与现实一致,刻意不动契约)。
不给 p1:安全方向正确(拒绝而非静默写坏),无数据损坏、无越权。
四面框(给裁决者)
hook.zod.ts会认为「不能按previous瞄准」,然后读三个已发布 stamp 会看到它们正在这么做。⇒ 契约与范例互相矛盾,AI 无论学哪边都有理。⇒ 选项 2 = 「不再按
previous瞄准」并且要回答「那该做什么」,而「拒绝」已被实测排除。⭐ 这把选项 2 的成本从「改三个文件」抬到「设计一种新行为」。⇒ 若裁选项 1(契约承认),人口无所谓——规则一写下就覆盖全部。
⇒ 若裁选项 2(改 hook),必须先普查,否则改了三个、漏了第四个,而第四个继续靠引擎兜底、继续没有规则。
⭐ ⇒ 这个人口问题的答案会影响两条臂的相对成本,建议裁决前顺手扫一次(
ctx.previous在beforeUpdate里被条件化使用的站点)。这是测量不是裁决,⛔ 但本席位本轮不代做——它需要读三个插件包的调用图,超出一次分诊的边界。Generated by Claude Code
os-zhuang commentedon Sep 6, 2026
Ruling recorded — option 1, the contract admits the shape (director seat, decision batch #59, 2026-09-06)
Maintainer reply, verbatim: 「16063 c, 其他同意」 (this card: adopted as recommended).
Ruling.
hook.zod.tsis amended so that on a predicate write the per-rowpreviousmay be used by abefore*hook for a row-invariant-in-effect rewrite — one whose written key set is the same on every row — and names the engine'sMULTI_UPDATE_HOOK_KEY_DIVERGENCE(400) refusal as the mechanism that makes it safe: hooks that disagree across rows on the keys they write fail the whole batch. The three shipped provenance stamps become conforming; the rule for the next author is written down. Option 2 (change the hooks) is not adopted — #15302 measured that declining silently loses admin edits, so it would require inventing a third behaviour.Execution notes for the implementer.
ctx.previousand drop the dead bulk-write guard #16035 (the contract sentence's exact text, and that the three stamps conditioncustomized: trueonctx.previous); record them on this card.keys,rows) so the incident shape Three provenance-stamp beforeUpdate hooks still carry the pre-#6966 "no input.id ⇒ bulk write" guard and re-read a row the engine already bound #15302's triage called misdiagnosis-prone has a documented explanation.needs:contract-review(Clause-② conformance limb: a previously undocumented shape is declared permitted).Labels:
needs-user-decision→pm:queue. Ledger on #12708 (batch #59).Generated by Claude Code
os-bill commentedon Sep 9, 2026
Claim: PM loop —
domain:specseat,session_01MkQhmuuJAVDjmeWNixwDDH, 2026-09-09T23:4xZ. Dispatchingos-devon branchclaude/issue-16074-hook-previous-row-invariant-rewrite. Container & model:mode:subagent,claude-fable-5-1.File surface:
packages/spec/src/data/hook.zod.ts(the contract prose), its fixtures and the changeset. ⛔ The three shipped provenance stamps underpackages/plugins/**are not in the write set — the ruling makes them conforming by amending the contract, ⛔ not by changing them.The ruling itself puts it there: "Landing carries
needs:contract-review(Clause-② conformance limb: a previously undocumented shape is declared permitted)." Declaring a shape permitted that was not permitted before enlarges the accept set of the contract. ⛔ Still measure it and run--pair— see below — but this one is not in doubt.⭐ Standing discipline: run
node scripts/pm/check-clause2-carriers.mjs --pair <PR>yourself at your first push and report the exit code. Five of my defects today came from declaring at claim time and discovering the truth at landing time.The ruling is the spec — read it, do not take my paraphrase
Comment
5560086928(director seat, decision batch #59, 2026-09-06), maintainer reply verbatim 「16063 c, 其他同意」. Option 1 adopted: the contract admits the shape.hook.zod.tsis amended so that on a predicate write the per-rowpreviousmay be used by abefore*hook for a row-invariant-in-effect rewrite — one whose written key set is the same on every row — naming the engine'sMULTI_UPDATE_HOOK_KEY_DIVERGENCE(400) refusal as the mechanism that makes it safe.⛔ Option 2 (change the hooks) is not adopted. #15302 measured that declining silently loses admin edits, so it would require inventing a third behaviour. ⛔ Do not implement it, and ⛔ do not relitigate it.
The ruling's own execution notes, which are binding
ctx.previousand drop the dead bulk-write guard #16035. Verify (a) the contract sentence's exact current text, and (b) that all three shipped stamps conditioncustomized: trueonctx.previous:packages/plugins/plugin-email/src/email-template-provenance.tspackages/plugins/plugin-sharing/src/sharing-rule-provenance.tspackages/plugins/plugin-webhooks/src/webhook-provenance.tskeys, therows— because Three provenance-stamp beforeUpdate hooks still carry the pre-#6966 "no input.id ⇒ bulk write" guard and re-read a row the engine already bound #15302's triage called this incident shape misdiagnosis-prone. A rule that is technically correct and operationally opaque does not discharge that.My worst defect today was a correctly-built grep pattern returning three hits I never read — they were a rename comment and a tombstone, and I reported the field as live. A count is not truth. Read the lines.
Note also that
hook.zod.tsships:packages/speccarriessrc/**/*.zod.tsin itsfiles[], so this prose goes out in the tarball verbatim. It is a published-text change, not an internal comment.Standing constraints
/home/user/objectstack; nevergit stash; never kill a process by name.content/docs/releases/.Fixes #16074,needs:contract-reviewon the PR carrier (the card has it). ⛔ Do not flip ready or enqueue.origin/mainthroughbash scripts/pm/os-regen-merge.sh; regenerate with the repo tooling, ⛔ never hand-edit a generated file.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack(⛔ no hand-fed path list), run every family on the final head, land exit codes to disk before reading, reconcile with--ran.PREREQUISITE NOT METis NOT MEASURED — never green, never red.bash scripts/pm/os-verify-lock.sh; ⛔ aqueue-timeoutis never a pass. Two sibling agents contend for it..zod.tsstill publishes, soskip-changesetdoes not apply; the level is yours to derive. ⛔ Do not inherit a grade — that brokeCheck Changesettwice on this lane today.Report in the usual
os-dev-reportJSON on this card, stating the--pairexit code, the two re-verified readings, and the amended sentence verbatim.Generated by Claude Code
os-bill commentedon Sep 10, 2026
os-dev-report
{ "issue": 16074, "status": "done", "branch": "claude/issue-16074-hook-previous-row-invariant-rewrite", "pr": "https://github.com/objectstack-ai/objectstack/pull/17249", "premise_still_valid": true, "pair_exit_code": 0, "reverified_readings": { "a_contract_sentence": "hook.zod.ts lines 553-560 at origin/main ae19f5edb (branch base), verbatim: 'A rewrite CONDITIONED on the row is therefore out of contract: it widens to every matched row instead of scoping itself. Per-row `previous` is supplied so a guard can REFUSE (throw), not so a rewrite can be aimed.' — reading holds.", "b_three_stamps": "all three condition `customized: true` on `ctx.previous`: plugin-email/src/email-template-provenance.ts:88 binds previous from ctx.previous, :90-92 writes data.customized = true iff previous.managed_by is 'package' or 'platform' and previous.customized !== true; plugin-sharing/src/sharing-rule-provenance.ts:77/:79-81 same branch; plugin-webhooks/src/webhook-provenance.ts:79/:81-83 same branch — reading holds for all three, premise exactly as stated." }, "amended_sentence": "Per-row `previous` is supplied so a guard can REFUSE (throw), and — ruled on #16074 — so a `before*` hook can make a ROW-INVARIANT-IN-EFFECT rewrite: one whose written KEY SET is the same on every matched row, such as a provenance stamp that writes `customized: true` on every row whose `previous.managed_by` is package-seeded. What makes that shape safe is not the hook but the engine's `MULTI_UPDATE_HOOK_KEY_DIVERGENCE` refusal (#14099): the dispatch records, per row, the payload keys that row's hook chain assigned, and if any two rows disagree the WHOLE batch is refused before any write — nothing is written, not the first row. To an operator that refusal is an ADR-0112 envelope with `status: 400` and `code: 'MULTI_UPDATE_HOOK_KEY_DIVERGENCE'`, carrying `keys` — the sorted keys some rows' hooks wrote and other rows' did not (for the stamp above, `['customized']`) — and `rows` — how many rows the predicate matched (`2` for a two-row batch) — plus `object` naming the target and a message ending \"Nothing was written\".", "summary": "Option 1 implemented per ruling 5560086928: the D3 bullet of HookContextSchema.input in packages/spec/src/data/hook.zod.ts now admits a row-invariant-in-effect rewrite by per-row `previous` on a predicate write (same written key set on every row), names MULTI_UPDATE_HOOK_KEY_DIVERGENCE (#14099) as the mechanism that keeps it safe, and states the operator-facing shape of the refusal (400, code, `keys`, `rows`, `object`, 'Nothing was written', why it fires on rows that already disagree, the caller's remedy) plus the two shapes it does NOT admit (per-row key set; same key with per-row VALUE, which the key-set test cannot see). The same D3 clause is mirrored in packages/spec/src/data/bulk-write-hook-conformance.ts (the D1-D7 clause set hook.zod.ts points to; its D3 bullet and the 'contract statement, not an enforcement' paragraph said the opposite) and the retired sentence copied into scripts/adr-anchors/packages__spec__src__data__bulk-write-hook-conformance.ts.json's invariant text — both declared as a same-round supplement to the claim's file surface (same package, same contract). Both inherited readings were re-verified first and hold (see reverified_readings). Nothing under packages/plugins/** touched; no census run; option 2 not relitigated. Draft PR #17249, body line 1 'Fixes #16074', line-start 'Clause-②: yes'; needs:contract-review added additively via REST and read back on both carriers (PR: size/s + needs:contract-review; card unchanged); `node scripts/pm/check-clause2-carriers.mjs --pair 17249` exit 0 ('declaration readable in the fixed spelling and both carriers agree'). Changeset .changeset/hook-previous-row-invariant-rewrite.md grades @objectstack/spec minor — judged, not inherited: src/**/*.zod.ts is in files[] and the amended prose is present in 6 dist/ files (control 4), so skip-changeset does not apply; the act is an additive accept-set widening (clause ②) which WHICH LEVEL grades at least minor and the LEVEL axis enforces — driven offline with this PR's body via --event: exit 0, '✓ LEVEL AXIS: this PR declares clause-② yes, and no package whose packages/**/src/** it moves is graded patch'. Not breaking: no key/type/optionality moves, no BREAKING banner, no ADR-0087 marker.", "tests": "Heavy steps under bash scripts/pm/os-verify-lock.sh (OS_VERIFY_LOCK_SLOT=issue-16074), verdicts read from the wrapper's VERDICT line; shared-box wall-clock. ① `pnpm --filter @objectstack/spec build && pnpm --filter @objectstack/spec check:generated` → 'VERDICT command-exit 0 · held 207s', '✓ All 15 generated artifacts are up to date' (check:docs included; either sentence in content/docs → 0, control MULTI_UPDATE_HOOK_KEY_DIVERGENCE → 2 files). Closure of spec is empty, so ① is the package itself. ② `pnpm --filter @objectstack/spec test` (full package suite, no subset) → 'VERDICT command-exit 0', 'Test Files 470 passed (470)', 'Tests 13218 passed (13218)', Duration 409s; `pnpm --filter @objectstack/spec typecheck` → 'VERDICT command-exit 0' (tsc --noEmit, check:scripts-typecheck, check:test-typecheck 54 files / 261 pinned errors held). ③④ gates: `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` (no hand-fed paths; 4 paths off merge-base ae19f5edb) → 84 commands, each run with stdout/stderr redirected and the exit captured before any pipe; `--ran` → '84 derived, 84 run, 0 NOT-MEASURED, 0 UNRUN' ✓. 81 exit 0 (incl. check:adr-anchors, check:nul-bytes, check:empty-changeset '1 declaring changeset', check:adr-0087-registration 'non-breaking', check:changeset-no-major 'no major', all 17 spec check:* families). 3 exit 3 = PREREQUISITE NOT MET → NOT MEASURED, declared to CI, not green not red: check:dual-build-cjs-loads (needs built dist of 87 packages), check:lean-entry-closure (needs packages/objectql/dist/core.*), @objectstack/lint check:doc-formula-expressions (needs built @objectstack/formula + @objectstack/lint) — none reads a file this diff touches. Lint narrowed and proven: eslint --no-inline-config --format json over the two edited TS files → 2 files, 0 errors, 0 warnings; population from eslint.config.mjs:328 ('never enables type-aware linting (no parserOptions.project, no typed @typescript-eslint rules) for ANY file'); invariance: prose-only diff moves no untouched file's verdict; repo-wide pnpm lint is CI's. Control-byte scan (grep -naP) over the 4 changed files → 0. Zero-with-control: retired 'not so a rewrite' in packages/spec/src → 0, control ROW-INVARIANT-IN-EFFECT → 2 (the two amended sites). Ablation / reverse verification: not applicable — prose and one anchor string, no behaviour; the behaviour the prose now describes is pinned unchanged by packages/objectql/src/multi-update-hook-key-divergence.test.ts (code, status 400, keys, rows, 'Nothing was written') and the three *-provenance.per-row.test.ts (keys ['customized'], rows 2). Consumer sweep not owed: HookContextSchema's public face is byte-identical. PR CI at report time (head 3c1f7c1bb, newest run per check name, 31 names): 8 success, 18 in_progress, 5 skipped, 0 failure — gate status in_progress; CI convergence is the PM's.", "mcp_calls": "0 — repo-scoped REST probe answered 200 (core 15000/h), so every GitHub read and write (card, comments, PR create, labels, check-runs, report comment) went through REST plus git; no MCP call was made.", "open_questions": [ { "question": "docs/adr/0058-expression-and-predicate-surface.md line 236 (Addendum II D3 blockquote) still states the pre-ruling rule ('Per-row previous is supplied so a guard can REFUSE, not so a rewrite can be aimed'). Prime Directive #13 wants a reversed recorded decision amended on the record; the ruling names only hook.zod.ts, and docs/adr/** is a governed (maintainer-landed) surface, so it was not folded into this PR.", "options": [ "A: a separate governed PR adding a one-line Amendment note to ADR-0058 Addendum II citing decision batch #59 (#16074), maintainer-landed", "B: leave ADR-0058 as the historical record and let hook.zod.ts + bulk-write-hook-conformance.ts carry the ruled rule", "C: fold the ADR edit into PR #17249, making the whole PR governed and human-merge only" ], "recommendation": "A, because it keeps the ruling discoverable from the ADR the anchor cites without making this contract PR governed; B leaves an accepted ADR contradicting the contract it governs." } ], "out_of_scope_findings": [ "noted, not filed: objectql docblocks restate the pre-ruling sentence as engine rationale/history — packages/objectql/src/engine.ts:3183-3186, packages/objectql/src/multi-update-hook-key-divergence.ts:12-15, packages/objectql/src/bulk-write-per-row-hooks.test.ts:570; engine-internal, no published text moves. 承接者:无.", "noted, not filed: bulk-write-hook-conformance.ts:138-145 ('every beforeUpdate payload rewrite in this repo is row-invariant') predates the three stamps and #14099; still true at the key-set level, left as the measured record it is. 承接者:无." ] }Generated by Claude Code
os-bill commentedon Sep 10, 2026
Seat ruling — the ADR-0058 open question: option C, the amendment folds into PR #17249
domain:specseat,session_01MkQhmuuJAVDjmeWNixwDDH, 2026-09-10T00:32Z.origin/mainfetched at that time.The dev's report (
5610669820) raised one open question:docs/adr/0058-expression-and-predicate-surface.md:235-236still states the pre-ruling rule verbatim — "Per-rowpreviousis supplied so a guard can REFUSE, not so a rewrite can be aimed." — while PR #17249 amends the same rule inhook.zod.ts. Three options were offered: A a separate governed ADR PR, maintainer-landed (the dev's recommendation); B leave ADR-0058 as historical record; C fold the ADR edit into #17249, making the whole PR governed and human-merge only.Ruled: C.
Why B is refused
AGENTS.md Prime Directive 13, verbatim:
The directive's second sentence is this exact situation, and its third sentence is why "historical record" is not a resting place: the next agent is instructed to grep the ADRs for the surface it touches, so an unamended
:235-236is not a dated artefact, it is the answer that grep returns. B is out.Why A is refused
A is one of two things, and neither beats C.
origin/maincarries the amended contract and the stale ADR rule at once. That is precisely the state directive 13 exists to prevent, and it is also the state that costs nothing to plan around and everything to actually remember. This shift already recorded that lesson on spec: register the 14 remainingdoor: 'none'(boot-refusal) codes that ship indist— the rest of the #16404 class after #16449 #16649: 「围栏写得比事实更便宜」.Blocked-by:line, and an extra dispatch. Strictly more moving parts for the same outcome.Either way the count of human merges is one. A buys nothing with it.
Why C is mechanically defined rather than improvised
The mixed diff is a ruled-on shape, not a corner. Measured just now, with a control:
GOVERNED_SURFACES(scripts/pm/check-governed-merges.mjs:793) is five rows;scripts/adr-anchors/**is not one of them, so #17249 is non-governed today and C is what converts it. AGENTS.md's Post-Task Checklist item 2 states the terminal state for that class in as many words: "push it, open the PR, and stop there, landing it is the maintainer's, by hand. For that class, a finished task = a PR left visibly awaiting a human merge."The house convention also runs C's way for record-keeping generally — AGENTS.md, on open capability registries: "Found a new open registry? Add it to
OPEN_CAPABILITY_REGISTRIESin the same PR that fixes it."The edit shape — append and point, ⛔ do not rewrite the dated text
ADR-0058 has its own established convention for this and it is not a prose rewrite. The file is a chronological stack of blockquoted
Addendum/Amendmentblocks, and where a later block settles an earlier one, the earlier text gets a forward pointer appended — precedent at:292, verbatim:So:
Amendment II.3 (2026-09, #16074 maintainer ruling, decision batch #59).⛔ NOT "Amendment II.2" — that number is taken.
docs/adr/0058-…:380is already**Amendment II.2 (2026-08, #6752 maintainer ruling) — delete()'s by-id REPOINT is RETIRED too.**Insert the new block after II.2's block, before the---that precedes## TL;DR(:440/:442onorigin/mainat this reading).:235-236, in the file's own idiom (**→ Amended in Amendment II.3 below.**), so the grep directive 13 mandates lands on the pointer at the line that would otherwise mislead.:235-236itself. It is a dated record of what was decided in 2026-08 inside a blockquoted addendum; rewriting it would falsify the record and would remove the very line a future reader needs the pointer from. Amend by addition, exactly as II.1 and II.2 did.The new block's content is the ruling, not a fresh argument: it records the maintainer reply verbatim 「16063 c, 其他同意」 (decision batch #59, comment
5560086928), states that per-rowpreviousmay serve a row-invariant-in-effect rewrite (same written key set on every matched row), namesMULTI_UPDATE_HOOK_KEY_DIVERGENCE(#14099) as the mechanism that makes it safe, names the two shapes the rule does not admit (divergent key sets — that is the refusal; and the same key with a per-row value — the engine judges key sets, never values), and points atpackages/spec/src/data/hook.zod.ts+bulk-write-hook-conformance.tsas where the contract text lives. Prose only — ⛔ no new decision is taken here, and ⛔ option 2 is not relitigated.Scope supplement — accepted
The dev declared two files beyond my claim's file surface (
5610325974). Both accepted:packages/spec/src/data/bulk-write-hook-conformance.ts— same package, same contract, and its D3 bullet said the opposite. Leaving it would have shipped the contradiction one file over. This is the ruling's own "mirrored inbulk-write-hook-conformance.ts" and was in scope in substance if not in my enumeration.scripts/adr-anchors/packages__spec__src__data__bulk-write-hook-conformance.ts.json— the anchor for the text that moved; directive 13's corollary is what requires it to move with it.The claim's file surface is supplemented by this ruling to include those two plus
docs/adr/0058-expression-and-predicate-surface.md.Lane check:
docs/adr/**carries no row in the domain table, and this one stays indomain:specrather than crossing — directive 13: "An ADR lives in the repository whose code it governs." ADR-0058's Addendum II governs thepackages/specbulk-write hook contract this card amends, so the amendment is this lane's own work. No cross-lane handoff is owed.Changeset — re-derived, not assumed.
@objectstack/specminorstands unchanged.docs/adr/**sits in no package'sfiles[], so the added path publishes nothing and moves no level. (Stated explicitly because this shift's #16649 patch round got exactly this wrong by not re-deriving after a file-surface change.)Clause ② stays
yes, unchanged by the supplement — the accept-set widening is thehook.zod.tstext itself. ⛔ Re-runnode scripts/pm/check-clause2-carriers.mjs --pair 17249at the next push and report the exit code; the first push measured exit 0.What C changes about how #17249 lands
references/contract-review.md: 「受管面人工合并 ⛔ 不替代复核,两道保障叠加」).needs:contract-reviewis on the PR and aCONTRACT_REVIEW_TIERPASS is still required before it goes to the maintainer. The all-checks-green landing pre-check is the one that does not apply here (same reference: 「受管面不适用,draft-only 终局不变」).## 维护者速读(草稿)section with a席位意见field, which this seat fills in after the at-tier review returns.Sibling watch — measured, not assumed. PR #17195 (card #16344, "beforeUpdate receives the persist image; the caller submission moves to …") also edits
packages/spec/src/data/hook.zod.ts, and it is queue-landable while #17249 waits for a human. Hunk ranges measured on both heads: #17195 at@@ -451,6 +451,21 @@and@@ -615,6 +630,79 @@; #17249 at@@ -554,10 +554,36 @@. Disjoint, so the text merges. ⛔ That is a statement about text and nothing more — #17195's subject is whatbeforeUpdatereceives as its input, which is the same D-clause family the amended D3 bullet reasons about. So before this PR goes to the maintainer, this seat mergesmainonce #17195 lands and re-reads the amended D3 text against it, per 并行纪律 ②③.Dispatching the narrow patch round for items 1–2 now. Verify lock read at 2026-09-10T00:27Z:
state: holder pid 4231,queue: empty⇒ arrival depth 1 (LOCK_DEPTH_HOLD= 2), so the dispatch does not wait.Generated by Claude Code
9 remaining items
os-bill commentedon Sep 10, 2026
os-dev-report
{ "issue": 16074, "status": "done", "branch": "claude/issue-16074-hook-previous-row-invariant-rewrite", "pr": "https://github.com/objectstack-ai/objectstack/pull/17249", "premise_still_valid": true, "summary": "Small prose round on the existing governed draft PR #17249: the four non-blocking findings F1-F4 of the delta contract review (comment 5611459907), no behaviour change, no contract change, no new argument. New commit 541636ebb on top of 5e7e22768, 2 files, 9 insertions / 8 deletions. F1: Amendment II.3's ruling paragraph now names the recording comment 5560086928. F2: round 3's rewrap stub is gone, the paragraph rewrapped to the block's own idiom, word-identical. F3: 'closing sentence' becomes 'closing sentences' (the block supersedes both of the D3 bullet's last two sentences at :234-236). F4: the changeset's 'Director ruling' becomes 'Maintainer ruling (recorded by the director seat, ...)' -- this one ships to consumers in CHANGELOG.md. Nothing else touched: no :236 forward pointer, no Amendment II.3 substance, no hook.zod.ts, no bulk-write-hook-conformance.ts, no anchor JSON, no packages/objectql, no content/docs. PR stays draft, needs:contract-review still on it, no ready-flip, no enqueue, no label writes at all.", "tests": "ALL GATES GREEN. Derived mechanically, not inherited: `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` = 89 commands over the PR's 5-path change set. Declared narrowing: this round's delta is prose-only in docs/adr/** + .changeset/**, so I ran the 18 families that can plausibly read those two trees and left the other 71 to CI (the spec TS carriers are byte-unchanged since 5e7e22768, where they were already green). Run + exit, each read from the gate's own verdict line, exit code landed to disk before reading, never through a pipe: pnpm check:adr-anchors 0 ('check-adr-anchors: OK -- 53 anchored file(s) ... 34383 citation(s) across 4273 file(s) resolve'); check-adr-links.mjs 0 ('680 relative link destination(s) under docs/adr/ resolve') + --self-test 0; check-adr-symbol-anchors.mjs 0 ('2071 anchors across 139 records resolve') + --self-test 0; check-adr-0087-registration.mjs --base origin/main 0; pnpm check:nul-bytes 0 ('scanned 8100 text file(s) ... no raw ASCII control bytes'); pnpm check:pm-governed-prose 0; pnpm check:doc-authoring 0 ('403 files clean'); check-empty-changeset.mjs --base origin/main 0 ('1 declaring changeset(s) added'); check-changeset-no-major.mjs --base origin/main 0; check-changeset-fixed.mjs 0; pnpm check:objectui-changeset 0; pnpm check:changeset-gate-self-tests 0 (this one runs the empty-changeset / adr-0087 / no-major self-tests inside it, so the reconciler's 'unrun' line for those three is a bookkeeping artifact of my run record, not a gap); pnpm check:pm-changeset-deadline-census 0; check-closing-keyword-parity.mjs 0 + --self-test 0. PLUS the prose-integrity proofs I ran on the edits themselves: F1 delta is exactly the intended addition and F2 is WORD-IDENTICAL before/after (whitespace-normalised paragraph compare against `git show HEAD:...`, printed both ways); new block max line width 79 chars, matching the block's own pre-existing 79; the verbatim maintainer quote keeps its DOUBLE SPACE; `grep -naP` control-character self-scan over both edited files = no hits. NOT A CLEAN RUN THE FIRST TIME, reported rather than hidden: `pnpm --filter @objectstack/spec run check:generated` FIRST exited 1 with 'api-surface/ stale'. That was a MISSING PREREQUISITE, not a red -- packages/spec/dist did not exist in the fresh worktree and build-api-surface.ts reads the built dist by its own docblock ('run after pnpm --filter @objectstack/spec build'). After `pnpm --filter '@objectstack/spec...' build` under scripts/pm/os-verify-lock.sh (VERDICT command-exit 0, held 129s, waited 0s, slot issue-16074-r4), the re-run exits 0: 'All 15 generated artifacts are up to date.' No ablation and no reverse-verification in this round: nothing executable changed, so there is no gate whose failure could be demonstrated -- reporting the absence rather than manufacturing a ritual.", "mcp_calls": "0 -- the whole run went through repo-scoped REST, probed first (HTTP 200): the review comment 5611459907, ruling comment 5560086928, PR 17249, the check-runs pages, and this report comment. REST was chosen for the 13 KB write too, so the body goes to the wire from a file rather than being retyped through a tool argument.", "open_questions": [], "out_of_scope_findings": [ "noted, not filed: the PR BODY carries the same misattribution F4 just fixed in the changeset -- line 5 'Director ruling on the card (comment 5560086928 ...)' and line 13 '执行总监席裁决(评论 5560086928 ...)'. The maintainer ruled; the director seat recorded. Not published to consumers (unlike CHANGELOG.md), and outside my four-edit order, so left alone. 承接者: the PM seat, which still has to fill this body's 席位意见 (line 17 is still 留空) before handover -- same edit, same pass.", "noted, not filed: the PR body's line 5 states the admitted shape as 'one whose written key set is the same on every matched row' with NO in-place condition -- it predates rounds 2-3, which added 'and is assigned IN PLACE' to every other carrier. Same class as F3. 承接者: the PM seat, in the same body pass as above.", "noted, not filed: the PR body's attribution footer is the BARE comment form `_Generated by [Claude Code](https://claude.ai/code)_`, where AGENTS.md ~:420-425 specifies the session-URL form for PR BODIES. Exactly one footer, correctly under a rule line -- so it is well-formed, just the wrong one of the two forms. Pre-existing from an earlier round. I did NOT touch it: the order forbids re-sending a body that already carries an appended footer, and a rewrite of a 22 KB governed body to fix a footer is the wrong trade. 承接者: the PM seat's body pass.", "noted, not filed: `node scripts/pm/dispatch-gates.mjs` warns STALE TREE -- this branch is 17 commits behind origin/main (7ddf13dca) and 4 files the derivation reads changed across that range (check-sdui-lockstep.mjs, engine-double-contract.pinned.json, check-half-states.mjs, check-skill-line-ratchet.mjs). None of those four families is reachable from a prose diff in docs/adr/** or .changeset/**, so the derived list is sound for this round; I did not rebase (the order forbids history rewrite). 承接者: none needed -- CI derives from the real merge." ] }The four edits, before and after
F1 —
docs/adr/0058-expression-and-predicate-surface.md:451-453 (now :451-454)BEFORE
AFTER
The card number is kept rather than dropped — the change is purely additive to a governed record.
F2 — same file, :473-478 (now :474-479). The stub line as it actually stood was
> matched). So anwith ONE space after the marker (the order's paraphrase shows three; the review itself has it right — flagging the discrepancy, not acting on the paraphrase).BEFORE
AFTER
Word-identical — proved by a whitespace-normalised compare against the pre-edit blob, not by eye.
F3 — same file, :445. BEFORE
> _Amends D3's closing sentence in Addendum II above by ADDITION: the 2026-08/ AFTER> _Amends D3's closing sentences in Addendum II above by ADDITION: the 2026-08. Verified against :234-236, which is two sentences: "A rewrite *conditioned* on the row is out of contract: it widens to every matched row rather than scoping itself." and "Per-rowpreviousis supplied so a guard can REFUSE, not so a rewrite can be aimed." The:236forward pointer was not touched.F4 —
.changeset/hook-previous-row-invariant-rewrite.md:7. BEFORE... Director ruling, decision batch #59 (2026-09-06), option 1: **the contract admits the shape.**AFTER... Maintainer ruling (recorded by the director seat, decision batch #59, 2026-09-06), option 1: **the contract admits the shape.**How comment id 5560086928 was verified
Fetched it directly and read the resolved fields, before writing it into the governed record:
issue_urlends/issues/16074— it is on THIS card, not another.created_at2026-09-06T15:02:20Z — matches the date the block already cited.That same body is also the primary evidence for F4 — it says "Maintainer reply, verbatim" and is titled "director seat", i.e. the maintainer ruled and the director seat recorded.
Changeset level — re-derived, not inherited
@objectstack/spec: minorSTANDS. Re-derived:packages/spec'sfiles[]carriessrc/**/*.zod.tsanddist;docs/adr/**and.changeset/**are in no package'sfiles[]. This round edits only ADR prose and changeset prose — it moves no published carrier, adds no export, and changes no behaviour, so nothing about what publishes or how breaking it is moved.check-changeset-no-major.mjsexit 0 andcheck-empty-changeset.mjsexit 0 at the new head.Step-4 exit codes, and which script copy produced them
Both run from an
origin/mainMIRROR at7ddf13dca, never the primary checkout:git archive origin/main scripts | tar -x -C .../mirror. All 7 loaded files byte-verified withgit hash-objectagainstgit rev-parse origin/main:PATH— the 2 entry scripts plus their 5 transitive local imports, every one MATCH:scripts/pm/check-clause2-carriers.mjs60b7562c5scripts/pm/check-governed-merges.mjs081df968dscripts/import-prerequisite.mjsef5f4df36scripts/invoked-as.mjs60700de08scripts/pm/check-half-states.mjs0ec90893dbecd0cecd)scripts/pm/check-widening-tells.mjsb85a2e840scripts/pm/git-history.mjs6c1b47423The mirror requirement was load-bearing:
check-half-states.mjsis genuinely stale in this branch's tree, so a run from the checkout would have loaded a different byte sequence.check-clause2-carriers.mjs --pair 17249→ exit 0, and it is a real reading, not the exit-3 environment path: "PR feat(spec): HookContext admits a row-invariant-in-effect rewrite by per-row previous on a predicate write, kept safe by the key-divergence refusal #17249 / cardhook.zod.tssays per-rowpreviouson a predicate write is for a guard to REFUSE with, not for a rewrite to aim by — three shipped provenance stamps aim by it, kept safe only by the engine's divergence refusal #16074 — the clause-② declaration is readable in the fixed spelling and both carriers agree",token: present, served 3 read(s). It read 0 before and it reads 0 now.check-governed-merges.mjs --testover all 5 diff paths → exit 3 (GOVERNED), intended: "1 of 5 path(s) hit the register ... docs/adr/* ×1 ... a human merge is the review record for this PR"*.CI — newest run per check name, at the new head 541636e
31 raw runs → 31 names: 11 success, 3 skipped, 17 in progress, 0 red. CI is still converging on the head pushed minutes ago;
in_progressis the honest value at report time, and convergence is the PM's read, not mine. In progress: Build Core, Dogfood Regression Gate (1-3/3), Dogfood Verify CLI, Lint & Repo Gates, Temporal Conformance, Test Core (1-6/6), Type Check × 4.Anything in the order found to be false
Nothing material. Two small corrections, reported rather than worked around:
> matched). So an(three spaces). The real bytes are> matched). So an(one space), which is what the review comment says. Paraphrase-only; the edit is unaffected.pnpm --filter @objectstack/spec run check:generatedas a gate to run, but that gate CANNOT pass in a fresh worktree — it exits 1 on a staleapi-surface/untilpackages/specis built, becausebuild-api-surface.tsreads the builtdist. Not a defect in the order, but the step it implies (build first) is required for the reading to mean anything; a run without it is NOT MEASURED, not red.PR is still draft, base
main, labels unchanged (needs:contract-reviewstill present), no ready-flip, no enqueue, no auto-merge, no review submitted, no force-push, no history rewrite.Generated by Claude Code
huangyiirene commentedon Sep 10, 2026
Contract review pointer — director seat, summon #18 segment 6 (
session_017Js5kTpTtxieBjPyScgxJ3,huangyiirene). PR #17249 @ head541636ebbf: PASS WITH FINDINGS (delta re-review), verdict comment on the PR: #17249 (comment) — tier-verified isolated review (95 stamps). F1/F2 of the REWORK closed and each corrected sentence verified against the engine onorigin/main; every D3 carrier agrees; CI fully green. The delta also carries ADR-0058 Amendment II.3 (seat ruling 5610769499, option C) ⇒docs/adr/**⇒ governed — draft-only, maintainer hand merge; F3/F5 follow-up cards still owed at ACCEPT.Generated by Claude Code
huangyiirene commentedon Sep 10, 2026
needs:contract-reviewcleared on card #16074 and PR #17249 (verdict #17249 (comment)); PR stays draft-only — governed surface, maintainer hand merge. Card state and assignee untouched.Generated by Claude Code
github-actions commentedon Sep 10, 2026
os-closed-card-sweep — machine-findable marker for this generated comment.
Removed the pm-loop state label(s) this closed card no longer claims:
pm:dispatched.681871eb44, merged intomain.enhancement,priority:p2,domain:spec— ownership, priority and outcome are not state claims.A state label claims work is in flight. This card is closed on a merged delivery, so the claim
is stale; every other label is left exactly as it was found. Nothing here is a judgement about
the card, and no verdict-bearing label is ever touched by this sweep.
posted by half-state-patrol run 34484272265 · trigger
scheduleGenerated by Claude Code
docs: correct `before*` bulk dispatch to the per-row model on two cus…
feat(spec): HookContext admits a row-invariant-in-effect rewrite by p…
docs: correct `before*` bulk dispatch to the per-row model on two cus…
docs(skills): before* hooks on a predicate write dispatch per row — t…
docs(objectql): state the #16074 per-row `previous` rule in the publi…
docs(objectql): re-key two hook-wrappers docblocks to the per-row bef…
docs(objectql): state the objectstack-ai#16074 per-row `previous` rul…
docs(objectql): state the per-row `before*` contract in the two hook-…