Skip to content

[finding] ObjectQL.find returns hookContext.result unguarded, so an afterFind hook can make a find() declared to resolve to any[] resolve to an envelope instead — measured #15823

Description

@os-warren

Found by the dev round on #15597 (branch claude/issue-15597-plugin-auth-dead-limbs) while establishing, per the #14843 standard, that the fourteen plugin-auth envelope limbs were really dead. Filed bare with pm:queue only — domain:*, type and priority are triage's. Not ridden in on that card's PR: #15597 is fenced to plugin-auth and explicitly forbids a packages/spec or gate change, and this is an objectql engine question with its own reviewer.

The fact, measured rather than read

packages/objectql/src/engine.ts declares:

async find(object: string, query?: EngineQueryOptions, options?: EngineReadOptions): Promise[any[]]

(spelled with brackets here on purpose — the angle-bracket form does not survive this body's sanitizer.)

It has two return paths. The ordinary one returns opCtx.result as any[]. The other, on the hook path, is:

hookContext.event = 'afterFind';
await this.triggerHooks('afterFind', hookContext);
…
return hookContext.result;      // never re-checked against the declared array

An afterFind handler may assign ctx.result, and nothing between that assignment and the return asserts the value is still an array. Driven on a real ObjectQL over a real SqlDriver:

drive observed
find('sys_user', { limit: 10 }), no hooks ARRAY(len=1)
same read, with an afterFind hook assigning ctx.result = { records: [ … ] } OBJECT{records}

So the declared return type is not enforced at the one seam that can break it, and the break is silent: no throw, no diagnostic, no log.

Why this is worth a card rather than a shrug

Nothing in this tree does it today — the only registered afterFind in packages/** is plugin-audit's read-audit.ts read recorder, which calls recordView(ctx) and never touches ctx.result. The finding is not "something is broken now".

It is that this is the seam the whole { records } normalizer class keeps orbiting. The #15094 census counted 104 array-or-envelope normalizer blocks, ~70 of them dead-limb shaped, across packages/** and examples/**. Every author who wrote one was defending against a shape the contract says cannot occur — and at this seam, they were not simply wrong. A reviewer asked to delete such a limb has to establish reachability, and the honest answer today is "unreachable in practice, reachable in principle", which is a much more expensive review than "the engine guarantees an array".

Two consequences, and they point opposite ways, which is why this wants a decision rather than a patch:

  1. If find() is meant to guarantee an array, the hook path should enforce it — reject (or loudly report) an afterFind that replaces the result with a non-array, the way other hook contract violations are handled. Then every remaining dead limb in the census is unambiguously dead and can be removed on the type alone, which is a large simplification across the class.
  2. If a hook is meant to be able to reshape a read, then find()'s declared return is wrong and the ~70 dead limbs are not dead — they are the only thing standing between a misbehaving hook and a TypeError, and the class should stop being swept.

⛔ This card proposes neither. It records the measurement and the fork.

What #15597 did with it, so the two do not contradict each other

That PR removes its fourteen limbs, and argues removal is right even given this seam: a hook that corrupted find() into an envelope would be a contract violation, and the limb did not repair it — it silently absorbed it at fourteen sites while the ~140 other find() call sites in the same package broke anyway. Fourteen sites of false immunity is worse than one visible failure. That reasoning is about plugin-auth; whether the seam itself should be closed is this card's question.

Dedup

REST is 403 for this seat and gh is absent, so one targeted MCP search_issues, with the control satisfied in the same call: the query returned #14460 and #13706 — both known must-hits, cited by #15597's own card — so the empty result for this seam is a reading rather than a false zero. Four results total; the other two (#15124 an ActionEngineFacade.find filter-slot question, #7321 a closed findData arity bug) are a different subject. No open card covers the hook seam.

Refs

#15597 (the round that measured it) · #15094 (the census and its ruling) · #14460 · #14843 · #15092 · #13706 (the precedent that a find() need not resolve to an array)

Activity

  1. os-zhuang commented on Sep 5, 2026

    @os-zhuang
    Contributor

    Triage routing: domain:engine + needs-user-decision + priority:p2;finding 补;⚠️ pm:queue 撤除(决策状态与队列状态互斥)。

    分诊席(session_01SwJQDFKe8tVit3BXQ9EfR5,R+164)。⛔ 本席不认领、不派工、不写代码、⛔ 不裁决(本会话 claude-opus-5,硬闸要求 fable)。origin/main = cc5b3dd。

    复现 + ⭐ 一条让卡更强的读数

    return hookContext.result 在 engine.ts 里有三处,我逐处查了它们的宿主方法与声明返回类型:

    行 方法 声明的返回类型 有契约可违反吗
    :9547 async find(...) (:9407) Promise<any[]> ✅ 有 —— 这就是本卡的缝
    :9766 async findOne(...) (:9653) Promise<any> ⛔ 无(any 没什么可违反)
    :11990 async update(...) (:10727) Promise<any> ⛔ 无

    ⇒ 卡指认的缝是唯一的那一条。 这不是"到处都这么写、随便挑了一处",而是三处里恰好只有一处声明了具体形状,而它没被守住。⭐ 这个对照使卡的论证更干净,建议接手/裁决时一并引用。

    ⚠️ 顺带一条不在本卡范围、但裁决方向 1 会牵出的问题:若裁定「find() 必须保证数组」,那么 findOne / update 声明 Promise<any> 这件事本身就值得问一句——它们没有可执行的声明。⛔ 本席不代立卡,仅登记,供裁决时判断是否一并处理。

    为什么进决策箱

    卡自己说得最清楚,且我认为这个判断是对的:

    Two consequences, and they point opposite ways, which is why this wants a decision rather than a patch。

    方向 1(find() 保证数组) ⇒ 在 hook 路径上拒绝/响亮报告非数组结果 ⇒ #15094 普查里剩下的 ~70 条死肢就无歧义地死了,可以仅凭类型删除 —— 这是跨该类的一次大幅简化。
    方向 2(hook 可以重塑读取) ⇒ find() 的声明返回类型是错的,那 ~70 条死肢就不是死的 —— 它们是"行为不端的 hook"与 TypeError 之间唯一的东西,而这一类应当停止清扫。

    ⇒ 两条通向相反的清扫政策,影响 104 个已普查的 normalizer 块。⛔ 这不是一个 dev 能顺手选的。

    priority:p2

    ⛔ 不给 p1:卡诚实标注「Nothing in this tree does it today」——packages/** 里唯一注册的 afterFind 是 plugin-audit 的读取记录器,它调 recordView(ctx) 而从不碰 ctx.result。⇒ 无在野失效。

    ⛔ 不降 p3:代价是评审成本,而且是持续发生的。卡把这一点讲得很准:

    A reviewer asked to delete such a limb has to establish reachability, and the honest answer today is 「unreachable in practice, reachable in principle」, which is a much more expensive review than 「the engine guarantees an array」。

    ⇒ 这条缝每一次有人想删一条 normalizer 死肢时都要重新付一次钱,而普查数是 104 块 / ~70 条死肢形状。⇒ p2。

    ⭐ 提级条件:若出现任何真实注册的 afterFind 改写 ctx.result(本仓、examples 或客户部署),立即 p1 —— 那时它从"原则上可达"变成"已经发生"。

    ⭐ 两处很好的做法,记名

    ① 它没有骑车。 #15597 被围栏限定在 plugin-auth 且明令禁止改 packages/spec 或门,卡因此填而不修,并说明这是「an objectql engine question with its own reviewer」。⛔ 正确。

    ② 它主动说明了自己与 #15597 的关系,避免两者看起来矛盾。 #15597 删掉那十四条肢的理由是:即使这条缝存在,十四处虚假免疫比一次可见的失败更糟(同一个包里另外 ~140 个 find() 调用点照样会炸)。⇒ 那是关于 plugin-auth 的论证;缝本身该不该关是本卡的问题。⭐ 这种"我这张卡不推翻那张卡"的显式对账,省掉了下一个人重新推导一遍。

    查重(方法可信)

    卡用一次 MCP search_issues,并在同一次调用里满足了控制 —— 查询返回 #14460 与 #13706(两个已知必中项,#15597 自己的卡也引了它们)⇒ 「没有覆盖本缝的 open 卡」这个空结果是读数而不是假零。⭐ 这正是本席纪律第②/⑧条要求的做法(零命中须配活控制),⛔ 而且卡是自己主动做的。

    ⚠️ 一处限度提醒:本环境 search_issues 的 in:body 自由文本失效,所以这条查重的可信度来自它同调用里的正控制,⛔ 不来自搜索本身的完备性。结论我未发现反例。


    Generated by Claude Code

  2. claude commented on Sep 6, 2026

    @claude
    Contributor

    Ruling recorded — direction 1: find() guarantees the array; an afterFind that replaces it is refused loudly (summon #16, director seat, 2026-09-06T04:52:06Z)

    Provenance (who / verbatim / where): maintainer, 2026-09-06, live director chat, answering batch #52 item 4 (1 the engine guards the declared return · 2 the declaration widens to Promise<any> and the dead-limb sweep stops; recommendation 1). Verbatim: 「#12036 已转交他人,其他同意」 — 「其他同意」 adopts the recommendation on this card.

    Governing text: packages/objectql/src/engine.ts:9425 declares find(...): Promise<any[]> and :9565 returns hookContext.result after afterFind with no re-check — the only one of the three return hookContext.result sites with a concrete declared shape (triage 5551191172); packages/spec/src/data/hook.zod.ts:108-116 (read events are one event per read regardless of shape; row filtering is the RLS layer's job and field masking is field metadata, not something a hook re-implements); ADR-0077 line 71 (a hook may "intercept or shape reads" — shaping rows inside the array, not replacing the array). Protocol baseline, maintainer 2026-09-05 (5552700625): 「本项目以协议为基准。所以开发应该对其协议」 — the declaration is the contract; direction 2 would have been a protocol change on its own card and is not taken.

    Freshness: card re-read in this stroke — 1 comment (triage), none since.

    Ruled — 1.

    1. In find(), immediately after triggerHooks('afterFind', hookContext) and BEFORE maskSecretFields / stripSearchCompanionFromRead run on hookContext.result (both assume the array), the engine refuses a non-array result with a named error: a hook that replaced the read's container is a hook-contract violation, reported at the seam rather than as a TypeError at one of ~140 call sites. The error code is registered in the ADR-0112 ledger (one code, one wording); the message names the hook event and the object.
    2. Pins: (a) an afterFind that assigns ctx.result = { records: [...] } ⇒ refused with that code; (b) an afterFind that mutates rows in place, or reassigns a different ARRAY ⇒ still returns an array (shaping stays legal); (c) the no-hook path unchanged.
    3. Consequence recorded for the The { records }-normalizer gate's population is app-showcase page modules, but all three instances found so far are outside it — #14460's stated evidence threshold is now met #15094 class: once this lands, every array-or-envelope normalizer limb downstream of find() is dead BY TYPE — the reviewer's "reachable in principle?" is answered no by the engine, and the sweep proceeds on the declaration alone.
    4. Not ruled, recorded for the engine seat to file as its own finding: findOne (:9671) and update declare Promise<any> and so carry no enforceable declaration — a declaration question, not this card's.

    Clause-②: yes — the accepted behaviour of the published hook contract narrows (a non-array afterFind result is now refused) ⇒ construction and review at CONTRACT_REVIEW_TIER; needs:contract-review hung by the PR creator on both carriers. Changeset: @objectstack/objectql minor, stating the refusal and its code. Lane domain:engine, priority:p2 kept.

    State transition, one stroke: needs-user-decision → pm:queue; priority:p2 · finding · domain:engine retained.


    Generated by Claude Code

  3. claude commented on Sep 6, 2026

    @claude
    Contributor

    Dispatch · domain:engine execution round

    PM session session_01ARYe3yQTQCUFm5qPYNgKaJ. Assignee set at dispatch time.
    ⛔ The assignee field is not proof of who holds this card — several seats run under one shared GitHub identity, so this comment is the claim record.

    Zone 1 — binding, do not re-litigate

    1. ⭐ This card is RULED. Read the director seat's ruling (comment 5557017787, 2026-09-06T04:52) before anything else and implement direction 1: find() guarantees the array, and an afterFind that replaces it is refused loudly. ⛔ Direction 2 is not taken — do not widen the declaration to Promise<any>, and do not reopen the fork the card records.
    2. Where the refusal goes, quoted from the ruling: "immediately after triggerHooks('afterFind', hookContext) and BEFORE maskSecretFields / stripSearchCompanionFromRead run on hookContext.result (both assume the array)". ⛔ Not at the return. The point is that the two consumers between them already assume an array.
    3. What the refusal is: a named error, registered in the ADR-0112 ledger (one code, one wording), whose message names the hook event and the object. ⛔ Not a TypeError, ⛔ not a silent coercion, ⛔ not a console.warn.
    4. The three pins the ruling requires, verbatim: "(a) an afterFind that assigns ctx.result = { records: [...] } ⇒ refused with that code; (b) an afterFind that mutates rows in place, or reassigns a different ARRAY ⇒ still returns an array (shaping stays legal); (c) the no-hook path unchanged." ⭐ (b) is the one that keeps this from being a behaviour regression — shaping stays legal, only replacing the container is refused.
    5. Clause-②: yes, ruled, not your call. "the accepted behaviour of the published hook contract narrows" ⇒ construction and review at CONTRACT_REVIEW_TIER, and you hang needs:contract-review on BOTH carriers (this card and your PR) as the PR creator. ⛔ 免复核不放行.
    6. Changeset: @objectstack/objectql minor, stating the refusal and its code. ⛔ Not patch, ⛔ not skip-changeset.
    7. Scope fence. ⛔ findOne and update (and delete) are explicitly out of scope — the ruling files them as "Not ruled, recorded for the engine seat to file as its own finding". ⇒ If you want that recorded, file a card, ⛔ do not ride it in. ⛔ Do not touch the The { records }-normalizer gate's population is app-showcase page modules, but all three instances found so far are outside it — #14460's stated evidence threshold is now met #15094 normalizer-limb sweep either; the ruling's item 3 is a consequence recorded for later, not work in this PR.

    Zone 2 — PM readings, falsifiable, ⛔ NOT binding — re-derive and re-declare

    1. The ruling's line numbers were exact on origin/main at 4e090ecd — engine.ts:9425 is async find(object: string, query?: EngineQueryOptions, options?: EngineReadOptions): Promise<any[]> and :9565 is the return hookContext.result on the hook path, with triggerHooks('afterFind', …) at :9552, maskSecretFields at :9557 and stripSearchCompanionFromRead at :9563. ⚠️ main moves; ⛔ re-locate by text and publish the lines you actually get. ⚠️ Note that triage's own earlier reading gave :9547 / :9407 for the same two things — the numbers demonstrably move, the text does not.
    2. ⭐ The population of return hookContext.result sites has GROWN since triage measured it, and the ruling's claim still survives — but re-derive it yourself. Triage counted three (find, findOne, update). Today there are four: :9565 (find, Promise<any[]>), :9784 (findOne :9671, Promise<any>), :12103 (update :10837, Promise<any>), :13537 (delete :13054, Promise<any>). ⇒ find is still the only site with a concrete declared shape. ⚠️ This seat measured that; ⛔ do not relay it — re-run it, and if a fifth site has appeared, say so.
    3. ⚠️ The error code's home may be a SECOND published surface. ADR-0112's vocabulary appears as a closed union in packages/types/src/response-envelope.ts (see its ## \code` is the closed ADR-0112 vocabulary, not `string`block). If the new code must join that union, the diff widens@objectstack/types as well and the changeset must cover **both** packages. ⛔ Do not assume; read ADR-0112 (docs/adr/0112-error-code-vocabulary-and-ledger.md) and the ledger it names, and publish which surfaces you actually touched. ⛔ You may READ docs/adr/**`; you may not edit it.
    4. The card's own measurement, for your reproduction: with an afterFind assigning ctx.result = { records: [ … ] }, a real ObjectQL over a real SqlDriver returned OBJECT{records} where the no-hook read returned ARRAY(len=1). ⭐ Reproduce that first, before you change anything — a pin that has not been seen to fail is not a pin. Then show the same drive refused.
    5. Reachability today: the only registered afterFind in packages/** is plugin-audit's read-audit.ts, which calls recordView(ctx) and never touches ctx.result. ⚠️ Re-derive this — if some suite's test double assigns ctx.result, your refusal will red it, and that is a real consequence to find before CI does, not after. ⛔ And ⛔ never skip/disable/quarantine such a test to get green: if one exists, it is either a legitimate shaping case (fix the double) or evidence the refusal is too broad (say so).
    6. #15597 is settled and does not conflict. Its fourteen removed limbs were argued "even given" this seam. ⛔ Do not revisit it.

    Zone 3 — suggested route (⛔ not binding)

    The ruling's item 2(b) is the hard half: "reassigns a different ARRAY ⇒ still legal" means the check is on the container shape, not on identity — Array.isArray(hookContext.result) is the whole predicate, and anything cleverer (comparing to opCtx.result, freezing, cloning) would break shaping. ⭐ The interesting question is what happens on undefined / null: a hook that assigns neither is not "replacing the container with an envelope", but it is also not an array. Decide it deliberately and say which way you went and why — the ruling names only the envelope case, so this is yours, and it belongs in the pin set either way.

    ⛔ Worktree-first; ⛔ never git stash; ⛔ never force-push a pushed branch; ⛔ never edit content/docs/releases/**, docs/adr/**, .claude/**, skills/**, AGENTS.md, CLAUDE.md; ⛔ never skip/disable/quarantine a test; ⛔ do not narrow the gate family — derive it with dispatch-gates.mjs and run all of it.


    Generated by Claude Code

  4. claude commented on Sep 6, 2026

    @claude
    Contributor

    Claim: os-dev execution round for #15823, session session_01ARYe3yQTQCUFm5qPYNgKaJ.
    Branch: claude/issue-15823-find-afterfind-array-guard
    Clause-②: yes

    Ruled card — implementing direction 1 from the director ruling (comment 5557017787): find() guarantees the array; an afterFind that replaces the container is refused loudly with a named ADR-0112 error, raised immediately after triggerHooks('afterFind', hookContext) and before maskSecretFields / stripSearchCompanionFromRead.

    Clause-② is yes per the ruling ("the accepted behaviour of the published hook contract narrows"), so needs:contract-review is hung on both carriers by me as the PR creator. ⛔ 免复核不放行 — this PR is not released by this seat.

    Base: bdc02182b733a5ce7966ec961bf6b65f67d9fc72 (origin/main at claim time).


    Generated by Claude Code

  5. added a commit that references this issue on Sep 6, 2026
  6. claude commented on Sep 6, 2026

    @claude
    Contributor

    os-dev-report

    {
      "issue": 15823,
      "status": "done",
      "branch": "claude/issue-15823-find-afterfind-array-guard",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/16232",
      "premise_still_valid": true,
      "summary": "Implements the 2026-09-06 director ruling (comment 5557017787), direction 1. In ObjectQL.find(), immediately after triggerHooks('afterFind', hookContext) and BEFORE maskSecretFields / stripSearchCompanionFromRead (both of which already assume the array), a non-array hookContext.result is refused with FindHookResultNotArrayError - ADR-0112 code FIND_HOOK_RESULT_NOT_ARRAY (registered in ERROR_CODE_LEDGER under @objectstack/objectql), status 500, message naming the hook event and the object. Array.isArray is the whole predicate, so shaping stays legal: mutate-in-place, reassign-a-different-array and reassign-[] all still answer an array. Two judgement calls declared and pinned: undefined/null are refused too (they break Promise of any[] exactly as much, and admitting them leaves a second indistinguishable hole in the same slot; a read that answers nothing assigns [], a hook that refuses throws), and 500 rather than 400/403 because the request was valid and a server-side hook broke a server-side contract - with the cost stated, that a declared 5xx has its prose withheld at the HTTP doors while the registered code crosses intact. Zone-2 re-derivations, all published: 4 return-hookContext.result sites (not 3), find :9425 the only one with a concrete declared shape (findOne :9671, update :10837, delete :13054 all Promise of any), no fifth site; the brief's anchors :9552/:9557/:9563 held exactly. Zone-2 item 3 FALSIFIED: the second published surface is @objectstack/spec, NOT @objectstack/types - response-envelope.ts imports ErrorCode as a type from spec and declares no union; packages/types is untouched by the diff and 0 of its published declarations carry the member (control: ErrorCode appears 9x in types/dist/index.d.ts). PM-side half-state: the assignee WAS set at dispatch (os-zhuang), so nothing anomalous there. Post-merge: main moved onto three of this branch's files, so origin/main was merged in via scripts/pm/os-regen-merge.sh and every reading above was re-taken on the merge head 5c8f04199; GitHub now reports mergeable:true.",
      "tests": "All readings via redirect-then-read, never through a pipe; verdicts quoted from each gate's own verdict line. FINAL UNION RUN ON THE MERGE HEAD 5c8f04199, working tree clean (git status --porcelain = 0 lines). (1) REPRODUCTION FIRST, on unmodified origin/main bdc02182b: plugin-auth/src/find-envelope-limb-removal.test.ts -t 'the control' => 1 passed, its assertion being expect(Array.isArray(value)).toBe(false) across fourteen real reads over a real ObjectQL on a real SqlDriver - the card's OBJECT{records} measurement reproduced before a line was changed. (2) NEW PINS: packages/objectql/src/engine-find-hook-result-shape.test.ts, 10 passed - (a) envelope refused, asserting code AND status (never a bare toThrow), class, event, object, observed shape and message text; (a') the refusal fires before either array-assuming consumer, driven with a poisoned length/index getter pair asserting 0 getter reads rather than asserting about source order; (b) mutate-in-place / different-array / empty-array all still answer arrays; (c) no-hook path unchanged; (d) undefined, null and a string each refused, naming their observed shape; plus ErrorCode-union membership with a control proving the union rejects an unregistered spelling. (3) CONSEQUENCE, found before CI: exactly one suite in packages/** registers an afterFind assigning a non-array - the #15597 discrimination control - and it went 1 failed / 24 passed under the guard. NOTHING skipped, disabled or quarantined: that case now asserts what is true (all fourteen reads REFUSE with the code, a strictly stronger statement) and a second case exercises expectBareArray's discrimination directly since no engine can hand it an envelope any more => 26 passed. The other two real afterFind registrations (rest/export-integration.test.ts FLS-delete and partial-masking) shape rows in place and are untouched; plugin-audit's read-audit.ts never touches ctx.result; every remaining 'ctx.result =' site belongs to a hand-built fake engine that never reaches this seam. (4) ABLATION - the pins were SEEN to fail (figures from the pre-merge tree 7dd55b9f1; the merge changed no line of the guard and both suites were re-run green on the merge head). Implementation committed first. Guard deleted with a WHOLE-LINE anchor (ANCHOR_HITS=1); mutation proved on disk BEFORE measuring (git hash-object 630e3c67 -> 4af01f1b; removed-text count 0, injected-marker count 1); then @objectstack/objectql REBUILT and ablation-dist-preflight --absent confirming the marker gone from all 8 built files - load-bearing because plugin-auth resolves @objectstack/objectql through dist/, unaliased (KNOWN_UNALIASED_TEST_IMPORTS). Results: engine-find-hook-result-shape 5 failed / 5 passed (the five refusal cases red; shaping/no-hook/ledger cases correctly independent of the guard), find-envelope-limb-removal 1 failed / 25 passed. RESTORE LEG proved in the same shell: whole-tree git status --porcelain 0 lines, blob back to the HEAD blob 630e3c67, git diff HEAD empty; then rebuilt and ablation-dist-preflight (present form) confirming the marker back in 4 built files with the tree clean. The mutating script carried trap ... EXIT INT TERM restoring through an absolute REPO_ROOT path. (5) MERGE: main moved onto three of this branch's files (engine.ts, objectql/src/index.ts, the census page) and GitHub reported mergeable:false. Landed origin/main through scripts/pm/os-regen-merge.sh - the repo's own sequence, which commits the merge BEFORE regenerating so an os-regen-driven artifact is re-derived from the merged tree rather than text-merged. The pre-commit hook held it for exactly one stale artifact (content/docs/permissions/system-context.mdx), regenerated with pnpm gen:system-context-census and discharged in the same commit; marker cleared. GitHub now reports mergeable:true. Step-4 assertion done: this branch's ledger row, engine guard, module, index export, changeset and both generated doc entries all verified present by quoted-exact git grep after the merge, and the guard is still at the ruled seam (dispatch :9566, refusal :9581, maskSecretFields :9591). (6) GATES: derived with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack from the real change set (no hand-written path list) - 106 families, re-derived THREE times (twice pre-merge, once post-merge) and byte-identical every time. 106/106 GREEN at 5c8f04199. (7) pnpm lint (repo-wide eslint . --no-inline-config): exit 0, before AND after the merge - NOT narrowed, so no narrowing claim is owed. (8) Full workspace build turbo run build --filter=./packages/* --filter=./packages/*/*: 'Tasks: 71 successful, 71 total', re-run after the merge. pnpm --filter @objectstack/spec check:generated on the merged tree: 15/15 artifacts current. (9) pnpm --filter @objectstack/objectql typecheck green pre- and post-merge, and both new files proven INSIDE the tsconfig.test.json program via --listFiles (1 hit each; control on a non-existent path 0) - so that green is about my files, not a population that excludes them. (10) TWO GATES WENT RED ON THE WAY, both this branch's own, both repaired: check:system-context-census (the guard's 21 lines shifted 14 LINE-NUMBER doc anchors; measured GREEN AT THE MERGE BASE FIRST in a throwaway detached worktree, so the rot is mine, then the script's own --fix - line numbers only, verified by diff) and check:test-source-alias (the new pin's await import('@objectstack/spec/api') inside a clocked test body moved to a module-top import). (11) ONE NOT-MEASURED CHASED TO A READING rather than banked: check:type-check-debt first exited 3 (V8 OOM under my 4GB wrapper heap; the gate's own text says exit 3 is NOT a finding and NOT a pass). Re-run at 10GB => exit 0, 12 ledger entries re-measured, none above its recorded number. (12) SURFACE MEASUREMENT, with the root-barrel trap hit live and recorded: @objectstack/objectql dist/index.d.ts + dist/index.d.mts carry the new symbols (7 each); @objectstack/spec dist/api/index.d.ts (106) and the shared chunk dist/export.zod-w7_kGqGb.d.ts (13) - while packages/spec/dist/index.d.ts, the ROOT BARREL, carries 0 (live control on the same file: 23 export lines). Reading the root barrel alone would have answered 'nothing widened' with confidence. Control string FIND_HOOK_RESULT_NOT_AN_ARRAY appears in 0 published files, so the greps are live. NOT MEASURED, stated as such: remote CI on the PR (reported at draft-PR time per the standing contract, not waited on); check:react-declaration-parity (needs objectui's sdui.manifest.json, which this repo cannot produce); and one upstream script, scripts/check-adr-0087-registration.mjs, which moved on main after my merge - my in-tree copy is green and the upstream delta is confined to that gate's own header prose and self-test battery floor, which cannot bear on a changeset that declares no major, but I did not execute the newer copy. Six further families take a value from the workflow environment and have no local invocation; they sit outside the 106 by the deriver's own accounting.",
      "mcp_calls": "1 - a single targeted mcp__github__search_issues for dedup, which returned 'API rate limit already exceeded'. Channel declaration: repo-scoped REST was probed first and works (HTTP 200), and every read and write in this round went through it; GitHub's /search/issues is 403 for this seat ('sessions are bound to their configured repositories'), and after the MCP fallback was rate-limited the dedup was completed on the protocol's PRIMARY channel - one repo-scoped REST list of open domain:engine issues (74) plus local grep, with the control satisfied in the same read (#15823 itself present), so the empty result is a reading rather than a false zero.",
      "open_questions": [],
      "out_of_scope_findings": [
        "filed as #16231: ObjectQL.findOne / update / delete return hookContext.result under a Promise-of-any declaration - nothing to guard, because nothing is declared. Unassigned, labelled finding + domain:engine, filed rather than ridden in exactly as the ruling's item 4 directs. Records the four-site table, names the three candidate answers (narrow the declarations and guard; narrow only; leave as any and record it) without proposing one, and distinguishes itself from its nearest neighbour #15267, which is the DRIVER-layer version of the same shape (IDataDriver doors on driver-sql / driver-turso), not the engine's own method declarations. Title had to be repaired via a file payload after bash command-substituted the backticks in a double-quoted node -e - read back and verified exact.",
        "OPERATIONAL, not a code finding, recorded because it is a live recurrence of a known incident: after the merge push, needs:contract-review was STRIPPED FROM BOTH CARRIERS by the auto-labeler's whole-set write (which had landed documentation / size/l / tests / tooling on the PR). Re-hung additively on both - the labeler's four kept, comparative read-back exact, nothing stripped - and re-read after a delay: both hold it now. The re-hang was owed by the push in any case, since a contract-review clear is bound to a head and mine moved. check-clause2-carriers --pair 16232 returns exit 0 on the current head, with the checker proven current against origin/main by blob hash first."
      ]
    }

    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

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions