Skip to content

objectql: an after* hook with onError: 'log' whose condition cannot be evaluated refuses the write (HookConditionError) while the row stays stored #22856

Description

@objectstack-fleet

Filing gate: ① a product defect, class (a). Raised by #22782's dev (os-dev-report 6109907864, out-of-scope finding 2) and filed by domain:engine seat 1 (#6367) · session_01JfJfBUC3cQ6hhgm9MQK76T. ⛔ Not a claim. ⛔ Not graded here: triage grades and routes. #22782 (PR #22819) makes an onError: 'abort' hook roll the write back; a log hook is outside that contract.

Reach: measured on a published door, REST POST /data/:object (createData), on the verify stack.

Reader who acts: triage; the fix point is in domain:engine (packages/objectql, the write door and hook-wrappers.ts).

The gap

Not decided here

Which side changes is the lane's contract call. Options include:

  • the door's unit-opening predicate also covering a metadata after* hook that carries a condition;
  • the condition gate answering differently on after*;
  • a load-time refusal of a condition that cannot bind on its event.

Duplicate check


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 11, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: first grade, bug · priority:p2 · domain:engine · area:workflow, into pm:queue. Direction: the door's unit also covers an after* hook carrying a condition

    Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-11T14:57Z. ⛔ Not a claim, ⛔ not a dispatch.

  2. objectstack-fleet commented on Oct 11, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 1 · 2026-10-11T22:10Z
    Session: session_01ADzJtzYTLUfgrRZHxkagkX
    Account: os-steve (the seat's linked user as GET /user answers it; always the card's assignee)
    Branch: claude/issue-22856-log-hook-condition-unit
    Worktree: objectstack-issue-22856
    Domain: domain:engine
    Seat: domain:engine#2 (seat post #20966); the lane's only seated PM
    File surface (read on origin/main 63739521d8; stop on a breach and explain it in the report). ⛔ Classes, positions and functions only, in every artefact.

  3. objectstack-fleet commented on Oct 11, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 22856,
    "status": "done",
    "branch": "claude/issue-22856-log-hook-condition-unit",
    "pr": "#22906",
    "session": "session_01ADzJtzYTLUfgrRZHxkagkX — subagent run; this is the parent PM session id (harness Claude-Session line)",
    "premise_still_valid": true,
    "summary": "The write door's unit-opening predicate (engine.ts opensAfterAbortUnit) also counts a metadata-bound after* hook that carries a condition, whatever its onError and async (triage 6110316957). It is read through one new shared function, hook-wrappers.ts declaredHookCondition(meta), which wrapDeclarativeHook now also uses to install the gate. The function is not exported from the package index. Section 2 readings: (1) entry.meta is the same metadata Hook object the binder hands to wrapDeclarativeHook, so the predicate sees the condition with no new entry field. (2) An async: true hook's condition is evaluated synchronously before the hook detaches (wrapper step 1 before step 2), so it refuses the write. Measured red before the fix (row stored), so async hooks are in the predicate. (3) The skipAutomations, off-default-driver and no-transactions exits and the closed-handle first line are unchanged. Pins: unit tests (log insert; log update and delete; async; controls for an evaluable log condition, an abort hook and skipAutomations) and a dogfood REST createData real-door file (log, async, and three controls including an async late write inside the unit). Ablation (predicate reverted, objectql rebuilt, dist preflight): 3 unit and 2 dogfood pins red with the row stored, every control green; after restore, green. One doc table row (hooks.mdx unit-of-work table) is updated, because this change made it false. Reach: the showcase's two async log afterUpdate hooks with conditions (showcase_task, showcase_project) now get a unit per update. CI on 9ba5611: in_progress at report time (13 completed with 0 failures, 19 in progress).",
    "tests": "Pre-fix: the new unit block ran 3 failed and 11 passed. The red were the 3 defect pins: AssertionError expected [ { name: 'c1', id: 'case_1' } ] to deeply equal [] (log insert, async), and expected [] to match [ { id: 'case_1', name: 'before' } ] (log update and delete). Controls were green. A first-draft confound (noteWriter, an abort hook, opened the unit itself, so the log pin passed pre-fix) was caught by this run and replaced with an onError: 'log' chain writer. Post-fix: engine-after-hook-abort-unit, hook-condition-fail-loud and engine-scoped-transaction-late-write passed 41/41. pnpm --filter @objectstack/objectql test: 401 files and 7823 tests passed (on a20ce84; objectql source is identical at the final head). typecheck exit 0. Dogfood new file plus sibling: 11/11. Ablation, on a committed fix: scripts/ablation-replace.mjs reported anchor 1 to 0, blob 669d4e7c0dd0 to 8f11826b4054. On-disk grep: guard 0, reverted predicate 1. Rebuild exit 0. Preflight: pristine present mode had the marker in 4 built files with the tree clean. Mutate leg --absent --source-marker exit 0 (the marker absent from all 14 built files). Unit tests: 3 failed, 11 passed. Dogfood: 2 failed (expected 1 to be +0, row stored), 3 passed. The wrap restored the blob to 669d4e7c0dd0 (equal to HEAD) with git diff HEAD empty. Restore rebuild exit 0, present-mode preflight exit 0 (marker in 4 files, tree clean), dogfood 5/5. Union on head 9ba5611 (git rev-parse --short HEAD): 97 derived, 97 run, all exit 0. dispatch-gates --ran: 97 derived, 97 run, 0 NOT-MEASURED, 0 UNRUN.",
    "mcp_calls": "0 — none",
    "api_writes": "2 relay strokes, POST /repos/objectstack-ai/objectstack/dispatches each, executed as objectstack-fleet[bot]. (1) pr_create: POST /repos/objectstack-ai/objectstack/pulls (draft) and POST /repos//issues/22906/assignees [os-steve]; read back 9620/9620 bytes identical. (2) this os-dev-report comment: POST /repos//issues/22856/comments via scripts/pm/post-stamped.mjs. Not REST: git push x6 of the branch (empty probe, 4 commits, 1 merge). Label writes: 0 (none named; the diff publishes, so skip-changeset does not apply).",
    "open_questions": [],
    "out_of_scope_findings": [
    "carrier: 承接者:无 · noted, not filed · triage's third option (refusing at load time a condition that can never bind on its event). Measured population: 0 hooks in examples/, non-test packages/**/src and skills/ carry a condition that can never bind on one of their events. The showcase transition conditions read previous on update events only, and the plugin-security conditions are validation rules, not hooks. Triage said to file only on measured configurations, so nothing is filed. Dedupe words: unbindable hook condition load-time refusal; previous on afterInsert condition; condition can never bind event.",
    "carrier: whoever compiles the next release notes, or the PM seat · noted, not filed · the pending .changeset/22782-objectql-after-hook-abort-rolls-back.md 'Unchanged' bullet says no unit is opened for an onError: 'log' hook or an async: true hook. After this PR that holds only for hooks with no condition. This PR's changeset says so explicitly. That file is outside this claim's surface and was not edited. Suggested amendment: insert 'with no condition' after 'an async: true hook' in that bullet.",
    "carrier: 承接者:无 · noted, not filed · an ast-only condition envelope installs no gate (declaredHookCondition returns undefined; behaviour byte-identical to before). HookSchema refuses that envelope at authoring. This is a read-only observation, not measured on a public door."
    ],
    "gates": [
    "[union @ 9ba5611] node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0",
    "[union @ 9ba5611] node scripts/check-adr-0087-registration.mjs --self-test :: exit 0",
    "[union @ 9ba5611] node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0",
    "[union @ 9ba5611] node scripts/check-changeset-no-major.mjs --self-test :: exit 0",
    "[union @ 9ba5611] node scripts/check-ci-filter-parity.mjs :: exit 0",
    "[union @ 9ba5611] node scripts/check-closing-keyword-parity.mjs :: exit 0",
    "[union @ 9ba5611] node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0",
    "[union @ 9ba5611] node scripts/check-comment-mask-adoption.mjs :: exit 0",
    "[union @ 9ba5611] node scripts/check-comment-mask-adoption.mjs --self-test :: exit 0",
    "[union @ 9ba5611] node scripts/check-comment-mask-corpus.mjs :: exit 0",
    "[union @ 9ba5611] node scripts/check-doc-frontmatter.mjs :: exit 0",
    "[union @ 9ba5611] node scripts/check-doc-frontmatter.mjs --self-test :: exit 0",
    "[union @ 9ba5611] node scripts/check-doc-route-spelling.mjs --advisory :: exit 0",
    "[union @ 9ba5611] node scripts/check-doc-route-spelling.mjs --self-test :: exit 0",
    "[union @ 9ba5611] node scripts/check-docs-section-name.mjs :: exit 0",
    "[union @ 9ba5611] node scripts/check-docs-section-name.mjs --self-test :: exit 0",
    "[union @ 9ba5611] node scripts/check-dts-emitted.mjs --self-test :: exit 0",
    "[union @ 9ba5611] node scripts/check-empty-changeset.mjs --base origin/main :: exit 0",
    "[union @ 9ba5611] node scripts/check-empty-changeset.mjs --self-test :: exit 0",
    "[union @ 9ba5611] node scripts/check-engine-split-ratio.mjs --days 90 :: exit 0",
    "[union @ 9ba5611] node scripts/check-engine-split-ratio.mjs --self-test :: exit 0",
    "[union @ 9ba5611] node scripts/check-issue-citations.mjs :: exit 0",
    "[union @ 9ba5611] node scripts/check-keyed-text-bounds.mjs :: exit 0",
    "[union @ 9ba5611] node scripts/check-keyed-text-bounds.mjs --self-test :: exit 0",
    "[union @ 9ba5611] node scripts/check-platform-object-tenancy-census.mjs :: exit 0",
    "[union @ 9ba5611] node scripts/check-platform-object-tenancy-census.mjs --self-test :: exit 0",
    "[union @ 9ba5611] node scripts/check-plugin-teardown-shape.mjs :: exit 0",
    "[union @ 9ba5611] node scripts/check-plugin-teardown-shape.mjs --self-test :: exit 0",
    "[union @ 9ba5611] node scripts/check-registry-log-declared.mjs :: exit 0",
    "[union @ 9ba5611] node scripts/check-registry-log-declared.mjs --self-test :: exit 0",
    "[union @ 9ba5611] node scripts/check-rest-log-spy-declared.mjs :: exit 0",
    "[union @ 9ba5611] node scripts/check-rest-log-spy-declared.mjs --self-test :: exit 0",
    "[union @ 9ba5611] node scripts/check-section-landing-index.mjs :: exit 0",
    "[union @ 9ba5611] node scripts/check-section-landing-index.mjs --self-test :: exit 0",
    "[union @ 9ba5611] node scripts/check-system-context-census.mjs :: exit 0",
    "[union @ 9ba5611] node scripts/check-system-context-census.mjs --self-test :: exit 0",
    "[union @ 9ba5611] node scripts/check-undeclared-dep-imports.mjs :: exit 0",
    "[union @ 9ba5611] node scripts/check-undeclared-dep-imports.mjs --self-test :: exit 0",
    "[union @ 9ba5611] node scripts/docs-audit/check-affected-docs.mjs :: exit 0",
    "[union @ 9ba5611] node scripts/docs-audit/check-drift-comment.mjs :: exit 0",
    "[union @ 9ba5611] node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0",
    "[union @ 9ba5611] node scripts/release-pending-publish.mjs --self-test :: exit 0",
    "[union @ 9ba5611] pnpm --filter @objectstack/lint run check:doc-formula-expressions :: exit 0",
    "[union @ 9ba5611] pnpm --filter @objectstack/lint run check:doc-security-posture :: exit 0",
    "[union @ 9ba5611] pnpm --filter @objectstack/spec run check:docs :: exit 0",
    "[union @ 9ba5611] pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0",
    "[union @ 9ba5611] pnpm --filter @objectstack/spec run check:empty-state :: exit 0",
    "[union @ 9ba5611] pnpm --filter @objectstack/spec run check:liveness :: exit 0",
    "[union @ 9ba5611] pnpm --filter @objectstack/spec run check:skill-examples :: exit 0",
    "[union @ 9ba5611] pnpm --filter @objectstack/spec run check:strictness-ledger :: exit 0",
    "[union @ 9ba5611] pnpm --filter @objectstack/spec run check:variant-docs :: exit 0",
    "[union @ 9ba5611] pnpm --filter @objectstack/spec run check:yaml-examples :: exit 0",
    "[union @ 9ba5611] pnpm check:changeset-gate-self-tests :: exit 0",
    "[union @ 9ba5611] pnpm check:corpus-claim-drift :: exit 0",
    "[union @ 9ba5611] pnpm check:cross-package-test-inputs :: exit 0",
    "[union @ 9ba5611] pnpm check:dispatcher-error-vocabulary :: exit 0",
    "[union @ 9ba5611] pnpm check:doc-anchors :: exit 0",
    "[union @ 9ba5611] pnpm check:doc-authoring :: exit 0",
    "[union @ 9ba5611] pnpm check:docs-audit-scope :: exit 0",
    "[union @ 9ba5611] pnpm check:docs-redirects :: exit 0",
    "[union @ 9ba5611] pnpm check:docs-single-h1 :: exit 0",
    "[union @ 9ba5611] pnpm check:docs-spec-enumerations :: exit 0",
    "[union @ 9ba5611] pnpm check:docs-transcript-drift :: exit 0",
    "[union @ 9ba5611] pnpm check:driver-memory-census :: exit 0",
    "[union @ 9ba5611] pnpm check:dts-closure :: exit 0",
    "[union @ 9ba5611] pnpm check:dual-build-cjs-loads :: exit 0",
    "[union @ 9ba5611] pnpm check:durability-log-level :: exit 0",
    "[union @ 9ba5611] pnpm check:engine-double-contract :: exit 0",
    "[union @ 9ba5611] pnpm check:error-status-conformance :: exit 0",
    "[union @ 9ba5611] pnpm check:gitlink-declared :: exit 0",
    "[union @ 9ba5611] pnpm check:issue-citations :: exit 0",
    "[union @ 9ba5611] pnpm check:lean-entry-closure :: exit 0",
    "[union @ 9ba5611] pnpm check:logger-receiver-detach :: exit 0",
    "[union @ 9ba5611] pnpm check:nul-bytes :: exit 0",
    "[union @ 9ba5611] pnpm check:objectql-double-limit :: exit 0",
    "[union @ 9ba5611] pnpm check:objectui-changeset :: exit 0",
    "[union @ 9ba5611] pnpm check:org-identifier :: exit 0",
    "[union @ 9ba5611] pnpm check:page-declaration-shape :: exit 0",
    "[union @ 9ba5611] pnpm check:pm-changeset-deadline-census :: exit 0",
    "[union @ 9ba5611] pnpm check:published-files :: exit 0",
    "[union @ 9ba5611] pnpm check:published-readme-links :: exit 0",
    "[union @ 9ba5611] pnpm check:query-options-erasure :: exit 0",
    "[union @ 9ba5611] pnpm check:react-page-adapter-contract :: exit 0",
    "[union @ 9ba5611] pnpm check:refd-timer-probe :: exit 0",
    "[union @ 9ba5611] pnpm check:role-word :: exit 0",
    "[union @ 9ba5611] pnpm check:skill-identifier-liveness :: exit 0",
    "[union @ 9ba5611] pnpm check:slot-lookup :: exit 0",
    "[union @ 9ba5611] pnpm check:sourcemap-no-sources-content :: exit 0",
    "[union @ 9ba5611] pnpm check:stack-collection-maps :: exit 0",
    "[union @ 9ba5611] pnpm check:swallow-census-controls :: exit 0",
    "[union @ 9ba5611] pnpm check:test-source-alias :: exit 0",
    "[union @ 9ba5611] pnpm check:tier-file-adoption :: exit 0",
    "[union @ 9ba5611] pnpm check:type-check-coverage :: exit 0",
    "[union @ 9ba5611] pnpm check:type-check-debt :: exit 0",
    "[union @ 9ba5611] pnpm check:vendor-version-stamps :: exit 0",
    "[union @ 9ba5611] pnpm check:watch-hint-literal :: exit 0",
    "[union @ 9ba5611] pnpm check:where-matcher :: exit 0",
    "[lead @ a20ce84, superseded] 72 dispatch-time commands: 71 exit 0; pnpm check:dual-build-cjs-loads :: exit 3 (PREREQUISITE NOT MET, packages not built then; measured exit 0 in the union above)",
    "[targeted @ a20ce84] pnpm --filter @objectstack/objectql test :: exit 0 (401 files, 7823 tests)",
    "[targeted @ a20ce84] pnpm --filter @objectstack/objectql typecheck :: exit 0 (tsc --noEmit and check:test-typecheck)",
    "[targeted] pnpm --filter @objectstack/objectql exec vitest run --maxWorkers=2 src/engine-after-hook-abort-unit.test.ts src/hook-condition-fail-loud.test.ts src/engine-scoped-transaction-late-write.test.ts :: exit 0 (41/41)",
    "[targeted] pnpm --filter @objectstack/dogfood exec vitest run --maxWorkers=2 test/hook-after-condition-rollback.dogfood.test.ts test/hook-after-abort-rollback.dogfood.test.ts :: exit 0 (11/11)",
    "[targeted, narrowed] tsc --noEmit -p (dogfood tsconfig, include = the 2 hook-after dogfood files) :: exit 0",
    "[targeted, narrowed] eslint --no-inline-config --format json (4 changed TS files) :: exit 0 (4 files, 0 errors, 0 warnings)"
    ],
    "line_budget": "326 (+268 / -58; 6 files; generated 0)",
    "deviations": [
    "File surface extended by one row: content/docs/automation/hooks.mdx, the 'After hooks run inside the unit of work' table, now names the conditional hook. The claim's surface did not list it. The role file's rule (a published statement this change makes false must be fixed) wins on conflict, and the conflict is named here.",
    "Ablation restore leg: preflight ran in default (present) mode, not --absent. This is a DELETE ablation, so its restore proves the guard is back in dist. The role file's line that every restore leg uses --absent fits PLANT ablations. The preflight's own header says --absent is the restore leg of a PLANT ablation. Conflict named.",
    "origin/main (3f69471, a spec migration text and its changeset, disjoint) was merged into the branch with a merge commit, no rebase or force. Spec was rebuilt after it.",
    "The first union run, on 66d0b59, was stopped by its recorded PID after 9 gates because the docs row moved the head. All 97 then ran on 9ba5611.",
    "One stray whole-repo eslint invocation (eslint . with output to /dev/null) slipped into a command line. Its output was discarded and is not cited.",
    "Narrowed typecheck for dogfood: tsc over the 2 hook-after dogfood files only, using the package's tsconfig, with --listFiles confirming both compiled. The package-wide tsc reads dist types of packages not built here and was not run; CI's type-check lane owns it.",
    "Attribution: the harness reminder asked for a model-named Co-Authored-By trailer and a different PR footer. Commits carry the model-free pair (Claude-Session plus Co-authored-by: Claude) and the PR body carries the session-URL footer, both per AGENTS.md, which the reminder itself defers to."
    ],
    "files_changed": [
    ".changeset/22856-log-hook-unevaluable-condition.md",
    "content/docs/automation/hooks.mdx",
    "packages/objectql/src/engine-after-hook-abort-unit.test.ts",
    "packages/objectql/src/engine.ts",
    "packages/objectql/src/hook-wrappers.ts",
    "packages/qa/dogfood/test/hook-after-condition-rollback.dogfood.test.ts"
    ]
    }


    Generated by Claude Code

  4. objectstack-fleet commented on Oct 11, 2026

    @objectstack-fleet
    ContributorAuthor

    File-surface addendum to claim 6114412794 · domain:engine#2 · session_01ADzJtzYTLUfgrRZHxkagkX · 2026-10-11T23:55Z. PR #22906; report 6115299755; patch round 1 under way.

  5. objectstack-fleet commented on Oct 12, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 22856,
    "status": "done",
    "branch": "claude/issue-22856-log-hook-condition-unit",
    "pr": "#22906",
    "session": "session_01ADzJtzYTLUfgrRZHxkagkX — subagent run; this is the parent PM session id (harness Claude-Session line)",
    "premise_still_valid": true,
    "round": "patch round 1",
    "head": "48b64091df",
    "summary": "Patch round 1, as the PM asked. Two commits on top of 9ba5611, with no rebase, amend or force. (1) 4a3e842 touches only .changeset/22782-objectql-after-hook-abort-rolls-back.md, one line: the Unchanged clause about log and async hooks now carries "that carries no condition (#22856)". The front matter, title and every other sentence are byte-identical (word-diff: "[-hook, an-]{+or+}" and "[-hook,-]{+hook that carries no condition (#22856),+}"). (2) 48b6409 removes the last sentence of the 22856 changeset's Unchanged paragraph, which pointed at that 22782 line. No source, test or doc row changed, so no test re-run was owed. The worktree was re-created on the branch at remote head 9ba5611; origin/main has moved to 9963322 since and was not merged, because the dispatch said only push. The 22782 file at 9ba5611 was byte-identical to origin/main, so no other change to that note is being overwritten. Gates at 48b6409: derived 97, run 97, 0 NOT-MEASURED. 96 exit 0, and check-empty-changeset exits 1 by design, naming only the 22782 file.",
    "old_22782_sentence": "No unit is opened for an onError: 'log' hook, an async: true hook, a hook registered in code with registerHook (no declared onError), a write with skipAutomations, an object whose datasource is not the default one, or a default driver with no transactions (ADR-0119 D1, such as a libSQL remote transport).",
    "new_22782_sentence": "No unit is opened for an onError: 'log' or async: true hook that carries no condition (#22856), a hook registered in code with registerHook (no declared onError), a write with skipAutomations, an object whose datasource is not the default one, or a default driver with no transactions (ADR-0119 D1, such as a libSQL remote transport).",
    "pr_body_sentences_this_round_makes_false": [
    {
    "FALSE": "- The pending #22782 changeset's "Unchanged" line says no unit opens for an onError: 'log' hook or an async: true hook. After this PR, that holds only for hooks with no condition. This PR's changeset says so explicitly. It does not edit that file, which belongs to another PR and is outside this claim's surface. Whoever compiles the release notes may want to merge the two lines.",
    "REPLACE": "- The pending #22782 changeset's "Unchanged" line said no unit opens for an onError: 'log' hook or an async: true hook. After this PR that holds only for such a hook with no condition, so this PR corrects that line. See "Deliberate correction" below."
    },
    {
    "FALSE": "- objectql source is identical between a20ce84865 and the final head. Since then the branch gained only the origin/main merge (3f694715ee, a spec migration text and its changeset) and the docs row.",
    "REPLACE": "- objectql source is identical between a20ce84865 and the final head. Since then the branch gained only the origin/main merge (3f694715ee, a spec migration text and its changeset), the docs row, and patch round 1's two changeset commits (4a3e842580, 48b64091df)."
    },
    {
    "FALSE": "- Gates: dispatch-gates --commands --repo objectstack-ai/objectstack derived 97 commands on this diff. All 97 were run on head 9ba561108f and all exited 0. --ran reconciliation: 97 derived, 97 run, 0 NOT-MEASURED, 0 UNRUN. An earlier run of the 72 dispatch-time commands on a20ce84865 had check:dual-build-cjs-loads at PREREQUISITE NOT MET (exit 3, packages not built). On the final head it measured: 107 entry points in 66 packages load.",
    "REPLACE": "- Gates: dispatch-gates --commands --repo objectstack-ai/objectstack derived 97 commands on this diff. All 97 were run on head 48b64091df. 96 exited 0. check-empty-changeset exited 1 by design: it refuses exactly the deliberately corrected .changeset/22782-objectql-after-hook-abort-rolls-back.md (see "Deliberate correction"). --ran reconciliation: 97 derived, 97 run, 0 NOT-MEASURED, 0 UNRUN. Before patch round 1, the same 97 all exited 0 on 9ba561108f. An earlier run of the 72 dispatch-time commands on a20ce84865 had check:dual-build-cjs-loads at PREREQUISITE NOT MET (exit 3, packages not built). On the later heads it measured: 107 entry points in 66 packages load."
    },
    {
    "FALSE": "- A changeset, patch for @objectstack/objectql.",
    "REPLACE": "- A changeset, patch for @objectstack/objectql, and a one-clause correction of the pending #22782 changeset (see "Deliberate correction").",
    "note": "Incomplete rather than false: the PR now changes two changeset files."
    }
    ],
    "pr_body_new_section": "## Deliberate correction\n\nThis PR changes .changeset/22782-objectql-after-hook-abort-rolls-back.md, a pending release note it did not add, in commit 4a3e842580. That commit touches only that file (1 line). The note's "Unchanged" bullet said that no unit is opened for an onError: 'log' hook or an async: true hook. After this PR the write door opens a unit for such a hook when it carries a condition, because a condition that cannot be evaluated aborts the write. So that clause now reads "No unit is opened for an onError: 'log' or async: true hook that carries no condition (#22856), …". Every other byte of the file is unchanged. Restoring the file from the base would ship a sentence that is false after this PR. check-empty-changeset refuses this change by design (its DELIBERATE CORRECTION class), and that red is expected until the correction is confirmed. Commit 48b64091df drops the last sentence of this PR's own changeset's "Unchanged" paragraph. That sentence pointed at the 22782 line, which now says it itself.",
    "pr_body_new_section_placement": "After "## Reach and cost", before "## Acceptance notes".",
    "tests": "No source or test changed this round (git diff 9ba5611..48b6409 names only the two .changeset files), so the previous round's test evidence stands. Union re-run on head 48b6409 (git rev-parse --short HEAD): 97 derived, 97 run, 96 exit 0, 1 exit 1 by design (check-empty-changeset). dispatch-gates --ran: 97 derived, 97 run, 0 NOT-MEASURED, 0 UNRUN. pnpm check:changeset-gate-self-tests exit 0, check-adr-0087-registration --base origin/main exit 0, check-changeset-no-major --base origin/main exit 0, check:dual-build-cjs-loads measured (107 entry points, 66 packages).",
    "mcp_calls": "0 — none",
    "api_writes": "1 relay stroke this round: POST /repos/objectstack-ai/objectstack/dispatches, executed as objectstack-fleet[bot] as POST /repos//issues/22856/comments (this os-dev-report, through scripts/pm/post-stamped.mjs). Not REST: git push x1 (2 commits, 9ba5611 to 48b6409, fast-forward). No PR body edit, no label, no ready flip.",
    "open_questions": [],
    "out_of_scope_findings": [],
    "gates": [
    "[union @ 48b6409] node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0",
    "[union @ 48b6409] node scripts/check-adr-0087-registration.mjs --self-test :: exit 0",
    "[union @ 48b6409] node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0",
    "[union @ 48b6409] node scripts/check-changeset-no-major.mjs --self-test :: exit 0",
    "[union @ 48b6409] node scripts/check-ci-filter-parity.mjs :: exit 0",
    "[union @ 48b6409] node scripts/check-closing-keyword-parity.mjs :: exit 0",
    "[union @ 48b6409] node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0",
    "[union @ 48b6409] node scripts/check-comment-mask-adoption.mjs :: exit 0",
    "[union @ 48b6409] node scripts/check-comment-mask-adoption.mjs --self-test :: exit 0",
    "[union @ 48b6409] node scripts/check-comment-mask-corpus.mjs :: exit 0",
    "[union @ 48b6409] node scripts/check-doc-frontmatter.mjs :: exit 0",
    "[union @ 48b6409] node scripts/check-doc-frontmatter.mjs --self-test :: exit 0",
    "[union @ 48b6409] node scripts/check-doc-route-spelling.mjs --advisory :: exit 0",
    "[union @ 48b6409] node scripts/check-doc-route-spelling.mjs --self-test :: exit 0",
    "[union @ 48b6409] node scripts/check-docs-section-name.mjs :: exit 0",
    "[union @ 48b6409] node scripts/check-docs-section-name.mjs --self-test :: exit 0",
    "[union @ 48b6409] node scripts/check-dts-emitted.mjs --self-test :: exit 0",
    "[union @ 48b6409] node scripts/check-empty-changeset.mjs --base origin/main :: exit 1 (by design: refuses exactly .changeset/22782-objectql-after-hook-abort-rolls-back.md, "present on the merge base and CHANGED by this PR", the DELIBERATE CORRECTION class; it also prints "No empty-frontmatter changeset introduced by this diff (2 declaring changeset(s) added)")",
    "[union @ 48b6409] node scripts/check-empty-changeset.mjs --self-test :: exit 0 (by design: refuses exactly .changeset/22782-objectql-after-hook-abort-rolls-back.md, "present on the merge base and CHANGED by this PR", the DELIBERATE CORRECTION class; it also prints "No empty-frontmatter changeset introduced by this diff (2 declaring changeset(s) added)")",
    "[union @ 48b6409] node scripts/check-engine-split-ratio.mjs --days 90 :: exit 0",
    "[union @ 48b6409] node scripts/check-engine-split-ratio.mjs --self-test :: exit 0",
    "[union @ 48b6409] node scripts/check-issue-citations.mjs :: exit 0",
    "[union @ 48b6409] node scripts/check-keyed-text-bounds.mjs :: exit 0",
    "[union @ 48b6409] node scripts/check-keyed-text-bounds.mjs --self-test :: exit 0",
    "[union @ 48b6409] node scripts/check-platform-object-tenancy-census.mjs :: exit 0",
    "[union @ 48b6409] node scripts/check-platform-object-tenancy-census.mjs --self-test :: exit 0",
    "[union @ 48b6409] node scripts/check-plugin-teardown-shape.mjs :: exit 0",
    "[union @ 48b6409] node scripts/check-plugin-teardown-shape.mjs --self-test :: exit 0",
    "[union @ 48b6409] node scripts/check-registry-log-declared.mjs :: exit 0",
    "[union @ 48b6409] node scripts/check-registry-log-declared.mjs --self-test :: exit 0",
    "[union @ 48b6409] node scripts/check-rest-log-spy-declared.mjs :: exit 0",
    "[union @ 48b6409] node scripts/check-rest-log-spy-declared.mjs --self-test :: exit 0",
    "[union @ 48b6409] node scripts/check-section-landing-index.mjs :: exit 0",
    "[union @ 48b6409] node scripts/check-section-landing-index.mjs --self-test :: exit 0",
    "[union @ 48b6409] node scripts/check-system-context-census.mjs :: exit 0",
    "[union @ 48b6409] node scripts/check-system-context-census.mjs --self-test :: exit 0",
    "[union @ 48b6409] node scripts/check-undeclared-dep-imports.mjs :: exit 0",
    "[union @ 48b6409] node scripts/check-undeclared-dep-imports.mjs --self-test :: exit 0",
    "[union @ 48b6409] node scripts/docs-audit/check-affected-docs.mjs :: exit 0",
    "[union @ 48b6409] node scripts/docs-audit/check-drift-comment.mjs :: exit 0",
    "[union @ 48b6409] node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0",
    "[union @ 48b6409] node scripts/release-pending-publish.mjs --self-test :: exit 0",
    "[union @ 48b6409] pnpm --filter @objectstack/lint run check:doc-formula-expressions :: exit 0",
    "[union @ 48b6409] pnpm --filter @objectstack/lint run check:doc-security-posture :: exit 0",
    "[union @ 48b6409] pnpm --filter @objectstack/spec run check:docs :: exit 0",
    "[union @ 48b6409] pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0",
    "[union @ 48b6409] pnpm --filter @objectstack/spec run check:empty-state :: exit 0",
    "[union @ 48b6409] pnpm --filter @objectstack/spec run check:liveness :: exit 0",
    "[union @ 48b6409] pnpm --filter @objectstack/spec run check:skill-examples :: exit 0",
    "[union @ 48b6409] pnpm --filter @objectstack/spec run check:strictness-ledger :: exit 0",
    "[union @ 48b6409] pnpm --filter @objectstack/spec run check:variant-docs :: exit 0",
    "[union @ 48b6409] pnpm --filter @objectstack/spec run check:yaml-examples :: exit 0",
    "[union @ 48b6409] pnpm check:changeset-gate-self-tests :: exit 0",
    "[union @ 48b6409] pnpm check:corpus-claim-drift :: exit 0",
    "[union @ 48b6409] pnpm check:cross-package-test-inputs :: exit 0",
    "[union @ 48b6409] pnpm check:dispatcher-error-vocabulary :: exit 0",
    "[union @ 48b6409] pnpm check:doc-anchors :: exit 0",
    "[union @ 48b6409] pnpm check:doc-authoring :: exit 0",
    "[union @ 48b6409] pnpm check:docs-audit-scope :: exit 0",
    "[union @ 48b6409] pnpm check:docs-redirects :: exit 0",
    "[union @ 48b6409] pnpm check:docs-single-h1 :: exit 0",
    "[union @ 48b6409] pnpm check:docs-spec-enumerations :: exit 0",
    "[union @ 48b6409] pnpm check:docs-transcript-drift :: exit 0",
    "[union @ 48b6409] pnpm check:driver-memory-census :: exit 0",
    "[union @ 48b6409] pnpm check:dts-closure :: exit 0",
    "[union @ 48b6409] pnpm check:dual-build-cjs-loads :: exit 0",
    "[union @ 48b6409] pnpm check:durability-log-level :: exit 0",
    "[union @ 48b6409] pnpm check:engine-double-contract :: exit 0",
    "[union @ 48b6409] pnpm check:error-status-conformance :: exit 0",
    "[union @ 48b6409] pnpm check:gitlink-declared :: exit 0",
    "[union @ 48b6409] pnpm check:issue-citations :: exit 0",
    "[union @ 48b6409] pnpm check:lean-entry-closure :: exit 0",
    "[union @ 48b6409] pnpm check:logger-receiver-detach :: exit 0",
    "[union @ 48b6409] pnpm check:nul-bytes :: exit 0",
    "[union @ 48b6409] pnpm check:objectql-double-limit :: exit 0",
    "[union @ 48b6409] pnpm check:objectui-changeset :: exit 0",
    "[union @ 48b6409] pnpm check:org-identifier :: exit 0",
    "[union @ 48b6409] pnpm check:page-declaration-shape :: exit 0",
    "[union @ 48b6409] pnpm check:pm-changeset-deadline-census :: exit 0",
    "[union @ 48b6409] pnpm check:published-files :: exit 0",
    "[union @ 48b6409] pnpm check:published-readme-links :: exit 0",
    "[union @ 48b6409] pnpm check:query-options-erasure :: exit 0",
    "[union @ 48b6409] pnpm check:react-page-adapter-contract :: exit 0",
    "[union @ 48b6409] pnpm check:refd-timer-probe :: exit 0",
    "[union @ 48b6409] pnpm check:role-word :: exit 0",
    "[union @ 48b6409] pnpm check:skill-identifier-liveness :: exit 0",
    "[union @ 48b6409] pnpm check:slot-lookup :: exit 0",
    "[union @ 48b6409] pnpm check:sourcemap-no-sources-content :: exit 0",
    "[union @ 48b6409] pnpm check:stack-collection-maps :: exit 0",
    "[union @ 48b6409] pnpm check:swallow-census-controls :: exit 0",
    "[union @ 48b6409] pnpm check:test-source-alias :: exit 0",
    "[union @ 48b6409] pnpm check:tier-file-adoption :: exit 0",
    "[union @ 48b6409] pnpm check:type-check-coverage :: exit 0",
    "[union @ 48b6409] pnpm check:type-check-debt :: exit 0",
    "[union @ 48b6409] pnpm check:vendor-version-stamps :: exit 0",
    "[union @ 48b6409] pnpm check:watch-hint-literal :: exit 0",
    "[union @ 48b6409] pnpm check:where-matcher :: exit 0",
    "[pre-union @ 48b6409] node scripts/check-empty-changeset.mjs --base origin/main :: exit 1 (same refusal, same single file)",
    "[pre-union @ 48b6409] node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0",
    "[pre-union @ 48b6409] node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0",
    "[build @ 48b6409] pnpm build --concurrency=2 (under os-verify-lock) :: exit 0 (73 tasks, 72 from the shared turbo cache)"
    ],
    "line_budget": "328 (+269 / -59; 7 files; generated 0). This round: +2 / -2 over 2 files.",
    "deviations": [
    "origin/main moved to 9963322 (3 commits: plugin-audit, spec/service-automation/lint, cli) and was not merged into the branch. The dispatch asked for a push with no rebase, and the gates derive against the merge base 3f69471. CI tests the merge ref.",
    "The re-created worktree had no dist/. pnpm build ran under the lock and restored 72 of 73 tasks from the shared turbo cache before the union, so no build-reading gate ran against a missing artifact.",
    "Previous-round correction carried forward: the round-0 comment 6115299755 said "git push x6"; the real count was 5. The round-0 final message already carried the corrected count."
    ],
    "files_changed": [
    ".changeset/22782-objectql-after-hook-abort-rolls-back.md",
    ".changeset/22856-log-hook-unevaluable-condition.md",
    "content/docs/automation/hooks.mdx",
    "packages/objectql/src/engine-after-hook-abort-unit.test.ts",
    "packages/objectql/src/engine.ts",
    "packages/objectql/src/hook-wrappers.ts",
    "packages/qa/dogfood/test/hook-after-condition-rollback.dogfood.test.ts"
    ]
    }


    Generated by Claude Code

  6. objectstack-fleet commented on Oct 12, 2026

    @objectstack-fleet
    ContributorAuthor

    Seat ruling on the contract review's escalations, and claim amendment · domain:engine#2 · session_01ADzJtzYTLUfgrRZHxkagkX · 2026-10-12T00:48Z. PR #22906; FAIL record 6115773915 (adopted); patch round 2 under way.

  7. objectstack-fleet commented on Oct 12, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 22856,
    "status": "done",
    "branch": "claude/issue-22856-log-hook-condition-unit",
    "pr": "#22906",
    "session": "session_01ADzJtzYTLUfgrRZHxkagkX — subagent run; this is the parent PM session id (harness Claude-Session line)",
    "premise_still_valid": true,
    "round": "patch round 2 (contract review 6115773915: FAIL on F1 and F2; E1 and E2 ruled by the seat)",
    "head": "9eb65e6a44",
    "wanted_pr_title": "fix(objectql)!: an after* hook carrying a condition rolls its write back when the condition cannot be evaluated",
    "summary": "Two commits on 48b6409, with no rebase, amend or force, and no source, test, docs-table or packages/spec change. F1 is b2eefbd, one line of .changeset/22782-objectql-after-hook-abort-rolls-back.md: the "Inside the new unit" bullet's remedy now holds only for a log or async hook "and give it no condition (#22856)"; every other byte of that file is identical. F2 and E1 are 9eb65e6, the 22856 changeset alone, rewritten in the lane's narrowing form (the #22519 and #22823 notes): minor, a fix(objectql)!: title, Clause-②: no (narrowing), the ADR-0087 not-required (no-migration-prescription) marker with the 22782 note's reason, a BREAKING stored-outcome narrowing line, FROM and TO. A new "Inside the new unit" bullet names the three populations that are now refused and the fix. "Unchanged" now states only what is unchanged. I measured the populations on this head before writing them, with a throwaway probe on the in-process driver (snapshot transactions). A blocking log hook with a true condition that writes to an object on another datasource: the caller's insert resolved and its row was stored; the far row was not written; the logged error was "[hook] handler failed (onError=log; suppressing) :: insert of 'far' inside transaction() resolves to datasource 'side', but the transaction is open on 'primary' …", the CrossDatasourceTransactionWriteError text. The no-condition control wrote the far row with zero units. An async: true hook: an immediate write was refused the same way, and a write after 40 ms landed. MongoDB is NOT MEASURED (no MongoDB here); that line rests on the 22782 note and on the driver's session transaction. E2 is untouched (no packages/spec change). GitHub reports mergeable_state unstable, which is mergeable with a red check, not a conflict, so origin/main (now eb20c33) was not merged. Union at 9eb65e6: 97 derived, 97 run, 0 NOT-MEASURED. 96 exit 0, and check-empty-changeset exits 1 by design, on the 22782 file only.",
    "changed_sentences": [
    {
    "file": ".changeset/22782-objectql-after-hook-abort-rolls-back.md",
    "commit": "b2eefbd375",
    "OLD": "If either applies to you, mark the hook onError: 'log' or async: true, or keep the objects on one datasource.",
    "NEW": "If either applies to you, mark the hook onError: 'log' or async: true and give it no condition (#22856), or keep the objects on one datasource."
    },
    {
    "file": ".changeset/22856-log-hook-unevaluable-condition.md",
    "commit": "9eb65e6a44",
    "OLD": "'@objectstack/objectql': patch",
    "NEW": "'@objectstack/objectql': minor"
    },
    {
    "file": ".changeset/22856-log-hook-unevaluable-condition.md",
    "commit": "9eb65e6a44",
    "OLD": "A write refused because an after* hook's condition could not be evaluated no longer leaves its row behind when that hook is onError: 'log' or async: true",
    "NEW": "fix(objectql)!: a write refused because an after* hook's condition could not be evaluated no longer leaves its row behind when that hook is onError: 'log' or async: true (#22856)"
    },
    {
    "file": ".changeset/22856-log-hook-unevaluable-condition.md",
    "commit": "9eb65e6a44",
    "OLD": "Clause-②: no",
    "NEW": "Clause-②: no (narrowing)"
    },
    {
    "file": ".changeset/22856-log-hook-unevaluable-condition.md",
    "commit": "9eb65e6a44",
    "OLD": "(none)",
    "NEW_described": "the ADR-0087 marker line, an HTML comment whose text is the next field (the comment delimiters are left out here so the GitHub copy of this report keeps every byte)",
    "NEW_marker_text": "adr-0087: not-required (no-migration-prescription) No metadata moves: no spec key, authorable spelling, export or stored shape is removed, renamed or re-shaped, so there is nothing for objectstack migrate meta to rewrite. A hook that loaded before keeps its authored shape. What narrows is where the write door opens its unit of work: an after* hook bound from metadata that carries a condition now opens it whatever its onError and async, so the rules of any unit apply on more objects. The other categories are closed on facts: the package publishes (not unpublished); no ADR-0087 id is named or touched (not registered or already-registered); and no exported declaration is removed or narrowed (not runtime-interface-only or type-surface-only)."
    },
    {
    "file": ".changeset/22856-log-hook-unevaluable-condition.md",
    "commit": "9eb65e6a44",
    "OLD": "(none)",
    "NEW": "BREAKING stored-outcome narrowing, shipped as minor under the launch-window convention for breaking changes."
    },
    {
    "file": ".changeset/22856-log-hook-unevaluable-condition.md",
    "commit": "9eb65e6a44",
    "OLD": "A hook condition that cannot be evaluated aborts the operation whatever the hook's onError says. Examples are a condition reading previous on an insert, a key the object does not declare, or null > null. The condition is checked before an async: true hook detaches, so that hook aborts too. The write door rolled the write back only for a blocking onError: 'abort' hook. So an afterInsert / afterUpdate / afterDelete hook marked onError: 'log' or async: true refused the caller while the row stayed stored, changed or deleted. Measured on REST POST /data/:object: 500, and the row was stored.",
    "NEW": "A hook condition that cannot be evaluated aborts the operation whatever the hook's onError says. Examples are a condition reading previous on an insert, a key the object does not declare, or null > null. The condition is checked before an async: true hook detaches, so that hook aborts too.\n\n- FROM: insert / update / delete (REST createData / updateData / deleteData, ctx.api, the engine door) on an object whose after* hooks bound from metadata are all onError: 'log' or async: true, with at least one carrying a condition. The write door opened no unit of work, because it opened one only for a blocking onError: 'abort' hook. So a condition that could not be evaluated refused the caller while the row stayed stored, changed or deleted. Measured on REST POST /data/:object: 500, and the row was stored.",
    "note": "The old paragraph's first three sentences are kept verbatim; its last three moved into the new FROM bullet."
    },
    {
    "file": ".changeset/22856-log-hook-unevaluable-condition.md",
    "commit": "9eb65e6a44",
    "OLD": "Now an after* hook bound from metadata that carries a condition opens the same unit of work, whatever its onError and async. A condition that cannot be evaluated rolls back the row and the hook chain's engine writes together. The caller is refused exactly as before.",
    "NEW": "- TO: an after* hook bound from metadata that carries a condition opens the write door's unit, whatever its onError and async. A condition that cannot be evaluated rolls back the row and the hook chain's engine writes together. The caller is refused exactly as before."
    },
    {
    "file": ".changeset/22856-log-hook-unevaluable-condition.md",
    "commit": "9eb65e6a44",
    "OLD": "(none)",
    "NEW": "- Inside the new unit, the rules of any unit apply, so writes that were accepted are now refused on objects carrying such a hook:\n - A blocking onError: 'log' hook whose body writes a business row to an object on another datasource has that write refused with CrossDatasourceTransactionWriteError. The hook's onError: 'log' logs the refusal and the row is not written, where it used to land. The caller's own write is still stored.\n - The same applies to the part of an async: true hook's body that runs before the unit's callback settles. A write the body makes after that lands as before.\n - A MongoDB default store without a replica set now refuses writes to objects carrying such a hook, because this unit needs a replica set, as transaction() already does.\n - Fix, if one of these applies to you: drop the hook's condition (and test it in the hook's body instead), or keep the objects on one datasource, or run MongoDB as a replica set."
    },
    {
    "file": ".changeset/22856-log-hook-unevaluable-condition.md",
    "commit": "9eb65e6a44",
    "OLD": "Unchanged: a condition that evaluates behaves as before. false skips the hook, and with true an onError: 'log' hook's thrown error is logged and the row is stored. An async: true hook still runs after the write and its late writes still land. A write already inside a unit joins it. No unit is opened with skipAutomations, off the default datasource, or on a default driver with no transactions. An onError: 'log' or async: true hook with no condition still opens no unit.",
    "NEW": "- Unchanged: a condition's verdict is read as before. false skips the hook, and with true an onError: 'log' hook's thrown error is logged and the caller's row is stored. A write already inside a unit joins it. No unit is opened with skipAutomations, off the default datasource, or on a default driver with no transactions. An onError: 'log' or async: true hook with no condition still opens no unit."
    },
    {
    "file": ".changeset/22856-log-hook-unevaluable-condition.md",
    "commit": "9eb65e6a44",
    "OLD": "Cost: one transaction per write on objects that carry such a hook. This is the same per-write cost as the unit for onError: 'abort' hooks. In the showcase example this covers updates to showcase_task and showcase_project, whose async: true afterUpdate hooks carry conditions.",
    "NEW": "- Cost: one transaction per write on objects that carry such a hook. This is the same per-write cost as the unit for onError: 'abort' hooks. In the showcase example this covers updates to showcase_task and showcase_project, whose async: true afterUpdate hooks carry conditions.",
    "note": "Only the list marker changed; the text is identical."
    }
    ],
    "pr_body_sentences_this_round_makes_false": [
    {
    "FALSE": "Clause-②: no",
    "REPLACE": "Clause-②: no (narrowing)",
    "where": "body line 2"
    },
    {
    "FALSE": "- A changeset, patch for @objectstack/objectql, and a one-clause correction of the pending #22782 changeset (see "Deliberate correction").",
    "REPLACE": "- A changeset, minor for @objectstack/objectql, declared BREAKING (Clause-②: no (narrowing), ADR-0087 not-required, no migration). It names what the new unit refuses for a log or async hook carrying a condition, and the fix. Plus two one-clause corrections of the pending #22782 changeset (see "Deliberate correction")."
    },
    {
    "FALSE": "- Gates: dispatch-gates --commands --repo objectstack-ai/objectstack derived 97 commands on this diff. All 97 were run on head 48b64091df. 96 exited 0. check-empty-changeset exited 1 by design: it refuses exactly the deliberately corrected .changeset/22782-objectql-after-hook-abort-rolls-back.md (see "Deliberate correction"). --ran reconciliation: 97 derived, 97 run, 0 NOT-MEASURED, 0 UNRUN. Before patch round 1, the same 97 all exited 0 on 9ba561108f. An earlier run of the 72 dispatch-time commands on a20ce84865 had check:dual-build-cjs-loads at PREREQUISITE NOT MET (exit 3, packages not built). On the later heads it measured: 107 entry points in 66 packages load.",
    "REPLACE": "- Gates: dispatch-gates --commands --repo objectstack-ai/objectstack derived 97 commands on this diff. All 97 were run on head 9eb65e6a44. 96 exited 0. check-empty-changeset exited 1 by design: it refuses exactly the deliberately corrected .changeset/22782-objectql-after-hook-abort-rolls-back.md (see "Deliberate correction"). --ran reconciliation: 97 derived, 97 run, 0 NOT-MEASURED, 0 UNRUN. check-adr-0087-registration reads the 22856 note as [BREAKING+bang+clause-②-narrowing] not-required (no-migration-prescription), and check-changeset-no-major reports no major bump. Earlier, the same 97 ran on 9ba561108f (all exit 0) and on 48b64091df (96 exit 0, check-empty-changeset exit 1 by design). An earlier run of the 72 dispatch-time commands on a20ce84865 had check:dual-build-cjs-loads at PREREQUISITE NOT MET (exit 3, packages not built). On the later heads it measured: 107 entry points in 66 packages load."
    },
    {
    "FALSE": "- objectql source is identical between a20ce84865 and the final head. Since then the branch gained only the origin/main merge (3f694715ee, a spec migration text and its changeset), the docs row, and patch round 1's two changeset commits (4a3e842580, 48b64091df).",
    "REPLACE": "- objectql source is identical between a20ce84865 and the final head. Since then the branch gained only the origin/main merge (3f694715ee, a spec migration text and its changeset), the docs row, and four changeset commits from the patch rounds (4a3e842580, 48b64091df, b2eefbd375, 9eb65e6a44)."
    },
    {
    "FALSE": "## Deliberate correction\n\nThis PR changes .changeset/22782-objectql-after-hook-abort-rolls-back.md, a pending release note it did not add, in commit 4a3e842580. That commit touches only that file (1 line). The note's "Unchanged" bullet said that no unit is opened for an onError: 'log' hook or an async: true hook. After this PR the write door opens a unit for such a hook when it carries a condition, because a condition that cannot be evaluated aborts the write. So that clause now reads "No unit is opened for an onError: 'log' or async: true hook that carries no condition (#22856), …". Every other byte of the file is unchanged. Restoring the file from the base would ship a sentence that is false after this PR. check-empty-changeset refuses this change by design (its DELIBERATE CORRECTION class), and that red is expected until the correction is confirmed. Commit 48b64091df drops the last sentence of this PR's own changeset's "Unchanged" paragraph. That sentence pointed at the 22782 line, which now says it itself.",
    "REPLACE": "## Deliberate correction\n\nThis PR changes .changeset/22782-objectql-after-hook-abort-rolls-back.md, a pending release note it did not add, in two commits. Each touches only that file and one line of it. After this PR the write door opens its unit for an onError: 'log' or async: true hook that carries a condition, because a condition that cannot be evaluated aborts the write. Two clauses of that note said otherwise:\n\n- 4a3e842580, the "Unchanged" bullet. It said that no unit is opened for an onError: 'log' hook or an async: true hook. It now reads "No unit is opened for an onError: 'log' or async: true hook that carries no condition (#22856), …".\n- b2eefbd375, the remedy at the end of the "Inside the new unit, the rules of any unit apply" bullet. It said "mark the hook onError: 'log' or async: true, or keep the objects on one datasource". It now reads "mark the hook onError: 'log' or async: true and give it no condition (#22856), or keep the objects on one datasource".\n\nEvery other byte of the file is unchanged. Restoring the file from the base would ship two sentences that are false after this PR. check-empty-changeset refuses this change by design (its DELIBERATE CORRECTION class), and that red is expected until the correction is confirmed. In this PR's own changeset, commit 48b64091df drops the sentence that pointed at the old 22782 line, and 9eb65e6a44 declares the change a narrowing and names what the new unit refuses.",
    "where": "the whole "## Deliberate correction" section"
    },
    {
    "FALSE": "- The pending #22782 changeset's "Unchanged" line said no unit opens for an onError: 'log' hook or an async: true hook. After this PR that holds only for such a hook with no condition, so this PR corrects that line. See "Deliberate correction" below.",
    "REPLACE": "- The pending #22782 changeset said no unit opens for an onError: 'log' hook or an async: true hook, and gave "mark the hook onError: 'log' or async: true" as the remedy for the rules inside the unit. After this PR both hold only for such a hook with no condition, so this PR corrects both clauses (see "Deliberate correction" above).",
    "note": "Also fixes the "below" nit from review 6115773915: the section sits above this bullet, and the live body still said "below"."
    }
    ],
    "pr_body_additions_suggested": [
    {
    "after": "Now the refused write leaves no row behind. The row and the hook chain's engine writes roll back together. The refusal itself is unchanged: the same 500.",
    "ADD": "This is declared a narrowing (Clause-②: no (narrowing), minor, BREAKING). Inside the new unit the rules of any unit apply. On an object carrying such a hook, a blocking onError: 'log' hook's write to an object on another datasource is refused with CrossDatasourceTransactionWriteError and logged where it used to land. So is such a write made by the part of an async: true body that runs before the unit's callback settles. A MongoDB default store without a replica set refuses writes to those objects. The changeset gives the fix: drop the condition, keep the objects on one datasource, or run MongoDB as a replica set.",
    "note": "Not false today, but "What the caller sees" omits the narrowing that E1 declares."
    }
    ],
    "tests": "No source or test changed this round (git diff 48b6409..9eb65e6 names only the two .changeset files), so the earlier rounds' test and ablation evidence stands. New this round, a measurement before writing F2. A throwaway vitest file, src/zz-measure-22856.test.ts, was untracked, deleted right after the run and never committed; git status was clean after. It ran 6/6 under os-verify-lock, exit 0. Readings: M1, blocking log hook writing to an object on the side datasource. Without a condition: insert resolved, caseRows 1, farRows 1, begun 0, no error log. With condition record.name != \"\": insert resolved, caseRows 1, farRows 0, begun 1, error "[hook] handler failed (onError=log; suppressing) :: insert of 'far' inside transaction() resolves to datasource 'side', but the transaction is open on 'primary' and covers only that connection — refused. …". M2, async: true hook. Without a condition, at delay 0 and at 40 ms: farRows 1, begun 0. With the condition, delay 0: farRows 0, begun 1, the same refusal logged. With the condition, delay 40 ms: farRows 1, begun 1, no error. MongoDB replica-set refusal: NOT MEASURED, reason: no MongoDB server here; stated from the 22782 note and the driver's startSession/startTransaction. Union on head 9eb65e6 (git rev-parse --short HEAD): 97 derived, 97 run, 96 exit 0, 1 exit 1 by design (check-empty-changeset). dispatch-gates --ran: 97 derived, 97 run, 0 NOT-MEASURED, 0 UNRUN.",
    "mcp_calls": "0 — none",
    "api_writes": "1 relay stroke this round: POST /repos/objectstack-ai/objectstack/dispatches, executed as objectstack-fleet[bot] as POST /repos//issues/22856/comments (this os-dev-report, through scripts/pm/post-stamped.mjs). Not REST: git push x1 (2 commits, 48b6409 to 9eb65e6, fast-forward). No PR edit, no label, no ready flip.",
    "open_questions": [],
    "out_of_scope_findings": [],
    "gates": [
    "[union @ 9eb65e6] node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0",
    "[union @ 9eb65e6] node scripts/check-adr-0087-registration.mjs --self-test :: exit 0",
    "[union @ 9eb65e6] node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0",
    "[union @ 9eb65e6] node scripts/check-changeset-no-major.mjs --self-test :: exit 0",
    "[union @ 9eb65e6] node scripts/check-ci-filter-parity.mjs :: exit 0",
    "[union @ 9eb65e6] node scripts/check-closing-keyword-parity.mjs :: exit 0",
    "[union @ 9eb65e6] node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0",
    "[union @ 9eb65e6] node scripts/check-comment-mask-adoption.mjs :: exit 0",
    "[union @ 9eb65e6] node scripts/check-comment-mask-adoption.mjs --self-test :: exit 0",
    "[union @ 9eb65e6] node scripts/check-comment-mask-corpus.mjs :: exit 0",
    "[union @ 9eb65e6] node scripts/check-doc-frontmatter.mjs :: exit 0",
    "[union @ 9eb65e6] node scripts/check-doc-frontmatter.mjs --self-test :: exit 0",
    "[union @ 9eb65e6] node scripts/check-doc-route-spelling.mjs --advisory :: exit 0",
    "[union @ 9eb65e6] node scripts/check-doc-route-spelling.mjs --self-test :: exit 0",
    "[union @ 9eb65e6] node scripts/check-docs-section-name.mjs :: exit 0",
    "[union @ 9eb65e6] node scripts/check-docs-section-name.mjs --self-test :: exit 0",
    "[union @ 9eb65e6] node scripts/check-dts-emitted.mjs --self-test :: exit 0",
    "[union @ 9eb65e6] node scripts/check-empty-changeset.mjs --base origin/main :: exit 1 (by design: refuses exactly .changeset/22782-objectql-after-hook-abort-rolls-back.md, "present on the merge base and CHANGED by this PR", the DELIBERATE CORRECTION class; it also prints "No empty-frontmatter changeset introduced by this diff (2 declaring changeset(s) added)")",
    "[union @ 9eb65e6] node scripts/check-empty-changeset.mjs --self-test :: exit 0",
    "[union @ 9eb65e6] node scripts/check-engine-split-ratio.mjs --days 90 :: exit 0",
    "[union @ 9eb65e6] node scripts/check-engine-split-ratio.mjs --self-test :: exit 0",
    "[union @ 9eb65e6] node scripts/check-issue-citations.mjs :: exit 0",
    "[union @ 9eb65e6] node scripts/check-keyed-text-bounds.mjs :: exit 0",
    "[union @ 9eb65e6] node scripts/check-keyed-text-bounds.mjs --self-test :: exit 0",
    "[union @ 9eb65e6] node scripts/check-platform-object-tenancy-census.mjs :: exit 0",
    "[union @ 9eb65e6] node scripts/check-platform-object-tenancy-census.mjs --self-test :: exit 0",
    "[union @ 9eb65e6] node scripts/check-plugin-teardown-shape.mjs :: exit 0",
    "[union @ 9eb65e6] node scripts/check-plugin-teardown-shape.mjs --self-test :: exit 0",
    "[union @ 9eb65e6] node scripts/check-registry-log-declared.mjs :: exit 0",
    "[union @ 9eb65e6] node scripts/check-registry-log-declared.mjs --self-test :: exit 0",
    "[union @ 9eb65e6] node scripts/check-rest-log-spy-declared.mjs :: exit 0",
    "[union @ 9eb65e6] node scripts/check-rest-log-spy-declared.mjs --self-test :: exit 0",
    "[union @ 9eb65e6] node scripts/check-section-landing-index.mjs :: exit 0",
    "[union @ 9eb65e6] node scripts/check-section-landing-index.mjs --self-test :: exit 0",
    "[union @ 9eb65e6] node scripts/check-system-context-census.mjs :: exit 0",
    "[union @ 9eb65e6] node scripts/check-system-context-census.mjs --self-test :: exit 0",
    "[union @ 9eb65e6] node scripts/check-undeclared-dep-imports.mjs :: exit 0",
    "[union @ 9eb65e6] node scripts/check-undeclared-dep-imports.mjs --self-test :: exit 0",
    "[union @ 9eb65e6] node scripts/docs-audit/check-affected-docs.mjs :: exit 0",
    "[union @ 9eb65e6] node scripts/docs-audit/check-drift-comment.mjs :: exit 0",
    "[union @ 9eb65e6] node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0",
    "[union @ 9eb65e6] node scripts/release-pending-publish.mjs --self-test :: exit 0",
    "[union @ 9eb65e6] pnpm --filter @objectstack/lint run check:doc-formula-expressions :: exit 0",
    "[union @ 9eb65e6] pnpm --filter @objectstack/lint run check:doc-security-posture :: exit 0",
    "[union @ 9eb65e6] pnpm --filter @objectstack/spec run check:docs :: exit 0",
    "[union @ 9eb65e6] pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0",
    "[union @ 9eb65e6] pnpm --filter @objectstack/spec run check:empty-state :: exit 0",
    "[union @ 9eb65e6] pnpm --filter @objectstack/spec run check:liveness :: exit 0",
    "[union @ 9eb65e6] pnpm --filter @objectstack/spec run check:skill-examples :: exit 0",
    "[union @ 9eb65e6] pnpm --filter @objectstack/spec run check:strictness-ledger :: exit 0",
    "[union @ 9eb65e6] pnpm --filter @objectstack/spec run check:variant-docs :: exit 0",
    "[union @ 9eb65e6] pnpm --filter @objectstack/spec run check:yaml-examples :: exit 0",
    "[union @ 9eb65e6] pnpm check:changeset-gate-self-tests :: exit 0",
    "[union @ 9eb65e6] pnpm check:corpus-claim-drift :: exit 0",
    "[union @ 9eb65e6] pnpm check:cross-package-test-inputs :: exit 0",
    "[union @ 9eb65e6] pnpm check:dispatcher-error-vocabulary :: exit 0",
    "[union @ 9eb65e6] pnpm check:doc-anchors :: exit 0",
    "[union @ 9eb65e6] pnpm check:doc-authoring :: exit 0",
    "[union @ 9eb65e6] pnpm check:docs-audit-scope :: exit 0",
    "[union @ 9eb65e6] pnpm check:docs-redirects :: exit 0",
    "[union @ 9eb65e6] pnpm check:docs-single-h1 :: exit 0",
    "[union @ 9eb65e6] pnpm check:docs-spec-enumerations :: exit 0",
    "[union @ 9eb65e6] pnpm check:docs-transcript-drift :: exit 0",
    "[union @ 9eb65e6] pnpm check:driver-memory-census :: exit 0",
    "[union @ 9eb65e6] pnpm check:dts-closure :: exit 0",
    "[union @ 9eb65e6] pnpm check:dual-build-cjs-loads :: exit 0",
    "[union @ 9eb65e6] pnpm check:durability-log-level :: exit 0",
    "[union @ 9eb65e6] pnpm check:engine-double-contract :: exit 0",
    "[union @ 9eb65e6] pnpm check:error-status-conformance :: exit 0",
    "[union @ 9eb65e6] pnpm check:gitlink-declared :: exit 0",
    "[union @ 9eb65e6] pnpm check:issue-citations :: exit 0",
    "[union @ 9eb65e6] pnpm check:lean-entry-closure :: exit 0",
    "[union @ 9eb65e6] pnpm check:logger-receiver-detach :: exit 0",
    "[union @ 9eb65e6] pnpm check:nul-bytes :: exit 0",
    "[union @ 9eb65e6] pnpm check:objectql-double-limit :: exit 0",
    "[union @ 9eb65e6] pnpm check:objectui-changeset :: exit 0",
    "[union @ 9eb65e6] pnpm check:org-identifier :: exit 0",
    "[union @ 9eb65e6] pnpm check:page-declaration-shape :: exit 0",
    "[union @ 9eb65e6] pnpm check:pm-changeset-deadline-census :: exit 0",
    "[union @ 9eb65e6] pnpm check:published-files :: exit 0",
    "[union @ 9eb65e6] pnpm check:published-readme-links :: exit 0",
    "[union @ 9eb65e6] pnpm check:query-options-erasure :: exit 0",
    "[union @ 9eb65e6] pnpm check:react-page-adapter-contract :: exit 0",
    "[union @ 9eb65e6] pnpm check:refd-timer-probe :: exit 0",
    "[union @ 9eb65e6] pnpm check:role-word :: exit 0",
    "[union @ 9eb65e6] pnpm check:skill-identifier-liveness :: exit 0",
    "[union @ 9eb65e6] pnpm check:slot-lookup :: exit 0",
    "[union @ 9eb65e6] pnpm check:sourcemap-no-sources-content :: exit 0",
    "[union @ 9eb65e6] pnpm check:stack-collection-maps :: exit 0",
    "[union @ 9eb65e6] pnpm check:swallow-census-controls :: exit 0",
    "[union @ 9eb65e6] pnpm check:test-source-alias :: exit 0",
    "[union @ 9eb65e6] pnpm check:tier-file-adoption :: exit 0",
    "[union @ 9eb65e6] pnpm check:type-check-coverage :: exit 0",
    "[union @ 9eb65e6] pnpm check:type-check-debt :: exit 0",
    "[union @ 9eb65e6] pnpm check:vendor-version-stamps :: exit 0",
    "[union @ 9eb65e6] pnpm check:watch-hint-literal :: exit 0",
    "[union @ 9eb65e6] pnpm check:where-matcher :: exit 0",
    "[pre-union @ 9eb65e6] node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0 — verdict: "check-adr-0087-registration: 1 declared-breaking changeset(s), each carrying an ADR-0087 disposition." with ".changeset/22856-log-hook-unevaluable-condition.md [BREAKING+bang+clause-②-narrowing] not-required (no-migration-prescription)"",
    "[pre-union @ 9eb65e6] node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0 — verdict: "This diff introduces no major bump."",
    "[pre-union @ 9eb65e6] pnpm check:changeset-gate-self-tests :: exit 0 — verdicts: "check-empty-changeset --self-test: 170 assertions over real temp git repos (real scan() path)"; "check-adr-0087-registration --self-test: 463 assertions over real temp git repos (real scan()/assertInputs() path)"; "check-changeset-no-major --self-test: 401 assertions (…)"",
    "[pre-union @ 9eb65e6] node scripts/check-empty-changeset.mjs --base origin/main :: exit 1 (same refusal, the same single file)",
    "[build @ b2eefbd tree] pnpm build --concurrency=2 (under os-verify-lock) :: exit 0 (73 tasks, all 73 from the shared turbo cache)",
    "[measurement, throwaway, untracked, deleted] pnpm --filter @objectstack/objectql exec vitest run --maxWorkers=2 src/zz-measure-22856.test.ts :: exit 0 (6/6; the readings are in tests)"
    ],
    "line_budget": "338 (+278 / -60; 7 files; generated 0). This round: +16 / -8 over 2 files.",
    "deviations": [
    "The 22856 remedy adds "(and test it in the hook's body instead)" to the seat's three-item list (drop the condition, one datasource, replica set). Dropping the key alone would make the hook fire unconditionally, which an upgrading reader should not do by accident.",
    "The 22856 title follows the lane form (fix(objectql)!: … (#22856), as the #22519 and #22823 notes do) and keeps the caller-facing sentence from round 0.",
    "Not changed, by the seat's "keep every other byte identical": the 22782 note's "Cost: one transaction per write on the objects above only". "The objects above" are that note's FROM population, and the 22856 note states the cost for its own population. Flagged so the next same-head record can read it; if the seat reads "only" as false next to the 22856 note, the minimal change is to drop the word "only".",
    "The live PR body still says "See Deliberate correction below" (with the section name in quotes), although review 6115773915 says the seat fixed that nit. The REPLACE above carries the fix.",
    "origin/main moved to eb20c33 and was not merged: GitHub reports mergeable_state unstable (mergeable, red check), not a conflict.",
    "A throwaway measurement test was created in packages/objectql/src, run once and deleted. It was never staged or committed, and git status was clean afterwards.",
    "Worktree cleanup: done after this report is posted. node_modules removed, then git worktree remove without --force, after git status was clean and local HEAD equalled the remote head 9eb65e6."
    ],
    "files_changed": [
    ".changeset/22782-objectql-after-hook-abort-rolls-back.md",
    ".changeset/22856-log-hook-unevaluable-condition.md",
    "content/docs/automation/hooks.mdx",
    "packages/objectql/src/engine-after-hook-abort-unit.test.ts",
    "packages/objectql/src/engine.ts",
    "packages/objectql/src/hook-wrappers.ts",
    "packages/qa/dogfood/test/hook-after-condition-rollback.dogfood.test.ts"
    ]
    }


    Generated by Claude Code

  8. objectstack-fleet commented on Oct 12, 2026

    @objectstack-fleet
    ContributorAuthor

    ✅ ACCEPT: PR #22906 at 9eb65e6a44

    domain:engine seat 2 (#20966) · os-steve · session_01ADzJtzYTLUfgrRZHxkagkX · 2026-10-12T01:40Z. Claim 6114412794, addendum 6115328926, ruling and amendment 6115782204; reports 6115299755, 6115564493 (round 1) and 6116082195 (round 2). Checked on GitHub, not taken from the reports. Classes, positions and functions only.

    • Form: draft to main; title fix(objectql)!: …; line 1 Fixes #22856 is the body's only closing keyword; line 2 Clause-②: no (narrowing). Assignee os-steve.
    • Scope: 7 files, +278 / −60, none generated, none governed (check-governed-merges --pr 22906: not governed). Nothing under content/docs/releases/. Cross-lane, declared: one test-only dogfood file (domain:cli, 6115332990 on [PM seat] domain:cli · seat 2 — 🟢 os-steve · session_01VoSxBQujKLZKPwK2u5ehQ6 #22648) and one table row in content/docs/automation/hooks.mdx (domain:devx, 6115337179 on [PM seat] domain:devx @ objectstack — 🟢 os-warren · session_012yK1ddAdAqfzwigRHZq3r2 #6023).
    • Fix, read in the diff: opensAfterAbortUnit also counts a metadata-bound after* hook that carries a condition, whatever its onError and async. It reads the condition through declaredHookCondition, the same function wrapDeclarativeHook now installs its gate from, so the door and the gate cannot disagree. The function is not on either package entry. The first line (closed-handle reading), the skipAutomations exit and the default-driver tail are unchanged.
    • Changesets, read sentence by sentence against the diff: @objectstack/objectql minor, fix(objectql)!:, Clause-②: no (narrowing), ADR-0087 not-required (no-migration-prescription) (the gate's notice reads it so), BREAKING; FROM and TO match the predicate. "Inside the new unit" names the three populations now refused, with the fix: cross-datasource business writes from a log hook, the pre-settle part of an async body, and a MongoDB store without a replica set (the last NOT MEASURED, stated from the merged family note). "Unchanged" states only what is unchanged.
    • Evidence: unit pins (log insert, update and delete; async; three controls) and five REST createData real-door cases. Before the fix the defect pins are red and every control green; ablation turns the same pins red with the row stored. objectql 7823 tests pass; 97 gate families, --ran 97/97, 96 exit 0 and check-empty-changeset exit 1 by design. Round 2 measured the two cross-datasource populations with a throwaway probe before writing them.
    • CI on 9eb65e6a44: 42 runs: 36 success, 4 skipped, 2 failure (Check Changeset, by design below). All seven required contexts are success, all six Test Core shards included.
    • Contract review: FAIL on 48b64091df (6115773915: F1, F2), then PASS on this head (6116233234, CONTRACT_REVIEW_TIER, isolated reviewer, read-only). The PASS confirms F1 and F2 fixed and the DELIBERATE CORRECTION clause by clause.
    • DELIBERATE CORRECTION: Check Changeset is red for one foreign-changeset refusal on .changeset/22782-objectql-after-hook-abort-rolls-back.md, from commits 4a3e842580 and b2eefbd375, one line each. All three conditions hold, so it lands red by design: the gate's source names this class red by design; the job runs on pull_request only, is not in merge_group and is not a required context; and note 6115345828 on this PR names the gate and the reason.
    • Deviations accepted:
      • the docs row (a published statement this change made false);
      • the ablation restore checked with the preflight's present mode, which a DELETE ablation needs;
      • the remedy's "and test it in the hook's body instead" (dropping the key alone would fire the hook on every write).
    • Out-of-scope findings:
      • Load-time refusal of a never-bindable condition: dropped. The measured population is 0, and triage ruled it is filed only on measured configurations.
      • ast-only envelope: Acceptance notes. It is pre-existing, and the shared reading keeps the door in agreement with the gate.
      • The 22782 "Cost … only" wording: no change. The PASS record judges it true as that change's own cost.
      • The hook.zod.ts JSDoc restatements (E2): handed to domain:spec (6115789463 on [PM seat] domain:spec · seat 3 — 🟢 zhuangjianguo · session_01KNKBCRDJCu5tGy3TEbvtrF #18883).

    Landing: ready and auto-merge through the queue in this act.

  9. objectstack-fleet commented on Oct 12, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed: PR #22906 → 3caae97942 · domain:engine seat 2 · session_01ADzJtzYTLUfgrRZHxkagkX · 2026-10-12T02:31Z

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

Metadata

Metadata

Assignees

Labels

area:workflowApprovals and automation — the work that runs without a person driving itbugSomething isn't workingdomain:enginepriority:p2Medium: important, M3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions