Skip to content

feat(verify): the handle gains a system-context update door and a predicate update door - #22517

Merged
objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/issue-22301-update-doors
Oct 9, 2026
Merged

objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/issue-22301-update-doors

Conversation

@objectstack-fleet

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

Copy link
Copy Markdown
Contributor

Part of #22301: items 2 and 3, as one stage (PM claim 6083271651). Items 4 to 8, and item 1's remaining composition gap, stay open on the card.

Clause-②: yes (widening)

What this adds

The in-process handle of @objectstack/verify gains the two update doors that hotcrm still reaches through local helpers (systemUpdate and predicateUpdate in hotcrm test/helpers/verify-stack.ts). Each door is the engine's own ObjectQL.update, the call those helpers make by hand on the booted kernel. Nothing is re-implemented and nothing writes to a driver directly.

  • The system-context update (item 2). hooks.run(object, 'update', { id, ...fields }, { system: true }) runs update(object, { ...input, id }, { where: { id }, context: { isSystem: true } }). That is the by-id write hooks.run already makes for a person, under the context a system job writes under. No permission gate applies. The bound hooks still run (they see session.isSystem and no userId), declared validations still refuse, and the record-change trigger fires its flows with no trigger user. seed remains the fixture door; it also sets skipTriggers and seedReplay.
  • The predicate update (item 3). hooks.updateWhere(object, where, data, opts) runs update(object, data, { where, multi: true, context }), the engine's predicate path. No REST door reaches that path: POST /data/:object/updateMany writes by id. The engine dispatches beforeUpdate and afterUpdate once per matched row (dispatch.mode: 'per-row'), each bound to that row's own pre-image as previous, so record-change flows evaluate per row. The door resolves the affected-row count. The caller is { as: token } or { system: true }.

Public API (@objectstack/verify minor, @objectstack/spec minor)

  • New exported type AsSystem: { system: true }, beside AsUser.
  • VerifyHandle.hooks.run gains a second overload, run(object, 'update', input, opts: AsSystem), which resolves an EngineRow. The existing signature is unchanged.
  • New member VerifyHandle.hooks.updateWhere(object, where: EngineRow, data: EngineRow, opts: AsUser | AsSystem), which resolves a number.
  • No new value export. Nothing in packages/rest or packages/objectql changes. @objectstack/spec (minor, .changeset/22301-spec-ledger-verify-provenance.md, Clause-②: yes (widening: a new owner provenance row in the published error-code ledger)): ERROR_CODE_LEDGER["@objectstack/verify"] lists INVALID_REQUEST. No code is added, and ErrorCode, RegisteredErrorCode and REGISTERED_ERROR_CODES are unchanged.

What the doors refuse themselves

Both doors answer INVALID_REQUEST / 400 (an existing ADR-0112 ledger code) before a caller is resolved or the engine is touched:

  • { system: true } on insert or delete. The system context is the update doors' only. A system insert of fixture rows is seed. A system insert or delete that fires record triggers has no door: it belongs to item 5's stage, not this one.
  • { as, system: true } together: a caller named twice.
  • hooks.updateWhere with a where that is not an object.
  • hooks.updateWhere called in a way the engine's own update dispatch (resolveEngineUpdateDispatch, exported by @objectstack/objectql for exactly this kind of caller) would write by id: a where naming only an id, or an id in data beside a where that selects by nothing else. The door is the predicate path, so it does not hand back a by-id row where it promises a count. An id inside a real predicate ({ id, batch }, the compare-and-set spelling) stays a predicate update. An id in data beside a real predicate is the engine's own refusal, unchanged.

Every other refusal (permission, validation, a hook's throw) is the engine's own error, rethrown unchanged.

Premises, re-read on origin/main e148ca9842

  1. seed only inserts: handle.ts seed calls ql.insert(object, rows, { context: SEED_CONTEXT }). Holds.
  2. hooks.run always runs as a person: it resolved contextFor(opts.as), which refuses a token that resolves to no userId. It addresses one row by input.id. Holds.
  3. REST updateMany iterates by id: metadata-protocol/src/protocol.ts runUpdateManyLoop refuses a row without id, then updates each one by id. Holds.
  4. The engine's predicate path binds each row's pre-image: ObjectQL.update takes the predicate branch on the dispatch verdict multi, then dispatchPerRowBeforeHooks / buildPerRowAfterContexts (ADR-0058's bulk addendum) dispatch once per matched row with that row's previous. It resolves the driver's affected-row count (engine.ts, the driver.updateMany exit). Holds.
  5. The system context (zone 2, assumption 2): { isSystem: true } is the spec's named system opt-in (ExecutionContext docs). isSystem alone does not suppress trigger dispatch; only skipTriggers does (SEED_WRITE_EXECUTION_CONTEXT docs, and seed-ownership-claim-dispatch.dogfood.test.ts measured a bare { isSystem: true } write firing app hooks and record flows). This PR re-measures it on the handle (pins below).
  6. "hotcrm is not reachable from this container" (zone 2, assumption 3): falsified. gh api is refused for that repository in this session, but a plain git clone --depth 1 --sparse over the proxy works. The helpers were read at hotcrm ac162c9, read-only. hotcrm's suite was not run.

hotcrm mapping (hotcrm ac162c9, read-only)

hotcrm local helper handle door engine call (the same in both)
systemUpdate(stack, object, doc): 61 calls in 26 files, all by id stack.hooks.run(object, 'update', doc, { system: true }) update(object, doc, { where: { id: doc.id }, context: { isSystem: true } })
predicateUpdate(stack, object, doc, where, as): 1 call (flow-billing-handoff.test.ts:232, where: { name }) stack.hooks.updateWhere(object, where, doc, { as }) update(object, doc, { where, multi: true, context: contextFor(as) })

escalation-task-subject.test.ts:143 expects systemUpdate to be refused by the app's own hook. Through the door that refusal is still the engine's own error.

Tests (at 992de817)

packages/verify/src/handle.update-doors.test.ts: 9 tests on one bootStack of a neutral fixture. The fixture has an editable upd_deal, an upd_case a fresh member may read but not edit, a capture hook on each (beforeUpdate / afterUpdate: caller, previous, dispatch.mode), and a record-change flow on each that writes a ledger row (from = previous, to = record).

  • Item 2: a member's hooks.run update of upd_case is refused PERMISSION_DENIED / 403 and the row is unchanged (control). The same update with { system: true } is written.
  • Item 2, a real system write: after a system update, both hooks saw isSystem: true, no userId, previous = the pre-image, mode: 'record'. The flow ran once, completed, with no trigger.userId, and wrote its ledger row. Control: the same update as the admin reaches the same hooks and the same flow, carrying the admin's id.
  • Item 2, validation still runs: a system update the declared rule refuses rejects with the engine's ValidationError (code: 'VALIDATION_FAILED', fields: [{ field: '_record', code: 'rule_violation' }]) and the row is unchanged. Control: an in-range value is written.
  • Item 2, call shape: { system: true } on insert and delete, and { as, system }, each answer INVALID_REQUEST / 400. Nothing is written and no hook is dispatched.
  • Item 3, exactly N: three rows match, one of them already won, and two do not. updateWhere resolves 3, all three carry the payload, and the two non-matching rows are unchanged.
  • Item 3, the predicate path: each matched row got exactly one beforeUpdate and one afterUpdate, each with previous = that row's own seeded stage, userId = the member, and mode: 'per-row'. Non-matching rows got none.
  • Item 3, record flows per row: the transition flow (stage != previous.stage) wrote one ledger row for each matched row whose own pre-image differed (2 rows, each from its own stage), and none for the row already won (it is in the count of 3). One shared previous would fire for all three rows or for none.
  • Item 3 with the system: a member's predicate update of upd_case is refused PERMISSION_DENIED / 403 and the rows are unchanged. The same call with { system: true } resolves 2, and the hooks saw isSystem, no user and mode: 'per-row'.
  • Item 3, call shape: where: { id }, {} with an id in data, and no where each answer INVALID_REQUEST / 400, with nothing written and no hook dispatched. Control: where: { id, batch } goes through and resolves 1.

The package: pnpm --filter @objectstack/verify test gave 24 files / 196 tests passed, and pnpm --filter @objectstack/verify typecheck exited 0. The test layer is compiled: tsconfig.test.json lists the new file.

Ablations

Each ablation went through node scripts/ablation-replace.mjs (WRAP mode, restore armed on EXIT/INT/TERM) on packages/verify/src/handle.ts at 992de817, under one lock acquisition. The test imports the handle from source (./harness.js), so the subject has no dist/ leg. The direction was predicted before the run. Each mutation landed (anchor 1 → 0, blob changed) and was restored to the HEAD blob db78a676 with git diff HEAD empty. The tree ended with 0 porcelain lines.

mutation predicted red observed
A1 the system door's context becomes the seed posture (SEED_CONTEXT, which adds skipTriggers and seedReplay) only "a real system write" (no flow run) 1 failed / 8 passed: exactly that pin
A2 the predicate door becomes a find-then-by-id loop, the REST updateMany shape only the two mode: 'per-row' pins; count, untouched rows, per-row previous and per-row flows stay green 2 failed / 7 passed: exactly those two
A3 the by-id dispatch refusal is disabled only the by-id refusal pin 1 failed / 8 passed
A4 the refusal of { system: true } on insert/delete is disabled only the call-shape pin 1 failed / 8 passed

A2 is why the dispatch-verdict pin exists. Per-row hooks with their own previous, an exact count and per-row flows also hold for a loop of by-id writes. Only the engine's dispatch.mode tells the two apart.

Gates

At ca122badb2, the final commit after the patch round (contract review FAIL 6085768920, seat order 6085806321), node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 88 commands. The spec path added 25, check:error-code-provenance among them. Each was run with its exit code captured before any pipe:

  • 85 exited 0, among them check:error-code-provenance ("OK — every registered-code stamp site is listed under its own owner key or carries a recorded waiver").
  • 3 are NOT MEASURED:
    • dual-build-cjs-loads: PREREQUISITE NOT MET (whole-tree dist), left to CI on the seat's instruction.
    • doc-formula-expressions and lean-entry-closure: a dist prerequisite that the shared verify lock could not serve, left to CI.
  • --ran verdict: ✓ dispatch-gates --ran: 88 derived famil(ies) accounted for — 85 run, 3 NOT-MEASURED (3 DERIVED from a recorded exit 3).
  • The 49 artifact-roster commands all exited 0. The 3 PR-context guards were run wired to this PR.

Round 3 at 257aeb788b (one changeset file, per review 6087465393): check-changeset-no-major, check-adr-0087-registration, check-empty-changeset, check:changeset-gate-self-tests, check-changeset-fixed and check:nul-bytes each exit 0.

Round 1 at 992de817 derived 63 commands, of which 62 ran and 1 was NOT MEASURED (dual-build-cjs-loads).

CI owns the jobs that no local command covers: Test Core, Dogfood, Build Core, the workspace type-check and pnpm lint.

Acceptance notes

  • The README is outside the claim's file surface. packages/verify/README.md enumerates every handle door and said "there is no way to run as nobody; seed and the default rows run as the system principal". Both lines describe the doors this PR adds, so they are updated here, and the deviation is declared in the report.
  • The engine's ValidationError carries no status. It has code and fields; the REST boundary maps VALIDATION_FAILED to 400. The validation pin therefore asserts code and fields, not a status. Not filed: no public door answers wrong. Carrier: none.
  • The durable sys_automation_run row lands after the triggering write returns. It was absent right after the system update and present 500 ms later, while the engine's run log (automation.listRuns) and the flow's own writes are there synchronously. The pins read the run log. A suite that reads sys_automation_run immediately after a write can race it; hotcrm's flowRuns helper reads that table. Root cause NOT MEASURED. Not filed (an observation). Carrier: none.
  • Overlap with item 5. Item 5 asks for a user-less record trigger. For an update, the system door now produces one (measured: the flow runs with no trigger.userId). A user-less insert or delete trigger, and a record the engine no longer holds, are still item 5's.
  • Pre-existing call-shape refusals. hooks.run's two older call-shape refusals (an unknown operation, a missing input.id) are still plain Errors with no code. The new ones carry INVALID_REQUEST / 400. Carrier: none.

Generated by Claude Code

claude added 2 commits October 9, 2026 15:04
… doors (WIP)

hooks.run takes { system: true } on 'update' (the { isSystem: true } context a
system job writes under), and hooks.updateWhere is the engine's predicate path
(multi: true), refused when the engine's own dispatch would write by id.

Claude-Session: https://claude.ai/code/session_01KNKBCRDJCu5tGy3TEbvtrF
Co-authored-by: Claude <[email protected]>
…r on the real engine

Each claim is read off what only the engine produces on that path: the
permission gate's refusal and its absence for the system, the hook chain's
caller, per-row pre-image and dispatch verdict, the declared validation's
refusal, and the record-change flow's runs and writes. Docs and changeset.

Claude-Session: https://claude.ai/code/session_01KNKBCRDJCu5tGy3TEbvtrF
Co-authored-by: Claude <[email protected]>
@github-actions github-actions Bot added the size/l label Oct 9, 2026
@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Oct 9, 2026
@github-actions

github-actions Bot commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/spec, @objectstack/verify, touching 11 documentable anchor(s). ⚠️ 2 changed file(s) yielded no anchor (packages/verify/README.md, packages/verify/src/index.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

16 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json ee8751d41e61a18f7819e4d3ad2c340f51ab2418.

⛔ 8 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 2 changed file(s) yielded no anchor (packages/verify/README.md, packages/verify/src/index.ts) — pages documenting those are invisible to this run
  • 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 — 140 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 ee8751d41e61a18f7819e4d3ad2c340f51ab2418 → packageMentionDocs.

Which tree this was computed on

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

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

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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 992de817ea43f026a8626c692e1d7d24f5b32665
Local-runs: none

PR #22517 (card #22301, items 2 and 3) read as the net diff against the merge-base e148ca984 (5 files, +601 / -13), the PR body and file list, the card body with all 24 comments (triage 6061416383, the ruling 6070767186, the claim 6083271651, the os-dev-report 6085542084, the seat's acceptance 6085582478), and the check-runs on this head. Nothing was built, run or re-run; the branch was fetched into a ref of its own and diffed.

① Derived judgments

Right:

  • AsSystem ({ system: true }) is a new type-only export from index.ts beside AsUser; no new value export (callShapeRefusal and namesSystem are module-private). Nothing in packages/rest, packages/objectql or packages/spec moves; seed, rows, validate, flows.* and actions.run are untouched, and the existing hooks.run(…, AsUser) signature is unchanged.
  • hooks.run(object, 'update', input, { system: true }) runs update(object, { ...input, id }, { where: { id }, context: { isSystem: true } }). Read against the engine's dispatch ladder (metadata-core/src/engine-update-dispatch.ts): payload id and where.id are the same scalar, no unhonoured key, so the call is by-id, the same path the person form takes. The context is right: isSystem bypasses the permission gate (spec/src/kernel/execution-context.zod.ts) and does not suppress trigger dispatch (SEED_WRITE_EXECUTION_CONTEXT pins skipTriggers as the suppressor), so a bare { isSystem: true } rather than SEED_CONTEXT is the system job's posture, and the record-change flow fires with no trigger user. Ablation A1 is the pin that tells them apart.
  • { system: true } on insert or delete, and { as, system } together, are refused before any token is resolved or the engine is touched; the second overload is typed 'update' + AsSystem only, so the refusal is declared at the type level and enforced at runtime.
  • hooks.updateWhere(object, where, data, opts) runs update(object, data, { where, multi: true, context }) and resolves a number: engine.ts's predicate branch returns the driver's affected-row count (isPredicateWrite, no row hydration), and the hooks dispatch once per matched row with dispatch.mode: 'per-row' (spec/src/data/hook.zod.ts). The as number cast holds on that branch.
  • The by-id refusal asks the engine's own resolveEngineUpdateDispatch(data, { where, multi: true }), exported on @objectstack/objectql's root barrel (index.ts:330). Each documented case maps onto the ladder: where: { id } alone stays by-id even under multi (the LifecycleService invariant the ladder names), where: {} beside a payload id is by-id, where: { id, batch } is multi (unhonoured keys plus multi and no payload id), and a payload id beside a real predicate is the engine's reject, which the door rethrows unchanged. A where that is not an object (null, an array, undefined) is refused by the door itself.
  • import { resolveEngineUpdateDispatch, type ObjectQL } from '@objectstack/objectql' turns a type-only import into a runtime one: @objectstack/objectql is already in verify's dependencies (workspace:*), so no dependency edge is new.
  • The fixture's requires: ['automation', 'triggers'] relies on item 1 stage 2 (97610a533), which is an ancestor of the merge-base.
  • README.md: the sentence "there is no way to run as nobody; seed and the default rows run as the system principal" would be false after this diff; the three system doors, the hooks.updateWhere entry in the API list and the refusal paragraph are the right corrections, and the README is the only hand-written page that enumerates the handle's doors (the content/docs hits are release-owned pages).
  • The new test file is inside tsconfig.test.json's src/**/* program and reads nothing outside its package.

Wrong:

  • callShapeRefusal stamps the registered code INVALID_REQUEST from @objectstack/verify (packages/verify/src/handle.ts:312, the objlit shape), and the ADR-0112 D3 ledger (packages/spec/src/api/error-code-ledger.zod.ts) holds no @objectstack/verify owner key at all. The ledger's provenance half says a code emitted by several packages is listed once per emitting package, with a comment recording reachability. check:error-code-provenance reds on exactly this line (see the check-runs below). A PROVENANCE_WAIVERS entry is not the honest shape: a waiver is for a door in another package that names the wire vocabulary, and here verify's own door stamps it. The fix is a row under a new '@objectstack/verify' owner key, with the comment saying the door is in-process (the refusal is the thrown Error carrying code, status and statusCode; no HTTP path).

② Semver level

  • .changeset/22301-verify-update-doors.md: @objectstack/verify minor, body carrying the declaration yes (widening). The PR body's third line carries the same declaration, and the claim 6083271651 declared the same. Right: the diff publishes three additive changes to the handle (a type, an overload, a member) and removes, renames or narrows nothing. yes takes at least minor; the widening arm adds no breaking signal, so no ADR-0087 disposition is owed. Check Changeset concluded success on both of its runs on this head.
  • The patch round's ledger row lives in packages/spec, but the union already holds INVALID_REQUEST; a provenance row under a new owner key changes no published shape, so the changeset and the declaration stay as they are.

③ Boundary flags

The report's open_questions is empty. Its deviations, each answered:

  • packages/verify/README.md outside the claim's file surface: answered, accepted. It is the doc of the package the diff changes, its old sentence would have been false, and the seat's acceptance accepted it too.
  • Zone-2 assumption 3 (hotcrm unreachable) falsified by a read-only sparse clone at hotcrm ac162c9: answered, a reading only. The 61 by-id systemUpdate calls and the one predicateUpdate call map one-for-one onto the two doors with the identical engine call.
  • The validation refusal pin asserts code and fields, not a status: answered, right. ValidationError (objectql/src/validation/record-validator.ts:238) carries code and fields only; the REST door maps it to 400.
  • git fetch --depth=1 origin 621a4876 into the shared object store: answered, an additive read for a self-test prerequisite.
  • Attribution in the model-free trailer pair and the session-URL footer: answered, the AGENTS.md form.

The three out-of-scope findings (carrier: none):

  • The engine-door ValidationError carries no status: not escalated. ADR-0112 D5's target puts the HTTP status on the transport; an in-process error naming code and fields is that contract, and the pin reads it as such.
  • The durable sys_automation_run row lands after the triggering write returns while automation.listRuns is synchronous: escalated to the seat as a filing candidate. hotcrm's flowRuns helper reads that table right after a write, so it races once hotcrm moves onto these doors; that is a named producer. This PR's pins read the run log, so it does not hold this PR.
  • The two older hooks.run call-shape refusals carry no code: answered, pre-existing; a later stage may align them.

Reviewer flags:

  • The dev's battery (63 derived families, --ran reconciled) could not have held check:error-code-provenance: dispatch-gates.mjs scores it silent for every card by construction (its declared literals are its own artifacts) and prints it in a roster block beside the derived list, not in it. So the CI red is the standing derivation gap the tool itself names, not a family the dev skipped. The patch round runs that command by name.
  • The second Check Changeset, Auto Label and Check PR Size runs on this head are the labeled re-trigger from needs:contract-review; both Check Changeset conclusions are success.

Check-runs on this head

Read at 2026-10-09T17:17Z; 36 check-runs: 29 success, 6 skipped, 1 failure, 1 in progress. The seven required contexts:

  • Lint & Repo Gates: failure. Step 120 of 199, Error-code provenance guard (pnpm --filter @objectstack/spec check:error-code-provenance), exit 1: "@objectstack/verify stamps 'INVALID_REQUEST' (objlit) at packages/verify/src/handle.ts:312 — not listed under its own owner key". The 82 later steps were skipped behind it (the migration-registry, dispatch-gates, auth-mount, vendor-export and sink-contract guards among them), so they carry no verdict on this head. On origin/main 4e9fe9ff6 the latest completed Lint & Repo Gates is success and the two newer runs are in progress, so this red is the diff's own.
  • TypeScript Type Check: success (workspace, source gates, consumer gates and the debt ledger all success).
  • Test Core: all six shards success; the rollup check-run is still in progress at the read, which is not a pass.
  • Dogfood Regression Gate: success (rollup and all three shards); Dogfood Verify CLI success.
  • Build Core: success.
  • Temporal Conformance (live PG + MySQL): success.
  • Governed Surface Queue Guard: success (no governed path in the diff).

Advisory: Check Changeset success (two runs), Check PR Size success, the claim and single-writer guards success, Part-of PR must not also close its card success, Check Documentation Links success, Flag docs affected by code changes success; Build Docs, Console Pin Gate and the packed-tarball smoke skipped by their filters.

Patch round

Same dev, same branch, one commit:

  1. packages/spec/src/api/error-code-ledger.zod.ts: add the owner key '@objectstack/verify' listing INVALID_REQUEST, with the comment recording the reachability test: the handle's two update doors refuse a malformed call in-process with a thrown Error carrying code, status and statusCode; no HTTP path.
  2. pnpm --filter @objectstack/spec build, then check:generated and --fix for whatever it proves stale (the ledger is spec source; commit any artifact the check names, none on principle).
  3. pnpm --filter @objectstack/spec check:error-code-provenance exit 0, and check:error-status-conformance and check:dispatcher-error-vocabulary unchanged.
  4. Push. A fresh ## Contract review on the new head, on which Lint & Repo Gates must report every step this head skipped and the Test Core rollup must conclude.

No other file. The spec touch is beyond the claim's named surface ("the generated artifacts the diff moves"); the seat's patch order authorises it.

Implemented-by: claude/issue-22301-update-doors
Reviewed-by: session_01KNKBCRDJCu5tGy3TEbvtrF

VERDICT: FAIL


Generated by Claude Code

claude added 2 commits October 9, 2026 17:22
…EST in the error-code ledger

The verify handle's two update doors refuse a malformed call in-process with a
thrown Error carrying code / status / statusCode, and the provenance guard
requires the stamping package's own owner key to list the code.

Claude-Session: https://claude.ai/code/session_01KNKBCRDJCu5tGy3TEbvtrF
Co-authored-by: Claude <[email protected]>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: ca122badb2055d034842962b2f8938ebe2fe6aff
Local-runs: none

Second review on PR #22517 (card #22301, items 2 and 3), after the FAIL 6085768920 on 992de817, the seat's adoption order 6085806321 and the patch-round os-dev-report 6087306097. Read: the net diff against the merge-base 4e9fe9ff6 (6 files, +605 / -13; the branch merged main there as a86e43b252, then added one commit), the PR body as the seat edited it, the file list, the card body with all 26 comments, and the check-runs on this head. Nothing was built, run or re-run; the branch was fetched into a ref of its own and diffed.

① Derived judgments

Right:

  • The five round-1 files are blob-identical at this head (.changeset/22301-verify-update-doors.md e991d4a1, README.md b2ea50d6, handle.ts db78a676, handle.update-doors.test.ts 97fc7a45, index.ts 246f066f), and main moved none of those paths between the two bases (e148ca984 to 4e9fe9ff6). Every round-1 judgment on the two doors therefore carries unchanged: AsSystem type-only; the 'update' + AsSystem overload is the engine's by-id path under a bare { isSystem: true }; hooks.updateWhere is update(object, data, { where, multi: true, context }) resolving the affected-row count with per-row hook dispatch; the by-id refusal asks resolveEngineUpdateDispatch; the README corrections are right and necessary; the fixture's requires rests on item 1 stage 2, which is in the base.
  • The merge a86e43b252 has two parents (992de817, 4e9fe9ff6) and leaves no residue in the net diff: the 6 files are exactly the PR's own, and no .gitattributes-routed artifact was deferred.
  • The round-1 finding is fixed as named. packages/spec/src/api/error-code-ledger.zod.ts gains the owner key '@objectstack/verify' listing INVALID_REQUEST, appended as the last key of ERROR_CODE_LEDGER, four lines, no other entry or shape touched, and the comment records the reachability adjudication (an in-process test door, no HTTP path, a thrown Error carrying code / status / statusCode), which is what callShapeRefusal does. A row, not a waiver, as the ADR-0112 D3 provenance half asks of a package that stamps the code itself. RegisteredErrorCode and REGISTERED_ERROR_CODES are unchanged (INVALID_REQUEST was already in the union through other owners), and no generated artifact moved (the spec-gate jobs below are green with the diff carrying no artifact).
  • check:error-code-provenance (Lint & Repo Gates step 120) is success on this head.
  • The PR body's edited Public API bullet is accurate: the published ERROR_CODE_LEDGER declaration on the @objectstack/spec/api entry (api/index.ts:47 re-exports the ledger module) gains one readonly key. That is an additive widening of a published surface, consistent with the body's declaration yes (widening). Its changeset consequence is ②'s finding.
  • The body's premises section still cites e148ca9842 and the tests section 992de817: those readings still describe this head, since the files they describe are byte-identical and the base did not move them. No body edit is owed for that.

Wrong: none in the diff's code. The one wrong judgment is in the changeset set, below.

② Semver level

  • .changeset/22301-verify-update-doors.md: @objectstack/verify minor, body carrying yes (widening); the PR body's third line and the claim 6083271651 declare the same. For @objectstack/verify that is right: three additive handle changes, nothing removed, renamed or narrowed. Check Changeset is success on both of its runs on this head.
  • Wrong: the diff now also publishes in @objectstack/spec, and no changeset names it. ERROR_CODE_LEDGER is a published const on the @objectstack/spec/api entry, and its declaration gains the readonly key '@objectstack/verify'. AGENTS.md's rule is a changeset for anything that publishes, and the repository already answers this exact shape: PR fix(service-storage,spec): the upload size refusal answers 413 PAYLOAD_TOO_LARGE #22359 (188494880a) added a provenance-only row for the existing code PAYLOAD_TOO_LARGE under @objectstack/service-storage and shipped it with its own .changeset/22314-storage-payload-too-large-provenance.md, frontmatter '@objectstack/spec': minor, declaration yes (widening: a new owner provenance row in the published error-code ledger), body "No code is added … What widens is the per-package list a reader consults to learn which packages answer a code." That file is still in .changeset/, unconsumed, so the precedent is the pending set's own. The report's changeset_verdict argues from the gate's silence (Check Changeset asks only that a changeset is added; no script checks per-package coverage): a gate's silence is not the rule. The lockstep fixed group moves @objectstack/spec's version with the verify minor either way; what the missing line forgoes is the sentence in packages/spec/CHANGELOG.md, the one input the release consumes and deletes. One file fixes it; the level minor and the widening arm are unchanged by it.

③ Boundary flags

The report's open_questions is empty. Its deviations, each answered:

  • The worktree was recreated, so three dist-reading families were NOT MEASURED locally and left to CI: answered by the check-runs. Every published require entry point actually loads (check:dual-build-cjs-loads) is Build Core step 15, success; The lean engine entry loads no forbidden or unlisted package (check:lean-entry-closure) is Build Core step 17, success; Check docs formula examples are valid CEL (check:doc-formula-expressions) is in Type Check · consumer gates, success.
  • The full 88-command battery was rerun on the final commit beyond the order's list: answered, more than owed, not less.
  • The three PR-context roster guards were run wired to this PR with reads only: answered, that is how they judge anything.
  • The ledger file is outside the original claim surface: answered, the seat's order 6085806321 widened the surface to that one file, and the diff touches nothing else in packages/spec.
  • The report's "fact for the reviewer" that ERROR_CODE_LEDGER's built declaration gains the key: answered in ②, and it is the reason a spec changeset is owed.

Out-of-scope findings: none new this round. The round-1 escalation (sys_automation_run lands about 500 ms after the triggering write while automation.listRuns is synchronous) is answered on the record by the seat's order: noted, not filed, a raw-table read right after a write is the repo:hotcrm seat's test-helper concern. That answer stands; nothing further from this review.

Reviewer flags:

  • Lint & Repo Gates on this head reports 190 of 199 steps success, 0 failure, 9 skipped: the two reporter self-tests that always skip, and seven gate-family steps whose if: reads steps.gate-families.outputs.* != 'skip' (migration registry, entry guards, PM dispatch-gates, declared-population, bare-root worklist, workflow-command and verify-lock self-tests), skipped because no path of this diff is in their population. So the 82 steps that carried no verdict on 992de817 now all do, or are path-filtered by design.
  • The second Check Changeset, Check PR Size, Auto Label and claim-guard runs on this head are the labeled re-trigger; every conclusion is success or a filter skip.

Check-runs on this head

Read at 2026-10-09T19:04Z; 42 check-runs: 37 success, 5 skipped, none failed, none in progress. The seven required contexts: Lint & Repo Gates success, TypeScript Type Check success (and its four Type Check · jobs), Test Core success (rollup and all six shards), Dogfood Regression Gate success (rollup and all three shards; Dogfood Verify CLI success), Build Core success, Temporal Conformance (live PG + MySQL) success, Governed Surface Queue Guard success (no governed path). Advisory: Check Changeset success (two runs), Spec property liveness success, Check PR Size success, the claim and single-writer guards success, Part-of PR must not also close its card success, Check Documentation Links and Flag docs affected by code changes success; Build Docs, Console Pin Gate and the packed-tarball smoke skipped by their filters.

Patch round

Same dev, same branch, one commit, one file:

  1. Add .changeset/22301-spec-ledger-verify-provenance.md modelled on .changeset/22314-storage-payload-too-large-provenance.md: frontmatter '@objectstack/spec': minor; a title line naming the row; its own declaration line reading yes (widening: a new owner provenance row in the published error-code ledger); one paragraph saying ERROR_CODE_LEDGER['@objectstack/verify'] now lists INVALID_REQUEST, the code the handle's two update doors refuse a malformed call with, in-process, with code / status / statusCode on the thrown Error and no HTTP path; and that no code is added, so ErrorCode, RegisteredErrorCode and REGISTERED_ERROR_CODES are unchanged.
  2. No code, no other file. Push; the changeset gates re-run in CI; a fresh ## Contract review on the new head.

Implemented-by: claude/issue-22301-update-doors
Reviewed-by: session_01KNKBCRDJCu5tGy3TEbvtrF

VERDICT: FAIL


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 257aeb788b4b1ce919cc392fdaf77b3592f6e895
Local-runs: none

Third review on PR #22517 (card #22301, items 2 and 3), after the round-2 FAIL 6087465393 on ca122badb2, the seat's adoption order 6087482015 and the round-3 os-dev-report 6087522043. Read: the net diff against the merge-base 4e9fe9ff6 (7 files, +614 / -13; this round adds one commit over ca122badb2), the PR body as the seat edited it, the file list, the card body with all 28 comments, and the check-runs on this head, which I waited for until every run had completed. Nothing was built, run or re-run; the branch was fetched into a ref of its own and diffed.

① Derived judgments

Right:

  • The six round-2 files are blob-identical at this head (.changeset/22301-verify-update-doors.md e991d4a1, error-code-ledger.zod.ts 0f8fd90f, README.md b2ea50d6, handle.ts db78a676, handle.update-doors.test.ts 97fc7a45, index.ts 246f066f), and the round's one commit 257aeb788b adds exactly one file, .changeset/22301-spec-ledger-verify-provenance.md (blob 54541486a1). Every judgment of the two earlier records on the code therefore carries unchanged: AsSystem type-only; the 'update' + AsSystem overload is the engine's by-id path under a bare { isSystem: true } (permission gate bypassed, trigger dispatch kept); hooks.updateWhere is update(object, data, { where, multi: true, context }) resolving the affected-row count with per-row hook dispatch; the by-id refusal asks the engine's own resolveEngineUpdateDispatch; the call-shape refusals carry INVALID_REQUEST / 400 before any caller is resolved; the README corrections are right and necessary; the ledger owner key '@objectstack/verify': ['INVALID_REQUEST'] is a row, not a waiver, appended last, additive only, with the reachability comment matching callShapeRefusal; the registered-code union is unchanged; no generated artifact moved.
  • main moved none of the PR's seven paths between the base and origin/main ee8751d41 (six commits), and its ledger still ends at the @objectstack/lint key, so the appended @objectstack/verify key merges cleanly when the queue rebuilds this PR onto current main.
  • The PR body's edited heading and bullet are accurate: the Public API section now names @objectstack/verify minor and @objectstack/spec minor, and the bullet names the spec changeset, its declaration line, and that no code is added. The premises and tests sections still cite the earlier heads; the files they describe are byte-identical here, so those readings still describe this head.

Wrong: none.

② Semver level

  • Two changesets, one per publishing package, and both match the diff. .changeset/22301-verify-update-doors.md: @objectstack/verify minor, declaration yes (widening), for three additive handle changes (a type, an overload, a member), nothing removed, renamed or narrowed. .changeset/22301-spec-ledger-verify-provenance.md: @objectstack/spec minor, its own declaration line reading yes (widening: a new owner provenance row in the published error-code ledger), one paragraph saying ERROR_CODE_LEDGER['@objectstack/verify'] now lists INVALID_REQUEST for the handle's two update doors, an in-process test door with no HTTP path, no code added, and ErrorCode, RegisteredErrorCode and REGISTERED_ERROR_CODES unchanged. That is the shape of the pending precedent .changeset/22314-storage-payload-too-large-provenance.md (still on main, unconsumed), and it is what the published ERROR_CODE_LEDGER declaration on the @objectstack/spec/api entry actually gains: one readonly key.
  • The PR body's third line declares yes (widening), which covers both widenings. Neither changeset carries a breaking signal (no major, no narrowing arm, no breaking banner), so no ADR-0087 disposition is owed. Check Changeset concluded success on both of its runs on this head. Right.

③ Boundary flags

The report's open_questions is empty and its out-of-scope findings are none. Its one deviation, answered: the worktree was recreated from the pushed head and the optional merge of the moved origin/main was not taken. Answered: the merge-base is unchanged, the six commits main gained since touch none of this PR's paths, GitHub reports the PR mergeable, and the queue rebuilds the PR onto current main before landing it, which is where any joint breakage would surface.

The round-2 changeset_verdict (that no spec line was owed) is withdrawn by the report itself and corrected on the record by the seat's order 6087482015, which owns the earlier order's wording. Answered; the file the correction asked for is the diff's one new file.

The round-1 escalation (sys_automation_run lands after the triggering write while automation.listRuns is synchronous) stays answered by the seat's order 6085806321: noted, not filed, a raw-table read right after a write is the repo:hotcrm seat's test-helper concern.

Reviewer flags:

  • Lint & Repo Gates on this head: 190 of 199 steps success, 0 failure, 9 skipped, the same nine as on ca122badb2: the two reporter self-tests that always skip and the seven gate-family steps whose if: reads steps.gate-families.outputs.* != 'skip', path-filtered by design. Error-code provenance guard and ESLint are success.
  • The duplicated Check Changeset, Check PR Size, Auto Label and claim-guard runs on this head are the edited re-trigger from the seat's body edit; every conclusion is success or a filter skip.
  • The PR body's Gates section reports the 88-command battery at ca122badb2 and the six changeset gates at 257aeb788b. Accurate: the code is byte-identical between the two heads, and the check-runs below are the gate verdicts on this one.

Check-runs on this head

Read at 2026-10-09T19:36Z, after the last run completed; 42 check-runs: 37 success, 5 skipped, none failed, none in progress. The seven required contexts: Lint & Repo Gates success, TypeScript Type Check success (and its four Type Check · jobs), Test Core success (rollup and all six shards), Dogfood Regression Gate success (rollup and all three shards; Dogfood Verify CLI success), Build Core success, Temporal Conformance (live PG + MySQL) success, Governed Surface Queue Guard success (no governed path in the diff). Advisory: Check Changeset success (two runs), Spec property liveness success, Check PR Size success, the claim and single-writer guards success, Part-of PR must not also close its card success, Check Documentation Links and Flag docs affected by code changes success; Build Docs, Console Pin Gate and the packed-tarball smoke skipped by their filters.

Nothing in this record holds the PR. The diff touches no governed surface and is 627 changed lines; the record is owed by the claim's own yes declaration, and landing is the owning seat's act through the queue.

Implemented-by: claude/issue-22301-update-doors
Reviewed-by: session_01KNKBCRDJCu5tGy3TEbvtrF

VERDICT: PASS


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants