Skip to content

fix(rest)!: an import undo runs under the object's declared controls for every caller but the system principal - #24431

Merged
objectstack-fleet[bot] merged 10 commits into
mainfrom
claude/issue-24380-import-undo-controls
Oct 19, 2026
Merged

objectstack-fleet[bot] merged 10 commits into
mainfrom
claude/issue-24380-import-undo-controls

Conversation

@objectstack-fleet

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

Copy link
Copy Markdown
Contributor

Fixes #24380

Clause-②: yes (narrowing)

Basis for the Clause-② line (from the claim, 6257221825, which names this case): the undo door stops taking an app's declared controls off its writes for every caller but the system principal, which is a narrowing; and the undo's published answer, UndoImportJobResponseSchema, gains one key, failures, which widens the answer and, because the key is required, also narrows the schema's accept set: a response without it is refused, and code that constructs an UndoImportJobResponse must set it (the shape ListAiConversationsResponseSchema.hasMore set). yes (narrowing) declares both. Both changesets carry the same line, each with its one ADR-0087 marker.

Executes the ruling 6257104178 (batch 366 item 2, letter B, maintainer 「24380 B」). Detail is withheld under the maintainer's disclosure ruling of 2026-08-18 on #9387: this body gives code positions and the fix's behaviour only.

What changes

