Skip to content

Commit 092d460

Browse files
os-elon-muskclaude
andauthored
spec: hold a predicate to what the engine can run; declare its fault semantics (ADR-0136) (#18985)
Fixes #17778 Clause-②: no The `domain:spec` half of the maintainer ruling on objectui#8069 (decision batch #119 item 3, 2026-09-12: 「同意」 to **A**, with **Q2 yes** and **Q3 yes**), **reworked** under decision batch #160 item 1 on #19003 (letter **A**, maintainer 「同意」 2026-09-18T11:58Z). The renderer half is objectui#8069 and is not in this PR. ## What this PR is now — a record, one producer fix, and nothing else Ruling A removed the schema change from this PR. Decision batch #122 item 2 (card #15811, comment [`5644350409`](#15811 (comment)), 2026-09-12) had already ruled the evaluated-slot narrowing across **all 36 declaring positions** — its own census names *「field / option / grid-column `visibleWhen` / `readonlyWhen` / `requiredWhen`」* — and **PR #18638 owns it under one ADR-0087 id and lands first**. Card #17778 ruled fault semantics, not the carrier symbol, so nothing ruled is lost. **Removed here** (commit `9f30a18a9`): the three `FieldSchema` triad-slot edits; the `PredicateSchema` / `PredicateInputSchema` rebinding, which go back to composing the persistence schemas, wide; the `field-rule-predicate-evaluated-slot-source-required` ADR-0087 entry (the entry file and, through `gen:migration-registry`, its registry rows) and the changeset marker; the triad pin test; the two ADR anchors; and every api-surface / reference-page row that existed only because of those. **Kept, per the same ruling**: ADR-0137 (the fault-semantics record, renumbered — see below), the ADR-0089 pointer addendum, and the producer fix. **Re-derived and removed** (see the measurement below): the ADR-0058 D7 roster entry. ### The revert is byte-exact, not "close enough" The three sources and the `field.zod` anchor were restored with `git checkout d8b12fc --` (the merged-main parent of this branch's merge commit, a pinned sha, not a moving ref); `git diff d8b12fc` is **empty** for each. `registry.ts` was not hand-edited — the entry FILE was deleted and `gen:migration-registry` re-emitted the generated regions; the result is byte-identical to merged main and `check:migration-registry` is green. After regenerating, the whole diff against merged main is **6 files**, and the generated half of it is **4 lines**: | artefact | delta vs merged main | |---|---| | `content/docs/references/**` | **byte-identical** — 0 rows | | `packages/spec/api-surface-declarations/` | **4 lines**, in `root.txt` and `shared.txt`, all of them the producer fix | | `packages/spec/api-surface/`, `export-origins/`, `declaration-map/` | unchanged — `check:api-surface` green | ## The producer fix, on its own terms `cel()` and `expression()` (hence `F` and `P`) always write a non-blank `source`, but were declared as returning `Expression`, whose `source` is optional — a declaration of a shape neither function can produce. The fix is at the PRODUCER (Prime Directive #12): the return type now states what the helper emits. The docblock was **rewritten** so it no longer rests on the triad requirement this rework removes; what it now says is the general fact plus the live consumer, `FlowEdgeSchema.condition`. ```diff -declare function cel(strings: TemplateStringsArray, ...values: unknown[]): Expression; +declare function cel(strings: TemplateStringsArray, ...values: unknown[]): EvaluatedExpression; -declare function expression(source: string, dialect?: ExpressionDialect, meta?: ExpressionMeta): Expression; +declare function expression(source: string, dialect?: ExpressionDialect, meta?: ExpressionMeta): EvaluatedExpression; ``` Narrowing a return type removes nothing from a caller — `EvaluatedExpression` is assignable to `Expression` — so no call site changes. ## `Clause-②` re-judged, and the changeset level follows it The previous body declared `Clause-②: yes (narrowing)`. With the accept-set narrowing gone that declared something this diff no longer does, and `Check Changeset` read it and reddened. **Re-judged against the six files that remain: `Clause-②: no`.** The judgement is measured, not asserted: | question the declaration asks | reading | instrument | |---|---|---| | does it widen an accept set? | **no** — no zod schema moves at all; `packages/spec/src` changes are two return types and two docblocks | `git diff d8b12fc -- packages/spec/src` | | does it narrow an accept set? | **no** — same reading; the narrowing left with commit `9f30a18a9` | as above | | does it expand the public surface? | **no** — no export added, removed or renamed | `check:api-surface` green, with no removed-or-narrowed report | ⇒ the changeset is **`patch`**, the BREAKING banner is gone and so is the ADR-0087 disposition marker — both belonged to the narrowing that owned them. Something published still moves (two return types in the shipped `.d.ts`), so `skip-changeset` would be wrong; a producer-side fix in a released package is exactly what a `patch` entry is for. Re-run locally against a `pull_request` payload carrying this body, before pushing: `GITHUB_EVENT_NAME=pull_request node scripts/check-changeset-no-major.mjs --base origin/main --event PAYLOAD_PATH` → **exit 0**, `NOT DECLARED — the clause-② declaration reads 'no'`. The same command against the OLD body reproduces the CI red, so the local run is a measurement and not a hopeful one. ## ADR-0136 is renumbered to ADR-0137 PR #18480 added `docs/adr/0136-declared-journeys-as-priority-anchor.md` about 42 hours earlier. `scripts/check-adr-anchors.mjs` prescribes exactly this — the NEW record takes the next free number, and renumbering an already-accepted record was ruled out, so before it is referenced is the only cheap moment. **`0137` re-verified free on this rework**, not inherited from the earlier sweep: | query | result | |---|---| | `git ls-tree origin/main docs/adr/` | tops out at **0135** — neither 0136 nor 0137 on `main` | | added `docs/adr/*` files across **all 31 open PRs** (`GET /pulls/{n}/files`) | two hits, both `0136`: #18480 and this PR. **Zero** on 0137 | The scan lit twice on `0136`, so the zero on `0137` is a reading and not a dead query. ## Three corrections the record owed **1. Its Status line claimed an implementation it no longer has.** It now says what is true: this record declares and implements nothing. D1's authoring refusal is batch #122 item 2's, carried by #18638; D2 / D3 / D4 are consumer-delivered in objectui#8069. **2. The gate-slot conversion is RULED and IN FLIGHT, not "filed as a follow-up".** The dangling sentence is gone. The record's claim that converting the gate slots "would bake a direction the ruling did not give" is true of batch **#119** and was silent about batch **#122 item 2**, which gave exactly that direction six days earlier, and about #18638 which implements it. The claim is now stated as what it is — a statement about which ruling authorizes what, not a reason the conversion should wait — and the lint-side cost (`validate-visibility-predicates.ts`'s `celRefusal` records the opposite position today) is named as a cost #18638 carries, not as an objection. **3. The hand enumeration is replaced by a citation of #15811's census, because the hand list had already rotted.** It omitted `packages/spec/src/system/settings-manifest.zod.ts:424` and `:686`, both `visible: SettingsVisibilityInputSchema`, which is `ExpressionInputSchema.superRefine(...)`. Measured through `SettingsManifestSchema.safeParse` on the built `dist`: | authored `visible` | manifest slot (`:686`) | specifier slot (`:424`) | |---|---|---| | `{ dialect: 'cel', ast: … }` | ACCEPTED | ACCEPTED | | `{ dialect: 'cel', source: ' ' }` | ACCEPTED | ACCEPTED | | bare `' '` | ACCEPTED | ACCEPTED | | **LIT CONTROL** `'data.provider.toUpperCase()'` | **REFUSED** `custom@visible` | **REFUSED** `custom@specifiers.0.visible` | | DARK CONTROL `"data.provider === 'smtp'"` | ACCEPTED | — | The refinement is live at both slots and narrows neither the `ast`-only nor the blank-`source` arm — `if (!source) return;` is the line, and the lit control is what makes the six ACCEPTEDs a reading. ## The ADR-0058 D7 roster entry — re-derived, then removed The ruling's KEEP list names it, and carries NO re-derive clause. The instruction to re-derive came from this seat's dispatch brief, not from the maintainer — recorded here because the earlier wording attributed it to the ruling. The re-derivation concluded the entry no longer belongs (measured: discovery finds 37 positions with the line present and 37 without, floor 37 unchanged and met at 37; the alias types zero slots), so the line is removed here pending the maintainer's explicit confirmation of that removal. The roster lists schemas that **declare** an expression surface — "a slot whose accepted grammar is narrower gets its own schema and must be listed here too". `PredicateInputSchema` earned its place only while the rebinding made it `= EvaluatedExpressionInputSchema` and bound the triad to it from another file. Reverted, it is a plain alias of `ExpressionInputSchema` typing no slot, and the ledger header's limitation 2 names it as the standing **latent** example — leaving it rostered would make that paragraph false. #18638 measured the same thing independently: *「`PredicateInputSchema` is a plain alias of `ExpressionInputSchema` with zero slot users; it stays wide with the schema it aliases」*. Measured twice on this branch with the revert already applied, by raising the `head` floor to 9999 through `ablation-replace.mjs` so the assertion prints the count: ``` roster entry PRESENT -> discovery found 37 position(s) via 'head' (floor 37) roster entry ABSENT -> discovery found 37 position(s) via 'head' (floor 37) ``` Identical, because the three triad positions are head-matched by `ExpressionInputSchema` again. Both mutations landed and both restores verified on disk (`anchor 1 -> 0`, then `blob == HEAD` and `git diff HEAD` empty). Identity grep agrees: `PredicateInputSchema` has **2** hits under `packages/spec/src/**/*.zod.ts` — its own definition and its `z.input` companion, zero slots — against a lit control of **19** files for `ExpressionInputSchema` and a dark control of **0**. ⛔ **Not a gate weakening.** The `head` floor stays **37** and is met at 37; no ledger row is deleted, no floor is lowered, no test is skipped or quarantined. The same three surfaces are discovered through the schema that types them. Both dogfood files are byte-identical to merged main. The same statement holds for the two other removals: the triad pin test and the ADR-0087 entry are removed because the behaviour they recorded is no longer in this PR — not to turn anything green. `packages/qa/dogfood` re-run after the removal: **7 passed (7)**. ## ⛔ GOVERNED SURFACE — this PR parks as a draft, by design `docs/adr/**` is on the register, so this is the regime's correct resting state, not a stall. An authorized approval is owed before any seat lands this. This seat has not flipped it ready, has not enqueued it and has not armed auto-merge. The `needs:contract-review` carrier is the review seat's and stays hung; a fresh at-tier review is owed on this head. ## Evidence - `pnpm --filter @objectstack/spec check:generated` — proved exactly **2 of 16** artifacts stale (`api-surface-declarations/`, `content/docs/references/**`) and `--fix` regenerated only those two. Re-run after: **16 of 16** up to date. - `check:migration-registry` green with the entry file deleted — the generated regions match `entries/`. - Gate families re-derived on this head with `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` and reconciled with `--ran`; results and every non-zero exit are in the report comment on #17778. - ⚠️ The derivation printed a **STALE TREE** warning: this branch is at least 14 commits behind `origin/main` and 10 of the files the families are derived from moved in that range. No `origin/main` merge was taken in this rework, on purpose — this PR rebases after #18638 lands, and a merge now would move the `merge=os-regen` artefacts for a base that is about to change. So the family list is this tree's, and CI on the merge queue's rebuilt generation is what covers the rest. ## Acceptance notes - **`packages/spec/src/data/field.zod.ts:1513` `expression`** (the formula slot) is an evaluated slot on `ExpressionInputSchema`. Untouched, and no longer this PR's business at all: it is inside #18638's 36-position census. - **`PredicateSchema` / `PredicateInputSchema` have zero slot users** — measured above. The ruling says they stay as they are and that a later card may retire them as dead symbols on their own measurement. No anchor is left claiming otherwise: the anchor this PR added for `shared/expression.zod.ts` is deleted, and the `field.zod.ts` anchor is back to its pre-PR text. - The ADR-0137 record's `Consumers` line still names `shared/expression.zod.ts` and `data/field.zod.ts`. That is the set of files the DECISION governs, which is unchanged; it is not a claim that this PR edits them for that reason. ## 维护者速读(草稿) > 席位意见一节留空,由席位在 at-tier 评审后定稿为评论。 **改了什么** — 按 batch #160 item 1 的裁决 A,把这个 PR 里的**协议收窄整段拿掉**:字段三条规则槽位(`visibleWhen` / `readonlyWhen` / `requiredWhen`)回到原样,`Predicate*` 两个别名回到原样(仍然是宽的持久化契约),对应的 ADR-0087 迁移条目、pin 测试、两个 ADR anchor、以及只因它们才产生的 api-surface 与参考文档行,全部删除。留下的是:**ADR-0137**(改号后的 fault 语义记录)、ADR-0089 的指针附录、`cel` / `expression` 的返回类型修复。另外按指示重新推导后,删掉了 ADR-0058 D7 的那一行 roster 条目。 **为什么改** — 同一个收窄早在六天前就被 batch #122 item 2 裁决过了,覆盖全部 36 个求值槽位(包含本卡的三条字段规则),并且由 PR #18638 用**一个** ADR-0087 id 承载、先落地。两个 PR 各带一份收窄,就是一次迁移两个 id、两份 CHANGELOG 说法。#17778 裁决的是 fault 语义,不是承载它的符号,所以记录留下、收窄交出去,**没有任何被裁决过的东西丢失**。 **风险与代价(含回滚)** — 风险很低:协议行为**零变化**(没有任何 zod schema 移动),对外只剩两个函数返回类型收窄,而 `EvaluatedExpression` 可赋值给 `Expression`,所有调用点照常编译。changeset 因此从 `minor` + BREAKING 降为 **`patch`**,`Clause-②` 重判为 **`no`**(三项读数在上表)。代价是本 PR 不再自带任何强制:D1 的编写期拒收要等 #18638;这一点在记录的 Status 和 Scope boundary 里明写了,不是留给读者去发现。回滚成本极低——本轮全部是删除与还原,恢复即 revert 这四个 commit。 **席位意见** — (留空) **你要做的** — 这个 PR 碰了 `docs/adr/**`,属受管面,停在 draft 等一个授权批准,这是制度的正常终态。需要你看的是三件事:① ADR-0137 现在**只声明不实现**——D1 交给 #18638、D2/D3/D4 交给 objectui#8069,这个归属你是否认可;② 记录里原来那句「gate 槽位转换会写进裁决没给的方向」已改写为「那只对 batch #119 成立;batch #122 item 2 给了这个方向,#18638 正在做」,这个更正你是否同意;③ D7 roster 那一行**被删**而不是保留——测量是:删与不删,`head` 发现数都是 37(地板 37),且 `PredicateInputSchema` 零槽位使用,所以它不再是「更窄的别名」。裁决原文把它列在「保留」里,**且未附任何重新推导的条款** —— 要求重新推导的是本席派发令,不是维护者(先前措辞把它归给了裁决,此处更正)。推导结论是该条目不再属于花名册(实测:该行在与不在,发现数都是 37,地板 37 未动且 met at 37;该别名零槽位),故此处删除,**等您明确确认这一删除**。 --- _Generated by [Claude Code](https://claude.ai/code)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent b1d3945 commit 092d460

4 files changed

Lines changed: 329 additions & 8 deletions

File tree

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
"@objectstack/spec": patch
3+
---
4+
5+
**ADR-0137 makes field-rule predicate fault semantics part of the contract** (#17778): what SUBMIT and RENDER do when a predicate cannot run.
6+
7+
Clause-②: no
8+
9+
**The predicate fault-semantics contract is recorded, not enforced, by this release.** ADR-0137 states what a field-rule predicate does when it cannot RUN: at SUBMIT a faulting predicate refuses the write and names the field and the rule (D2); at RENDER visibility stays fail-OPEN, so a rule that could not run never hides a control and lets the form write `null` over a column the user never saw (D3); a blank or faulting GATE predicate is diagnosed, never a silent `true` (D4); and the evaluation helper's fallback stays freely specifiable (D5), because fault-to-flag and fault-to-throw both exist only because it is a parameter. Those are consequences CONSUMERS deliver — `packages/spec` carries no business logic — and they land in the ObjectUI half. D1's authoring refusal (an `ast`-only envelope and a blank `source` are refused at authoring) is ruled by decision batch #122 item 2 and ships with the evaluated-slot narrowing that owns it, under that change's own ADR-0087 entry.
10+
11+
**ADR-0089 gains an addendum, not a reopening.** It unified the `visibleWhen` / `visibleOn` / `visibility` family under one name; ADR-0137 owns what that family does when a predicate cannot run, and ADR-0089 itself is unchanged by this release.
12+
13+
**Not carried by this entry: the `cel` / `expression` return-type narrowing to `EvaluatedExpression`.** This card touched that signature too, but main shipped the identical narrowing first, under #18638 (card #15811) — see that release's own changeset for the `EvaluatedExpression` story and the TS2322 it fixes. Restating it here would announce, a second time, a fact this release has already shipped under a different entry.

‎docs/adr/0089-unify-visibility-predicate-naming.md‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,16 @@
88

99
---
1010

11+
> **Addendum (2026-09-18, #17778) — what the `*When` family does when it cannot RUN is decided by [ADR-0137](./0137-predicate-fault-semantics-are-contract.md), not here.**
12+
> This record unified the family under one NAME and is unchanged by that one. ADR-0137 D1 holds a
13+
> predicate slot to what the engine can actually run — an `ast`-only envelope and a `source` blank
14+
> after trimming are refused at authoring, which is the field-rule row of the rule decision batch
15+
> #122 item 2 gave across every evaluated slot; D2 refuses the SUBMIT loudly on a fault, naming the
16+
> field and the rule; D3 keeps visibility fail-OPEN at RENDER. A reader who arrived here asking what
17+
> a broken `visibleWhen` does should read that record.
18+
19+
---
20+
1121
## TL;DR
1222

1323
One concept — *"show this only when the CEL predicate is TRUE"* — is spelled three

0 commit comments

Comments
 (0)