Commit 092d460
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.
- 1 parent b1d3945 commit 092d460
4 files changed
Lines changed: 329 additions & 8 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
8 | 8 | | |
9 | 9 | | |
10 | 10 | | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
11 | 21 | | |
12 | 22 | | |
13 | 23 | | |
| |||
0 commit comments