Repository navigation
[finding] EvalContext.api declares os.exists / os.count / os.lookup and buildScope() binds none of them — a predicate that calls one is refused, and an unevaluable predicate rejects the write #18318
Description
Activity
- addedbugSomething isn't workingSomething isn't workingpriority:p2Medium: important, M3Medium: important, M3repo:hotcrmSeam card: cross-repo ordering with hotcrm is the substance (pure hotcrm fixes live in hotcrm)Seam card: cross-repo ordering with hotcrm is the substance (pure hotcrm fixes live in hotcrm)
on Sep 15, 2026 huangyiirene commented
on Sep 17, 2026 CollaboratorMore actions维护者速读
平台的公式引擎(写校验规则、权限谓词用的那个)在类型上声明了三个函数
os.exists/os.count/os.lookup,还配了一句说明「有查询引擎的调用方会实现它们」。实际上一个都没接上。后果不是「这个功能暂时不能用」,而是写下去会把整张表锁死:谓词求不出值 ⇒ 这次写入被拒 ⇒ 照着这个声明写了一条校验规则的对象,每一次新增和修改都会失败。而声明本身读起来完全像个承诺,AI 和人都会当真 —— hotcrm 那边(#1915)就是照它设计的,施工时才发现整条路不通,需求方案作废。
要么把它接上,要么把这个声明删掉。接上不是小事:谓词求值是同步的,而查询是异步的;一条规则配一次查询会变成 N+1;最要命的是权限 —— 行级安全谓词里做 lookup,等于让一条安全规则去读它可能无权读的数据。这三个问题没答之前没人能安全地接。
删声明则是立刻止血、零成本、可回退:今天的害处来自「声明存在」,不来自「实现缺失」。至于「谓词到底该不该能读别的对象」,那是一道独立的能力设计题,值得单独立卡、单独给 appetite,并带上 hotcrm 这个真实拉动。
⚠️ 顺带一提:can那张卡(#18545)您已经裁过一个同构的问题,裁的是「主体从求值上下文取」—— 也就是说「CEL 函数从上下文拿数据」这条路您已经认可了。所以选项 A 不是没有先例,只是它要答的那三个问题比can难得多(can读的是已在上下文里的主体,lookup要发起一次新的查询)。席位推荐 B。请裁:A / B / C?
选项
做法 代价 可回退 A 在 buildScope()里接上ctx.api提供的三个函数必须先答同步/异步、N+1、RLS 谓词内 lookup 的权限语义三问;是能力新增 难 —— 一旦发布就是已声明契约 B(推荐) 从 EvalContext删掉这块api声明,删除处写明理由;另立一张「谓词能否跨对象读取」的能力设计卡,携 hotcrm #1915 作拉动证据低;hotcrm 的需求仍然没有答案,但它今天本来就没有 易 —— 重新声明是一次提交 C 按 ADR-0049 第 2 态标 experimental,留着不接低 易 ⛔ 为什么不推荐 C:ADR-0049 确实允许「显式标注为尚未强制」这一态,但本例的害处载体是类型字段本身 —— AI 读到
api?: { lookup?... }就会用它,一个 docblock 上的experimental标注拦不住它,陷阱只是变浅没有消失。且维护者 2026-08-27 已裁「项目在创业阶段,用户也很少,短期不考虑渐进」,C 正是那条裁决排除的渐进态。⛔ 本卡不裁:
exists/count与lookup是否同一个结论。前两个可下推成查询、lookup要取整行,代价不同 —— 若裁 A,三者要分开定。Governing text:维护者 2026-09-10 裁决,逐字:「我们的项目以objectstack 协议为准,文档应该以实际实现为准。协议不正确的应该先修改协议。」⇒ 声明而未兑现是实现缺口,补实现或退役,⛔ 不在消费端收窄。本卡的 A 与 B 正是该裁决给出的两个合法出口;C 是 ADR-0049(docs/adr/0049-no-unenforced-security-properties.md,Accepted 2026-06-15)三态中的第 2 态。ADR-0049 的理由句与本例同形:「A security property that parses but does nothing is worse than absent: it produces a false sense of compliance.」
四棱
① 实际业务需求 —— 有一个具名拉动,但只有一个。 hotcrm #1915(REQ-0003)要在
crm_opportunity的validations[]上读所属客户的分类,施工席逐条量了三条路全部走不通,本卡是第三条。⇒ 跨对象读取在谓词里确有真实场景,⛔ 不是投机能力面。但它是一个场景,且os.exists/os.count至今零消费方 —— 拉动指向的是「读一个关联字段」,⛔ 不是指向声明中的这整块 API。⇒ 该轴支持立卡答能力题,⛔ 不支持照着现有声明整块实现。② 项目长远合理性 —— 明确指向 B。 「声明即强制」是本项目的架构底线,一块零实现的声明是它的反面。B 不预设能力题的答案:删掉一个今天为假的声明,⛔ 不关闭任何未来形态;真要实现时重新声明是一次提交。A 则是在三个未答的架构问题上先付款。
⚠️ 该轴权重恒 ≥50%,此处与③④同向,⛔ 无需权衡。③ 防 AI 写元数据犯错 —— 本卡最尖的一棱,指向 B。 陷阱形态完整:有类型、有字段名、有一句像承诺的说明,失败形态是拒写。 AI 照它写一条校验规则,拿到的不是「函数不可用」,而是该对象上每一次写入全部被拒。这正是框架点名的 (c) 类。⛔ 消费端宽容不适用:这里不存在可以
??回退的消费端,唯一的收紧手段就是让这个声明不存在。⚠️ 反向读数也要如实记:若裁 A,实现后「声明即强制」反而成立,该轴会转为支持 A —— 所以③真正反对的是维持现状,A 与 B 都能消解它,只是 B 今天就消解。④ 创业阶段不扩散 —— 指向 B。 A 是能力扩张,且要先答三个架构问题(同步求值 vs 异步查询、N+1、RLS 谓词内的权限语义),其中第三个是安全/权限边界,属人工地板。⛔ 已声明零消费不因沉没成本获得豁免 ——
os.exists/os.count至今没有任何消费方,lookup的唯一消费方是一个因它不存在而作废的需求。⇒ 按 implementation-first 处置。
读数(本席 2026-09-17T08:49Z 在
origin/main重取,⛔ 非引用卡片两天前的测量)读数 结果 git grep -n "ctx\.api|context\.api" origin/main -- packages/formula/src0 命中 同一仪器的必中对照 ctx.useronstdlib.ts3 ⇒ 仪器活,那个 0 ⛔ 不是哑火 EvalContext.api声明仍在 packages/formula/src/types.ts:70buildScope()实际绑定只有 os.useros.orgos.env(stdlib.ts:322-326)can是否已注册(#18545 的同一函数表)0 ⇒ 两张卡指向同一条缝,⛔ 但互不阻塞 ⇒ 卡片的四条前提今日全部成立。
下游
hotcrm #1915(REQ-0003) 的规格前提已被本卡证伪,正等一个方向。
⚠️ 该单在另一个仓,本席未改它的状态 —— 若裁 A 或 B,它各自需要不同的后续单。
⛔ 本席不代裁:两条出路都落在人工地板上 —— A 是功能新增加安全/权限边界,B 是删除已发布的契约声明。置信门第②条(不在人工地板)不成立,故本卡进决策箱而非代裁通道。已同笔转
needs-user-decision、退出派发队列。
Generated by Claude Code
Ruling: batch #148 item 1 · letter B (remove the never-bound
apideclaration fromEvalContext; the real need — reading a parent record's field inside a predicate — gets its own capability card) · maintainer 「同意」 2026-09-17T14:26ZDirector seat, summon #24,
session_01Wj1HUjzyeiBQ8atRf1ZhaL. Presented in detail with the recommendation B; the maintainer agreed. Facts (this thread; engine seat re-read onorigin/main):packages/formula/src/types.ts:66-75declaresapi?: { exists, count, lookup }with a docblock promising opportunistic implementation;stdlib.tsnever readsctx.api(0 hits, controlctx.user3); a predicate callingos.lookupfaults at runtime and an unevaluable predicate rejects the write (#4649), so a validation rule written to the declaration locks every write on its object; hotcrm #1915 (REQ-0003) designed against it and had to abandon the design;os.exists/os.counthave zero consumers.Ruling — B
- Delete the
apimember fromEvalContext(and theos.exists / os.count / os.lookupdocblock) with the reason written at the deletion site: the harm came from the declaration existing, not from the implementation missing. Any type-level consumer that referencedctx.apiis fixed in the same PR (the dev measures: expected zero outsidepackages/formula). - Published type narrowing ⇒
Clause-②: yes, contract review at tier; changeset@objectstack/formulaminor. - ⛔ A — binds I/O into synchronous predicate evaluation before the sync/async, N+1 and RLS-permission questions are answered; ⛔ C — the type field is the trap carrier, a docblock tag does not stop an AI from using it (and the maintainer's 2026-08-27 ruling excludes the gradual state).
- The need is real and mainstream (Salesforce cross-object formulas read parent fields in validation rules): filed as its own capability card, relationship traversal in predicates (
record.crm_account.type), ⛔ notos.lookupqueries — see the follow-up comment for the number. It is serialised after Registercanin the@objectstack/formulaCEL engine function table — the objectstack half of the ruled cross-repo split forcurrent_user.can(object, verb)(objectui#4421 batch #13) #18545 on the samebuildScope/EvalContextseam.
Four-facet reading: ① a zero-implementation declaration is the reverse of 「声明即强制」; ② the one named pull points at parent-field access, not at query functions; ③ the trap is complete (type, name, promise, failure = write refusal) — B removes it today; ④ A pays three architecture questions up front.
Execution
needs-user-decision→pm:queue,domain:engine, p2 stands,repo:hotcrmstays (the downstream REQ). hotcrm #1915 waits on the capability card, not on this one.
Generated by Claude Code
- Delete the
Capability card filed — director seat · 2026-09-17T14:27Z: the real need behind the removed declaration lives at #18682 (relationship traversal in predicates — one hop through a lookup,
record.crm_account.type;pm:blockedon #18545, sameEvalContextseam,domain:engine, p2). hotcrm #1915 (REQ-0003) waits on #18682, ⛔ not on this card.
Generated by Claude Code
huangyiirene commented
on Sep 17, 2026 CollaboratorMore actionsHeld one position behind #18554 — ruling received, ⛔ not a merit stop
domain:engineexecution seat,session_01CqmCgU5RGDoJYhHUMVp2af, R1, 2026-09-17T14:38Z. ⭐ The ruling landed and matches this seat's recommendation (batch #148 item 1, letter B). The card is dispatchable; it is held only by the formula seam being serialised.Why held, and by what
Three cards now sit in
packages/formula: #18554, this one, and #18545. The #18545 ruling imposes the sequencing itself, verbatim:Serial constraint: #18318 … sits on the same
buildScope/EvalContextseam — its ruling (bind-or-remove) comes in batch #148; the two land in sequence, not in parallel.⇒ This seat extends that to #18554 as well:
ExprSchemaHint.scopeandEvalContextplausibly sharepackages/formula/src/types.ts, and ⛔ no itinerary can be established without a PR.#18554 goes first on the take-order — it carries
pm:blocking(which outranks p2) and it holds up a ruled p2 arm in objectui (objectui#8167 / PR objectui#9643). ⇒ Ordering, ⛔ not merit.What the ruling requires, carried here so it is not re-derived
- Delete the
apimember fromEvalContextand theos.exists / os.count / os.lookupdocblock, with the reason written at the deletion site: ⭐ 「the harm came from the declaration existing, not from the implementation missing」. - Any type-level consumer that referenced
ctx.apiis fixed in the same PR.⚠️ The ruling records the expectation as zero outsidepackages/formula— and explicitly makes it the dev's measurement, ⛔ not an assumption. Clause-②: yes(published type narrowing), contract review at tier, changeset@objectstack/formulaminor.- ⛔ A (bind the API) and ⛔ C (mark it experimental) are both refused, each with a stated reason — C because 「the type field is the trap carrier, a docblock tag does not stop an AI from using it」.
⭐ The real need did NOT vanish with the declaration
The ruling filed the underlying capability separately: #18682 — relationship traversal in predicates, one hop through a lookup (
record.crm_account.type), ⛔ notos.lookup-style queries. It ispm:blockedon #18545, same seam,domain:engine, p2.⚠️ hotcrm #1915 (REQ-0003) waits on #18682, ⛔ not on this card. ⇒ Closing this one does not unblock the downstream requirement, and ⛔ must not be reported as though it does.
Generated by Claude Code
- Delete the
2 remaining items
huangyiirene commented
on Sep 17, 2026 CollaboratorMore actionsClaim: PM loop round 4
Session:session_01CqmCgU5RGDoJYhHUMVp2af
Branch:claude/issue-18318-remove-evalcontext-api
Worktree:objectstack-issue-18318
Domain:domain:engine
Seat:domain:engine#1
File surface:packages/formula/src/(stop on breach; explain in the report)
Container & model:M,mode:subagent,model: opus (default judgement tier)— quoting this dispatch's owndispatch-gates.mjs --tier packages/formula/src/types.ts: 「Model tier — no path-derived mandate … The tier stays the PM's per-card judgment call (floor sonnet · default opus · ceiling fable).」 ⇒ default judgement tier: removing a declared member from a published type is a level judgement, ⛔ not a deletion.
Clause-②: yes
Thread-read: 5716223600
Serial constraints cleared:⭐ THE FORMULA SEAM IS RELEASED AND THIS CARD IS THE CHAIN HEAD. #18554 landed — PR #18696 merged as squash e75cc3c2f8, parent count 1, verified by CONTENT on origin/main. The #18545 ruling fixes the order verbatim (「#18318 … sits on the same buildScope / EvalContext seam … the two land in sequence, not in parallel」), so #18545 stays held behind THIS card and ⛔ must not be taken in parallel. ⚠️ origin carries claude/issue-18545-formula-can-function with ZERO commits ahead of main and ZERO PRs on that head — a probe branch, ⛔ not in-flight work. Other in-flight surfaces at claim time: #18617 (packages/runtime/src/), plus #18616 and #18620 whose devs have reported and whose PRs (#18717, #18712) touch driver-turso and packages/core — all disjoint from packages/formula. Verify lock: FREE, arrival depth 0.⭐ The landing repo is objectstack, and that was established rather than assumed
⚠️ This card carriesrepo:hotcrm, and the seat post standing caution says a card carrying it must have its landing repo established before it is claimed. Done, from the thread:- The ruling's deliverable is 「remove the never-bound
apideclaration fromEvalContext」 — that declaration lives atpackages/formula/src/types.ts, i.e. this repo. - The director seat wrote the hotcrm relationship out explicitly: 「hotcrm fix(automation): record-change flows see multi-lookup fields + array-index interpolation (#1872) #1915 (REQ-0003) waits on formula/objectql: relationship traversal in predicates — a validation rule or visibility predicate reads one hop through a lookup (
record.crm_account.type); replaces the removedos.lookupdeclaration (#18318, batch #148) #18682, ⛔ not on this card.」
⇒
repo:hotcrmhere records where the real-world pull came from, ⛔ not where the fix lands. ⛔ Do not open anything in hotcrm.The ruling — execution, ⛔ not a fresh decision
Maintainer ruling, batch #148 item 1, letter B, 「同意」 at
5716041368: remove the declaration. The competing option (implementos.exists/os.count/os.lookup) was declined, and the three questions that made it hard — sync predicate evaluation against async queries, N+1, and a row-level-security predicate doing a lookup it may have no right to read — are ⛔ not yours to answer here. The real capability need lives at #18682.⇒ ⛔ Do not implement the three functions. ⛔ Do not leave a deprecation shim, an alias, or a 「reserved for later」 comment: this repo retires immediately, with no window and no dual spelling.
⚠️ The sharp question this card turns on — LEVEL, and it is yours to measureRemoving an optional declared member from a published type is the shape a
majorexists to announce: a TypeScript consumer that today writesapi: { … }on anEvalContextstops compiling.⛔ The seat is ⛔ not pre-ruling it, because a pre-ruling from here would bias the measurement. What it owes you instead is the precedent from this same round, so you do not have to invent the reasoning:
PR #18717 (#18616) graded a refusal
minoralthough a previously-succeeding call now throws, on the ground that what changed 「never delivered the semantics it reported」 — depending on it meant depending on silent data loss. The launch-windowmajorban (scripts/check-changeset-no-major.mjs) also applied, but it was recorded as a constraint, ⛔ not the justification.⇒ Ask the same question here and answer it with a reading: can any consumer today set
apiand get anything out of it? The card's own measurement saysctx.apiis read zero times instdlib.ts. Extend that to the whole tree — ⛔ by type, not by text.⚠️ This seat tried a text grep (api: *{) and got 234 files, which is a useless number and is reported here as an instrument failure rather than a population. Derive it properly, with a firing control.⚠️ And check the other direction too: does anything in-repo or in a sibling package construct anEvalContextliteral carryingapi? If yes, that is a compile break you must repair inside your file surface or report as a blocker — ⛔ not discover at CI.Clause-② is
yes, and the gate is already hungneeds:contract-reviewis on the card as of this claim stroke, and the seat hangs it on the PR the moment one exists. ⛔ Do not hang it, unhang it, or wait on it — report its presence and thenode scripts/pm/check-clause2-carriers.mjs --pair PR-NUMBERexit code as readings.⛔
skip-changesetis wrong here:@objectstack/formulapublishes and a declared type member is being removed. Measure it the way your role file prescribes anyway (builtfiles[]grep with a positive control) — ⛔ do not take this line as the measurement.⚠️ Two rules from the CURRENT charter that your loaded role file does not carryMeasured this minute:
node scripts/pm/check-harness-current.mjsexits 1 —.claude/agents/os-dev.md's latest touch011121238dis not in this container's checkout. The seat enumerated the whole delta; two clauses bind you, so they are restated here. ⛔ These are the role file's own current text, ⛔ not the seat overriding it:- Copy the claim's
Clause-②:line to column 0 of the PR body in the opening write.Check Changesetreads the PR body, ⛔ not the card. - Write the PR body exactly once, in the
POST /pullsstroke. ⛔ NeverPATCHit afterwards. If it needs changing later, name the change in your report and the seat makes the edit.
Everything else in
.claude/agents/os-dev.mdgoverns your report field set, write budget and label discipline as loaded; where this dispatch and the role file genuinely differ, the role file wins and you say so in the report.
Generated by Claude Code
- The ruling's deliverable is 「remove the never-bound
huangyiirene commented
on Sep 17, 2026 CollaboratorMore actionsos-dev-report
{ "issue": 18318, "status": "done", "branch": "claude/issue-18318-remove-evalcontext-api", "pr": "https://github.com/objectstack-ai/objectstack/pull/18736", "session": "session_01CqmCgU5RGDoJYhHUMVp2af", "premise_still_valid": true, "summary": "Ruling B executed: the never-bound `api?: { exists, count, lookup }` member is gone from `EvalContext` (packages/formula/src/types.ts), with the reason written at the deletion site inside the interface docblock — no shim, no alias, no reserved-for-later comment, and none of os.exists / os.count / os.lookup implemented. Premise re-measured on the base df1b275c7 and it holds exactly: `ctx.api` reads 0 times in packages/formula/src (control `ctx.user` 3 in stdlib.ts), and the three function names appear exactly once in the whole tree outside CHANGELOGs and dist — the docblock being deleted — so no stdlib row, validator, doc page or skill file had to follow. LEVEL graded `minor` by measurement, not assumption: buildScope() never read ctx.api, so nothing a consumer set there ever reached an evaluation scope, and the whole of the break is a compile error on an EvalContext literal carrying `api`, of which the tree and the pinned sibling hold zero; same shape as PR #18717's grading, with the launch-window major ban recorded as a constraint rather than the justification. File surface held at packages/formula/src/ plus the changeset — no breach; the one `ctx.api` mention outside it (content/docs/data-modeling/formulas.mdx:415) was read and is the LIVE hook ScopedContext api, an unrelated surface this PR does not touch, which is also why a text grep for `api` is worthless here.", "tests": "① packages/formula build + `pnpm --filter @objectstack/formula test` + `typecheck`, under scripts/pm/os-verify-lock.sh (VERDICT command-exit 0, held 147s): 31 test files / 876 tests pass (30 -> 31 files, the new pin file ran); `tsc --noEmit` clean; check:test-typecheck reports the SAME ledger as before — 3 files / 7 errors / 4 signatures — which is the live proof the new `@ts-expect-error` is USED (an unused directive is TS2578 against an exact, shrink-only ledger). New pin src/eval-context-no-query-api.test.ts: one type-level pin (api rejected in an EvalContext literal) and two runtime pins (buildScope binds nothing from a context still carrying api; a predicate calling os.lookup is refused even when an implementation is supplied). ② CONSUMER SWEEP BY TYPE — population 19 workspace packages declaring @objectstack/formula, every one carrying a typecheck script (no zero-script filter). Measurement leg at d706b60b57 after `turbo run build` (73 tasks successful, so every package resolves formula through its rebuilt dist/index.d.ts): 19/19 typecheck green, `error TS` count 0 (VERDICT command-exit 0, held 791s). ③ FIRING CONTROL on the same 19-package run: a REQUIRED member `__ablationProbeFiringControl: 'fire'` injected into EvalContext, on-disk proof first (marker grep count 1, git diff --stat non-empty), then `pnpm --filter @objectstack/formula build`, then `node scripts/ablation-dist-preflight.mjs @objectstack/formula '__ablationProbeFiringControl'` green (marker present in 2 built files) BEFORE any colour was read; result 12 of 19 packages RED, 21 distinct error TS2345 supply sites in 13 files plus 2 more files caught by the test-typecheck ledger gate (platform-objects, plugin-auth) — e.g. metadata-protocol/src/seed-loader.ts:1026, objectql/src/validation/rule-validator.ts x6, service-automation/src/engine.ts, plugin-approvals/src/approval-service.ts. Restore leg: `git checkout HEAD -- packages/formula/src/types.ts`, hash-object == HEAD blob 27f991bbd7553cff9465c4774f3db3acd6c594d6, whole-tree `git status --porcelain` EMPTY, formula rebuilt, preflight `--absent` green; a trap on EXIT/INT/TERM carried the same restore. So the green in ② is a measurement, not a silence, and no mutated byte survives in dist/. ④ Pinned sibling (Console Pin Gate): objectui at .objectui-sha 53ded82bf7a494f54e344e19099dbf00854b8694 names EvalContext ZERO times; control in the same tree, four distinct `from '@objectstack/formula'` import lines (ExpressionEngine, validateExpression, collectCelRootIdentifiers, firstUndeclaredReference) across packages/app-shell. No sibling fix and no pin bump owed. ⑤ GATES: `scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derived 57 families from the 3-path change set; all 57 run at branch head 75ca0e2749, every one exit 0; `--ran` with exit codes recorded reconciles 57 derived / 57 run / 0 NOT-MEASURED / 0 UNRUN. ⑥ LINT, declared narrowing with its three readings: population from eslint's own config 6819 files (git ls-files filtered to lintable extensions, each asked through ESLint#isPathIgnored; 0 ignored), narrowed run linted 2 files (--format json result length) with 0 errors / 0 warnings exit 0, and invariance — the config enables no type-aware linting (no parserOptions.project, no projectService, no typed rules), so this diff cannot move any untouched file's verdict. ⑦ skip-changeset measured with a positive control: formula's files[] is dist + README.md + CHANGELOG.md, and after build `grep -c \"api?:\" packages/formula/dist/index.d.ts` = 0 while the control `grep -c EvalContext` = 10 in that same published file — published bytes move, the label would be wrong, the changeset is present and grades `minor` with its FROM -> TO migration line. NOT MEASURED: CI on the PR (open as draft, checks not read — per role file the report does not wait on CI); `pnpm lint` repo-wide (CI's run; the narrowing above is declared). ⑧ Clause-② carriers: `node scripts/pm/check-clause2-carriers.mjs --pair 18736` exits 4 — row C1: `needs:contract-review` sits on card #18318 while PR #18736 does not carry it (PR labels at report time: none). That is the expected half-hung state at this instant — the seat hangs the PR side, and the dev neither hangs, unhangs nor waits on it. Its run record: board resolved by default, 4 token reads, pair derived by closing keyword from the body line `Fixes #18318`, head-sha 75ca0e2749e3b87dd7f74f1b254b782b5c9023d1, governing claim comment 5718014345, card claim Clause-② DECLARED yes, PR body Clause-② DECLARED yes. ⑨ PR body read back after the single write: byte-identical to what was sent except the platform stripped the trailing newline (8347 -> 8346 bytes), first line `Fixes #18318`, `Clause-②: yes` at column 0, exactly one attribution footer in the session-URL form under an intact rule line. This report comment was likewise read back after posting.", "mcp_calls": "0 — no MCP GitHub tool was called at all; every GitHub read and write went through the REST proxy with curl. One non-GitHub session tool was used: mcp__Claude_Code_Remote__add_repo (read) to reach the sibling objectui checkout for the Console Pin Gate reading.", "api_writes": "2 REST writes — POST /repos/objectstack-ai/objectstack/pulls (draft PR #18736, body written once in that stroke and never PATCHed) and POST /repos/objectstack-ai/objectstack/issues/18318/comments (this report). Plus 3 `git push` strokes on the branch (empty-branch routing probe, implementation commit, changeset commit). NO label write: `needs:contract-review` is the seat's and was neither hung nor removed, `skip-changeset` was measured wrong, and nothing else is owed by the dev on this PR — recent dev PRs (#18717, #18712, #18696) carry only bot-applied labels.", "open_questions": [], "out_of_scope_findings": [ "noted, not filed: eslint.config.mjs's own header states 'across all 4659 linted files' while the population reads 6819 today (ESLint#isPathIgnored over git ls-files). A stale prose count in a config header — not a reproducible defect, not a declared-contract violation, not a metadata-authoring trap. Carrier: the next PR that edits that header." ] }
Generated by Claude Code
huangyiirene commented
on Sep 17, 2026 CollaboratorMore actionsSeat finding on PR #18736 — one patch round owed before the contract review can PASS. ⛔ Not a FAIL: the work is right, the changeset under-declares. Written 2026-09-17T17:39Z.
domain:engineexecution seat,session_01CqmCgU5RGDoJYhHUMVp2af, R4. Report:5718655228.needs:contract-reviewis now hung on both carriers (--pair 18736exit 0; the C1 half-hung state the dev reported was the expected one at report time — the PR side is the seat's stroke).The gap
The changeset grades
@objectstack/formula: minorand carries no**BREAKING**banner. The level is right and this is ⛔ not a request to raise it. What is missing is the banner, and the repo says why in its own words —.github/workflows/pr-automation.yml:723-724:During the launch window
majorstays refused bycheck-changeset-no-majorand breaking-ness is carried by the BREAKING banner plus the ADR-0087 disposition, not by the level.⇒ A level forced to
minoris ⛔ not the same statement as 「this change is not breaking」. The banner is where breaking-ness lives when the level cannot carry it.⭐ And this exact class already has a category, built because of this exact erosion
scripts/check-adr-0087-registration.mjs:2485+documentstype-surface-only(#13080), verbatim from its own header:a published type-surface narrowing … declares
**BREAKING**truthfully … It then finds every disposition above closed to it … and the move that remains is to DROP the**BREAKING**token. Four changesets took it (#8140, #11925, #12034, #12104). That is the erosion #13080 was filed about: a whole recurring class of genuinely compile-breaking change nudged away from this repo's breaking-change marker.⇒ Landing as it stands would be the fifth instance of the erosion that category was built to end — ⛔ not a fresh judgement call.
⭐ The dev's own measurements already satisfy the category's predicates:
@objectstack/formulapublishes;grep -c "api?:" dist/index.d.ts= 0 after, against a control ofEvalContext= 10 in that same published file, so published bytes really move; and the break is confined to the type surface becausebuildScope()never readctx.api. Its FROM → TO migration line is also exactly whyno-migration-prescriptionis unavailable — consistent with the category it wants.⛔ Part of this is the seat's fault, and it is recorded rather than passed on
The dispatch order handed the dev PR #18717's
minorreasoning as the precedent to weigh against.⚠️ That precedent under-determines this case: #18717 added a refusal and removed nothing from a published type surface, so the banner question never arose there. The dev applied the precedent it was given, correctly, to a case the precedent did not cover. ⇒ The gap is in the order, ⛔ not in the dev's judgement, and the patch round says so.The ask, and only this
In
.changeset/18318-evalcontext-no-query-api.mdalone: keepminor, add**BREAKING**, add exactly oneadr-0087:disposition in thetype-surface-onlycategory naming the member ref. ⛔ No code, no tests, no other file. Exit codes forcheck-adr-0087-registration.mjs(and its--self-test) andcheck-changeset-no-major.mjsto be reported, so the banner is proven not to trip the major ban.Two operational questions the dev raised, answered
- The
refactor(formula)!:commit subject against aminorchangeset — leave it as pushed. Verified: the BREAKING detectors read the changeset, ⛔ not the commit log. Force-rewriting a pushed branch for a cosmetic marker costs more than the mismatch, and once the banner lands the!is simply honest. - The shallow objectui clone left at
/home/user/objectstack-ai/objectui— fine, and deliberately left for a concurrent turn. ⛔ No action.
⏹️ The rest of the round is strong and is ⛔ not re-opened by this: the consumer sweep by TYPE over 19 packages, 19/19 green, with a firing control that injects a required member and turns 12 of 19 RED across 21 distinct supply sites — ⭐ that is what makes the green a measurement rather than a silence, and it is exactly what the dispatch asked for after the seat's own text grep (
api: *{, 234 files) failed as an instrument.
Generated by Claude Code
- The
huangyiirene commented
on Sep 17, 2026 CollaboratorMore actionsContract review
Served-tier:
CONTRACT_REVIEW_TIER
Head-sha:2b053aebd7ba4bd24fe469c91790308d31333b28domain:engineexecution seat,session_01CqmCgU5RGDoJYhHUMVp2af, in-seat review of the clause-② it dispatched. Written 2026-09-17T17:46Z. PR #18736, after the patch round the seat asked for. ⛔ Every reading is command output taken by this seat, ⛔ not the report's account of it.Fuse. Served tier read from
get_session→external_metadata.last_served_model, ⛔ notsession_context.model.dispatch-gates.mjs --tier packages/formula/src/types.ts: no path-derived mandate ⇒ the bar is the default judgement tier and the reading meets it.① Derived judgments
-
A declared member is removed from a published type —
EvalContext.api?: { exists, count, lookup }(packages/formula/src/types.ts). That is the deliverable of maintainer ruling B (5716041368), executed as written: ⛔ no shim, ⛔ no alias, ⛔ no reserved spelling, and the three functions ⛔ not implemented. The reason lives at the deletion site. Right. -
⭐ Nothing a consumer ever set there reached an evaluation scope.
buildScope()never readctx.api— 0 reads in the package against a firing control of 3ctx.userreads instdlib.ts. ⇒ No runtime result moves for anyone, however completely a caller populated it. Right, and it is what makes the level judgement below possible at all. -
⭐⭐ The consumer sweep is a MEASUREMENT, not a silence — and this is the round's best work. Population derived by TYPE: 19 workspace packages declaring
@objectstack/formula, every one carrying a typecheck script (⛔ no zero-script filter quietly shrinking the population), all re-measured after a fullturbo run buildso each resolves formula through its rebuiltdist/index.d.ts. Result 19/19 green,error TScount 0.
⇒ Then the control: a required member injected intoEvalContext, proved on disk and proved to reach the artifact (ablation-dist-preflight.mjsgreen) before any colour was read — 12 of 19 packages RED, 21 distinct supply sites. Restore hash-verified against the HEAD blob with a whole-treegit status --porcelainempty andpreflight --absentgreen.
⭐ This is the direct answer to the dispatch's own admission that the seat's text grep (api: *{→ 234 files) was an instrument failure, ⛔ not a population. Right. -
The pinned sibling was checked rather than assumed. objectui at
.objectui-sha 53ded82bf7namesEvalContextzero times, against a control of four distinctfrom '@objectstack/formula'import lines in the same tree. ⇒ ⛔ No sibling fix and ⛔ no pin bump owed. Right — and the control is what stops that zero being a mis-typed path. -
The new pin bites, and that is shown rather than claimed.
eval-context-no-query-api.test.tsadds a type-level pin plus two runtime pins. ⭐ Its@ts-expect-erroris proven used bycheck:test-typecheckreporting the SAME shrink-only ledger as before (3 files / 7 errors / 4 signatures) — an unused directive would beTS2578against an exact ledger. ⇒ A type-level assertion that cannot silently stop asserting.
⭐ The structural point: the removal is safe not because the member looked unused, but because the one function that would have had to read it never did — and the sweep that says so was shown capable of going red.
② Semver level
minoron@objectstack/formula, plus a**BREAKING**banner and an ADR-0087 disposition. All three together, and each is load-bearing:minoris right and is forced.majoris refused outright during the launch window, and the reason it is also correct is that no behaviour moves: the whole of the break is a compile error on anEvalContextliteral carryingapi, of which the tree and the pinned sibling hold zero.- The banner was this seat's finding on the first head, and the reasoning is the repo's own (
pr-automation.yml:723-724): whilemajoris refused, 「breaking-ness is carried by the BREAKING banner plus the ADR-0087 disposition, not by the level」. ⇒ A forcedminoris ⛔ not the statement 「this change is not breaking」. Landing without it would have been the fifth instance of the erosiontype-surface-only([finding] check-adr-0087-registration has no honest disposition for a published TYPE-surface narrowing — every category is refused, so the class is pushed to drop the**BREAKING**token instead #13080) was built to end.
⚠️ ⛔ And the seat's suggested CATEGORY was wrong — recorded, because the record is right only because the dev refused it. The patch order namedtype-surface-only; the dev derivedruntime-interface-onlyinstead and wrote its own justification. The gate settles it, and the seat ran it rather than arguing:✓ check-adr-0087-registration: 1 declared-breaking changeset(s), each carrying an ADR-0087 disposition. [BREAKING] not-required (runtime-interface-only) -- verified: packages/formula/src/types.ts#EvalContext (interface)exit 0, with
--self-testalso exit 0 (384 assertions). ⇒ The seat reasoned from a docblock narrative about the #12104 shape — a published SDK method's return moving offany— which is a different shape from an interface member deletion. ⭐ The patch order said 「⛔ do not take my spelling on trust; derive it from the gate and prove it」, and that instruction is the only reason this changeset carries a category the gate can verify instead of one the seat guessed.check-changeset-no-major.mjsexit 0 ⇒ the banner did ⛔ not trip the major ban.- ⛔
skip-changesetwould be wrong, and the dev measured it with a positive control: after build,api?:indist/index.d.ts= 0 againstEvalContext= 10 in that same published file ⇒ published bytes move. check-clause2-carriers.mjs --pair 18736exit 0, both carriers agree.
③ Boundary flags
open_questionswas empty. One out-of-scope finding and two operational questions, all answered.F1 —
eslint.config.mjs's header says 「across all 4659 linted files」 while the population reads 6819 today. Agreed classification: a stale prose count in a config header, ⛔ not a reproducible defect, a declared-contract violation, or a metadata-authoring trap. Carrier as named — the next PR that edits that header. ⛔ Not filed, correctly.Q1 — the
refactor(formula)!:commit subject against aminorchangeset. Ruled on the card: leave it as pushed. Verified that the BREAKING detectors read the changeset, ⛔ not the commit log, so nothing downstream misreads it; and force-rewriting a pushed branch for a cosmetic marker costs more than the mismatch. ⭐ With the banner now in place the!is simply true.Q2 — the shallow objectui clone left at
/home/user/objectstack-ai/objectui. Left deliberately for a concurrent turn. ⛔ No action.⏹️ One fact recorded rather than judged: the dev used
add_repoto reach the pinned objectui checkout for flag (4). That is a read, it is declared in its ownmcp_callsline as the single non-GitHub session tool used, and itsmcp_callsfor GitHub is 0. ⇒ Consistent with the budget, and ⛔ not a finding.Implemented-by:
claude/issue-18318-remove-evalcontext-api
Reviewed-by:session_01CqmCgU5RGDoJYhHUMVp2af⇒
mode:subagent, so the pair compares a branch against a session and is ⛔ not SELF-REVIEW by the charter's reading: in-seat review of a subagent's diff is the design.VERDICT: PASS
⏹️ ⛔ Landing is NOT cleared by this record alone. ① is now in case; ② will be re-read at the clearing stroke; ③ is not met — 13 checks are still running on this head. The carriers stay hung until all three are in case.
Generated by Claude Code
-
huangyiirene commented
on Sep 17, 2026 CollaboratorMore actionsos-dev-report
{ "issue": 18318, "status": "done", "branch": "claude/issue-18318-remove-evalcontext-api", "pr": "https://github.com/objectstack-ai/objectstack/pull/18736", "session": "session_01CqmCgU5RGDoJYhHUMVp2af", "premise_still_valid": true, "summary": "PATCH ROUND on the contract-review finding: the changeset now carries the `**BREAKING**` banner and exactly one ADR-0087 disposition, and the level stays `minor` (head 2b053aebd7, `.changeset/18318-evalcontext-no-query-api.md` only — no code, no test, no other file). The prescribed category was NOT usable and that was measured rather than argued: `type-surface-only packages/formula/src/types.ts#EvalContext.api` is refused because a dotted member path is walked only through an object literal or a class body, never through an interface; naming the interface instead, `type-surface-only packages/formula/src/types.ts#EvalContext`, is refused by predicate 4 with the gate's own words — at the merge base the interface `was already CONCRETE … not any / unknown / unannotated`, the shape predicate 4 exists to refuse — and its printed fix is 'register the migration'. The disposition that IS honest here is the fifth category, `runtime-interface-only`, which the gate verifies positively at `packages/formula/src/types.ts#EvalContext (interface)`: a published runtime TypeScript interface with no Zod schema, no `packages/spec` declaration and no stored representation, so `objectstack migrate meta` has nothing to rewrite and the compiler at the consumer's call site is the channel — the #8277 shape the category was built from (a published runtime interface that LOST a member), and distinguishable from the #6048 founding case, which was a RENAME shipping a re-pointing prescription while this removal has nothing to re-point. ⚠️ Stated so the seat can weigh it rather than inherit it: `runtime-interface-only` also inherits the no-migration-prescription refusal and the detector did not fire on this body's `api: { … }` -> delete the property line; the claim does not rest on that silence — all four of the category's predicates were verified positively — but the miss is the same one this gate's own header records against #8277's changeset.", "tests": "Gate exit codes, each captured before any pipe, at head 2b053aebd7: `node scripts/check-adr-0087-registration.mjs --self-test` = 0 (384 assertions over real temp git repos); `node scripts/check-adr-0087-registration.mjs --base origin/main` = 0 — '1 declared-breaking changeset(s), each carrying an ADR-0087 disposition … not-required (runtime-interface-only) -- verified: packages/formula/src/types.ts#EvalContext (interface)'; `node scripts/check-changeset-no-major.mjs --self-test` = 0; `node scripts/check-changeset-no-major.mjs --base origin/main` = 0 — 'This diff introduces no `major` bump', so the banner did not trip the launch-window ban. The level axis is NOT APPLICABLE to a local run (no pull_request payload), so it was driven offline with the real payload: `--event` built from GET /pulls/18736 gives exit 0 and '✓ LEVEL AXIS: this PR declares clause-② `yes`, and no package whose packages/**/src/** it moves is graded `patch`', carrier line '`needs:contract-review` IS on this PR', arm 'none declared'. The probe sequence that produced the two refusals is reproducible from the changeset alone (the gate reads COMMITTED changeset rows, so an uncommitted edit reads as 'no declared-breaking changeset' — that is how the first probe came back 0 before the banner was committed). `node scripts/pm/check-clause2-carriers.mjs --pair 18736` now exits 0 ('both carriers agree') where it exited 4 in the first report — the seat has since hung `needs:contract-review` on the PR. Full re-run of the derived gate families at the patched head: 57 derived, 57 run, all exit 0, `--ran` reconciliation 0 NOT-MEASURED / 0 UNRUN. ⚠️ First pass of that re-run reported 25 non-zero and every one was `PREREQUISITE NOT MET` in a freshly re-created worktree (no `node_modules`, no `dist/`) — read as NOT MEASURED, not as red, then re-measured after `pnpm install` and a full `turbo run build` (exit 0) under the verify lock (VERDICT command-exit 0, held 438s); all 25 then exited 0. File read back from the pushed remote ref and byte-identical to the worktree copy; tree clean.", "mcp_calls": "0 — no MCP GitHub tool was called in this round either; the PR payload for the offline level-axis run came from a REST GET with curl.", "api_writes": "1 REST write this round — POST /repos/objectstack-ai/objectstack/issues/18318/comments (this patch-round report). Plus 1 `git push` of commit 2b053aebd7 to the same branch. The PR body was NOT patched: the two facts it would now state — the banner and the disposition — are named here for the seat to edit in if it wants them on the PR.", "open_questions": [], "out_of_scope_findings": [ "noted, not filed: eslint.config.mjs's own header states 'across all 4659 linted files' while the population reads 6819 today. A stale prose count in a config header — not a defect, a contract violation or an authoring trap. Carrier: the next PR that edits that header.", "noted, not filed: `check-adr-0087-registration`'s refusal text for `type-surface-only` at predicate 4 prints 'fix: register the migration' without naming `runtime-interface-only`, which is the category that actually fits a member removed from a concrete runtime interface with no metadata surface. It cost one probe cycle here. An improvement to a refusal message, not a defect in the gate's verdict — the verdict was right both times. Carrier: whoever next edits that message." ] }
Generated by Claude Code
huangyiirene commented
on Sep 17, 2026 CollaboratorMore actionsAddendum to the contract review above — the patch round's own hand-back, weighed rather than absorbed. ⛔ The PASS stands; nothing below reopens it. Written 2026-09-17T17:59Z.
The dev did not stop at 「the gate said yes」. It handed up a residual it explicitly declined to rest its claim on:
⚠️ Stated so you can weigh it rather than inherit it:runtime-interface-onlyalso inherits the no-migration-prescription refusal and the detector did not fire on this body'sapi: { … }→ delete the property line; the claim does not rest on that silence — all four of the category's predicates were verified positively — but the miss is the same one this gate's own header records against #8277's changeset.Verified by the seat, ⛔ not taken on trust.
check-adr-0087-registration.mjs:204-214records exactly that, in its own words: on #8277's real changesetfindMigrationPrescriptionreturnsnullalthough the body does prescribe, 「So the exemption was held by a DETECTOR MISS, not by a positive finding.」 And this round's changeset carries 「Migration —api: { … }→ delete the property.」 — a prescription with the arrow the header says #8277's lacked — while the gate still returned exit 0.⇒ ⭐ This round is sound for the reason the dev gives, and that reason is load-bearing: the exemption rests on four positively verified predicates and the gate's own
-- verified:line, ⛔ not on the silence. Had it rested on the silence, this review would not have passed it.⏹️ Filed as #18745, bare (
findingonly), because the gap has no open carrier: #8299 isclosed/completedand it closed the category gap — it createdruntime-interface-only— ⛔ not the detector gap, which appears in that work only as the context that motivated it. Both readings taken onorigin/mainthis act.⭐ Worth naming as behaviour, not just as a finding: a dev that had wanted an easy pass would have let the detector's silence carry the exemption and said nothing. This one measured the silence, named it as silence, proved its claim another way, and handed the weighing up to the seat. ⇒ That is the whole point of the
open_questions/ out-of-scope channel, and it is the second time this round the same dev has done it (the first being the two refused categories, measured rather than argued).⛔ No change is owed to PR #18736, its changeset, or its body.
Generated by Claude Code
huangyiirene commented
on Sep 17, 2026 CollaboratorMore actionsLanding reconciliation — PR #18736 is MERGED; this card is closed out. Written 2026-09-17T19:12Z.
domain:engineexecution seat,session_01CqmCgU5RGDoJYhHUMVp2af, R4 close. ⛔ Every criterion is command output, ⛔ not the merge event's echo.criterion reading squash commit 9be2b594af00a890f93f74b7e6a241db9cfa550e, merged 2026-09-17T19:09:53Zparent count 1 ⇒ genuinely a squash api?:inpackages/formula/src/types.ts0 — the removal landed ⭐ firing control, same file, same instrument EvalContext3 ⇒ the zero above is a reading, ⛔ not a dead grep on a path that movedthe new pin file present the changeset's **BREAKING**bannerpresent on mainlabel residue pm:dispatchedstripped, read back; bothneeds:contract-reviewcarriers were cleared pre-landingWhat landed, and the two things worth carrying forward
EvalContext.api— the never-bound kernel query API behindos.exists/os.count/os.lookup— is gone, per maintainer ruling B. ⛔ No shim, ⛔ no alias, ⛔ no reserved spelling.⭐ The consumer sweep was a measurement, not a silence. 19 packages by TYPE, 19/19 green — and a control that injects a required member turns 12 of 19 RED across 21 supply sites, with the mutation proved to reach the built artifact before any colour was read. ⇒ That control is the whole reason the green means anything, and it exists because the dispatch admitted its own text grep (
api: *{→ 234 files) was an instrument failure and demanded a by-type sweep instead.⚠️ The declaration half of the round is a seat lesson, ⛔ not a dev one. The changeset first shippedminorwith no**BREAKING**banner. The level was right and forced; the banner was missing, andpr-automation.yml:723-724is explicit that while the launch window refusesmajor, breaking-ness is carried by the banner plus the ADR-0087 disposition, ⛔ not by the level. Landing without it would have been the fifth instance of the erosiontype-surface-only(#13080) was built to end. ⇒ The seat caught it — and then named the wrong category in its patch order, which the dev refused and replaced withruntime-interface-only, positively verified by the gate. ⭐ The record is right because the order also said 「⛔ do not take my spelling on trust」.⏹️ One observation was filed out of this round and is ⛔ not residue of the repair: #18745 — the
no-migration-prescriptiondetector missed a FROM → TO line again. ⛔ It does not touch this PR's verdict, whose exemption rests on four positively verified predicates rather than on that silence.⭐ The serial seam this releases
packages/formulawas held to one card at a time and this was the one. ⇒ #18545 (ruled A) is now the chain head, in the order its own ruling fixes verbatim: 「#18318 … sits on the samebuildScope/EvalContextseam … the two land in sequence, not in parallel.」⏹️ Residue on this card: zero.
Generated by Claude Code
- added 4 commits that reference this issue
on Sep 28, 2026
读数
packages/formula/src/types.ts:66-75在EvalContext上声明了一块 kernel API,并用文档注释点名了它支持的三个 CEL 函数:而
packages/formula/src/stdlib.ts:309-326里构造os作用域的那段,只读三个字段:ctx.api在整个stdlib.ts里一次都没有被读取:⇒ 三个函数一个都没绑。 调用方即使老老实实传了
api,作用域里也不会出现它们。为什么这是 (c) 类而不只是一个未实现的可选项
os.lookup(...)在谓词里就能用。实际写下去拿到的是运行时错误:
而且这个错误不是惰性的。 下游实测:一条不可求值的谓词会拒绝这次写入(objectstack#4649 的形状)。⇒ 照着这块声明写一条校验规则,结果是该对象上的每一次写入、插入更新全部被拒。
「声明存在、实现为零、失败形态是拒写」——这三条凑齐,就是让作者写出运行时静默拒收的元数据的那个陷阱。
下游实例(本条是怎么被发现的)
hotcrm #1915(REQ-0003)的需求记录第 128 行把一道能力闸钉在
crm_opportunity的validations[]上,要它读所属客户的分类。施工席逐条量了三条路,全部走不通:checkPredicate()以{ record: merged, previous }求值 —— 都是本对象的普通数据,不传api。record.crm_account.type→runtime: No such key: type。os.lookup("crm_account", …)也不行 —— 就是本卡这条。⇒ 那张卡的规格前提被证伪,闸门无法按记录指定的构造写出来。一个平台侧的死声明,直接让一条下游需求的落地方案作废。
⛔ 本卡不预判的事
⛔ 不预判该实现还是该删。两条都成立,取舍要量:
buildScope()里绑上ctx.api提供的三个函数。api从EvalContext上移除,并在移除处写明理由。⛔ 也不预判
exists/count与lookup是否同一个结论 —— 三个的代价不一样(前两个可以下推成查询,lookup是取整行),要分开量。查重词
EvalContext api lookup · buildScope os scope · os.exists os.count · declared but never bound · no matching overload dyn.lookup
Generated by Claude Code