POST /api/v1/data/import/jobs/:jobId/undo (packages/rest/src/data-action-routes.ts) reverses a finished import: it deletes the records the job created and restores the fields it updated.

  • Whose undo skips the controls. The reversal writes keep skipAutomations and skipStateMachine for the system principal only (ExecutionContext.isSystem === true, an in-process caller such as a migration job). Every other caller, every signed-in user and administrator included, undoes under the object's metadata-bound hooks, its record-change triggers and its state_machine rule. The predicate is the one the import doors ask before they honour a control opt-out (importControlOptOutRefusal in import-prepare.ts, landed in 741aa85da): only the system principal may take an app's declared controls off a write, and the undo door is not an exception. The write context is built in one place, importUndoWriteContext. No permission, capability or config key is read or added.
  • A refused row is reported, never counted as reverted. Each reversal that throws (a hook's refusal, the state machine, a record lock, or any other failure) is caught for its row. The answer lists it in a new failures array, one { id, action, code, error } per row, where action is delete or restore and code and error are what PATCH / DELETE /data/:object/:id answer the same thrown error with (mapDataError, then the nested ADR-0112 envelope's code; a hook's refusal reads in the hook's own sentence, and a fault's driver text stays withheld). failed is the length of failures, and success is false when any row is listed.
  • The job's history says what happened. The job row is stamped reverted_at only when every row was reverted. An undo that left rows unreverted stamps nothing and rewrites the job's undo log to exactly those rows, each refusal beside them under failures. The job stays undoable, and undoing it again retries only those rows, never one already reverted.
  • Unchanged. A historical job (treat_as_historical) keeps preserveAudit on its undo for every caller. The job-row write that records the outcome stays engine-owned (isSystem: true, ADR-0103). The 404, 409 ALREADY_REVERTED and 422 NOT_UNDOABLE answers are unchanged. The import doors and their opt-out gate are not touched. The undo writes go through the protocol's deleteData / updateData, not through @objectstack/core's import runner, which is unchanged.

The cost, named: undoing an ordinary import may fire record-change automation again (a notification, an approval re-opening on a restored amount). That is the app's declared behaviour for those writes.

Spec and docs

  • packages/spec/src/data/hook.zod.ts: the skipAutomations describe on the hook context's session names the system principal's import and undo as the skip cases and states that the import undo door is not an exception.
  • packages/spec/src/kernel/execution-context.zod.ts: the skipAutomations TSDoc says the data-import runner (runImport) sets it from its caller's runAutomations: false, which the import doors admit over HTTP from the system principal alone while an in-process caller of the runner passes its own setting, and that the import undo sets it only when the system principal undoes a job; the skipStateMachine TSDoc says the same of the undo.
  • packages/spec/src/api/export.zod.ts: UndoImportJobResponseSchema gains failures; the success and failed describes match it; revertedAt on ImportJobProgressSchema and ImportJobSummarySchema says it is set only once every row is reverted.
  • content/docs/permissions/system-context.mdx gains row 30c for the new isSystem read (packages/rest/src/data-action-routes.ts#importUndoWriteContext); content/docs/protocol/objectql/state-machine.mdx says the undo walks the state machine for every caller but the system principal.
  • Generated artifacts come from the generators only: content/docs/references/api/export.mdx, content/docs/references/data/hook.mdx, packages/spec/authorable-surface/api.json and the strictness-ledger counts.
  • packages/platform-objects/src/audit/sys-import-job.object.ts: the undo_log field's description adds that a partial undo keeps only the unreverted rows there, with a failures list giving each one's id, action, code and refusal. Its en i18n bundle (packages/platform-objects/src/apps/translations/en.objects.generated.ts) is regenerated with node scripts/check-i18n-bundles.mjs --filter=platform-objects --write; the translated locales keep their values (merge mode).

Changesets

  • .changeset/24380-import-undo-controls.md: '@objectstack/rest': minor, summary fix(rest)!:, Clause-②: yes (narrowing), one ADR-0087 marker (not-required (no-migration-prescription)), and a **BREAKING** sentence that names the cost and the remedy (resolve what refuses the row on the app and undo again; a reversal that must skip the controls runs in-process as the system principal).
  • .changeset/24380-undo-response-failures.md: '@objectstack/spec': minor, summary fix(spec)!:, Clause-②: yes (narrowing), one ADR-0087 marker (not-required (no-migration-prescription): no authored key or stored shape moves), and a **BREAKING** sentence saying who notices the required failures: a response parsed with UndoImportJobResponseSchema is refused without it; code constructing an UndoImportJobResponse must set it, [] on a full revert; readers of client.data.undoImportJob are unchanged. failures stays required.

Tests

packages/rest/src/import-undo-controls.test.ts, over the real routes, a real ObjectQL engine, the real protocol and a SQLite :memory: driver. The fixture app declares an approval-gate hook, a closed-record freeze hook (on update and delete) and a state_machine rule with a terminal state.

  1. A signed-in caller's undo runs the object's hooks on its reversal writes and reverts every row (the default path: success: true, failures: [], the job stamped reverted).
  2. The system principal's undo keeps the skips: no metadata-bound hook runs on its reversal writes.
  3. A signed-in caller's undo walks the state machine: a restore it refuses is reported per row in the answer and in the job's stored undo log, is not counted as reverted, and leaves the job undoable; the system principal's second undo retries only that row and stamps the job.
  4. A delete and a restore the freeze refuses are each reported per row, and the job keeps both.
  5. A historical job keeps preserveAudit on a signed-in caller's undo, which carries no control skip.
  6. A historical job keeps preserveAudit on the system principal's undo, beside its skips.

Readings at head 79b4ccf16:

  • pnpm --filter @objectstack/rest exec vitest run --maxWorkers=2 src/import-undo-controls.test.ts: 6 passed.
  • pnpm --filter @objectstack/rest exec vitest run --project local --maxWorkers=2: 299 files passed, 6085 tests passed, 326 skipped.
  • pnpm --filter @objectstack/rest run typecheck: exit 0; check:test-typecheck OK, and --listFiles on tsconfig.test.json includes the new suite.
  • pnpm --filter @objectstack/spec run typecheck: exit 0. Spec suites over the edited schemas (src/api/export.test.ts, src/data/hook.test.ts, src/kernel/execution-context.test.ts, src/type-alias-convention.pin.test.ts): 169 passed; --project repo src/api/export-job-family-retirement.test.ts: 7 passed.
  • eslint --no-inline-config over the six changed TypeScript files: exit 0, no output.

Patch round 1, readings at head 83425d5bc:

  • pnpm --filter @objectstack/rest exec vitest run --maxWorkers=2 src/rest-write-response-internal-fields.tripwire.test.ts src/import-undo-controls.test.ts: 2 files, 21 passed. The tripwire's undo row keeps no-record-echo; its why now says the answer carries counts and per-row failures (record id, action, code, refusal sentence), never a record body.
  • pnpm --filter @objectstack/rest run typecheck: exit 0.
  • pnpm --filter @objectstack/platform-objects exec vitest run --maxWorkers=2: 77 files, 1184 passed; pnpm --filter @objectstack/platform-objects run typecheck: exit 0.
  • pnpm --filter @objectstack/spec run typecheck: exit 0; src/kernel/execution-context.test.ts and src/api/export.test.ts: 70 passed.
  • eslint --no-inline-config over the four files this round changed: exit 0, no output.

Ablation of the gate (one-off, not kept): scripts/ablation-replace.mjs replaced the system-principal conditional in importUndoWriteContext with unconditional skips (anchor hit 1 time, blob bf6a57a75bbf to 01b7721efd21), ran the suite, and restored. Exactly the four signed-in-caller pins turned red, each on its own assertion (the approval gate did not re-open approval; the state machine refused nothing, so failed read 0; the freeze refused nothing, so the delete and restore both went through; the historical undo carried skipAutomations). The two system-principal pins stayed green. Restore proven: blob after restore bf6a57a75bbf equals the blob at HEAD, and git diff HEAD is empty. The rest suite resolves data-action-routes.ts from src/, so no build sat between the mutation and the run.

Gates

Local subset at head 79b4ccf16, the derivation's own list: node scripts/pm/dispatch-gates.mjs --commands printed 111 commands for this change set (14 paths vs merge base 8baeda520, +571 / -44 lines, under the 3000-line threshold). All 111 were run one by one with each exit code captured before any pipe, and all 111 exited 0. node scripts/pm/dispatch-gates.mjs --ran over that record: "111 derived, 111 run, 0 NOT-MEASURED, 0 UNRUN", exit 0. Patch round 1 at head 83425d5bc: the derivation printed 113 commands (17 paths, +584 / -48; the two new ones are pnpm check:i18n and pnpm check:i18n-stale-fill); all 113 were run with each exit code captured before any pipe and all 113 exited 0 (two first answered exit 3, prerequisite not met in a fresh worktree, and exited 0 once the named packages were built); --ran: "113 derived, 113 run, 0 NOT-MEASURED, 0 UNRUN", exit 0. Verdict lines from the families this diff is about, at the round's head:

  • check-adr-0087-registration.mjs --base origin/main: "2 declared-breaking changeset(s), each carrying an ADR-0087 disposition" (24380-import-undo-controls.md and 24380-undo-response-failures.md, each with signals BREAKING+bang+clause-②-narrowing and not-required (no-migration-prescription)).
  • check-changeset-no-major.mjs --base origin/main: "This diff introduces no major bump". Its level axis reads the PR body, so it was also driven offline with this body as the event payload (--event): "this PR declares clause-② yes (narrowing), and no package whose published source ... it moves is graded patch", exit 0.
  • check-empty-changeset.mjs: no empty-frontmatter changeset, 2 declaring changesets added.
  • check-system-context-census.mjs: OK, 144 elevation read sites; the page cites 138 symbols against 138 required (row 30c anchors the new read).
  • @objectstack/spec: check:generated "All 14 generated artifacts are up to date"; check:docs "227 generated files in sync"; check:api-surface unchanged; check:authorable-surface, check:strictness-ledger green.
  • check:nul-bytes OK; check:route-envelope green; check:cross-package-test-inputs OK.
  • check:i18n "OK (10 package(s) — all bundles in sync, no undeclared authoring keys)"; check:i18n-stale-fill OK; check:keyed-text-bounds green.
  • Local-only check:dev-prereqs (the spec build was touched): exit 0.

Outside the derived list and owned by CI: the type-check lanes, Test Core, Build Core, Build Docs, the declared-wide families and the repo-wide pnpm lint.

Resumed work

This branch resumes a dev run stopped by a container restart before it reported (a23786da7, five commits). All five were kept as they were, after reading each against the ruling and the claim; this run merged origin/main (no conflict, nothing generated touched) and added no other commit.

Patch round 1 (record 6258647218, ② FAIL on the spec changeset's arm, and the seat's REWORK) added four commits: aabf9edb5 (the spec changeset as a narrowing; the ExecutionContext TSDoc names runImport's in-process caller), 7c779806f (the tripwire's undo row), 483069d1f (the undo_log description) and 83425d5bc (its regenerated en bundle). failures stays required.

Acceptance notes

  • objectui's Import Wizard (packages/plugin-grid/src/ImportWizard.tsx, useImportJobUndo, read at objectui 79cb8c0) awaits the undo and does not read its answer, so after a partial undo it shows no message for the rows left unreverted, and the job stays listed as undoable. A code reading, not measured; filed as #24433.
  • GET /import/jobs/:jobId and /results do not project the stored failures; the undo answer and the job row carry them, which meets the ruling's "job's history". The three translated locales of the undo_log help text keep their earlier sentence (merge mode); check:i18n-stale-fill is green.
  • packages/objectql/src/engine.ts, the comment above the skipAutomations read in the hook trigger: it names "import undo" among the callers that set the flag, which stays true of the system principal's undo. An internal comment, not a published describe; left as is.
  • docs/qa/platform-checklist/areas/records-forms.json, item records-forms.import-job-undo-cancel, grades an undo that leaves rows behind a FAIL. On its fixture, with no refusing control, that still holds; where a control refuses a row, the row is now left and reported in failures by design. Not edited.
  • packages/spec/src/data/hook.zod.ts is also named by Hook ctx.title is documented as part of every hook context, but @objectstack/verify bootStack runs app hooks without it: "ctx.title is not a function" under the test harness, while objectstack dev serves it #24289 (a different field). Whichever lands second merges main and regenerates through scripts/pm/os-regen-merge.sh.

Generated by Claude Code

…for every caller but the system principal

The undo door's restore and delete writes keep skipAutomations and
skipStateMachine only for the system principal. A reversal a control
refuses is reported per row in the undo answer and kept, with its
refusal, in the job's undo log; reverted_at is stamped only when every
row was reverted.

Claude-Session: https://claude.ai/code/session_01SYEJqDL3NXxrJRBVqaMu9T
Co-authored-by: Claude <noreply@anthropic.com>
…s names no undo exception

Claude-Session: https://claude.ai/code/session_01SYEJqDL3NXxrJRBVqaMu9T
Co-authored-by: Claude <noreply@anthropic.com>
…port-undo-controls

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

github-actions Bot commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/platform-objects, @objectstack/rest, @objectstack/spec, touching 22 documentable anchor(s). ⚠️ 1 changed file(s) yielded no anchor (packages/spec/authorable-surface/api.json), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

24 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 db1f1fd20fba543408b209746e566b48373a4622.

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

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/spec/authorable-surface/api.json) — pages documenting those are invisible to this run
  • 1 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 — 146 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 db1f1fd20fba543408b209746e566b48373a4622 → packageMentionDocs.

Which tree this was computed on

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

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

⚠️ 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 db1f1fd20fba543408b209746e566b48373a4622 → 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

席位核对

ACCEPT (lane review; ⛔ not yet queueable: a CONTRACT_REVIEW_TIER review is owed) · domain:cli seat 1 (seat post #6024) · zhuangjianguo · session session_01SYEJqDL3NXxrJRBVqaMu9T · 2026-10-19T02:36Z · on head 79b4ccf16 · #24380 (p1, security, claim 6257221825, report 6258416381). This is the seat's own review at the default tier, read against GitHub, not the report. No reproduction is written here (the disclosure ruling, 2026-08-18, #9387).

  1. Shape:
    • Draft against main, Fixes objectstack-ai/objectstack#24380, title fix(rest)!:, and a line-initial Clause-②: yes (narrowing) with its basis: the undo door narrows its skips, and UndoImportJobResponseSchema gains failures. That is the case the claim named.
    • 14 files, +571 / −44. Not governed, under 3,000.
    • Two changesets:
      • @objectstack/rest minor in the narrowing shape: one ADR-0087 marker, **BREAKING** with the ruling's cost and a remedy;
      • @objectstack/spec minor, Clause-②: yes (widening), for the added key and the describes.
    • The run resumed from a23786da7 after a container restart. All 5 resumed commits were read and kept, plus one clean merge of main.
  2. The ruling, read on the diff (6257104178):
    • Item 1. importUndoWriteContext (data-action-routes.ts) keeps skipAutomations / skipStateMachine only when isSystem === true, the predicate of 741aa85da. Every other caller's deletes and restores run under the object's hooks, triggers and state machine. No permission, capability or standing is read. The engine-owned job-row write keeps isSystem.
    • Item 2. A thrown reversal is caught per row and classified as the single-record doors classify it (mapDataError, which withholds driver text). It is listed in failures and failed counts it, success is false, and the job's undo log is rewritten to exactly the unreverted rows beside their failures. reverted_at is stamped only when every row reverted, so a retry touches only what remains.
    • Item 3. A historical job keeps preserveAudit for every caller.
    • Item 4. The skipAutomations describe (hook.zod.ts) names the system principal's undo and says the undo door is not an exception. execution-context.zod.ts and the docs (state-machine.mdx, system-context.mdx, the generated export.mdx and hook.mdx) say the same.
    • Item 5. The changeset names the re-fired automation as the app's declared behaviour.
  3. Tests (import-undo-controls.test.ts, new, 6). They run on real routes with ObjectQL and SQLite, using fixture approval-gate, freeze and state-machine controls:
    • a signed-in caller's undo runs the controls;
    • the system principal keeps the skips;
    • refused rows are reported and kept, and a retry reverts only those;
    • a historical job keeps preserveAudit;
    • the default path reverts everything with failures: [].
    • Ablation (the gate made unconditional): exactly the 4 signed-in pins red, the 2 system-principal pins green, with the restore proven.
    • rest full suite: 299 files, 6,085 passed. rest and spec typecheck exit 0. eslint exits 0.
  4. Gates:
    • dispatch-gates: 111 derived, 111 run, 0 NOT-MEASURED.
    • The ADR-0087 and LEVEL readers pass, including offline with the PR body as the event.
    • check:system-context-census, spec check:generated / check:docs / check:api-surface / check:authorable-surface / check:strictness-ledger all exit 0.
    • CI on this head was running at this stamp.
  5. Findings:
  6. Landing:
    • ⛔ Not ready until a ## Contract review record at CONTRACT_REVIEW_TIER reads PASS on the current head. Its scope is the whole non-prose diff, because this is a narrowing that also widens a published schema. needs:contract-review is on the PR until then.
    • After that, ready plus auto-merge through the queue once every check is green.
    • Cross-lane: [PM seat] domain:spec — 🟢 os-warren #6017 6257228801.

Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 79b4ccf165dd936ecfe111c8c8a01e08099974ee
Local-runs: none
Scope: packages/spec/src/api/export.zod.ts, packages/spec/src/data/hook.zod.ts, packages/spec/src/kernel/execution-context.zod.ts + the narrowing floor, every non-prose path of the diff: packages/rest/src/data-action-routes.ts, packages/rest/src/import-undo-controls.test.ts, packages/rest/src/rest-write-route-hook-refusal-sentence.ledger.test.ts, .changeset/24380-import-undo-controls.md, .changeset/24380-undo-response-failures.md, packages/spec/authorable-surface/api.json, docs/audits/2026-07-unknown-key-strictness-ledger.counts/api.md; the cited code, read at this head: packages/rest/src/import-prepare.ts (importControlOptOutRefusal), packages/rest/src/rest-server.ts (parseUndoLog, importJobUndoable, importJobToProgress, the resolveExecCtx seam), packages/rest/src/error-response.ts (nestedRefusalEnvelope, logUnexpectedRouteError), packages/types/src/data-error-classification.ts (mapDataError), packages/objectql/src/engine.ts (the skipAutomations / skipStateMachine reads), packages/platform-objects/src/audit/sys-import-job.object.ts, packages/client/src/index.ts (data.undoImportJob), packages/rest/src/import-job-integration.test.ts; as claims about the code: content/docs/permissions/system-context.mdx, content/docs/protocol/objectql/state-machine.mdx, content/docs/references/api/export.mdx, content/docs/references/data/hook.mdx; and, for the consumer flag, objectui packages/plugin-grid/src/ImportWizard.tsx and packages/types/src/data.ts at 79cb8c0.

Read against card #24380 (body and every comment: triage 6253801522, the director delta 6256872219, the ruling 6257104178, the claim 6257221825, the interruption note, the dev report 6258416381), the PR body, its 14-file list, the three-dot diff from merge base 8baeda520 (+571 / -44, the same 14 paths GitHub lists), and the check-runs on this head. The lane ACCEPT 6258459177 was read as claims to verify, not adopted. Detail is withheld under the maintainer's disclosure ruling of 2026-08-18 on #9387: code positions and the fix's behaviour only; nothing here reproduces anything.

① Derived judgments

Each ruling item (6257104178, items 1 to 5), and each change the diff implies, judged right or wrong on the code.

  1. Who skips (item 1) — right. importUndoWriteContext (data-action-routes.ts, new, called once at the undo route) spreads the resolved caller context and adds skipAutomations: true, skipStateMachine: true only when isSystem === true, read as (context as { isSystem?: unknown } | null | undefined)?.isSystem === true — byte-for-byte the predicate importControlOptOutRefusal asks at import-prepare.ts:285 (741aa85da). rest-server.ts states twice that isSystem is never set on inbound HTTP, and at this head it carries no spelling of skipAutomations, skipStateMachine, preserveAudit or seedReplay, so no HTTP caller can carry a skip in through the spread. Every other caller's deletes and restores therefore run under the object's metadata-bound hooks (engine triggerHooks: the skip reads session.skipAutomations and suppresses entry.meta hooks only), its record-change triggers (engine: skipAutomations implies skipTriggers) and its state_machine rule (shouldSkipStateMachine reads skipStateMachine / seedReplay and nothing else). The engine-owned job-row write keeps context: { ...context, isSystem: true } (ADR-0103). No permission, capability or config key is read or added. Pinned: tests 1, 2, 5, 6; the ablation reported in the PR body is consistent with that pin set.

  2. A refused row (item 2) — right. Each reversal is tried per row; a throw pushes { id, action, code, error } onto failures and the row onto unreverted (the created id, or the updated { id, before } snapshot). deleted / restored count successes only, failed is failures.length, success is failures.length === 0. reverted_at is written only when failures is empty; otherwise the job row's undo_log is rewritten to { created: the unreverted ids, updated: the unreverted snapshots, failures } and nothing is stamped. parseUndoLog (rest-server.ts:944) reads created and updated only and tolerates the extra key; importJobUndoable is true while that log is non-empty and reverted_at is unset. So a retry reads exactly the rows that remain and never one already reverted, and GET /import/jobs/:jobId keeps answering undoable: true with no revertedAt. Pinned: test 3 (one refused restore, the stored log, the retry reverting only that row and stamping), test 4 (a refused delete and a refused restore, both kept). The ruling's "the job's history" is met by the stored row; see ③ for what the progress route does not project.

  3. Withholding — right. importUndoFailure classifies with mapDataError (packages/types, the classification PATCH / DELETE /data/:object/:id use): a sandboxed hook's refusal answers its innerMessage sentence, a validation rule its VALIDATION_FAILED message, and anything unclassified lands in UNCLASSIFIED_FAULT — 500 INTERNAL_ERROR with the fixed sentence and no driver text. nestedRefusalEnvelope fills code from the catalog's floor when the classification declared none, so code is always a string as the schema requires, and message is the classified body's error. logUnexpectedRouteError(error, mapped) logs a genuine fault once, as those doors do. Pinned: VALIDATION_ERROR with the hook's own sentence (test 4), VALIDATION_FAILED with the rule's message (test 3). The fault arm has no pin of its own here; the classification is the shared one and its own tests hold it.

  4. preserveAudit (item 3) — unchanged, right. job.treat_as_historical ? { preserveAudit: true } : {} for every caller, beside the skips for the system principal and alone for everyone else. Pinned both ways (tests 5, 6, on the contexts the protocol actually received).

  5. Other callers — read. packages/client data.undoImportJob (two seams) returns Promise<UndoImportJobResponse> by cast and parses nothing; readers gain failures. rest-route-ledger.ts row unchanged. import-job-integration.test.ts asserts the undo answer with toMatchObject, unaffected. The 404, 409 ALREADY_REVERTED and 422 NOT_UNDOABLE answers are untouched; so are the import doors and runImport. objectui data-objectstack wraps the answer as its own ImportJobUndoResult (@object-ui/types, no failures member, so no compile effect there) and ImportWizard.tsx useImportJobUndo awaits the call without reading the answer: the consumer gap, objectui Import Wizard: an import undo that leaves rows unreverted (success: false with failures, after #24380) shows no message, and the job is still listed as undoable objectui#13015 (③).

  6. Spec describes and prose — true of the code, one imprecision. hook.zod.ts skipAutomations: true of the engine (metadata-bound hooks only; implies skipTriggers) and of both doors. export.zod.ts: the revertedAt, success, failed and failures describes match the route line for line (deletes first, retry only the remaining rows, empty on a full revert). execution-context.zod.ts: true of the HTTP doors; "set by the data-import runner when the system principal opts out" describes the doors, while an in-process non-system caller of runImport still sets its own flags (as 741aa85da's changeset says). Not false of the published surface; a note. system-context.mdx row 30c anchors importUndoWriteContext and is true (the census gate is green); the state-machine.mdx bullet is true. Generated export.mdx, hook.mdx, authorable-surface/api.json (UndoImportJobResponse:failures) and the strictness count 445 to 446 are what the generators produce; Type Check · source gates, the lane carrying spec check:generated / check:docs, is green on this head.

  7. The failures shape — right as a shape. A required array, always emitted ([] on a full revert), is a sound shape for a server-emitted answer and is the shape the repo chose for ListAiConversationsResponseSchema.hasMore ("required rather than optional so a server that does not compute it is off-contract"). What a required key implies for the declaration is ②.

  8. Tests. Six, on the real routes, a real ObjectQL engine, the real protocol and a sqlite memory driver, with fixture approval-gate and freeze hooks and a terminal-state state_machine. Every ruling item has a pin (item 1: tests 1, 2, 5, 6; item 2: tests 3, 4; item 3: tests 5, 6); test 3 also parses the answer with UndoImportJobResponseSchema.strict(). The ledger test's change is a comment only; its two undo/cancel entries are unchanged and still right.

② Semver level

  • @objectstack/rest changeset — right. minor, fix(rest)!:, line-initial Clause-②: yes (narrowing), exactly one marker not-required (no-migration-prescription), a **BREAKING** sentence naming the cost the ruling names and the remedy. 741aa85da's shape, and what the diff publishes: fewer controls taken off the writes, plus the answer's new key and the changed meaning of success and reverted_at. The marker's category holds: nothing an author writes moves, the undo log stays an engine-owned json column, no arrow-bearing prescription in the body.
  • PR body — right. Clause-②: yes (narrowing), line-initial, with its basis. Both moved packages are graded minor, so the level axis reads clean (Check Changeset green on both runs).
  • @objectstack/spec changeset — wrong arm. It reads minor, Clause-②: yes (widening), no **BREAKING**, no marker, no !, with the basis that spec "narrows nothing". UndoImportJobResponseSchema gains a REQUIRED key: at the merge base UndoImportJobResponseSchema.parse accepted an undo answer without failures, at this head it refuses one (the PR's own test 3 parses with .strict()), and any code that constructs a UndoImportJobResponse must now supply it. That is an accept-set narrowing of a published schema, and the repo's own practice names it: optional additions ship as widenings (GetMetaItemResponseSchema.registration, "a required key would have broken all of them"), and the required hasMore on ListAiConversationsResponseSchema shipped under yes (narrowing) with a **BREAKING** paragraph ("Who notices: code that constructs ... must now set hasMore, and a response parsed with the schema is refused without it"). The gates cannot see this one: yes (widening) at minor is a legal declaration, so check-adr-0087-registration never judges the spec changeset (its signals are major, **BREAKING, a bang, or the narrowing arm) and the CHANGELOG entry that ships to spec consumers carries no break notice — the gap the arm exists to close (clause2-line.mjs, the feat(platform-objects): sys_job.timezone and sys_report_schedule.timezone are validated against the IANA domain #16296 lesson). Remedy, changeset-only, in .changeset/24380-undo-response-failures.md: (a) Clause-②: yes (narrowing); (b) a fix(spec)!: summary and a **BREAKING** sentence saying who notices — a response parsed with UndoImportJobResponseSchema is refused without failures; code constructing a UndoImportJobResponse must set it, [] on a full revert; readers of client.data.undoImportJob are unchanged — shipped as minor under the launch-window convention; (c) one marker not-required (no-migration-prescription), since no authored key or stored shape moves. The PR body's line stays as it is. The alternative, .optional() on failures keeping yes (widening), weakens the contract against the hasMore precedent and is not recommended.

③ Boundary flags

  • Disclosure — kept. The PR body, both changesets, the test file and its six test names state code positions and the fixed behaviour; the ablation paragraph describes the test's own assertions and stays within what the card body, triage and the director delta already published. No request, payload or steps anywhere on the PR or the card.
  • #24433 — adequate. Filed with Blocked-by: objectstack-ai/objectstack#24380, no reproduction, both sides read in code (the wrapper returns success: false without throwing; the hook never reads the answer). Its remedy will need the rows and reasons, which today only the undo answer and the job row carry (next flag).
  • The three acceptance notes. (i) sys_import_job.undo_log's description names created / updated only; after a partial undo the column also carries failures. A note is within rule 10, but this PR's own behaviour change makes the line stale and it is a one-line edit: recommended in the same landing, with the changeset fix. (ii) The engine.ts comment stays true of the system principal; fine. (iii) The QA item records-forms.import-job-undo-cancel grades rows left behind a FAIL; that holds on its fixture (a scratch object with no refusing control); a trap line that a failures-listed row is left by design would help the next runner; fine as a note.
  • Not raised by the dev, none blocking. (a) GET /import/jobs/:jobId and /results do not project the stored failures: over the API the rows and reasons are visible in the undo answer and in a read of the job row, while the progress answer says only undoable: true and no revertedAt. The ruling's "job's history" is met by the stored row; a follow-up candidate beside objectui Import Wizard: an import undo that leaves rows unreverted (success: false with failures, after #24380) shows no message, and the job is still listed as undoable objectui#13015, carrier none. (b) rest-write-response-internal-fields.tripwire.test.ts:392 keeps the undo route as no-record-echo with why: 'Undo summary (counts).': the kind still holds (ids and sentences, no record body), the why prose is stale. (c) The execution-context.zod.ts TSDoc over-narrows the in-process runImport case (① 6).
  • hook.zod.ts is also named by Hook ctx.title is documented as part of every hook context, but @objectstack/verify bootStack runs app hooks without it: "ctx.title is not a function" under the test harness, while objectstack dev serves it #24289; whichever lands second regenerates through os-regen-merge.sh. Noted on the PR; fine.
  • Check-runs on 79b4ccf16, read at 2026-10-19T02:50Z: 30 success, 8 skipped by their if: (the main battery), 6 in progress — Test Core (1/6) to (6/6). Of the five required contexts, Lint & Repo Gates, TypeScript Type Check, Build Core and Governed Surface Queue Guard are success and Test Core was still pending; Check Changeset is success on both of its runs, and all four Type Check · lanes are success. Nothing red. Their conclusions are the gate verdicts; this record adds no local run.

Implemented-by: claude/issue-24380-import-undo-controls
Reviewed-by: session_01SYEJqDL3NXxrJRBVqaMu9T

VERDICT: FAIL

One finding blocks: the @objectstack/spec changeset's Clause-② arm (②), a changeset-only fix. ① and the rest of ② hold on this head, and the recommended undo_log description line rides along; a fresh record on the new head is owed after the fix.

Rendered 2026-10-19T02:54Z · read-only: the diff, the card and the head's check-runs, no local run.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 — REWORK (patch round 1: the spec changeset's Clause-② arm, and three lines this PR made stale)

domain:cli seat 1 (seat post #6024) · zhuangjianguo · session session_01SYEJqDL3NXxrJRBVqaMu9T · 2026-10-19T02:56Z · on head 79b4ccf16 · #24380 (p1, security). The CONTRACT_REVIEW_TIER record 6258647218 reads FAIL on ②, and the seat takes it.

The seat's miss, named: the lane ACCEPT 6258416381 accepted the @objectstack/spec changeset as yes (widening). UndoImportJobResponseSchema gains a REQUIRED failures, so a response parsed with the schema is refused without it, and code constructing an UndoImportJobResponse must set it. That narrows the schema's accept set. The repository's precedent, the required hasMore on ListAiConversationsResponseSchema, shipped as yes (narrowing) with a **BREAKING** paragraph.

Owed in this round:

  1. .changeset/24380-undo-response-failures.md:
    • Clause-②: yes (narrowing);
    • a fix(spec)!: summary;
    • a **BREAKING** (...) sentence saying who notices: a response parsed with UndoImportJobResponseSchema is refused without failures; code constructing an UndoImportJobResponse must set it, [] on a full revert; readers of client.data.undoImportJob are unchanged. It is shipped as minor under the launch-window convention.
    • Exactly one ADR-0087 marker, not-required (no-migration-prescription).
    • failures stays required; making it .optional() is not taken.
    • The PR body's line stays as it is.
  2. packages/platform-objects/src/audit/sys-import-job.object.ts, the undo_log description. It names {created, updated} only, and after a partial undo the column also carries failures. Add one line in the same landing. It is domain:engine's file, declared on [PM seat] domain:engine — 🟢 os-project-manager #6367 in this act. Regenerate whatever generated artefact follows from it, and run the i18n gates the derivation names.
  3. packages/rest/src/rest-write-response-internal-fields.tripwire.test.ts (about :392): the undo route's why: 'Undo summary (counts).' is stale now that the answer carries failures. The no-record-echo kind still holds.
  4. packages/spec/src/kernel/execution-context.zod.ts: the TSDoc this PR added over-narrows the in-process runImport case (record ① 6). Make it exact.

Acceptance notes, not changed here: GET /import/jobs/:jobId and /results do not project the stored failures. The undo answer and the job row carry them, which meets the ruling's "job's history".

⛔ Do not patch the PR body. Give exact old/new pairs for any section the round makes stale; the seat writes them. A fresh CONTRACT_REVIEW_TIER record on the new head is then owed. needs:contract-review stays on.


Generated by Claude Code

…wing; ExecutionContext names runImport's in-process caller

The @objectstack/spec changeset now reads fix(spec)!:, Clause-②: yes
(narrowing), one ADR-0087 marker and a BREAKING sentence saying who
notices the required failures key. The skipAutomations TSDoc says the
import doors admit the opt-out over HTTP from the system principal alone
while an in-process caller of runImport passes its own setting.

Claude-Session: https://claude.ai/code/session_01SYEJqDL3NXxrJRBVqaMu9T
Co-authored-by: Claude <noreply@anthropic.com>
…failures it answers

Claude-Session: https://claude.ai/code/session_01SYEJqDL3NXxrJRBVqaMu9T
Co-authored-by: Claude <noreply@anthropic.com>
…eps the unreverted rows with a failures list

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

node scripts/check-i18n-bundles.mjs --filter=platform-objects --write; the
translated locales keep their values (merge mode).

Claude-Session: https://claude.ai/code/session_01SYEJqDL3NXxrJRBVqaMu9T
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 — amendment for head 83425d5bc

ACCEPT carried to head 83425d5bc · domain:cli seat 1 (seat post #6024) · zhuangjianguo · session session_01SYEJqDL3NXxrJRBVqaMu9T · 2026-10-19T03:37Z · #24380 (p1, security). This amends the lane ACCEPT 6258416381 on 79b4ccf16 after REWORK 6258663796 and report 6259079305. No reproduction is written here (#9387).

  1. What moved, read on the compare 79b4ccf16...83425d5bc: four commits, five files.
    • .changeset/24380-undo-response-failures.md is now fix(spec)!: with a line-initial Clause-②: yes (narrowing). readClause2Line reads it as yes / narrowing. It carries one not-required (no-migration-prescription) marker and a **BREAKING** sentence: a response parsed without failures is refused, a constructor must set it, and client.data.undoImportJob readers are unchanged. failures stays required.
    • sys-import-job.object.ts: only the undo_log description changes, plus its regenerated en bundle. Declared on [PM seat] domain:engine — 🟢 os-project-manager #6367.
    • The tripwire's undo why now names the per-row failures (ids, codes, sentences, never a record body).
    • The execution-context.zod.ts TSDoc is exact about runImport's in-process caller and the system principal's undo.
  2. PR body: the seat wrote the report's 11 exact edits, each matching once. They were sent through the relay and read back identical (15,045 bytes). The first line reads Fixes objectstack-ai/objectstack#24380, which closes the same card.
  3. Gates, from the report: check-adr-0087-registration now reads "2 declared-breaking changeset(s), each carrying an ADR-0087 disposition". The LEVEL axis passes offline with the corrected body. dispatch-gates: 113 derived, 113 run, 0 NOT-MEASURED. check:i18n and check:i18n-stale-fill are green. The rest, spec and platform-objects suites and typechecks pass.
  4. Landing: a fresh CONTRACT_REVIEW_TIER record is owed on 83425d5bc, and needs:contract-review stays on until it reads PASS. Then come the pre-checks and the queue.

Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 83425d5bc1cb39855f7f991131b7d0d8d3c60e69
Local-runs: none
Scope: packages/spec/src/api/export.zod.ts, packages/spec/src/data/hook.zod.ts, packages/spec/src/kernel/execution-context.zod.ts + the narrowing floor, every non-prose path of the diff: packages/rest/src/data-action-routes.ts, packages/rest/src/import-undo-controls.test.ts, packages/rest/src/rest-write-response-internal-fields.tripwire.test.ts, packages/rest/src/rest-write-route-hook-refusal-sentence.ledger.test.ts, .changeset/24380-import-undo-controls.md, .changeset/24380-undo-response-failures.md, packages/spec/authorable-surface/api.json, docs/audits/2026-07-unknown-key-strictness-ledger.counts/api.md, packages/platform-objects/src/audit/sys-import-job.object.ts, packages/platform-objects/src/apps/translations/en.objects.generated.ts; of these, the five paths the compare 79b4ccf16...83425d5bc moves, read in full at this head: .changeset/24380-undo-response-failures.md, packages/platform-objects/src/audit/sys-import-job.object.ts, packages/platform-objects/src/apps/translations/en.objects.generated.ts, packages/rest/src/rest-write-response-internal-fields.tripwire.test.ts, packages/spec/src/kernel/execution-context.zod.ts; the cited code, read at this head: packages/core/src/utils/import-runner.ts (runImport's write context), packages/rest/src/import-prepare.ts (importControlOptOutRefusal, readImportControlFlags), packages/platform-objects/package.json, packages/platform-objects/scripts/i18n-extract.config.ts, packages/platform-objects/src/apps/translations/{zh-CN,ja-JP,es-ES}.objects.generated.ts and their .source-hashes.generated.ts companions, .changeset/config.json; the gates' readers: scripts/pm/clause2-line.mjs, scripts/check-adr-0087-registration.mjs, scripts/check-changeset-no-major.mjs, scripts/check-i18n-bundles.mjs, the Check Changeset job in .github/workflows/pr-automation.yml and the check:i18n step in .github/workflows/lint.yml; as claims about the code: the PR body after its 11 edits, content/docs/permissions/system-context.mdx, content/docs/protocol/objectql/state-machine.mdx, content/docs/references/api/export.mdx, content/docs/references/data/hook.mdx.

Re-review after record 6258647218 (FAIL on ②, head 79b4ccf16). Read against card #24380 (body and all nine comments: triage 6253801522, the director delta 6256872219, the ruling 6257104178, the claim 6257221825, the interruption note 6257885956, the dev reports 6258416381 and 6259079305, the lane notes 6258475538 and 6258678601), the PR body, its 17-file list, the three-dot diff from merge base 8baeda520 (+584 / -48, the same 17 paths GitHub lists), the compare 79b4ccf16...83425d5bc (four commits aabf9edb5, 7c779806f, 483069d1f, 83425d5bc; five paths, +23 / -14, verified with git diff --stat), and the check-runs on this head. The lane REWORK 6258663796 and amendment 6259110796 were read as claims to verify, not adopted. Detail is withheld under the maintainer's disclosure ruling of 2026-08-18 on #9387: code positions and the fix's behaviour only; nothing here reproduces anything.

① Derived judgments

Carried. The compare moves none of data-action-routes.ts, import-undo-controls.test.ts, export.zod.ts, hook.zod.ts, the ledger test, the @objectstack/rest changeset, authorable-surface/api.json or the strictness count, so items 1 to 5, 7 and 8 of record 6258647218's ① stand on this head byte for byte: who skips (importUndoWriteContext, isSystem === true alone), the per-row failures and the stored undo log, mapDataError withholding, preserveAudit unchanged, the other callers, the required-array shape, and the six pins. Its item 6 is re-judged below where the bytes moved.

  1. sys_import_job.undo_log description — true of what the route stores. The new sentence reads: a partial undo keeps only the unreverted rows here, with a failures list giving each one's id, action, code and refusal. At data-action-routes.ts the undo route (about :736–:745) writes reverted_at when failures is empty and otherwise undo_log: { ...unreverted, failures }, where unreverted holds exactly the created ids and updated { id, before } snapshots whose reversal threw, and each failures entry is { id, action, code, error } (importUndoFailure, :260). So "only the unreverted rows" is exact, and the four members are named in order, the last one by its content rather than its key: the member is error, and it carries a refusing hook's or rule's own sentence, or the door's fixed withheld-fault sentence when the write failed for another reason. "Refusal" over-names the fault case by one word; a field description, not a schema, and not false of any row a control refuses. Pinned on the stored row: tests 3 and 4 read undo_log back and assert created, updated and failures equal to the answer's.

  2. The regenerated en bundle — the generator's output. The compare's one bundle line is undo_log.help, now byte-equal to the source description (the TS \' resolved to ' inside the double-quoted leaf). pnpm check:i18n runs the config's own documented extract with --check and fails when the committed bundles differ from a fresh extract (check-i18n-bundles.mjs, scripts/i18n-extract.config.ts); it runs in Type Check · consumer gates (lint.yml), which is success on this head. The en locale is rewritten from the source on every run, so a hand edit could not have survived that gate. The three translated bundles did not move, and their .source-hashes.generated.ts companions carry no undo_log entry, which is what the merge-mode claim means in this package: those leaves were translator-written, are legacy-trusted, and are never reported stale (③).

  3. The execution-context.zod.ts TSDoc — exact. skipAutomations now says the runner (runImport) sets it when its caller opts out with runAutomations: false, that over HTTP the doors admit the opt-out from the system principal alone, that an in-process caller passes its own setting, and that the undo sets it only for the system principal. Each clause has its line: import-runner.ts:758 sets skipAutomations: !runAutomations from the option as passed; import-prepare.ts:279–:285 refuses runAutomations: false and treatAsHistorical: true unless isSystem === true, and the doors ask it before the body is parsed; importUndoWriteContext adds the skips on isSystem === true alone. Record 6258647218 ① 6's over-narrowing is gone. The skipStateMachine block only re-wrapped its SCOPE sentence; its undo clause is true (import-runner.ts:766 sets the flag from treatAsHistorical, and the undo sets it for the system principal alone).

  4. The tripwire's undo row — kind still holds, why now true. rest-write-response-internal-fields.tripwire.test.ts:392 keeps POST …/import/jobs/:jobId/undo as no-record-echo, defined at :252 as a body that carries no record of a user data object. The answer carries counts and failures rows of a record id, an action, a code and the door's sentence, never a record body or field values; the why now says exactly that. The row's classification is unchanged, so the tripwire's scan set is unchanged.

  5. PR body after the 11 edits — consistent with the head. The basis paragraph, the execution-context bullet, the platform-objects bullet, both changeset bullets, the round-1 readings and gates, the resumed-work paragraph and the acceptance notes each describe what the five moved files now hold. One citation drifted under it: the body's #24433 is the card's pre-transfer number; GitHub now redirects it to objectstack-ai/objectui#13015, the number the lane ACCEPT cites. One card, two spellings; a note.

② Semver level

  • @objectstack/spec changeset — now right. .changeset/24380-undo-response-failures.md: '@objectstack/spec': minor; first body line fix(spec)!: (the gate's bang signal, breakingDeclaration); a line-initial Clause-②: yes (narrowing) that readClause2Line reads as declared / yes / narrowing (the fixed key at column 0, the value token first, the arm the first token of the parenthetical); exactly one adr-0087: HTML marker, not-required (no-migration-prescription), whose category holds: no authored key, export or error code is removed or renamed, the undo log stays an engine-owned json column that parseUndoLog reads by created / updated and that tolerates the extra failures member on old and new rows alike, and the body carries no FROM-to-TO prescription for the detector to contradict it with; and a **BREAKING** sentence that says who notices: a response parsed with UndoImportJobResponseSchema is refused without failures, code constructing an UndoImportJobResponse must set it ([] on a full revert), readers of client.data.undoImportJob are unchanged. failures stays required (export.zod.ts, no .optional()), the shape 6258647218 ① 7 accepted. The Check Changeset run on this head is success on both of its runs, which is the ADR-0087 gate's verdict on two declared-breaking changesets.
  • @objectstack/rest changeset — unchanged, still right. Not in the compare; minor, fix(rest)!:, Clause-②: yes (narrowing), one not-required (no-migration-prescription) marker, the **BREAKING** sentence naming the ruling's cost and the run-time remedy. Record 6258647218 ② stands on it.
  • PR body — line and basis agree. Clause-②: yes (narrowing) is the first key-initial line of the body (the reader returns on it; the later "Basis for the Clause-② line" sentence carries the key without its colon and is not a declaration). The basis now says the required key both widens the answer and narrows the schema's accept set, and that both changesets carry the same line with one marker each, which is true at this head. On the level axis (check-changeset-no-major.mjs, judgeLevel), the moved packages a changeset names are spec and rest, both minor, so no offender exists and the verdict is clean; Check Changeset success.
  • @objectstack/platform-objects — no changeset of its own, and none is owed by a gate. Its dist is published: package.json has no private, publishConfig.access: public, files: [dist, README.md, CHANGELOG.md], and main / exports resolve into dist/, so the new help sentence ships in the next tarball. The gates do not ask for a changeset for it: Require a changeset counts changesets ADDED per PR (two here), the level axis grades only the packages a changeset names (a package with none is neither offender nor raised), and the ADR-0087 gate judges declared-breaking changesets only. The package is in the one fixed group of 73 with spec and rest, so it versions with them. What moved in it is a field description that follows the stored shape the rest changeset already documents, not a schema, export or error code. Judged: not owed; a patch changeset naming the help text would be the complete form if another round is cut, and is not a reason to hold this one.

③ Boundary flags

  • Disclosure — kept. The four commits add a changeset body, a field description, a TSDoc, a why string and one regenerated line; the body's 11 edits describe them. No request, payload or steps anywhere on the PR, the card or in this record; what is named is positions and the fixed behaviour the card body, the triage and the director delta already published.
  • GET /import/jobs/:jobId and /results do not project failures — answered, not blocking. The ruling's item 2 says the undo result and the job's history; the undo answer carries the rows and reasons, and the job row's undo_log keeps them with reverted_at unset, which importJobUndoable reads as still undoable. The progress answer says undoable: true with no revertedAt and nothing more, so a client that only polls progress cannot see which rows remain or why. The letter is met by the stored row; the projection is the natural remedy seam for objectstack-ai/objectui#13015, whose wizard today drops the one answer that carries the reasons. Carrier: none; a card when that one is picked up, Blocked-by cleared by this landing.
  • The translated undo_log help texts — answered, not blocking, and the green is "not seen", not "verified". zh-CN, ja-JP and es-ES keep the earlier sentence without the partial-undo clause. Their .source-hashes.generated.ts companions carry no entry for the path, which by that file's own contract means the leaves are translator-written, legacy-trusted and never reported stale; check:i18n-stale-fill green therefore says nothing about them. The lag is a translation task for the i18n owner, not a contract defect, and this PR's acceptance note names it; the note's "green" should not be read as a verification.
  • #24433 → objectstack-ai/objectui#13015. The card was transferred; the body's bare number now redirects to the objectui card, which carries Blocked-by: objectstack-ai/objectstack#24380. Adequate as filed (record 6258647218 ③); a note on the spelling only.
  • hook.zod.ts and Hook ctx.title is documented as part of every hook context, but @objectstack/verify bootStack runs app hooks without it: "ctx.title is not a function" under the test harness, while objectstack dev serves it #24289 — unchanged since the previous record; whichever lands second regenerates through os-regen-merge.sh.
  • Check-runs on 83425d5bc, read at 2026-10-19T03:46Z and again at 2026-10-19T03:51Z: 48 runs, all completed — 41 success, 7 skipped by their if: (the main battery: Console Pin Gate, Dogfood Regression Gate shards, Dogfood Verify CLI, Temporal Conformance, Packed-tarball smoke, and the duplicate Auto Label / Check PR Size rows). Nothing in progress, nothing red. The five required contexts are success: Lint & Repo Gates, TypeScript Type Check, Test Core (and its six shards), Build Core, Governed Surface Queue Guard. Check Changeset success on both runs; Type Check · consumer gates (carries check:i18n), Type Check · source gates (carries spec check:generated / check:docs) and Lint · docs & code guards (carries check:i18n-stale-fill) success. Their conclusions are the gate verdicts; this record adds no local run.

Implemented-by: claude/issue-24380-import-undo-controls
Reviewed-by: session_01SYEJqDL3NXxrJRBVqaMu9T

VERDICT: PASS

The one blocking finding of record 6258647218 is closed: the @objectstack/spec changeset declares the required failures as yes (narrowing) in the full breaking shape, and the Check Changeset gate reads two declared-breaking changesets with one disposition each. The four ride-alongs (the undo_log description, its regenerated en line, the tripwire why, the execution-context TSDoc) are each true of the code at this head; nothing else in the non-prose diff moved. Every check on the head is complete and none is red.

Rendered 2026-10-19T03:51Z · read-only: the diff, the card and the head's check-runs, no local run.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 19, 2026 03:55
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 19, 2026 03:55
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 19, 2026
Merged via the queue into main with commit adb7910 Oct 19, 2026
52 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-24380-import-undo-controls branch October 19, 2026 04:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation protocol:data size/l tests tooling

Projects

None yet

2 participants