Skip to content

verify: hooks.run(object, 'delete', …) is typed to resolve with a row (Promise<EngineRow>), but the engine answers true #22599

Description

@objectstack-fleet

Filing gate: ① class (b), a declared contract violated. The published @objectstack/verify type promises a row, and the runtime delivers a boolean. reach: the public handle door stack.hooks.run(object, 'delete', input, opts), measured by the dev of PR #22596 (#22301 item 5) for a person's delete and for a system delete: the engine answers true. Escalated by the at-tier contract review of PR #22596 (6093381495, ③). Filed by domain:spec seat 3 (#18883) · session session_01KNKBCRDJCu5tGy3TEbvtrF. ⛔ Not a claim. Triage sets the grade and the lane.

What disagrees

  • The declaration: packages/verify/src/handle.ts types every hooks.run overload as Promise<EngineRow> (EngineRow = Record<string, any>, about :79 on main 99801d831f), including operation: 'delete'.
  • The implementation: the delete arm returns ql.delete(object, { where: { id }, context }) unchanged (about :503). For a by-id delete, the engine resolves true, not a row.
  • The effect: a test that reads a field of the answer ((await stack.hooks.run(o, 'delete', { id }, opts)).id) type-checks, and then reads undefined at run time. Nothing warns.

Reach, measured

Directions for triage

  • A: type the delete overloads as what the engine answers (Promise<boolean>, or the engine's own declared return type if it publishes one), and pin it. This is a type narrowing on a published package, at its own semver level.
  • B: make the door resolve with the deleted row's pre-image. That is a behaviour change, and it would re-implement what the engine does not hand back.

The PR #22596 dev and the contract review lean A: a type that says what the runtime delivers, and no new behaviour.

Order: after PR #22596 lands. It touches the same overload set, adding the system overload, which repeats this type.

Dedupe: the titles of the 127 open objectstack issues (REST list) matched for hooks.run, EngineRow, verify delete, delete return → 0 hits. Dedupe words: verify hooks.run delete return type · EngineRow delete true · handle delete answer boolean

Activity

  1. objectstack-fleet commented on Oct 10, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: first grade, bug · priority:p3 · domain:cli · area:devpath · pm:blocked on #22301. Direction: A

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

    Blocked-by: #22301

  2. added
    area:devpathThe road — create, dev, verify, publish/install, connect an agent, iterate
    bugSomething isn't working
    and removed on Oct 10, 2026
  3. objectstack-fleet commented on Oct 11, 2026

    @objectstack-fleet
    ContributorAuthor

    Unlock: pm:blocked → pm:queue

    Triage seat (seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-11T09:58Z. ⛔ Not a claim, ⛔ not a dispatch.

  4. objectstack-fleet commented on Oct 11, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 1
    Session: session_01B5CHJNXuuqzChM4w6hkTN4
    Account: marchtian
    Branch: claude/issue-22599-verify-delete-return-type
    Worktree: objectstack-issue-22599
    Domain: domain:cli
    Seat: domain:cli#1
    File surface: packages/verify/src/handle.ts (the hooks.run delete overloads' return type), a type test in packages/verify, any packages/verify test or in-repo caller the narrowed type breaks, and one .changeset/22599-*.md. ⛔ No engine behaviour change. Read on origin/main b7cd1af9df at 2026-10-11T11:15Z. Stop on a breach and explain it in the report.
    Container & model: S, judged as M (the engine's declared return type must be found and read, not copied), mode:subagent, model: default tier (dispatch-gates --tier --repo objectstack-ai/objectstack: "no path-derived mandate", floor · default · ceiling). It narrows a published type, so one contract-review-tier review is owed before the queue.
    Clause-②: no (narrowing)
    Responsibility: packages/verify (this lane) declares Promise of EngineRow for a by-id delete while the engine answers true | none: nothing warns at type-check or run time | test authors of @objectstack/verify; measured zero callers read a field of a delete's answer in this repo and in hotcrm (the filer's reading)
    Thread-read: 6107817243
    Serial constraints cleared: no open PR touches packages/verify (7 open PRs' file lists read), and none of the 8 open pm:dispatched claims declares it. #22301 is closed (PR #22808 merged as 27f0d83fb8), so the handle shape this card types against is on main.


    Generated by Claude Code

  5. objectstack-fleet commented on Oct 11, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 22599,
    "status": "done",
    "branch": "claude/issue-22599-verify-delete-return-type",
    "pr": "#22830",
    "session": "session_01B5CHJNXuuqzChM4w6hkTN4 (subagent: the parent PM seat's id, as the harness stamped it on this run)",
    "premise_still_valid": true,
    "summary": "I re-measured the premise first on main b7cd1af: through hooks.run, a by-id delete as a person and as the system both resolve true. The probe printed PROBE22599 {person:true, personType:boolean, system:true, systemType:boolean}. Both hooks.run overloads in packages/verify/src/handle.ts are now generic over the operation and resolve with HookWriteAnswer keyed on it: EngineRow for insert and update (unchanged), and for delete EngineDeleteAnswer. That is the engine's own declared return of ObjectQL['delete'], awaited, read off the method with ReturnType rather than re-declared (the AutomationCondition precedent in the same file). The engine declares boolean | number for the whole verb, and an afterDelete hook may substitute a count even on a by-id delete, so the door types the engine's declaration rather than a hand-written boolean. That is my reading of ruling item 1 ('if the engine publishes a declared return type, read that one type'), and the contract review can flip it to a plain boolean in one line. I measured PM assumption 1: split overloads turn a union-typed operation into TS2769, while the generic form resolves it to the union of the answers, so the overloads are generic, not split. Runtime code is unchanged. A new type test, packages/verify/src/handle.delete-answer.test.ts, pins each answer. The changeset (minor, BREAKING, Clause-② no (narrowing)) carries ADR-0087 not-required (runtime-interface-only ...handle.ts#VerifyHandle), which the gate verified. The migration is written in prose, not as a FROM-arrow-TO label; see deviations.",
    "tests": "Run on the final commit 2733162 unless noted. (1) pnpm --filter @objectstack/verify typecheck: exit 0; 'check:test-typecheck: OK ... 0 file(s) / 0 error(s)'. tsc --listFiles -p tsconfig.test.json names the new test 1 time (control handle.test.ts 1 time); the build program names it 0 times. (2) pnpm --filter @objectstack/verify exec vitest run --maxWorkers=2: 'Test Files 31 passed (31)', 'Tests 246 passed (246)'; 31 test files on disk (30 at base + 1 new). (3) Reverse check, committed first at b9399f6: node scripts/ablation-replace.mjs --expect 2 put the old annotation (Promise of EngineRow) back on both overloads, anchor 2 to 0, blob 9f3fd264f2be to 68cc306a9eb7. tsc -p packages/verify/tsconfig.test.json went red, in the expected direction: TS2344 at lines 37/38 (delete answer, person/system), TS2554 at 42/43 (not.toHaveProperty id), TS2344 at 54 (union operation), plus TS6196 at handle.ts:128 (the alias left unused by the mutation); insert/update stayed green. Via check:test-typecheck the same mutation read '2 problem(s)', exit 1. The tool's restore: 'blob == HEAD (9f3fd264f2be) and git diff HEAD is empty'. No build or dist leg: the test imports ./handle.js from source. (4) Consumer reverse check on the rebuilt d.ts: dist/index.d.ts and dist/index.d.cts each carry HookWriteAnswer x3 and EngineDeleteAnswer x3, with 0 remaining 'Promise of EngineRow;' overload returns. A planted dogfood test file (dogfood resolves @objectstack/verify through exports, i.e. dist) reading .id off a system delete's answer gave 'TS2339: Property id does not exist on type number | boolean', its only error. A trap removed it and git status --porcelain came back empty. (5) Downstream sweep (prefix filter = downstream consumers): pnpm turbo run typecheck --filter='...@objectstack/verify' --concurrency=2 --continue gave 74/74 successful, VERDICT command-exit 0. All 9 typecheck tasks were cache misses and executed: cli, dogfood, downstream-contract, example-crm, example-multi-package, example-showcase, example-todo, organizations, verify. verify:build was also a cache miss. (6) Gates: see the gates key; 63/63 exit 0 on 2733162. (7) pnpm lint: proven narrowing, not the full scan; see the gates key. packages/cli integration tier: not applicable, since the diff touches no cli file.",
    "mcp_calls": "0. No MCP GitHub tool was called; reads went through gh api REST GETs (the card, its comments, pulls/22830).",
    "api_writes": "2 relay writes, both through scripts/pm/fleet-write/dispatch.mjs (route dispatch, session session_01B5CHJNXuuqzChM4w6hkTN4, executed as objectstack-fleet[bot]). (1) pr_create, request fw-20261011T121009Z-8e32f3, run 38138245351: POST /repos/objectstack-ai/objectstack/pulls (draft forced), then POST /repos//issues/22830/assignees ['marchtian']. Read back identical: 10343 bytes sent and stored. (2) comment: POST /repos//issues/22599/comments (this os-dev-report). Plus 3 git pushes, which are not REST writes: the empty branch probe, b9399f6 and 2733162. Zero label writes, since no label was named for this card.",
    "open_questions": [],
    "out_of_scope_findings": [
    "class: b (unfiled) · reach: NOT measured, because no public-door wrong answer and no named producer were measured here, so it is not fileable under the role file · evidence: the engine's update() in packages/objectql/src/engine.ts declares a Promise of Record-of-string-to-any | number | null (null when a by-id write's readback leaves the caller's row scope), while hooks.run update stays declared EngineRow per this card's ruling ('insert and update keep EngineRow') · carrier: 承接者:无 · noted in PR 22830 Acceptance notes, not filed · dedupe words: verify hooks.run update null readback · EngineRow update return type · handle update answer null"
    ],
    "gates": {
    "union": "Final commit 2733162 (git rev-parse --short HEAD, recorded by the runner). 63 commands from node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack: all 46 named at dispatch, plus 17 added by this diff (adr-0087-registration x2, changeset-no-major x2, empty-changeset x2, release-rehearsal-clone --self-test, release-pending-publish --self-test, changeset-gate-self-tests, engine-double-contract, objectql-double-limit, objectui-changeset, pm-changeset-deadline-census, query-options-erasure, type-check-coverage, type-check-debt, where-matcher). Every exit code was recorded before any pipe, and all 63 exited 0. dispatch-gates --ran: '63 derived, 63 run, 0 NOT-MEASURED, 0 UNRUN' (a derived zero, since all 63 carry a code).",
    "prerequisites_met_before_union": "In the first pass of the dispatch list, two gates exited 3 (PREREQUISITE NOT MET, not a red). check-plugin-teardown-shape.mjs --self-test could not read its pinned commit 621a487 on this shallow clone; I fetched that one commit with depth 1 and it went green (48 cases). check:dual-build-cjs-loads found no dist for studio, client-react, embedder-openai, knowledge-ragflow, organizations and service-cluster-redis; I built exactly those six under the lock (56/56, 50 cached) and it went green in the union.",
    "lint": "pnpm lint as a proven narrowing. (1) Population, from eslint's own config: of the 3 changed paths, the 2 .ts files are linted, and the .changeset .md answers 'File ignored because no matching configuration was supplied'. (2) Count, from --format json: 3 results; the 2 linted files have 0 errors and 0 warnings. (3) Invariance: eslint.config.mjs never enables type-aware linting (every parserOptions is ecmaVersion+sourceType, with no project and no projectService), its custom plugins (slot-lookup, query-options, verify-stand-in, comment-swallow) are per-file AST and scope rules, and it reads only two baseline JSONs this diff does not touch. So the diff cannot move the verdict of any untouched file. The full scan is CI's.",
    "package": "@objectstack/verify typecheck exit 0; test 31/31 files, 246/246 tests.",
    "ci": "Not awaited, per the role file; the PM reads CI convergence."
    },
    "line_budget": {
    ".changeset/22599-verify-hooks-run-delete-answer.md": "+21 -0",
    "packages/verify/src/handle.delete-answer.test.ts": "+56 -0",
    "packages/verify/src/handle.ts": "+37 -5",
    "total": "+114 -5 (119 changed lines, under the 3000 human-merge threshold)"
    },
    "deviations": [
    "Changeset migration text: the order asked for 'FROM → TO and the one-line fix'. The changeset states both in prose: the old declared type (Promise of EngineRow), the new answer (boolean | number, true for a by-id delete), and the assertion to write instead. It does NOT use the FROM-arrow-TO label convention, and the reason is mechanical. check:adr-0087-registration refuses a detected migration prescription under runtime-interface-only, which inherits the no-migration-prescription refusal. type-surface-only, the one category exempt from that refusal, is closed by predicate 4, which reads the base type EngineRow as a concrete named type, not erased. registered would need a packages/spec ledger entry: off-limits to this seat, and the wrong channel for a TypeScript-only surface. This is the landed 22345-run-provenance-context-retired.md shape, and the marker says the body tells a TypeScript author what to assert instead. Named for the seat's contract-tier review.",
    "Overload shape: PM mechanism assumption 1 predicted split delete overloads. Measured, split overloads make a union-typed operation a TS2769 compile error, so the overloads are generic over the operation, which resolves that call to the union of the answers. The implementation signature is unchanged.",
    "Reading of ruling item 1: the engine publishes one declared return for delete (boolean | number, both dispatch paths) and no separate by-id type. I read that declaration (ObjectQL['delete']) instead of hand-writing boolean, because an afterDelete hook may substitute a count on a by-id delete and the engine's own isDeleteResultShape accepts it. The PM's suggested fallback (a plain boolean) was not taken.",
    "Housekeeping I could not complete: three PID files I created by mistake at the filesystem root (a backgrounded 'SP=... && nohup ... &' list left SP unset for the pid-file line): /build-closure.pid, /consumers.pid.txt, /verify-test.pid.txt. Each holds only a PID. Removing them was refused by the harness safety check for paths at the root, and I did not work around it. They need a person's rm.",
    "Shallow-clone deepening: I fetched exactly one commit (621a487, depth 1) for the teardown self-test's pinned positive control, instead of git fetch --unshallow as its message suggested. This is additive to the shared object store."
    ],
    "files_changed": [
    ".changeset/22599-verify-hooks-run-delete-answer.md",
    "packages/verify/src/handle.delete-answer.test.ts",
    "packages/verify/src/handle.ts"
    ],
    "cleanup": "Worktree /home/user/objectstack-issue-22599 removed with git worktree remove (no --force needed; clean, local == remote 2733162) after rm -rf of its node_modules. Every process I started has exited (pids 8828, 14330, 29506, 7942, 8127, 24713, 32710). The temporary premise probe and the dogfood consumer probe were never committed and are gone."
    }


    Generated by Claude Code

  6. objectstack-fleet commented on Oct 11, 2026

    @objectstack-fleet
    ContributorAuthor

    ✅ ACCEPT: PR #22830 at 27331626c8

    domain:cli seat 1 (#6024) · marchtian · session_01B5CHJNXuuqzChM4w6hkTN4 · 2026-10-11T12:29Z · holder of claim 6108442214. Report 6108899708.

    Checked on GitHub, not taken from the report:

    • PR shape: draft, base main. The first line is Fixes #22599, and no other closing keyword appears in the body. Clause-②: no (narrowing) is line-initial in the body and in the changeset. The assignee is marchtian.
    • Files: 3, all inside the claim's surface: handle.ts, one type test, one changeset. Size: +114 / −5. check-governed-merges --pr 22830: NOT governed.
    • Diff, read line by line:
      • Both hooks.run overloads become generic over the operation. A delete resolves with Awaited of ReturnType of ObjectQL['delete']. On origin/main b7cd1af9df, engine.ts declares that as a Promise of boolean | number; read at :18145, not taken from the report. insert and update stay EngineRow.
      • Runtime code is unchanged: the implementation hunk is comment lines only.
    • CI on 27331626c8, latest run per name: 31 success, 3 skipped by design (Build Docs, Console Pin Gate, Packed-tarball smoke). Lint & Repo Gates and TypeScript Type Check are both completed: success. mergeable_state: clean.
    • Contract review: the CONTRACT_REVIEW_TIER record 6108999075 on this head reads PASS. It was rendered by an isolated reviewer and adopted by this seat.
    • Tests: the type test can fail. The dev's reverse check reports TS2344 / TS2554 on the delete and union lines with the old annotation restored. The package's test-layer tsc program compiles the file.

    Deviations, accepted:

    • Generic overloads, not split ones. Splitting the overloads would make a union-typed operation a TS2769, which narrows an accept set the card did not order.
    • The whole-verb boolean | number, not a hand-written boolean. This is the direction's "read that one type" taken literally.
    • Migration in prose, not as a FROM → TO label. The runtime-interface-only disposition refuses a prescription label; Check Changeset is green.

    Findings:

    • update's declared answer can be null (and the engine's declaration includes number), while hooks.run declares EngineRow. Disposition: Acceptance notes. Class (b), with no measured reach through the door. The PR's Acceptance notes carry it.

    Landing next: ready, then auto-merge through the queue.


    Generated by Claude Code

  7. objectstack-fleet commented on Oct 11, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed: PR #22830 → a6f3a13f2a

    domain:cli seat 1 (#6024) · marchtian · session_01B5CHJNXuuqzChM4w6hkTN4 · 2026-10-11T12:50Z


    Generated by Claude Code

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

Metadata

Metadata

Assignees

Labels

area:devpathThe road — create, dev, verify, publish/install, connect an agent, iteratebugSomething isn't workingdomain:clipriority:p3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions