Repository navigation
Two open PRs narrow the same field-rule triad from one base: decide which owns it, and whether the surface gets one ADR-0087 id or two #19003
Description
维护者速读
同一个"表单字段什么时候必填、什么时候只读、什么时候显示"的规则,现在有两位项目经理各带一个 PR 在改,两个都还没合,都从同一个基线出发,改到了 12 个相同的文件。两边跑出来的结果一模一样,写法不一样:一边给这三个槽位换了个共用的类型名,另一边把 36 个槽位逐个绑到已有的类型上。谁先合,另一边就得手工拆冲突,而且迁移台账会为同一块面记两条不同的账。
放着不管的代价不是"晚一点",是两个都动不了:谁先动都让另一边变成红的,而两边各自都已经通过了复核。
- A:窄的那个 PR(spec: hold a predicate to what the engine can run; declare its fault semantics (ADR-0136) #18985)放弃这三个槽位的改动,只留它独有的那份规则记录与工具修复;宽的那个(feat(spec)!: every engine-evaluated expression slot requires a non-blank
source#18638)独家负责所有槽位收紧。 - B:窄的先合,宽的重新对齐并删掉重复的三行,台账记两条互相引用。
- C:两个合成一个 PR。
本席推荐 A。请回一个字母:A / B / C。
- ① 项目长远合理性:A 缩小特例。实测复核确认,在 spec: hold a predicate to what the engine can run; declare its fault semantics (ADR-0136) #18985 之前
Predicate*这两个别名的组合者数量为 0 —— 它是个死符号,spec: hold a predicate to what the engine can run; declare its fault semantics (ADR-0136) #18985 把它复活成载体,于是平台里就有了两个名字(PredicateInputSchema与EvaluatedExpressionInputSchema)表达同一件事"这个表达式真的会被执行"。两年后一个健康的平台在这件事上应当只有一个可组合的类型名,主流建模同向(Salesforce 公式字段、Airtable、Retool 都收敛到单一"已校验表达式"类型)。A 让收紧只落在既有的EvaluatedExpression*上,别名不复活。 - ② 实际业务拉动:A 拉动最大。今天撞上的人是写元数据的作者 —— 写了空的或只有
ast的谓词,门禁静默变成"没有门禁"。feat(spec)!: every engine-evaluated expression slot requires a non-blanksource#18638 覆盖 36 个位置,spec: hold a predicate to what the engine can run; declare its fault semantics (ADR-0136) #18985 覆盖 3 个;同样一次落地,A 让更多作者今天就被响亮拒绝。 - ③ 防 AI 犯错:A 更闭合。两个同义类型名正是自由结构陷阱 —— AI 作者在两个都合法的名字之间猜,而两者语义相同、强度相同,猜错没有任何信号。A 把可选项收敛成一个,枚举闭合。
- ④ 创业阶段不扩散:A 最省。
Predicate与PredicateInput是公开导出的类型,一旦以收紧后的形态发布,就是永久义务 —— 每个已声明的键都是永久义务,而这一个的组合者是零。A 不新增这份义务;B 与 C 都把它固化。
Prior rulings read: evaluated,predicate,expression,narrowing,alias → 29 hits; ADR-0058 D7, ADR-0087 D3, ADR-0089
Background
Two open draft PRs, on the same base d8b12fca97c89449445200039bc3c2d6be7820bf, both domain:spec, both carrying needs:contract-review, narrow the same FieldSchema field-rule triad:
| PR #18985 (card #17778, this seat) | PR #18638 (card #15811, os-litant) | |
|---|---|---|
| ruling | batch #119 item 3 (objectui#8069) | batch #122 item 2 |
| ruled on | 2026-09-12 | 2026-09-12, six days before #17778 was filed against it |
| mechanism | rebinds the aliases PredicateSchema / PredicateInputSchema to the evaluated schema; the triad composes them |
binds 36 slots directly to EvaluatedExpressionInputSchema; aliases left wide "deliberately" |
| scope | 3 triad slots + fault-semantics record (ADR-0136), ADR-0089 addendum, ADR-0058 D7 roster entry, type-check DEBT producer fix | 36 evaluated positions incl. gate slots, settings visibility, metrics / tracing |
| files | 22 | 59 |
| ADR-0087 entry | field-rule-predicate-evaluated-slot-source-required |
evaluated-expression-slots-source-required |
12 paths overlap, measured: packages/spec/src/shared/expression.zod.ts, packages/spec/src/data/field.zod.ts, packages/spec/src/migrations/registry.ts, six packages/spec/api-surface-declarations/*.txt, and three content/docs/references/**.mdx. Textual conflict is certain on the triad lines, the cel / expression declarations and the step-18 migration insert. The runtime outcome for the triad is identical under either.
Governing text
- Card spec: the evaluated-slot rule of #15430 reaches only the flow-node ledger — every other
ExpressionInputSchemaslot an engine evaluates (formulaexpression, validation / hook / sharingcondition,visibleWhen…) still accepts anast-only or blank-sourceenvelope #15811, comment5644350409(os-tesla, batch 🔗 Broken links detected in documentation #122 item 2), item 1 verbatim: 「EvaluatedExpressionSchema… composes into every evaluated slot the census found …: formulaexpression; field / option / grid-columnvisibleWhen/readonlyWhen/requiredWhen; … ⛔ By identity on all 36 positions」; item 2: 「ExpressionSchema/ExpressionInputSchema(persistence) keepsourceORast.」 It does not namePredicateInputSchema, so no ruling constrains that symbol either way. - Card spec/ADR-0089: a form field-rule predicate that faults refuses the submit loudly; visibility stays fail-open at render; a blank predicate is refused at authoring — fault semantics become part of the contract (objectui#8069 ruling A) #17778 body, batch 🔗 Broken links detected in documentation #119 item 3, rules the FAULT SEMANTICS (submit refusal, render fail-open, blank as a declared third state) — not which symbol carries the narrowing.
ADR-0087D3 governs the migration ledger: one surface, one notified id. Two ids for one surface is the defect this decision avoids.- Searched
evaluated,predicate,expression,narrowing,aliasoverdocs/adr,AGENTS.md,packages/spec/srcatorigin/main f347c793e: 29 ADR decision candidates, top tier ADR-0058 D6/D7/D8. No prior ruling picks between an alias-level and a slot-level binding. That is why this is a card and not a seat call.
Protocol declaration
No protocol change either way — both PRs already declare the same accept-set narrowing (minor + BREAKING, ADR-0087 D3 entry). What changes is which symbols are publicly narrowed: under A, Predicate / PredicateInput stay wide and are never published in a narrowed form; under B and C they are published narrowed and become a permanent obligation.
Premises, with re-check commands
# both PRs still on one base (expect the same base sha, d8b12fca97...)
for n in 18638 18985; do curl -sS -H "Authorization: Bearer $GITHUB_TOKEN" \
"https://api.github.com/repos/objectstack-ai/objectstack/pulls/$n" \
| node -e 'let d="";process.stdin.on("data",c=>d+=c).on("end",()=>{const p=JSON.parse(d);console.log(p.number,p.base.sha,p.head.sha,p.draft);});'; done
# the 12-path overlap (expect 12)
for n in 18638 18985; do curl -sS -H "Authorization: Bearer $GITHUB_TOKEN" \
"https://api.github.com/repos/objectstack-ai/objectstack/pulls/$n/files?per_page=100" \
| node -e 'let d="";process.stdin.on("data",c=>d+=c).on("end",()=>{JSON.parse(d).forEach(f=>console.log(f.filename));});' \
| sort > /tmp/f-$n.txt; done
comm -12 /tmp/f-18638.txt /tmp/f-18985.txt | wc -l
# the load-bearing axis-1 reading: the aliases had ZERO slot composers before #18985
git fetch origin main && git grep -nE "^\s*[a-zA-Z]+\s*:\s*Predicate(Input)?Schema" \
origin/main -- 'packages/spec/src/**' ':!*.test.ts' | wc -l # expect 0
git grep -nE "^\s*[a-zA-Z]+\s*:\s*ExpressionInputSchema" \
origin/main -- 'packages/spec/src/**' ':!*.test.ts' | wc -l # lit control, expect 29The question
Which PR owns the triad narrowing, and does the FieldSchema triad surface get one ADR-0087 notified id or two?
Options and real cost
| option | what happens | cost the customer can feel |
|---|---|---|
| A | #18985 drops the alias rebinding and the three triad lines, keeping ADR-0136, the ADR-0089 addendum, the D7 roster entry and the DEBT producer fix; #18638 is the only narrowing PR; one notified id | #18985's clause-② PASS covered the alias narrowing, so its code half needs a fresh review, not a re-affirmation — real rework in this lane. ADR-0136 D1's sentence must be rewritten to describe #18638's binding |
| B | #18985 lands first as written; #18638 rebases, drops the three duplicated lines, keeps its other 33 positions; two ids, cross-referenced | two public type exports (Predicate, PredicateInput) published in narrowed form with zero composers — a permanent obligation bought for nothing; the ledger carries two ids for one surface, which every future reader of that surface must reconcile |
| C | one PR under one card | discards one of the two seats' completed reviews and collapses two rulings into one seat's PR; the largest single diff and the slowest path to any landing |
Business translation
- A = "there is exactly one way to say this expression will really be run, and everything points at it." One vocabulary word, 36 places using it.
- B = "there are two synonyms for the same rule, both published, both permanent." Ships sooner in this lane, and every later author has to learn which of two identical names to reach for.
- C = "stop both, rebuild as one." Cleanest ledger, longest blockage, and it throws away review work already done and paid for.
Anti-AI-error axis — what a wrong choice looks like
Under A a wrong metadata predicate is refused loudly at the slot, by name, at authoring time. Under B the failure is not a refusal at all but a choice: an AI author picking PredicateInputSchema and an AI author picking EvaluatedExpressionInputSchema both produce valid, identical-strength metadata, so nothing anywhere ever signals that one of the two names was the wrong reach — the cost is silent, permanent vocabulary drift rather than a visible error. That asymmetry, not the landing order, is the reason this axis favours A.
Recommendation
A. Fallback: B, if the maintainer judges this lane's rework cost higher than carrying two synonymous public types — B is coherent and ships this lane sooner, it just buys a permanent obligation with zero measured demand.
Self-check: 只看①选 A;②③④ 是否翻转:否 —— all four point the same way, so the letter is not being propped up by one axis.
Confidence gap, stated plainly. I have NOT read #18638's 59-file diff and I have not verified its quality; its clause-② verdict is a thread verdict with 0 GitHub reviews on the PR, which I did not audit. So I am recommending that the work concentrate in a PR I have not reviewed. If #18638 turns out to be weaker than #18985, A costs more than this card claims and B becomes the better letter. Also note the bias this recommendation runs against: A shrinks my own card's scope and sends the substantive narrowing to another seat's PR — I state that so it is visible rather than argued around.
Execution after the ruling
- On A: spec: hold a predicate to what the engine can run; declare its fault semantics (ADR-0136) #18985's dev drops the alias rebinding and the three triad lines, rewrites ADR-0136 D1 to describe feat(spec)!: every engine-evaluated expression slot requires a non-blank
source#18638's binding, renumbers ADR-0136 to 0137 (PR docs(adr): ADR-0136 declared journeys as the priority anchor #18480 claimed 0136 about 42 hours earlier;0137measured free on main and across all 32 open PRs), cites batch 🔗 Broken links detected in documentation #122 / spec: the evaluated-slot rule of #15430 reaches only the flow-node ledger — every otherExpressionInputSchemaslot an engine evaluates (formulaexpression, validation / hook / sharingcondition,visibleWhen…) still accepts anast-only or blank-sourceenvelope #15811 / feat(spec)!: every engine-evaluated expression slot requires a non-blanksource#18638 in the Scope boundary, folds the ADR-0087 entry into a cross-reference, and fixes the migration entry'ssurfacewording. Then a fresh at-tier clause-② review, then the record, then it parks for human approval as a governed record. - On B: spec: hold a predicate to what the engine can run; declare its fault semantics (ADR-0136) #18985 keeps its shape, takes the same renumber and Scope-boundary citations, re-affirms at tier against the new head, then parks for approval; os-litant's seat is handed the semantic note (not the text) that the triad lines are already bound.
- Either way the Scope-boundary reconciliation and the renumber happen — they are not contingent on the letter.
- ⛔ This seat does not touch spec: the evaluated-slot rule of #15430 reaches only the flow-node ledger — every other
ExpressionInputSchemaslot an engine evaluates (formulaexpression, validation / hook / sharingcondition,visibleWhen…) still accepts anast-only or blank-sourceenvelope #15811 or feat(spec)!: every engine-evaluated expression slot requires a non-blanksource#18638 beyond evidence already posted (comment5728924245, no label, assignee or state change), and does not approve or enqueue spec: hold a predicate to what the engine can run; declare its fault semantics (ADR-0136) #18985 —docs/adr/**is governed.
Related cards and PRs
#17778 (this lane, pm:dispatched), #15811 (os-litant, pm:dispatched), PR #18985, PR #18638, PR #18480 (ADR number collision), #19000 (closed as duplicate of #15811 — my error, recorded there).
Filed by Seat: domain:spec#3 at 2026-09-18T10:54Z. No domain:* and no priority:* set — those are the triage seat's to produce.
Generated by Claude Code
Activity
hotlong commentedon Sep 18, 2026
Ruling: batch #160 item 1 · letter A (PR #18985 drops its three triad-slot changes and the
PredicateSchema/PredicateInputSchemarebinding, keeping only what is its own — the fault-semantics record, the ADR-0089 addendum, the ADR-0058 D7 roster entry, the type-check DEBT producer fix; PR #18638 owns every slot narrowing under one ADR-0087 id,evaluated-expression-slots-source-required; thePredicate*aliases stay wide and are never published in a narrowed form) · maintainer 「同意」 2026-09-18T11:58ZDirector seat, summon #24,
session_01Wj1HUjzyeiBQ8atRf1ZhaL. Presented in detail with the recommendation A; the maintainer agreed. Facts (this card, re-checked by the director on the two PRs): PR #18985 (card #17778, 22 files, heade1978a0f) and PR #18638 (card #15811, 59 files, head70407326) are both draft on based8b12fca, bothneeds:contract-review, and both narrowFieldSchema'svisibleWhen/readonlyWhen/requiredWhen; 12 paths overlap (expression.zod.ts,field.zod.ts, the migration registry, six api-surface declarations, three reference pages); the runtime outcome for the triad is identical; each registers a different ADR-0087 id for the same surface. Before #18985 thePredicate*aliases had zero slot composers — a dead symbol #18985 revives as a carrier, leaving two public names for 「an expression that is really evaluated」.Ruling — A
field-rule-predicate-evaluated-slot-source-requiredledger entry and the overlapping api-surface / reference-page rows; keep the fault-semantics record (ADR-0136), the ADR-0089 addendum, the ADR-0058 D7 roster entry and the type-check DEBT fix. Its card spec/ADR-0089: a form field-rule predicate that faults refuses the submit loudly; visibility stays fail-open at render; a blank predicate is refused at authoring — fault semantics become part of the contract (objectui#8069 ruling A) #17778 (batch 🔗 Broken links detected in documentation #119 item 3) ruled fault semantics, not the carrier symbol — nothing ruled is lost.source#18638 (card spec: the evaluated-slot rule of #15430 reaches only the flow-node ledger — every otherExpressionInputSchemaslot an engine evaluates (formulaexpression, validation / hook / sharingcondition,visibleWhen…) still accepts anast-only or blank-sourceenvelope #15811, batch 🔗 Broken links detected in documentation #122 item 2 「by identity on all 36 positions」) is the sole owner of the accept-set narrowing, one ADR-0087 id, and lands first; spec: hold a predicate to what the engine can run; declare its fault semantics (ADR-0136) #18985 rebases after it.PredicateSchema/PredicateInputSchemastay as they are; ⛔ not narrowed, ⛔ not published narrowed. A later card may retire them as dead symbols on their own measurement.Four-facet reading: ① one composable type name for an evaluated expression; ② 36 positions refuse an empty / ast-only predicate for authors in one landing; ③ two synonymous legal names is the free-form trap an AI guesses between; ④ no new permanent public obligation.
Execution
Graded here (the card arrived bare):
domain:spec,priority:p2.needs-user-decisionstripped; closed not_planned — the ruling executes on the two carrying cards (#17778: seat 3 rebases PR #18985 per the first bullet; #15811: PR #18638 proceeds to its at-tier review and the queue). Pointers posted on both.Generated by Claude Code
revert(spec): drop the predicate narrowing — #18638 owns every evalua…
spec: hold a predicate to what the engine can run; declare its fault …