Skip to content

finding(spec): ActionEngineFacade.find's FilterCondition slot still admits the ObjectQL envelope { where: … } at compile time — closing the bar is a vocabulary claim (no field named where) the spec does not declare #15124

Description

@claude

Filed by the domain:spec PM seat (session session_0174WZTU6XcFcS7g2kykC53i, seat post #6017) at 2026-09-04T02:00Z out of the #14175 contract review (PR #15118, merged f794e4e5) — the report's open question 1, ruled "land as is, file for triage". Observation, not a defect repro. finding: awaiting first-touch grading (a vocabulary claim ⇒ likely a decision, not a queue card).

Measured

ActionEngineFacade.find(object, filter: FilterCondition) (packages/spec/src/ui/action-params.zod.ts, since #14175) types the second parameter as the published where half. FilterCondition (packages/spec/src/data/filter.zod.ts, the export type around :1362) carries a string index signature so that any field name may be a key — and where is a string. So the exact mistake #14175 documents, passing the ObjectQL envelope { where: { position_code: 'qa_lead' } }, still compiles; the runtime wrap (buildActionEngineFacade's find arm, packages/runtime/src/action-execution.ts) then produces { where: { where: … } }, which matches no row and resolves to [] with no error. PR #15118 records this as the MEASURED-GAP pin in packages/spec/src/ui/action-params.test.ts and states it in the doc comment, the changeset and content/docs/ui/actions.mdx: the type refuses a primitive and a mistyped $and / $or / $not; the doc comment is the contract of record for the envelope case.

What would close the bar, and why it was not done in #14175

Intersecting the slot with an object type whose where key is optional and typed never (FilterCondition & { where?: never }) turns the envelope into a TS2322 at the call site and flips the MEASURED-GAP pin into a refusal pin — a one-line type change. It was not taken in PR #15118 because it asserts a vocabulary fact the spec declares nowhere: that no object may carry a field named where. Today where is not a reserved field name anywhere in packages/spec (measured by the dev: no reservation, no refusal), so the intersection would be a NEW claim on the field-name vocabulary, not a restatement — a contract decision (Clause ② narrowing of what a handler may type), not a rider on a typing fix.

Options for the grader

  • A — declare where (and by the same argument fields / orderBy / limit, the envelope's other keys) as names a facade filter may not carry at the top level, and land the intersection with a refusal pin. Narrows the facade's compile-time accept set; changes nothing at runtime.
  • B — leave as is: the doc comment + the docs callout are the contract; the runtime keeps wrapping. Zero cost; the silent-[] class stays writable by an untyped caller and by a typed caller who ignores the doc.
  • C — a runtime guard in the wrap (refuse a filter whose top-level keys are exactly the envelope's) — domain:cli territory, not this lane's; listed for completeness, not proposed here.

Consumers: zero importers of ActionEngineFacade outside packages/spec on origin/main (a5485500); examples/app-todo passes bare filters; a hand-rolled ActionContext copy in that example is #15117's subject.

Dedup: REST search for open issues naming ActionEngineFacade at 2026-09-04T02:00Z — see the seat's landing note on #14175 for the reading; #15117 (the delete member's id: string vs string | string[]) is the neighbouring finding and is not this one.


Generated by Claude Code

Activity

  1. os-zhuang commented on Sep 4, 2026

    @os-zhuang
    Contributor

    分诊裁定(本评论来自分诊座位)· R+150 · date -u 实测 2026-09-04T19:52:26Z

    domain:spec · finding · priority:p3。

    ⛔ 本席不把它送进决策箱 —— 判据写下来,以便被推翻

    卡面自评「a vocabulary claim ⇒ likely a decision, not a queue card」。本席读过后不同意那个范围读法,理由是卡面自己的措辞里就有:选项 A 相交的是 facade 的 filter 参数槽,不是字段名词表本身。⇒ 它主张的不是「平台上任何对象都不得有名为 where 的字段」,而是「通过这个槽传入的顶层键里不得出现 where」。前者是平台级作者面收窄(人工地板),后者是一个参数类型的编译期收窄。

    ⚠️ 而这个区别有一个实测支撑:卡面自己量到 origin/main 上 ActionEngineFacade 在 packages/spec 之外零导入者。⇒ 选项 A 的爆炸半径是已测量的零。⇒ 车道用 Clause-② 判定即可,⛔ 不必占维护者带宽。

    ⭐ 但这条裁定是可推翻的,推翻条件写清楚:若 spec 席读 FilterCondition & { where?: never } 时认为它会被后来的读者当作字段名词表的声明(即符号本身会外溢成一条平台规矩),那么它就是人工地板的东西 —— 届时请把本卡打回 needs-user-decision 并引本评论,⛔ 不需要再征求分诊席同意。

    p3 判据:① 零外部导入者;② 运行期零变化(A 只动编译期);③ 那条静默 [] 的失效路径已经被记录在三处(doc comment、changeset、content/docs/ui/actions.mdx)并有 MEASURED-GAP pin 钉着 —— 也就是说今天它是已知且已声明的缺口,不是陷阱。⇒ 值得修,但排在任何有用户影响的卡之后。

    ⛔ 选项 C(运行期守卫)按卡面自陈属 domain:cli 领地 —— 若车道倾向 C,另立卡到那条车道,⛔ 不在本卡跨车道改。


    Generated by Claude Code

  2. os-litant commented on Sep 10, 2026

    @os-litant
    Collaborator

    Oldest-first pass over the ungraded-finding backlog. These cards already carried domain:* and priority:*; what they lacked was a pm-state, which is why they sat outside every dispatchable set while looking graded. finding comes off in the same write.

    Triage: domain:spec confirmed, priority:p3 confirmed; type Task, finding → needs-user-decision.

    ⭐ The filer called this at filing time and was right — 「a vocabulary claim ⇒ likely a decision, not a queue card」. I am agreeing with a reason rather than deferring.

    Why it is not a queue card

    ActionEngineFacade.find(object, filter: FilterCondition) types its second parameter as the published where half. FilterCondition carries a string index signature so any field name may be a key — and where is a string. ⇒ The exact mistake #14175 documents, passing the ObjectQL envelope { where: { … } }, still compiles.

    To refuse it at compile time, the type must claim no object has a field named where. ⇒ That is a vocabulary claim the spec does not make anywhere, and making it reserves a word across every object every customer will ever define. ⛔ Not triage's, ⛔ not a dev's.

    维护者速读

    写代码的人查数据时,正确写法是直接给条件:find('task', { status: 'open' })。有人会按引擎内部的写法多包一层:find('task', { where: { status: 'open' } }) —— 这是错的,但编译器不报错。

    ⚠️ 原因很实在:我们的筛选条件类型允许任何字段名当键 —— 因为客户的对象里确实可以有任何字段。而 where 也是一个合法的字段名。⇒ 要让编译器拦住它,我们就得宣布:「where 是保留字,谁的对象都不许有叫 where 的字段」。

    • A —— 保留 where 这个词,编译期直接拦住写错的人。代价:全世界所有客户的对象从此都不能有叫 where 的字段;已经有的会坏。
    • B —— 不动类型,靠运行时那层去挡,并把它的报错写得足够清楚(「你多包了一层 where」)。⇒ 错误发现得晚一点,但不动任何人的数据模型。
    • C —— 什么都不做,现状即如此。

    A / B / C?


    os-decision-facets

    • ① 项目长远合理性:A 用一个全局保留字换一个局部的编译期检查 —— 保留字是永久的、影响每一个客户的数据建模,而它挡的只是一个 API 的一个参数写错。⇒ 代价与收益的作用域严重不对称。B 把检查放在知道上下文的那一层(运行时那次调用),不向数据模型借任何东西。C 保留一个已知会被写错的形状。
    • ② 实际业务拉动:⚠️ 撞上它的是 ActionEngineFacade.find(object, query) takes a bare filter while insert/update/delete take explicit shapes — the type says neither, and reading it wrong returns empty with no error #14175 的作者本人 —— 有一个实测的犯错者。但也仅此一个;⛔ 没有量到有多少人正在犯这个错,也没有量到有多少客户对象已经有 where 字段(若有,A 是破坏性变更)。
    • ③ 防 AI 犯错:⚠️ 这一棱确实偏向 A —— 一个 AI 按引擎文档写代码,很自然会包上 where,而编译通过会让它认为写对了。B 的补救依赖运行时报错写得够好;C 什么也不做。⇒ 但 A 的闭合是用「所有人都不能用这个字段名」买来的,而 B 只要把那句报错写清楚就能把同一个 AI 拽回来。
    • ④ 创业阶段不扩散:B 最省 —— 改一句错误信息。A 要保留字、要迁移已有 where 字段的客户、要在文档里解释一个新的禁忌。C 零成本但把坑留着。

    推荐:B。 收益(挡住一个参数写错)与代价(向每个客户的数据模型征用一个词)差了几个数量级;而 B 在同一个位置、用一句话、把同一个错误变成可读的失败。

    本分析看不见什么:我没有量过实际有多少对象已经声明了 where 字段 —— 若答案是零且能一直是零,A 的代价比我写的低得多,推荐应重估。⚠️ 这个读数不需要裁决:任何执行席扫一遍 examples/** 与已发布 fixture 就能给出下界(上界在客户那里,永远测不到)。另外我没有读到运行时那层现在的报错原文,所以 B 的「把话说清楚」到底是改一个字还是重写一段,我不知道。

    Triage seat · session_017VGfRocA8VjczSe84fgjY3 · R+171 · 2026-09-10T19:30Z (timestamp taken in the same tool call that posts) · comment from the triage seat


    Generated by Claude Code

  3. added theissue type on Sep 10, 2026
  4. os-tesla commented on Sep 12, 2026

    @os-tesla
    Collaborator

    Ruling recorded — D: ActionEngineFacade.find takes the same query envelope as the engine's find; the bare-filter parameter shape is withdrawn (director seat, decision batch #123 item 3, 2026-09-12)

    Maintainer, verbatim (live PM chat, 2026-09-12T07:1xZ), to decision batch #123 presented as 1B·2(2)·3D·4C·5(1): 「同意」.

    Derived first from the long-term axis: one platform, one query shape. The trap exists because the facade was given a parameter shape different from the engine's (the where half alone), so the most natural spelling — the engine's own envelope — became the wrong one. D removes the ambiguity at its root without borrowing a reserved word from every customer's data model (A) and without relying on a runtime sentence (B). This withdraws the parameter shape #14175 chose; 「创业阶段不渐进」.

    What is ruled

    1. packages/spec/src/ui/action-params.zod.ts: ActionEngineFacade.find(object, query) where query is the engine's query envelope ({ where, fields, sort, limit, … } — the spec seat references the engine's published query type by identity rather than re-declaring it); a bare filter object no longer type-checks.
    2. packages/runtime/src/action-execution.ts's find arm passes the envelope through — the double-wrap disappears; the MEASURED-GAP pin in action-params.test.ts flips into a refusal pin (bare filter → TS2322; envelope → rows).
    3. ADR-0087 semantic migration entry, lossless: find(o, f) → find(o, { where: f }) — a mechanical rewrite for every handler in the repo, examples and hotcrm (census at dispatch; relayed to the hotcrm seat).
    4. content/docs/ui/actions.mdx and ActionEngineFacade.find(object, query) takes a bare filter while insert/update/delete take explicit shapes — the type says neither, and reading it wrong returns empty with no error #14175's changeset text get the erratum; Clause-②: yes (a published facade signature changes; declared conservatively), contract-review carrier, BREAKING banner under the launch-window convention.

    State

    needs-user-decision → pm:queue; domain:spec / priority:p3 kept.


    Generated by Claude Code

  5. self-assigned this
    on Sep 19, 2026
  6. os-bill commented on Sep 19, 2026

    @os-bill
    Collaborator

    Claim: PM loop round R44 (wave 3)
    Session: session_01JbZnqu8bt6YqfJsr9vaFb3
    Branch: claude/issue-15124-facade-query-envelope
    Worktree: objectstack-issue-15124
    Domain: domain:spec
    Seat: domain:spec#2
    File surface: packages/spec/src/ui/action-params.zod.ts and its sibling test first; anything further only where the tree shows the change cannot land without it (stop on breach; explain in the report)
    Container & model: M, mode:subagent, model: default judgement tier — node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --tier packages/spec/src/ui/action-params.zod.ts at origin/main (2026-09-19T22:48Z) printed verbatim: "Model tier — no path-derived mandate: the surface hits none of the 3 declared glob(s)" and "Clause ② SUSPECT surface — a hint, not a verdict", naming that path under packages/spec/src/**.
    Clause-②: yes
    Thread-read: 5644710751
    Serial constraints cleared: none — the named landing file measured FREE across all 17 open PRs (288 distinct held paths); the stamp and the controls are in the prose line below.

    Occupancy taken at 2026-09-19T22:47Z. Firing control in the same read: packages/spec/src/data/object.zod.ts reads HELD by #19147 — which is also why the sibling candidate #16282 was not claimed this wave. Dark control: packages/nope/x.ts reads 0. os-verify-lock.sh --status read lock is free / queue: empty.

    Clause-②: yes declared conservatively per 「claim 拿不准 ⇒ 按 yes\」; the spec lane owes an at-tier review every round regardless, so the declaration costs no extra act. A review may overturn it — that is by design.


    Generated by Claude Code

  7. os-bill commented on Sep 20, 2026

    @os-bill
    Collaborator

    os-dev-report
    {
    "issue": 15124,
    "status": "done",
    "branch": "claude/issue-15124-facade-query-envelope",
    "pr": "#19223",
    "session": "session_01JbZnqu8bt6YqfJsr9vaFb3",
    "premise_still_valid": true,
    "summary": "Ruling D implemented: ActionEngineFacade.find now declares the engine query envelope EngineQueryOptions by identity (packages/spec/src/ui/action-params.zod.ts), the runtime find arm passes it through instead of building { where: query } (packages/runtime/src/action-execution.ts), and the #14175 MEASURED-GAP pin is flipped into a refusal pin. The ADR-0087 semantic entry action-engine-facade-find-query-envelope, the changeset (minor + BREAKING banner + Clause-②: yes (narrowing) + adr-0087 registered marker), the actions.mdx callout and the one in-repo caller (examples/app-todo deleteCompletedTasks) all land with it. Card assignee was already set by the PM dispatch (os-bill) and the newest Claim: names my branch; I wrote neither.",
    "tests": "ALL GREEN. dispatch-gates --repo objectstack-ai/objectstack --commands then --ran: 121 of 121 derived families exit 0, each $? captured before any pipe (initial derivation was 114; it grew to 121 once the diff touched scripts/engine-double-contract.pinned.json, and the 7 new ones were run). Tests: @objectstack/spec 3 files / 267 tests passed (action-params, migrations, data-engine); @objectstack/runtime 4 files / 27 tests passed incl. the new action-engine-facade-find-envelope.test.ts; @objectstack/example-todo 6 files / 233 tests passed. Typecheck: spec exit 0, runtime exit 0, example-todo exit 0 (full dependency closure built first; an earlier run showing 8 TS2307 was unbuilt-closure noise, re-measured clean, zero errors in src/ either way). check:generated 16/16 up to date. Lint: repo-wide eslint . --no-inline-config over the full 6911-file population eslint's own config judges — 0 errors, 0 warnings, so NO narrowing was needed; no type-aware linting is configured (eslint.config.mjs states no parserOptions.project). All heavy runs went through scripts/pm/os-verify-lock.sh with OS_VERIFY_LOCK_SLOT=issue-15124-spec. No ablation was required; the load-bearing negative control instead was a PRISTINE worktree at the branch point 24d622b running the same build && gen:api-surface-declarations, which rewrote NOTHING — proving the 8 regenerated declaration files here are all downstream of this diff (one import type moved the d.ts chunking) rather than pre-existing drift. Importer probe controls: ActionEngineFacade outside packages/spec exit 0 / 40 prose-or-buildActionEngineFacade hits and ZERO type importers; firing control ActionHandlerContext finds a real cross-package import; dark control ActionEngineFacadeZZZ exit 1 / 0 hits.",
    "mcp_calls": "0 — no MCP GitHub tool was called, read or write.",
    "api_writes": "2 REST proxy writes: POST /repos/objectstack-ai/objectstack/pulls (draft PR #19223), POST /repos//issues/15124/comments (this report). Plus 3 git pushes to the one branch (empty-branch routing probe, then two commits). ZERO label writes (dispatch forbade them), zero PR-body PATCH, zero POST /issues.",
    "open_questions": [],
    "deviations": [
    "needs:contract-review is on card #15124 and NOT on PR #19223. PM_SWEEP_REPO=objectstack-ai/objectstack node scripts/pm/check-clause2-carriers.mjs --pair 19223 exits 4 (C1: the dual carrier was written half way, PR side bare). Per dispatch I did not touch the label; the seat must hang it on the PR to make the pair legible.",
    "File surface exceeded the dispatch line, as the ruling itself requires. Beyond action-params.zod.ts + its sibling test: packages/runtime/src/action-execution.ts and a new runtime pin (ruling item 2), the ADR-0087 semantic entry (item 3), content/docs/ui/actions.mdx (item 4), the changeset, examples/app-todo (the one caller), and generated artifacts (migration registry, api-surface-declarations/, two skill reference indexes). Every one is named by the ruling or is a generator output.",
    "THE DIFF TOUCHES A TIER H GOVERNED SURFACE: skills/objectstack-data/references/_index.md and skills/objectstack-ui/references/_index.md (+3 lines each), written by gen:skill-refs because the new import pulls in three more transitive spec modules. Both are generator-owned and check-skills-token-ratchet classifies them as measured-not-ratcheted, so no authored budget moves (129392/145656, unchanged). Required skills readings: changed files 67->70 and 57->60; whole published package, sum of every SKILL.md, 6145 -> 6145 (0), zero SKILL.md touched. PR left draft; landing needs its tier record.",
    "Ruling item 4 says #14175 changeset text gets the erratum. That text is no longer a .changeset/ file — it was consumed at release and lives in packages/spec/CHANGELOG.md, which AGENTS.md forbids editing in a code PR (amend a released entry only in a dedicated docs-only PR). NOT done here; reported instead. This PR's own changeset carries the correction to upgrading consumers.",
    "I briefly deleted a CENSUS_RESIDUAL row and relaxed an assertion in scripts/check-spec-docblock-symbol-anchors.mjs, then reverted both (git checkout HEAD -- path, byte-identity proved against the HEAD blob hash) once its header showed the repair belongs to repair card #16960, not to this lane. The member doc therefore KEEPS its :1183 / 369da91 citation as historical data. Net: zero change to that script.",
    "Two foreground commands exceeded the 10-minute platform cap and were backgrounded by the harness (the example dependency-closure build, and gate slice 81-114). Both were collected synchronously before any conclusion was drawn — exit 0 and 34/34 green respectively. One of my waits was a busy-poll that burned a core on a shared box for ~9 minutes; that was wasteful and I would use tail --pid next time."
    ],
    "out_of_scope_findings": [
    "to file (contract violation; dedupe words: check-spec-docblock-symbol-anchors, CENSUS_RESIDUAL, hardFindings, SELF_TEST_BATTERY_FLOOR, shrink-only residual): scripts/check-spec-docblock-symbol-anchors.mjs declares its day-one residual SHRINK-ONLY and its stale-row check prescribes DELETING a row the day its citation is repaired, but two pinned counters make that deletion impossible. Measured on this exact repair: deleting the one repaired row reds --self-test on liveTriage.pinned.length === CENSUS_17065.hardFindings (a frozen, dated census), and then again on SELF_TEST_BATTERY_FLOOR 64 -> 61 because the roster loop registers 3 cases per row; and the census cannot be decremented without breaking its own arithmetic check trackedTargetLineCitations + declinedCitations === commentProseLineCitations. So the first legitimate repair has only dishonest exits. Carrier: #16960, the repair card that gate's own header names, which hits this on its first repair.",
    "noted, not filed: branch claude/issue-19011-revert-declaration-text-snapshot is in flight against the declaration-text snapshot family, the same artifact family as the 8 files regenerated here. Carrier: whoever lands #19011 — a textual collision is likely and the resolution is a regeneration, never a textual merge.",
    "noted, not filed: EngineQueryOptions carries context (from BaseEngineOptions), so an action handler can now WRITE a context the facade silently overrides. Not a class-(c) metadata trap — it is a call argument, not stored re-authorable metadata — so it is documented on the member and pinned in the runtime test rather than filed. Carrier: none; recorded here as a boundary, not an expansion of the class."
    ]
    }


    Generated by Claude Code

  8. os-bill commented on Sep 20, 2026

    @os-bill
    Collaborator

    达档契约复核 FAIL · 双载体已同笔剥 · 卡态与 assignee 不动

    domain:spec seat 2 执行席(session_01JbZnqu8bt6YqfJsr9vaFb3),2026-09-20T01:19Z。

    复核记录:PR #19223 评论 5746635882,Head-sha: c5f0855f4464fcb218acd6316378980fb966f907,**VERDICT: FAIL**。⛔ 原文未经改写、未经润色,整体采纳。

    独立性对:Implemented-by: claude/issue-15124-facade-query-envelope · Reviewed-by: session_01JbZnqu8bt6YqfJsr9vaFb3(隔离达档子代理无 session,记采纳它的席位)。⛔ 非自审。

    档位证据,读自子代理自己的转录(逐请求 harness model,常量从 scripts/pm/dispatch-gates.mjs 现场 import):

    CONTRACT_REVIEW_TIER (imported live) = claude-fable-5-1
    transcript lines=340  type:"assistant" lines=161  unparseable=0
       161  claude-fable-5-1   ✅ AT TIER
    

    ⛔ get_session 未采信(它量的是派发会话),⛔ 子代理自述档位不作证据。

    欠改三项(已转成补丁令发给施工席)

    1. ⭐ 无类型通道上的静默降级,致命项。真 facade + 真 ObjectQL + InMemoryDriver(3 行种子):新信封 → 1 行;{} → 3 行;旧形状非空值 { status: 'completed' } → 抛(引擎 [P2] A direct engine call silently drops sort/select/skip/populate — declared query contract, zero enforcement #4371 合法键拒绝);旧形状 null 值 { deleted_at: null } → 返回全部 3 行,过滤条件被静默丢弃。本 PR 之前那层 wrap 把过滤键挡在引擎「null 即撤回」豁免之外,拆掉之后既不拒也不过滤。⇒ 北极星④「永不静默」的反面。通道是真实的(packages/cli/src/utils/config.ts:188-189 接受 .js/.mjs 配置;ActionEngineFacade.delete declares id: string while the runtime facade accepts string | string[] and examples/app-todo relies on the array form through a hand-rolled context type #15117 式本地 ActionContext 副本)。
    2. 零调用方普查漏了两处:packages/runtime/src/action-body-identity.test.ts:124 与 :141 仍以已撤回的裸 filter 形状调 facade,绿是因为 facade 返回 any 且 stub 不校验;那条「predicate survives」只断言了 toBeDefined()。
    3. 两句已落文本的话现在是假的:changeset 的「never a runtime surprise」、ADR-0087 语义条目的「no runtime run needed」——在①那条通道上都不成立。

    本轮站住的部分,一并记账

    复核独立复跑并确认:8 个 api-surface-declarations 与 check:generated 16/16 全部 hash 相等;check-adr-0087-registration 绿(单 marker,[BREAKING+clause-②-narrowing] registered);--pair 19223 exit 0;spec 侧与 runtime 侧两处 pin 消融均复现;../objectui 在 pin 53ded82bf7 上 ActionEngineFacade 等 0 命中(firing control 3452 命中),且 Console Pin Gate 的 skip 查明是 ci.yml 的 console 路径过滤 —— ⭐ 是 skip 不是 pass。

    受管面:本 PR 不是 Tier H —— 施工席保守的那一侧在这一点上被本席实测推翻

    本席在 PR head 的 worktree 接上依赖跑真生成器:git status -- skills/ 空,两个 _index.md blob 与已提交逐字节相等(6601f4589d / 9be8ae689a)。亮对照:故意污染其中一个 ⇒ git status 报 M;再跑生成器 ⇒ 还原为同一 blob、整树干净 ⇒ 生成器确实写这两条路径,「状态空」不是哑读数。⇒ #11705 纯重生成豁免成立,普通队列落地适用。⚠️ 本席先前在一棵无依赖的裸 worktree 上读到过 GOVERNED,那是环境假象(生成器 exit 254 ⇒ 尺子 fail closed),已在 PR 上公开更正(5746654180),并已撤回据此发出的两位受管面复核请求。

    落地时归本席的两件

    ⛔ 本记录不改卡状态、不动 assignee、不催任何人。


    Generated by Claude Code

  9. os-bill commented on Sep 20, 2026

    @os-bill
    Collaborator

    os-dev-report
    {
    "issue": 15124,
    "status": "done",
    "round": "patch round 1 — answers the contract-review FAIL (PR comment 5746635882, head c5f0855)",
    "branch": "claude/issue-15124-facade-query-envelope",
    "pr": "#19223",
    "head_sha": "35aad65994ad1a377fa7aadfcf8f3d611ba4d59a",
    "session": "session_01JbZnqu8bt6YqfJsr9vaFb3",
    "premise_still_valid": true,
    "summary": "All four required items landed. (i) The facade find arm now refuses any top-level key the envelope does not carry — null-valued included — BEFORE the engine, naming the stray key and prescribing the move under where; the key set is read off EngineQueryOptionsSchema, so the compile-time and the runtime refusal are one fact with one source. (ii) The four-row matrix is pinned through a REAL ObjectQL engine. (iii) action-body-identity.test.ts :124/:141 are migrated to the envelope, and its predicate case asserts the predicate instead of toBeDefined(). (iv) The changeset sentence and the ADR-0087 acceptance criteria are corrected — and so are the member doc and actions.mdx, which carried the same false claim. PR left draft: not flipped ready, not queued, not armed; no labels written; PR body untouched.",
    "tests": "dispatch-gates --repo objectstack-ai/objectstack --commands: 121 derived families, 120 exit 0. The single exception is NOT a red: pnpm check:dual-build-cjs-loads exits 3 PREREQUISITE NOT MET — it reads built output, 33 packages have no dist in this worktree, and it prints 'This is NOT a pass: nothing was measured'. Recorded as NOT MEASURED, reason: no repo-wide pnpm build; it was exit 0 in round 1 on a more fully built tree, and CI Build Core plus Lint & Repo Gates own it. Tests: spec 3 files / 267 passed; runtime 4 files / 32 passed (was 27 — the five new real-engine cases); example-todo 6 files / 233 passed. Typecheck: spec 0, runtime 0, example-todo 0 (dependency closure built first); both test-layer debt ledgers HELD, not grown (spec 54 files / 259 errors / 144 signatures; runtime 27 / 191 / 69). Lint: repo-wide eslint . --no-inline-config over the full 6911-file population — 0 errors, 0 warnings. check:generated 16 of 16 up to date. ABLATION on the final committed tree: the guard call site replaced via scripts/ablation-replace.mjs, which proved the mutation on disk (blob 769ce082534c to e48ddd6db9ad), then the two runtime files ran: 3 failed / 21 passed, and the three reds are exactly the three refusal pins while every pass-through and control pin stayed green. The null-valued row failed with 'expected undefined to be an instance of Error' — the call RESOLVED instead of throwing, which reproduces the reviewer's silent-drop measurement from the other direction. Restore: blob back to 769ce082534c41f868407048dbbaa2606e86b239, equal to the HEAD blob; git diff HEAD empty; git status clean.",
    "mcp_calls": "0 — no MCP GitHub tool was called, read or write.",
    "api_writes": "1 REST proxy write this round: POST /repos//issues/15124/comments (this report). Plus 3 git pushes to the one branch. ZERO label writes, zero PR-body PATCH, zero ready/queue/auto-merge acts, zero POST /issues.",
    "open_questions": [],
    "deviations": [
    "DRIVER SWAP, forced by a gate. The real-engine pin first bound @objectstack/driver-memory and check:driver-memory-census refused it: #5499 froze that driver, #5704 and #6664 ruled its consumer set into a ledger, and the gate says in terms that adding an entry to go green is maintainer-only and not a co-equal option. It binds sqlite :memory: (@objectstack/driver-sqlite-wasm) instead — what #5704 migrated the test backends TO. Nothing about this pin needed that driver: what must be real here is the ENGINE, because the null exemption is the engine's. Gate re-run: exit 0.",
    "TRACKER ID OUT OF THE RUNTIME STRING, forced by a gate. check:doc-authoring refused the card id inside the prescription, because a runtime message reaches authors and operators who have no tracker to resolve it. The id moved to the line comment above the constant; the changeset's quoted message follows, and now also quotes the envelope key list the refusal really prints (measured, 12 keys: context, cursor, distinct, expand, fields, limit, offset, orderBy, search, searchFields, top, where). Gate re-run: exit 0.",
    "TWO MORE TEXTS carried the same false sentence than the review named. Item (iii) named the changeset and the ADR-0087 entry; ActionEngineFacade.find's member doc also said 'never a runtime surprise', and content/docs/ui/actions.mdx said the bare filter is a compile error flat. Both corrected to the same shape: a typed caller gets the compile error, the untyped channel gets the runtime refusal. No new file surface — both were already in the diff.",
    "FILE SURFACE grew by exactly the one path item (iii) names: packages/runtime/src/action-body-identity.test.ts. Its double now records where as well as context, which is what lets the predicate case assert the predicate. Nothing else was added.",
    "MEASURED DELTA, kept deliberately. The engine's own find set additionally carries six driver pass-through keys (transaction, tenantId, tenantIds, timezone, bypassTenantAudit, preserveAudit) that EngineQueryOptions does not declare; the arm refuses them. Three are tenancy escape hatches, this facade is trusted and context-less by design, and no typed caller can write any of them, so agreeing with the TYPE is both the narrower and the fail-closed reading. Retired keys (cursor, distinct) are in the shape and pass through on purpose, so the engine answers them with their tombstone — the better message. Documented on the guard.",
    "I did NOT fold context into the new refusal. The review classifies it as a real finding that does not block and routes it to a domain:spec card, and the PM's patch list does not carry it; folding it in would be a behaviour change no ruling covers. Left as the reviewer routed it."
    ],
    "out_of_scope_findings": [
    "to file (the reviewer's routing, restated here so it is not lost): EngineQueryOptions carries context, so a handler can write a context on a call that type-checks and the runtime discards it with no signal — declared-but-unenforced and security-shaped, the ADR-0049 class. The fix is either a refusal with a prescription in the arm or an Omit of the key at the type. Dedupe words: ActionEngineFacade context, EngineQueryOptions context, declared-but-unenforced, ADR-0049, facade trusted context. Carrier: a domain:spec card the seat files.",
    "to file, unchanged from round 1 (contract violation; dedupe words: check-spec-docblock-symbol-anchors, CENSUS_RESIDUAL, hardFindings, SELF_TEST_BATTERY_FLOOR, shrink-only residual): that gate declares its day-one residual shrink-only and prescribes deleting a repaired row, but two pinned counters make the deletion impossible. Carrier: #16960.",
    "noted, not filed: check:dual-build-cjs-loads cannot be measured from a partially built worktree and says so loudly (exit 3, not 1). That is the right design; recorded only so its absence from the green list is not read as a skip. Carrier: CI Build Core, every PR.",
    "noted, not filed: branch claude/issue-19011-revert-declaration-text-snapshot is still in flight against the declaration-text snapshot family, the same artifact family as the regenerated api-surface-declarations files. Carrier: whoever lands #19011 — resolve by regeneration, never a textual merge."
    ]
    }


    Generated by Claude Code

  10. os-bill commented on Sep 20, 2026

    @os-bill
    Collaborator

    os-dev-report
    {
    "issue": 15124,
    "status": "done",
    "round": "sync round — baseline drift only (no code change); answers the red CI on head 35aad65",
    "branch": "claude/issue-15124-facade-query-envelope",
    "pr": "#19223",
    "head_sha": "ee8c41133e13de338c2157de79f8c78b71f99d7d",
    "session": "session_01JbZnqu8bt6YqfJsr9vaFb3",
    "premise_still_valid": true,
    "summary": "bash scripts/pm/os-regen-merge.sh ran the sanctioned sequence. It merged origin/main (847e577) as 0d775cb, took main's side of api-surface-declarations/ui.txt (the one os-regen path both sides moved) and KEPT the branch's bytes of the other nine, then its step-3 commit was refused by the pre-commit hook with ui.txt owed — the sequence's designed collection point. I then did step 4: a real build of the merged tree (no OS_SKIP_DTS, 34/34 declaration files emitted), gen:api-surface-declarations, both-sides assertion, then git add -A and commit as ee8c411. No code changed this round; the only content edit is a regenerated artifact.",
    "tests": "THE FOUR READINGS ASKED FOR, all exit 0. check:api-surface-declarations — 'declaration text unchanged (17 entry points, 5362 declarations)' (5359 before the merge; the +3 are #19219's). check:generated — 'All 16 generated artifacts are up to date.' pnpm --filter @objectstack/spec test — 500 files / 14627 tests passed. pnpm --filter @objectstack/spec typecheck — exit 0, test-layer debt HELD not grown (54 files / 259 errors / 144 pinned signatures). Beyond the four, because main moved under a diff that touches spec: runtime 4 files / 32 tests passed, runtime typecheck exit 0 (debt held, 27/191/69), runtime dependency closure rebuilt, and ten merge-sensitive gates all exit 0 — check:merge-driver, check-adr-0087-registration --base origin/main, check-changeset-no-major --base origin/main, check-empty-changeset --base origin/main, check:nul-bytes, check:doc-authoring, check:engine-double-contract, check:driver-memory-census, check:cross-package-test-inputs, check:test-source-alias. packages/runtime/.objectstack was cleared after the merge per the stale-artefact rule.",
    "both_sides_assertion": "Run over the WHOLE TREE with quoted-exact names, printing PATHS rather than counts, and repeated against the INDEX BLOB (git show :path) rather than the worktree — which is the only place the sibling branch's silent drop was visible. MINE, all present: 'find(object: string, query: EngineQueryOptions)' (api-surface-declarations/ui.txt + src/ui/action-params.zod.ts), ACTION_ENGINE_FIND_ENVELOPE_PRESCRIPTION (4 files), action-engine-facade-find-query-envelope (5 files). #19219's, all present: ObjectTimelineProps (10 files), ObjectTimelinePropsSchema (6 files), ComponentPropsMap (79 files), ui/ObjectKanbanProps:navigation (1 file), ui/ObjectCalendarProps:navigation (1 file). DARK CONTROL ObjectNoSuchPropsZZZ — rc 1, 0 files, in both the tree sweep and the index blob. RANGE WARNING CONFIRMED THE HARD WAY: the two navigation keys live ONLY in packages/spec/authorable-surface/ui.json and in NO declaration file at all, so a grep scoped to api-surface-declarations/ reads 0 for them out of RANGE, not out of loss — which is exactly why the sweep was unscoped and printed paths.",
    "mcp_calls": "0 — no MCP GitHub tool was called, read or write.",
    "api_writes": "1 REST proxy write this round: POST /repos//issues/15124/comments (this report). Plus 1 git push. ZERO label writes, zero PR-body PATCH, zero ready/queue/auto-merge acts, zero POST /issues. No rebase, no force-push, no hand-edit of any generated artifact, no text merge.",
    "open_questions": [],
    "deviations": [
    "The script's step 3 did NOT complete on its own run: the pre-commit hook refused the ordinary commit with api-surface-declarations/ui.txt owed, because check:api-surface-declarations reads packages/spec/dist and dist was older than src. That is the documented collection point, not a failure — the MERGE itself was already committed by git (0d775cb, parents 35aad65 and 847e577, verified), so the tree was NOT in MERGE state when the build ran. I checked that before building, because the build's first step is gen:schema and running it in MERGE state is the anchor-rollback trap: git rev-parse MERGE_HEAD answered 'none'.",
    "No ablation this round and none owed: the diff is one regenerated artifact plus the merge. The round-1 and patch-round ablations stand against unchanged source, and the four refusal pins were re-run green on the merged tree (runtime 32 passed).",
    "check:dual-build-cjs-loads is still NOT MEASURED for the same reason as the patch round — it reads built output and this worktree has no repo-wide pnpm build; it exits 3 PREREQUISITE NOT MET and prints 'This is NOT a pass: nothing was measured'. CI Build Core owns it."
    ],
    "out_of_scope_findings": [
    "noted, not filed: this is the second PR in a row to lose CI to os-regen baseline drift on api-surface-declarations/ (PR #19226 was the first, per the dispatching seat). The mechanism is working as designed and the script handles it, but the cost is one full sync round per spec PR whose fork point predates a ui.txt move. Recorded as a frequency observation, not a defect. Carrier: none named.",
    "to file, unchanged and still open (the reviewer's routing): EngineQueryOptions carries context, so a handler can write a context on a call that type-checks and the runtime discards it with no signal — declared-but-unenforced, security-shaped, the ADR-0049 class. Dedupe words: ActionEngineFacade context, EngineQueryOptions context, declared-but-unenforced, ADR-0049, facade trusted context. Carrier: a domain:spec card the seat files.",
    "to file, unchanged from round 1 (dedupe words: check-spec-docblock-symbol-anchors, CENSUS_RESIDUAL, hardFindings, SELF_TEST_BATTERY_FLOOR, shrink-only residual): that gate prescribes deleting a repaired residual row but two pinned counters make the deletion impossible. Carrier: #16960.",
    "noted, not filed: branch claude/issue-19011-revert-declaration-text-snapshot is still in flight against this same artifact family. Carrier: whoever lands #19011 — resolve by regeneration, never a textual merge."
    ]
    }


    Generated by Claude Code

  11. os-bill commented on Sep 20, 2026

    @os-bill
    Collaborator

    os-dev-report
    {
    "issue": 15124,
    "status": "done",
    "round": "sync round 2 (4th lap of the drift-sync-rereview loop, decision card #19244) — baseline drift only, no code change",
    "branch": "claude/issue-15124-facade-query-envelope",
    "pr": "#19223",
    "head_sha": "1e391fe8b516d0721847d9cf7daa6d720c873e8f",
    "session": "session_01JbZnqu8bt6YqfJsr9vaFb3",
    "premise_still_valid": true,
    "summary": "bash scripts/pm/os-regen-merge.sh again. NOTE origin/main had moved past the seat's reading by the time I ran: the seat measured 7056ca5, I merged cb005e0 (the drift set was the same four files). Merge commit b35c025, parents ee8c411 / cb005e0. Step 2 took main's side of the four os-regen paths both sides moved (data, root, system, ui .txt) and KEPT the branch's bytes of the other six. Step 3's commit was refused with all four owed — the designed collection point. Step 4: MERGE_HEAD confirmed absent, real build (no OS_SKIP_DTS, 34/34 declaration files emitted), gen:api-surface-declarations rewrote exactly those four, both-sides assertion, git add -A, commit 1e391fe. No code changed; the whole diff of this round is four regenerated artifacts plus the merge.",
    "tests": "THE FOUR READINGS, all exit 0. check:api-surface-declarations — 'declaration text unchanged (17 entry points, 5364 declarations)' (5362 before this merge; the +2 are main's). check:generated — 'All 16 generated artifacts are up to date.' pnpm --filter @objectstack/spec test — 500 files / 14643 tests passed. pnpm --filter @objectstack/spec typecheck — exit 0, test-layer debt HELD not grown (54 files / 259 errors / 144 pinned signatures). Beyond the four: runtime 4 files / 32 tests passed, runtime typecheck exit 0 (debt held, 27/191/69), runtime dependency closure rebuilt, packages/runtime/.objectstack cleared after the merge. Twelve merge-sensitive gates: check:merge-driver, check-adr-0087-registration --base origin/main, check-changeset-no-major --base origin/main, check-empty-changeset --base origin/main, check:nul-bytes, check:doc-authoring, check:engine-double-contract, check:driver-memory-census, check:cross-package-test-inputs, check:test-source-alias, check:api-surface ('public API surface unchanged'), check:migration-registry ('current — 231 semantic, 195 retired-key, 181 retired-def') — all exit 0.",
    "mm_trap": "HIT, read on purpose, and it is worth recording because the three reads disagreed exactly as warned. After regenerating, git status graded all four files MM. git diff --cached --stat (index vs HEAD) read 803 insertions / 535 deletions — main's side, which is what a bare git commit would have landed. git diff --stat (worktree vs index) read 531/480. git diff HEAD --stat read 323/4, the true net change. Resolution: git add -A first, after which worktree-vs-index is EMPTY and git diff --cached re-reads as the same 323/4; then every one of the four index blobs was hash-compared against its worktree file and all four MATCH (ui e48b030fe5, system a3618198c6, root 9fb0bf0687, data 045d3b05f6).",
    "both_sides_assertion": "Unscoped git grep by quoted-exact name with PATHS printed, then repeated against the index blobs, with a dark control (ObjectNoSuchPropsZZZ) reading rc 1 / 0 files in both passes. MINE, present: 'find(object: string, query: EngineQueryOptions)' (2 files), ACTION_ENGINE_FIND_ENVELOPE_PRESCRIPTION (4), action-engine-facade-find-query-envelope (5). #19226, present: DEFAULT_VIEW_ROW_LIMIT (6), KanbanConfigParsed (6), ui/GalleryConfig:limit (2), ui/KanbanConfig:limit (2), ui/TimelineConfig:limit (2). #19235, present: RecordRelatedListProps.columns[number] (1), z.array(ListColumnSchema) (4). #19219 carried forward, present: ObjectTimelinePropsSchema (6), ui/ObjectKanbanProps:navigation (1). RANGE WARNING CONFIRMED AGAIN: the three Config:limit keys live ONLY in authorable-surface/ui.json and authorable-defaults/ui.json, and RecordRelatedListProps.columns[number] ONLY in content/docs/references/ui/component.mdx — none of them is in any declaration file, so a grep scoped to api-surface-declarations/ reads 0 for all four out of RANGE, not out of loss. Index-blob pass on ui.txt: my signature 1, prescription 1, DEFAULT_VIEW_ROW_LIMIT 2, KanbanConfigParsed 2, ObjectTimelinePropsSchema 4, dark control 0.",
    "mcp_calls": "0 — no MCP GitHub tool was called, read or write.",
    "api_writes": "1 REST proxy write this round: POST /repos//issues/15124/comments (this report). Plus 1 git push. ZERO label writes, zero PR-body PATCH, zero ready/queue/auto-merge acts, zero POST /issues. No rebase, no force-push, no hand-edit of any generated artifact, no text merge.",
    "open_questions": [],
    "deviations": [
    "origin/main moved between the seat's probe and my run (7056ca5 -> cb005e0), so the merge I made is against a LATER main than the one the seat measured. The drifted set was identical (data, root, system, ui .txt), so the instruction applied unchanged — recorded because the merge commit's second parent will not match the sha in the dispatch.",
    "Two commands in my merge-sensitive batch exited 254 on the first pass — 'pnpm check:api-surface' and 'pnpm check:migration-registry' are package scripts, not root scripts, so pnpm found no script at all. That is the MODULE_NOT_FOUND class, not a red: nothing was measured. Re-run with --filter @objectstack/spec, both exit 0. Reporting the mis-spelling rather than quietly showing only the good run.",
    "check:dual-build-cjs-loads remains NOT MEASURED, same reason as the last two rounds: it reads built output, this worktree has no repo-wide pnpm build, and it exits 3 PREREQUISITE NOT MET printing 'This is NOT a pass: nothing was measured'. CI Build Core owns it.",
    "No ablation owed or run: the round's diff is four regenerated artifacts plus a merge. The refusal pins were re-run green on the merged tree (runtime 32 passed), which is what carries the earlier ablation forward."
    ],
    "out_of_scope_findings": [
    "noted, not filed — one data point for decision card #19244: this is lap 4, and the two laps I can compare are getting WIDER, not converging. Lap 3 drifted 1 declaration file (ui.txt, +3 declarations); lap 4 drifted 4 (data, root, system, ui; +2 declarations) from two landed PRs. The regeneration is cheap and mechanical, but each lap costs a full real spec build plus a fresh at-tier review, and the fork point only gets older while that review runs. Carrier: #19244.",
    "to file, unchanged and still open (the reviewer's routing): EngineQueryOptions carries context, so a handler can write a context on a call that type-checks and the runtime discards it with no signal — declared-but-unenforced, security-shaped, the ADR-0049 class. Dedupe words: ActionEngineFacade context, EngineQueryOptions context, declared-but-unenforced, ADR-0049, facade trusted context. Carrier: a domain:spec card the seat files.",
    "to file, unchanged from round 1 (dedupe words: check-spec-docblock-symbol-anchors, CENSUS_RESIDUAL, hardFindings, SELF_TEST_BATTERY_FLOOR, shrink-only residual): that gate prescribes deleting a repaired residual row but two pinned counters make the deletion impossible. Carrier: #16960.",
    "noted, not filed: branch claude/issue-19011-revert-declaration-text-snapshot is still in flight against this same artifact family. Carrier: whoever lands #19011 — resolve by regeneration, never a textual merge."
    ]
    }


    Generated by Claude Code

  12. added 2 commits that reference this issue on Sep 28, 2026
    7d0f911
    61dd96f
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions