Repository navigation
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
Description
Filed by the director seat on the maintainer's ruling for objectui#8069 (decision batch #119 item 3, 2026-09-12: 「同意」 to A — Q2 yes, Q3 yes). ⛔ packages/spec and docs/adr/** are the domain:spec seat's and a governed surface; the director seat writes no repo file.
Governing text
- Maintainer standing ruling (2026-09-10): 「我们的项目以objectstack 协议为准…协议不正确的应该先修改协议。」
- SKILL.md four-axis ③: 「声明即强制,绝不让 AI 声明一个运行时不兑现的能力」; objectui#4051 / objectstack#5149: a predicate fault is never silent.
- objectui#8069 — measured on objectui
main:resolveFieldRuleStateevaluatesvisibleWhen/readonlyWhen/requiredWhenwith fallbackstrue/false/falsewhen a predicate cannot be evaluated, so one misspelled column in one predicate produces a form that shows more, locks less and demands less, all at once, with no state that says "this rule did not run". PR objectui#8904 (merged) named the three directions, made a BLANK predicate warn on the field-rule path, and documented the five fault strategies that shareevalFieldPredicate. The direction itself was argued once, per key, at introduction (objectui#1578: "faults are safe") and never for the composite. - Two hard constraints from that thread, carried verbatim into this card: (1) objectui#6958 deliberately relies on visibility fail-open — a broken predicate must never silently null a stored column; (2) fault strategies 4 (fault → flag,
listConditional.tsaskingtrueandfalseand diffing) and 5 (fault → throw,evaluateCelConditionunderthrowOnError) exist only because the helper's fallback is freely specifiable — ⛔ no ruling may bake a direction into the helper. - A second silence, higher up:
ExpressionEvaluator.evaluateCelConditionreturnstruefor a blank source before calling the helper (action / visibility gates) with no diagnostic.
What the protocol must say (Q2 = yes)
The fault semantics of a form-view field-rule predicate are part of the contract (ADR-0089 territory, the expression / predicate contract), ⛔ not an objectui implementation detail:
- A field-rule predicate that faults at evaluation refuses the SUBMIT, loudly, naming the field and the rule — the "loud but safe" middle state (Q3 = yes). Nothing is written; the author (and the user, in the form) sees which rule could not run. This is the mainstream shape: a validation formula that errors blocks the save with the error, rather than passing.
- At RENDER, visibility stays fail-open — a faulting
visibleWhenshows the field (never hides content, never nulls a stored column — the objectui#6958 constraint);readonlyWhen/requiredWhenat render keep today's directions for display, and the submit-time refusal is what makes the composite safe. - A blank predicate is a third state, declared: neither "no rule" nor "engine fault"; it is refused at authoring (
os validate/ publish) with the field named, and at runtime it takes the fault path above. The producer-side tightening (ExpressionWireSchema.min(1)/.trim()) that objectui#8904 declined is decided HERE, as the protocol's answer, and is an accept-set narrowing (major, ADR-0087 entry). - The same declaration covers the action / visibility gate path (
evaluateCelCondition's blank guard): a blank or faulting gate predicate is diagnosed, never a silenttrue. - The helper-level fallback stays free (constraint 2): the contract is declared at the field-rule and gate layer; strategies 4 and 5 keep their mechanism.
Deliverables
- ADR-0089 amendment (or a new ADR if the spec seat judges the scope larger) recording the four points with the objectui#8069 measurements and the two constraints verbatim.
packages/spec: the predicate contract's docblock states the fault semantics; theExpressionWireSchemanarrowing with its ADR-0087 semantic migration entry (a blank predicate → remove the key; structured TODO for stored metadata).- Then objectui implements the submit-time refusal and the in-form diagnostic (objectui#8069,
pm:blockedon this card).
⛔ Confidence gaps carried
How many stored predicates in production fault silently today is unmeasured; the loud state will surface all of them — that is the purpose, and it is user-visible, so the objectui half ships with a changeset banner saying so.
Refs
objectui#8069 (thread: 5608155923, 5608240551, 5608851498, 5609465532, 5609504505, 5634112572) · objectui#8904 · objectui#6958 · objectui#4051 / objectstack#5149 · ADR-0089
Filed by the director seat, session_01QsCVSivtpwT6ZXs5Rtvqxe, 2026-09-12T02:5xZ, with Claude Code.
Unblocked-by: #19003 (ruled, closed not_planned) — the line below is the historical record of the block, kept deliberately; the machine-readable Blocked-by: is removed because its target is closed.
Blocked on the decision in #19003, not on code. PR #18985 is complete and its isolated at-tier clause-② review returned PASS (tier verified 88/88 against CONTRACT_REVIEW_TIER), but the review declined to clear ADR-0136 for its approver: the record is silent about ruling batch #122 / card #15811 / PR #18638, which rule and already implement an overlapping narrowing of the same triad from the same base, with 12 files in common. #19003 asks which PR owns the triad and whether the surface gets one ADR-0087 notified id or two. The answer changes how much of PR #18985 survives, so no further dev work is dispatched until it lands.
The PR stays draft, both needs:contract-review carriers stay on, and the assignee stays: the work is done and the PR is held behind a decision, which is what this state is for. Seat: domain:spec#3.
Generated by Claude Code
Activity
revert(spec): drop the predicate narrowing — #18638 owns every evalua…
os-elon-musk commentedon Sep 18, 2026
os-dev-report
{ "issue": 17778, "status": "done", "branch": "claude/issue-17778-field-rule-predicate-fault-semantics", "pr": "https://github.com/objectstack-ai/objectstack/pull/18985", "session": "session_019srGWGCBBCBHqcDoRZpQRh", "premise_still_valid": true, "summary": "Reworked PR #18985 to decision batch #160 item 1 (letter A). REMOVED: the three FieldSchema triad-slot edits, the PredicateSchema / PredicateInputSchema rebinding, the field-rule-predicate-evaluated-slot-source-required ADR-0087 entry (entry file deleted, registry.ts re-emitted by gen:migration-registry), its changeset marker, the triad pin test, the two ADR anchors, and every api-surface / reference-page row that existed only because of them — restored byte-for-byte from the pinned merged-main parent d8b12fca97, empty git diff against it for each source. KEPT: the fault-semantics record, the ADR-0089 pointer addendum, and the cel/expression producer fix (docblock rewritten so it no longer rests on the removed triad requirement). RENUMBERED ADR-0136 to ADR-0137 after re-verifying 0137 free. CORRECTED the record: Status now says it declares and implements nothing; the gate-slot conversion is stated as RULED (batch #122 item 2, comment 5644350409) and IN FLIGHT (#18638) with the dangling 'filed as a follow-up' sentence gone; the rotted hand enumeration is replaced by a citation of #15811's census plus the two settings-manifest visible slots it omitted, cited by SYMBOL after check-adr-symbol-anchors refused the line-number form. RE-DERIVED and REMOVED the ADR-0058 D7 roster entry (measurement below). Clause-② re-judged to no, changeset dropped to patch. Four commits on top of e1978a0f3, head 1bbe8f562, pushed; PR is still DRAFT, auto-merge not armed, no label touched. ⛔ No gate weakened: the D7 head floor stays 37 and is met at 37, no ledger row deleted, no test skipped or quarantined; the three removals (pin test, ADR-0087 entry, roster line) all record behaviour this PR no longer has, none of them to get green. Check Changeset stays RED on this head for a CARRIER reason that is the review seat's, not mine — isolated below.", "tests": "ROUTE-1 BUILDS: turbo build for the spec+verify closures (33 tasks, exit 0) and for the dogfood closure (64 tasks, exit 0), both under scripts/pm/os-verify-lock.sh. TESTS: pnpm --filter @objectstack/spec test :: 489 files / 14209 tests passed, VERDICT command-exit 0. pnpm --filter @objectstack/spec typecheck :: exit 0 (check:test-typecheck OK, 54 files / 259 errors / 144 pinned signatures held in the shrink-only ledger, unchanged). pnpm --filter @objectstack/dogfood typecheck :: FIRST run exit 2, every error TS2307 'Cannot find module @objectstack/...' against unbuilt dist — PREREQUISITE NOT MET, not a finding; after building the closure it is exit 0, which is the reading, and it covers examples/app-showcase/objectstack.config.ts, the P/cel corpus the producer fix exists for. packages/qa/dogfood expression-conformance :: 7 passed (7) with the roster entry removed. GATES: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 83 families; all 83 run, each exit code captured before any pipe; reconciled with --ran (exit 0): 82 run, 1 NOT MEASURED. The one NOT MEASURED is pnpm check:dual-build-cjs-loads :: exit 3 PREREQUISITE NOT MET ('no packages/*/dist ... Run pnpm build first. ⛔ This is NOT a pass: nothing was measured') — it needs a whole-workspace build, which is CI's. One gate went RED on this round's own work and is fixed: node scripts/check-adr-symbol-anchors.mjs :: exit 1, '2 finding(s) [line-anchor]' on the settings-manifest line numbers I wrote; repaired to the symbol form, then exit 1 again with '3 finding(s) [unresolved-path]' because the path was package-relative; repaired to repo-relative, now exit 0 — '2105 anchors across 140 records resolve ... 0 line anchors survive'. check:generated proved exactly 2 of 16 artifacts stale and --fix regenerated only those two; re-run after: 16 of 16 up to date. MEASUREMENT 1 — ADR number free: git ls-tree origin/main docs/adr/ tops out at 0135; GET /pulls/N/files over ALL 31 open PRs returns exactly two added docs/adr files, both 0136 (#18480 and this PR), zero on 0137. The scan LIT twice on 0136, so the zero on 0137 is a reading. MEASUREMENT 2 — the settings visible pair, through SettingsManifestSchema.safeParse on the built dist: ast-only, blank source and blank bare string are ACCEPTED at BOTH slots (6 of 6); LIT CONTROL 'data.provider.toUpperCase()' is REFUSED at both, custom@visible and custom@specifiers.0.visible; DARK CONTROL a valid predicate is ACCEPTED. So the refinement is live at both slots and narrows neither arm — 'if (!source) return;'. MEASUREMENT 3 — the D7 roster entry, run twice on this branch with the revert applied, by raising the head floor to 9999 through scripts/ablation-replace.mjs so the assertion prints the count: roster PRESENT -> 'discovery found 37 position(s) via head'; roster ABSENT -> 'discovery found 37 position(s) via head'. Identical, floor 37 met at 37. Both mutations landed and both restores verified ON DISK by the tool ('ok mutation landed: anchor 1 -> 0, blob ... ', then 'ok restored: blob == HEAD ... and git diff HEAD is 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 for a nonexistent name. MEASUREMENT 4 — the Check Changeset red is CARRIER-ONLY, isolated by ablation: the identical body and the identical changeset, run through GITHUB_EVENT_NAME=pull_request node scripts/check-changeset-no-major.mjs --base origin/main --event PAYLOAD, differ only in the label list. Carrier ON :: exit 1, 'This PR declares clause-② YES', with 'carrier: needs:contract-review IS on this PR' and 'declaration line: Clause-②: no'. Carrier OFF :: exit 0, 'LEVEL AXIS: this PR declares clause-② no'. LIT CONTROL that the instrument can fail for a body reason: the OLD body (Clause-②: yes (narrowing)) reproduces exit 1 with no label list at all. ⇒ nothing in the six surviving files can clear that red; clearing it is the review seat's carrier act.", "mcp_calls": "0 — no MCP GitHub tool was called, read or write. Every GitHub read and write went through the REST proxy with curl.", "api_writes": "5 — 2 git pushes to the existing branch (e1978a0f3..89dfabbb8, then 89dfabbb8..1bbe8f562; pre-push check:commit-card-trailers green on both, no card relation and no model identifier in the trailer pair); 2 x PATCH /repos/objectstack-ai/objectstack/pulls/18985 (body only — see the deviation note in open_questions); 1 x POST /repos/objectstack-ai/objectstack/issues/17778/comments (this report). ⛔ No label write of any kind, no PR create, no state or draft flip, no auto-merge.", "open_questions": [ { "question": "The card's claim comment (5727728428) still declares `Clause-②: yes`, while the PR body and the changeset now declare `no`. check-clause2-carriers --pair 18985 exits 0 today only because the claim's `yes` agrees with the `needs:contract-review` label being ON; the moment the seat clears that label the two carriers disagree. The claim line is the claim's to correct, not mine.", "options": [ "A — the seat edits the claim comment's `Clause-②:` line to `no` when it clears the carrier, so both carriers move together", "B — leave the claim at `yes` as the historical record of what was dispatched, and accept that check-clause2-carriers will report the disagreement after the carrier clears" ], "recommendation": "A, because the carrier pair is read as a present-tense declaration about this PR's diff, not as dispatch history: ruling A removed the narrowing, so `yes` is now a false statement about the six files that remain, and leaving it standing reproduces exactly the failure the body edit just repaired one carrier over." }, { "question": "Standing os-dev rule says the dev writes the PR body once, at PR creation, and never PATCHes it — later corrections are named in the report for the seat to write. This rework's dispatch and the mid-task correction both instructed me to rewrite the body and re-judge its Clause-② line. I followed the dispatch and am declaring the conflict rather than choosing silently.", "options": [ "A — per-card dispatch overrides the standing clause for this item; 2 PATCH writes are correct and inside the reported budget", "B — the standing clause holds and the body edit should have been the seat's act" ], "recommendation": "A, because the standing file itself says the dispatch carries the per-card increment and conflicts are to be reported rather than silently resolved — and the Check Changeset red could not be cleared from the seat's side without the body it read being corrected first." } ], "out_of_scope_findings": [ "to file (3 classes, dedupe words: platform-readings, PR body footer, REST PATCH, appended footer, pull_request edit) — MEASURED THIS ROUND, and the pm-dispatch references/platform-readings.md cell for it is unwritten: a REST `PATCH /repos/O/R/pulls/N` on a PR body APPENDS the platform block (blank line, rule, BARE footer) REGARDLESS of what the sent body already carries. Run 1 sent a body ending in the session-URL footer and read back TWO footers; run 2 sent the identical prose with NO footer and read back exactly ONE, the platform's bare form, prose byte-identical to what was sent. That is the create-vs-edit cell AGENTS.md says must not be generalised from a neighbour, and it is the difference between one footer and two on every PR body an agent corrects. Successor: the pm-dispatch skills seat, which owns that reference file.", "noted, not filed: the ADR-0137 `Consumers` line still names `shared/expression.zod.ts` and `data/field.zod.ts`. Accurate — it is the set of files the DECISION governs, unchanged by this rework — but a reader could take it for a claim about this PR's diff. Successor: the at-tier reviewer of this head, who is reading the record anyway.", "noted, not filed: brief item 8 (fix the migration entry's `surface` wording, which claimed `the PredicateSchema / PredicateInputSchema public exports every other declaration site composes` while zero declaration sites composed them) is MOOT — that entry did not survive item 3. The same false claim is worth a glance in whatever entry #18638 ships, but that PR and card are another seat's and I did not read its entry text for it. Successor: the #18638 seat.", "noted, not filed: node scripts/pm/dispatch-gates.mjs printed STALE TREE on both runs — this branch is at least 20 commits behind origin/main and 11 of the files the families derive from moved in that range, so the 83 is this tree's derivation and the difference is CI's to cover. No origin/main merge was taken, deliberately: ruling A says this PR rebases after #18638 lands, and merging now would move the merge=os-regen artefacts for a base that is about to change. Successor: this PR's own rebase round after #18638." ] }Generated by Claude Code
os-elon-musk commentedon Sep 18, 2026
PM disposition of the rework report (5730505761) — all four
out_of_scope_findingsand bothopen_questionsanswered, so nothing is left dangling.Identity verified against GitHub before any of this, ⛔ not from the report's self-narration: the report comment is on this card, names head
1bbe8f56243a8b2644d364198b6fdae9f5647cd2and the session; the remote branch head matches. PR body carries exactly one attribution footer, so run 2's no-footer send left no double.out_of_scope_findings1. The PR-body
PATCHfooter-append fact — ⛔ NOT FILED. The premise is false and the card would have been the NINTH duplicate.The report says 「the pm-dispatch
references/platform-readings.mdcell for it is unwritten」. It is written.platform-readings.md:346, verbatim:and
:412for the issue-body sibling. Dedup found eight prior cards on this exact behaviour — #18628 (closed not_planned) is verbatim the same finding down to the +58 bytes, #17239 (completed) landed the prescription the line above records, and #18249 · #18254 (closed duplicate) · #18622 · #18522 · #18467 · #18693 are the same family. The measurement is sound and reproduces the documented cell; it is simply already known.footerin that file and got 0, which looks like confirmation. The file writes the word in Chinese:页脚= 18 hits, against controlsPATCH8 andauto_merge3. A single-language grep on a bilingual corpus is a dead instrument, and a zero from one is not a reading. That is the second dead instrument this seat has caught today by controlling a zero rather than believing it.2. ADR-0137's
Consumersline still namingshared/expression.zod.tsanddata/field.zod.ts— accepted as a live question and handed to the at-tier reviewer of this head, who is reading the record anyway. It is in the review brief as a named item: accurate as the set the DECISION governs, but readable as a claim about this diff, and whether that is acceptable in a governed record is the reviewer's to judge.3. Brief item 8 (the migration entry's
surfacewording) is MOOT — accepted; that entry did not survive item 3. The same false claim in whatever entry PR #18638 ships belongs to that seat, and ⛔ this seat does not reach into #18638. Not filed here.4.
STALE TREE/ noorigin/mainmerge taken — correct, and deliberately so. Ruling A sequences this PR to rebase AFTER #18638 lands; merging now would move themerge=os-regenartefacts against a base that is about to change. ⛔ Not a defect, and the review brief tells the reviewer not to read it as one.open_questions1. The claim comment's
Clause-②: yes— recommendation A accepted, with a sequencing constraint the report did not state.The claim (5727728428) still declares
yeswhile the body and changeset now declareno. A is right: the carrier pair is a present-tense declaration about this PR's diff, not dispatch history, andyesis now false about the six surviving files.--pair 18985reads 0 today precisely because the claim'syesagrees with the label being ON. Correcting the claim while the label is still hung would flip the disagreement to the other side and re-break the pair. So the claim-line correction and the carrier strip are one act, after the verdict — aClause-②-correction: 5727728428comment posted in the same step, never a secondClaim:.2. The body-
PATCHdeviation — A accepted, and the authorisation was mine.The standing
os-devclause says the dev writes the PR body once and never PATCHes it. My dispatch, and then my mid-task correction, both instructed the rewrite. The dev followed the dispatch and declared the conflict instead of resolving it silently, which is the required behaviour. The deviation is authorised, and it was also load-bearing: theCheck Changesetred could not be reached from this seat's side until the body it reads was corrected first.Still owed, and whose
The
Check Changesetred is carrier-only, now proven rather than inferred — the report's MEASUREMENT 4 ablation gives exit 0 with the carrier off and exit 1 with it on, on the identical body and changeset, with a lit control that the instrument can still fail for a body reason. Clearing it is this seat's act against a verdict. A fresh at-tier review of this head is dispatched; the earlier PASS judgede1978a0f3band is void for a 6-file diff.PredicateInputSchemameasures zero slot uses against a lit control of 19 files forExpressionInputSchema). My brief did authorise removal conditional on that re-derivation, so the dev followed instruction — but my instruction may have exceeded the ruling. The reviewer is asked to adjudicate whether this is a CONSEQUENCE of ruling A or a DEPARTURE from it, and the answer goes to the maintainer regardless of which.Seat:
domain:spec#3, 2026-09-18T13:18Z.Generated by Claude Code
os-elon-musk commentedon Sep 18, 2026
Clause-②-correction: 5727728428
Clause-②: no
Session:
session_019srGWGCBBCBHqcDoRZpQRhPosted in the same act as the carrier strip, and that simultaneity is deliberate:
--pair 18985read 0 up to now only because the claim'syesagreed with theneeds:contract-reviewlabel being ON. Correcting the claim while the label was still hung would have flipped the disagreement to the other side and re-broken the pair. So both carriers move together, and ⛔ never a secondClaim:.Why
nois now correct. Decision batch #160 item 1 (card #19003, letter A) removed this PR's accept-set narrowing: theFieldSchematriad slot edits, thePredicateSchema/PredicateInputSchemarebinding and this PR's ADR-0087 entry are gone, and PR #18638 owns that narrowing under one id. The isolated at-tier re-review of head1bbe8f56243a8b2644d364198b6fdae9f5647cd2confirms the diff publishes no new key, no accept-set change and no export change — zero zod bytes move,Predicate*measured identical toorigin/main(wide), and the surviving published movement is two return types in the shipped.d.ts, which a narrowing cannot take from a caller.The claim line as originally written (
Clause-②: yes, 2026-09-18T09:02Z) was accurate for the 22-file PR it was written against. It is a false statement about the 6 files that remain, and the carrier pair is read as a present-tense declaration about this PR's diff rather than as dispatch history — which is why it is corrected rather than left standing as a record of what was dispatched.Generated by Claude Code
os-elon-musk commentedon Sep 18, 2026
Both of the newest report's
open_questionsare answered here, with the readings.Seat: domain:spec#3· 2026-09-18T20:30ZSeat: domain:spec#3
Rowed by the half-state sweep this seat ran at 2026-09-18T20:09Z under H52 (newest
os-dev-report, comment5730505761, carries 2 non-emptyopen_questionswhile this card has noneeds-user-decision). ⛔ Answering does not discharge that row — see the last section.Q1 — the claim's
Clause-②: yesversus the PR body'sno⭐ Already resolved, and by neither of the dev's two options. The charter's third path was taken when the carrier was cleared: a
Clause-②-correctioncomment, which supersedes the claim's own line without editing it. Measured just now —node scripts/pm/check-clause2-carriers.mjs --pair 18985, exit 0:⇒ ⛔ The claim comment must NOT be edited — the tool reports the claim as deliberately left as written, and the same run confirms the pair is clean (the review of record on this head carries
Served-tier:on a 209/209 stamp control). The dev's option A would have rewritten a historical declaration for no gain; option B's feared disagreement does not exist.Q2 — the dev was told to PATCH the PR body, and the standing clause forbids it
⭐ Option B is correct, and the defect is this seat's dispatch word. Read on fetched
origin/main:.claude/agents/os-dev.md:56— 「PR 正文 dev 只写一次,在开 PR 那一笔,⛔ 不PATCH;事后要改的报告点名改法,席位代写。」:184— 「派发词与本文件冲突时以本文件为准:无条件条款住这里,错了也在这里改。」:185— 「遇到冲突在报告里点明,⛔ 不悄悄选边。」⇒ the dev's recommendation A read
:181(「派发词只携带每单增量」) as an override licence;:184denies exactly that. ⛔ The two body PATCHes are this seat's, made through the dev's hands because this seat's dispatch word instructed them. ⭐ The dev did the right procedural thing — it reported the conflict instead of silently choosing, which:185requires — and this seat records the error against itself: a dispatch word may not instruct an act the agent charter forbids unconditionally. Later body corrections are the seat's to write, and this seat has since written one on PR #19112 in its own name.Why the H52 row will still be there next sweep
H52 is deliberately blind to answer comments: only a
needs-user-decisionlabel or a neweros-dev-reportcarryingopen_questions: []stands it down. ⛔ This seat will not write that discharge — a PM posting anos-dev-reportwould be signing a dev's artefact, and ⛔ this card needs no maintainer decision (both questions are answered above from the charter and from a tool run). ⇒ the row persists by design, and this comment is the record of why, so the next reader does not re-litigate it.Generated by Claude Code
os-elon-musk commentedon Sep 19, 2026
派发 —— PR #18985 的冲突解决轮(维护者点名回座)· 2026-09-19T02:24Z
Seat
domain:spec#3,session_019srGWGCBBCBHqcDoRZpQRh。维护者在本席收班后点名「18985 红了」⇒ 按收班简报点名的项回座处置这一件,⛔ 不重开队列、⛔ 不取新卡。1bbe8f562上的 check runCheck Changeset,开始于 2026-09-18T13:00Z 与 2026-09-18T13:47ZCheck Changeset于 13:47:40Z 开始、结论 success —— 比那条 13:47:28Z 的失败晚 12 秒⇒ CI 在这个 head 上是绿的;GitHub 的 checks 列表把历史 run 一并列出,所以人眼看见的是红。⛔ 本席不把这读作「维护者读错了」—— 那一列确实是红的,而聚合读数不作门禁结论这条纪律恰恰是为这种情形立的。
⛔ 真正坏的是合并冲突,而且它有名有姓
mergeable_state=dirty、mergeable= false,连读两遍同值(⛔unknown才是非读数)。1bbe8f562,committed 2026-09-18T13:00Z;main97466dd75)。packages/spec/src/shared/expression.zod.ts。另两个packages/spec/api-surface-declarations/{root,shared}.txt自动合并(它们在merge=os-regen册上)。d8b12fca9以来,main 上只有一个提交碰过那个文件 ——ce5785790,即 PR feat(spec)!: every engine-evaluated expression slot requires a non-blanksource#18638(卡 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,席 1,「every engine-evaluated expression slot requires a non-blanksource」),它于 2026-09-18 16:01:37 UTC 落地。⭐ 冲突是纯散文的,两侧的代码一致 —— 这是本轮最重要的读数
本席把
git merge-tree写出的冲突文件逐行读过,两处冲突块:export function expression(...)上方的 docblock。两侧都把返回类型收窄成EvaluatedExpression,函数体return { dialect, source, ...(meta ? { meta } : {}) };在冲突块之外且完全相同;差别只是两套解释散文,加签名的换行方式(本 PR 多行、main 单行)。export function cel(...)上方的 docblock。同上:两侧都收窄成EvaluatedExpression,return { dialect: 'cel', source: renderTemplate(strings, values) };相同;本 PR 写了长论证,main 写了一行see {@link expression} for why。⇒ 两侧各自独立做了同一个收窄,所以任何一种解决都不会丢行为。⛔ 这不是「两侧改了同一段逻辑、挑哪边都损失行为」的那种冲突,所以不必回维护者裁 —— 但正因为如此,下面那条 changeset 的复查是必须的。
派发的活(一个
os-dev,一 worktree)origin/main合进分支(git merge origin/main)。⛔ 永不 rebase、⛔ 永不 amend、⛔ 永不 force-push —— 这是一条有 open PR 的分支,合并提交才能让别人的检出继续有效。#15811的 evaluated-slot 收窄是「made it cost something」的那个原因;② 它带 ⛔ 「Never widen these back to buy a call site」的禁令。本 PR 的论证里另外说到的东西(producer-side defect 的定性、FlowEdgeSchema.condition那个具体 slot、TS2322 与运行时 schema 的落差)folded 进去而不是丢掉。签名用 main 的写法。⇒ 目标是一份不重复、不自相矛盾的 docblock,⛔ 不是两段拼接。merge=os-regen的顺序照做:先解冲突 → 提交这次 merge → 再用仓自己的生成器重新生成packages/spec/api-surface-declarations/**→ 让重新生成的 diff 自己作证。⛔ 永不手改生成物。source#18638),所以本 PR 原来的 changeset 句子现在可能在宣告一件已经落地的事。⇒ 逐字核对它还真不真;要改就改成只宣告本 PR 自己剩下的那部分(ADR-0136 的 fault semantics)。⭐ 这正是裁决 甲 被撤回的那一类错:句子对不上交付。packages/spec的那套(typecheck、包测试、check:generated或仓里等价的那道),逐条把命令与输出写进报告;⛔ 退出码在任何管道之前取。红线(与收班无关,照旧)
docs/adr/**⇒ ⛔ 不提交批准 review、⛔ 不合并、⛔ 不翻 ready、⛔ 不入队、⛔ 不挂 auto-merge。解冲突不是批准。Generated by Claude Code
os-elon-musk commentedon Sep 19, 2026
冲突解决轮 r2 —— 上一个 dev 静默死亡,零产出;冲突面重量后未变 · 2026-09-19T07:54Z
Seat
domain:spec#3(留守姿态,⛔ 非重新坐席),session_019srGWGCBBCBHqcDoRZpQRh。r1 的结局:死了,且没交终报。 派发令
5738591057发于 2026-09-19T02:24Z;现读 2026-09-19T07:53Z:分支顶仍是1bbe8f562(committed 2026-09-18 13:00:02 UTC,自派发前就没动过)· 本卡无任何os-dev-report· PR #18985 仍mergeable_state: dirty(连读两遍)。⇒ 一个字节都没产出。宿主未回报中止,子代理消失是正常死法(本班第二次:#17852 的 dev 也这样死在容器重启里)⇒ 走死认领回收重派,⛔ 报告缺席永不读作成功。重量后的冲突面 —— 与 r1 逐项相同,只有一个数变了:
eeaa88245)d8b12fca9d8b12fca9—— 未变packages/spec/src/shared/expression.zod.ts,仅此一个ce5785790= PR #18638,卡 #15811)api-surface-declarations/{root,shared}.txtmerge=os-regen)docs/adr/0089、docs/adr/0137⇒ 派发令
5738591057的每一条仍然成立,逐字沿用,只把「83」读作「97」。特别是那条最重要的读数不变:两处冲突都是散文,两侧各自独立把expression()与cel()的返回类型收窄成EvaluatedExpression,函数体在冲突块之外且逐字相同 ⇒ 任何解决都不丢行为,⛔ 不必回维护者裁。git push到自己的分支),确认推送通道活着;r1 死得太早,本席无法排除它连推都没推成。若探针就失败,立即回报,⛔ 不要先干两小时再发现推不上去。⛔ 受管面红线照旧:不批准、不合并、不翻 ready、不入队。本卡欠维护者的两件(D7 roster 行确认 + 受管面批准)不因本轮改变。
Generated by Claude Code
docs(changeset): narrow the #17778 entry to what it delivers alone
os-elon-musk commentedon Sep 19, 2026
os-dev-report
{ "issue": 17778, "status": "done", "branch": "claude/issue-17778-field-rule-predicate-fault-semantics", "pr": "https://github.com/objectstack-ai/objectstack/pull/18985", "session": "session_019srGWGCBBCBHqcDoRZpQRh", "premise_still_valid": true, "summary": "Merged origin/main (eeaa88245) into the PR branch, resolved the single prose-only conflict in packages/spec/src/shared/expression.zod.ts by combining both sides' docblocks into one, regenerated api-surface-declarations, and narrowed the changeset to drop the return-type-narrowing paragraphs that duplicate what PR #18638 already shipped. Correction to the round-2 premise: round 1 did not produce zero output -- it left a valid, uncommitted-to-origin merge commit in the leftover worktree /home/user/wt-17778 (merged to main tip 97466dd75, docblocks already resolved correctly) before dying. I verified that resolution against the dispatch's requirements, reused it, then merged the remaining main drift on top (eeaa88245) rather than redoing the conflict resolution from scratch -- so the branch now carries two merge commits instead of one. Pushed sha f11ea36be35bb0858c57d8f7aeec5777b3fec6ab.", "tests": "pnpm --filter @objectstack/spec build: exit 0 (34/34 dts emitted). pnpm --filter @objectstack/spec typecheck: exit 0 (tsc --noEmit + check:scripts-typecheck + check:test-typecheck, 259 pinned test-typecheck-debt errors unchanged). pnpm --filter @objectstack/spec test: exit 0, 496 test files / 14558 tests passed, duration 185.68s. pnpm --filter @objectstack/spec check:generated: exit 0, all 16 generated artifacts current (including check:api-surface-declarations). node scripts/check-nul-bytes.mjs: exit 0. node scripts/check-changeset-fixed.mjs, check-changeset-no-major.mjs --base origin/main, check-empty-changeset.mjs --base origin/main, check-adr-0087-registration.mjs --base origin/main, check-adr-links.mjs, check-adr-symbol-anchors.mjs, check-spec-docblock-symbol-anchors.mjs: all exit 0. All exit codes captured before any pipe. CI on the new head (f11ea36be) is in_progress at report time (14 in_progress, 1 success, 1 skipped, 1 queued) -- not chased further per instructions.", "mcp_calls": "0 -- no mcp__github__* tool called this round (all GitHub reads/writes went through the REST proxy or plain git)", "api_writes": "2 total: (1) POST /repos/objectstack-ai/objectstack/issues/17778/comments (this report); (2) prior probe GET-only reads do not count -- git push (twice: first attempt rejected LOCALLY by the pre-push os-regen hook before any network contact, confirming no 403; second attempt succeeded and updated the ref) is a git-protocol write, not counted in this REST tally per the write-budget split", "open_questions": [], "out_of_scope_findings": [ "noted, not filed: commit f11ea36be's message body has a shell-quoting artifact -- an unescaped backtick pair around `cel`/`expression` was evaluated by bash during `git commit -m`, leaving a stray '/' where the intended text was 'the identical `cel`/`expression` return-type narrowing'. Cosmetic only (no card relation, no model identifier, no false claim); left as-is per the never-amend rule. Carrier: whoever next touches this branch's history, or none if it merges as-is.", "noted, not filed: two merge commits (ec4bfc5df merging to 97466dd75, then f11ea36be's parent merging to eeaa88245) now sit on this branch instead of one, because round 1's real-but-unpushed merge commit was reused rather than discarded. Not a defect -- both are clean, ordinary merges -- but it deviates from the single-merge shape the dispatch described. Carrier: none, informational only." ] }Resolved docblocks -- before / after
expression()-- this PR's side had a multi-line signature and a short paragraph citing only the producer-side-defect framing; main's side had a single-line signature and cited#15811's evaluated-slot narrowing plus the "never widen back" prohibition. Merged (main's signature formatting, both sides' prose folded into one, non-repeating block):cel()-- this PR's side wrote a long, standalone argument repeating the same reasoning; main's side pointed atexpression()'s docblock. Took main's one-liner sinceexpression()'s merged docblock (above) now carries the combined reasoning and a second copy would repeat itself:Both function bodies are byte-identical to both pre-merge sides and were outside the conflict block throughout.
Generated artifacts
packages/spec/api-surface-declarations/root.txt(2469 lines touched) andshared.txt(4 lines) were regenerated viapnpm --filter @objectstack/spec run gen:api-surface-declarationsafter building spec's dist. The diff is dominated by main's own #18638 narrowing (everysource?: stringbecomessource: stringacross ~36 declaring positions) and the ordinary drift of catching this branch up 97+ commits -- not by anything this PR's docblock merge introduced (docblocks are not captured in this artifact; only the two exported signatures are, and both already read): EvaluatedExpressionpre- and post-regen). Exported-name count moved 212 -> 219 (new exports landed on main in the interim, e.g. printCelAst).check:generatedconfirms all 16 spec-generated artifacts, including this one, are current on the merged tree.Changeset -- what changed and why
.changeset/17778-field-rule-predicate-fault-semantics.mdoriginally opened with three paragraphs announcing thecel/expressionreturn-type narrowing toEvaluatedExpression"Fixed at the producer (#17778)". That is no longer true post-merge: main's PR #18638 (.changeset/15811-evaluated-expression-slots-source-required.md) already documents the identical narrowing in its own closing paragraph ("So do the four expression constructors ... which now return EvaluatedExpression instead of Expression"), and ADR-0137's own status line states outright that "this record's own PR carries no schema change" -- D1 (the narrowing) is #18638's, not #17778's. Keeping those three paragraphs would have published the same fact twice in one release under two different entries, one of them misattributed. I removed the three redundant paragraphs and kept only what #17778 delivers alone: the ADR-0137 predicate fault-semantics contract (D2-D5) and its ADR-0089 addendum, plus a one-line pointer to #18638 for the narrowing story instead of restating it. Bump level (patch) and theClause-2: nomarker are unchanged.Pushed sha
f11ea36be35bb0858c57d8f7aeec5777b3fec6ab(was1bbe8f562...before this round).git merge-base --is-ancestor origin/main HEADconfirms origin/main (eeaa88245) is now a full ancestor.PR mergeable: true,mergeable_state: blocked(branch-protection/review gating on a draft with governed files, not a conflict). Draft state, labels and assignees are untouched, per the hard constraints; no review submitted, no ready flip, no merge, no PR-body PATCH.Generated by Claude Code
os-elon-musk commentedon Sep 19, 2026
验收 —— 冲突已解,并更正本席一条说过头的断言 · 2026-09-19T08:21Z
Seat
domain:spec#3(留守姿态)。dev 终报见5740437439。⛔ 先认错:本席说 r1「一个字节都没产出」,说过头了
本席在
5740324584里写:r1「一个字节都没产出」。依据是三个读数:分支顶未动、卡上无终报、PR 仍dirty。那三个读数本身没错 —— 但它们只能回答「有没有东西到达 origin」,而本席把结论写成了「有没有产出」。⇒ r2 现场发现:r1 其实解完了冲突,把一个合到97466dd75的有效 merge commit 留在残存 worktree/home/user/wt-17778里,docblock 已按派发词正确并好,只是死在推之前。r2 核过那份解决、复用它,再把剩下的 main 漂移合在其上。⭐ 教训写死,因为它正是本班反复栽的同一类:仪器答的是「到没到 origin」,而本席把它读成了「有没有工作」。⇒ 后任判死认领前,除了 GitHub 侧三读数,再看一眼
/home/user/wt-<issue>还在不在 —— 一个死掉的 dev 的 worktree 可能握着真工作,而任何 GitHub 侧的仪器都看不见它。⛔ 「没到 origin」⛔ 不等于「没做」。(附带后果,记而不改:分支上因此有两个 merge commit 而不是一个 —— r1 的与 r2 的。两个都是干净的普通合并,⛔ 永不 amend/rebase 去「整理」它。)
本席独立自验的(⛔ 不采信终报自述)
mergeable: true连读两遍(ms=blocked是受管面等批准的正常态)expression.zod.ts里 0 个ec4bfc5df合 main →28ebef33b重新生成声明面 →f11ea36be收窄 changeset ⇒merge=os-regen的顺序对⛔ Never widen these back to buy a call site与#15811引用在;本 PR 的PRODUCER (Prime Directive #12)、EvaluatedExpression可赋给Expression的论证也在;两侧各举的 slot 被合并点名(visibleWhen+FlowEdgeSchema.condition);cel()收成一行交叉引用,不重复api-surface-declarations/*.txt重新生成后与 main 逐字相同,从 diff 里掉出去 ⇒ 本 PR 现在完全不改已发布声明面。这是重新生成自己作的证minor→patch,并新增「Not carried by this entry」段明写EvaluatedExpression收窄由 #18638 先发 ⇒ ⛔ 不二次宣告已落地的事。这正是裁决 甲 被撤回的那一类错,这次没踩check-clause2-carriers.mjs --pair 18985真退出码 0(⛔ 不取管道后的$?):卡侧经更正5727728428读作Clause-②: no,与 changeset 的Clause-②: no一致按声明收下、⛔ 不作读数的:build 34/34 dts · typecheck 干净(259 条 pinned debt 未变)· 496 测试文件 / 14558 用例通过 ·
check:generatedexit 0(16 个产物全新)· 另 8 道 changeset/ADR 门禁 exit 0。本席的检出没有node_modules,复跑不了;⇒ 记为 dev 的声明,由 CI 独立作证。CI 在新 head
f11ea36be上(现读 2026-09-19T08:20Z):32 个 check 名,16 个在跑,0 红。⛔ 在跑不读作绿。一条值得留下的平台事实
dev 的第一次
git push被本地 pre-push 的 os-regen hook 在任何网络接触之前拒掉 —— 所以它不是 403、不是通道问题。⇒ 「推不上去」的第一因可能在本地钩子,排查顺序应先看钩子输出再怀疑凭据。余下
⛔ 本 PR 的面含受管
docs/adr/**,且现在只剩两件,两件都只有维护者能动:① D7 roster 行的确认;② 受管面批准。⇒ 在批准在案之前,本席不翻 ready、不入队、不合并。CI 转全绿后本席只补一条读数,⛔ 不做别的。Generated by Claude Code
os-elon-musk commentedon Sep 19, 2026
读数:CI 在
f11ea36be上全绿,PR 现读mergeable_state: clean。2026-09-19T08:42Z逐名归约(每个 check 名取绑在当前 head 的最新 run):35 个名字 —— 32 success · 3 skipped · 0 非绿;另查 legacy commit-status 通道:success(1 条,即 Vercel 部署)。⇒ 冲突已解 + 门禁全绿,两样都成立。
ms=clean⛔ 不是放行。 它此前读blocked是因为检查在跑;检查过完就转clean。⇒ 这说明没有任何机制门禁在这条路径上拦着合并 —— 拦着它的是章程,不是平台:docs/adr/**(0089与0137两份)。⭐ 这一条值得单独写出来,正因为
clean读起来像「可以走了」。平台此刻不会阻止一次错误的合并,所以这里唯一的防线是这段字和挂在它上面的纪律。⛔ 本席除本读数外不动本 PR。事件仍订阅着:转红或状态变化会把本席叫起来,那时本席只报读数,⛔ 不自行放行。
Generated by Claude Code
os-elon-musk commentedon Sep 20, 2026
更正:本席对这张 PR 落地机制的描述不准,工具的原话比本席的说法严 · 2026-09-20T09:46Z
维护者问「为什么不合并」,本席跑了权威仪器(此前本席一直凭「面含
docs/adr/**」的自推,⛔ 没在本会话对这张 PR 跑过它):读数逐字:
governed-surface predicate: **2 of 4** path(s) hit the register—— 命中的两条是docs/adr/0089-unify-visibility-predicate-naming.md与docs/adr/0137-predicate-fault-semantics-are-contract.md;未命中的两条是.changeset/…与packages/spec/src/shared/expression.zod.ts。它并注明「One hit governs the whole PR —— 混合 diff 一条命中即整 PR 分叉;proportion is not a question」。本席说法与工具原话的差别,以工具为准
本席此前反复写的是「欠受管面批准;批准在案后席位落地」。工具的原话是:
⇒ 差别是实质的,不是措辞:在这个 regime 下并不存在「批准在案 → 席位落地」这一步 —— 人工合并本身就是复核记录。所以从翻 ready 起,每一步都不是席位的,包括翻 ready 这一步本身。本席此前的说法把一个不存在的中间步骤写进了卡上,更正在此。
现状(现读)
ms=clean·mergeable: true· draft · CI 在f11ea36be上 35 个 check 名全绿(32 success · 3 skipped · 0 非绿)· legacy commit-status success。⇒ 机械面无一处欠,它等的就是一次人工合并。ms=clean⛔ 不是放行 —— 平台此刻不会阻止一次错误的合并,拦着它的是章程。本席不翻、不入队、不挂 auto-merge、不提交批准 review。仍欠的实质问题(⛔ 不是流程)
D7 roster 行的确认:删除已执行;恢复须连 docblock 一起改真,因为
e1978段落断言的那次重绑已不存在。这一条需要维护者的判断,与上面的合并动作是两件事。Generated by Claude Code
spec: hold a predicate to what the engine can run; declare its fault …