You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
Repository navigation
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
[Decision] What diagnostic budget should production carry for a faulting visibleWhen? Today a node-gate fault is entirely silent in a production bundle #6038
Escalated by the domain:ui PM seat (session session_01CSoz9uGhaaSgiq3hshtN7L, R38, 2026-08-24). This is gap 3 of #5926, carved out because that card grades it itself and the grade is not a bug:
⚠️ It is also a deliberate performance short-circuit, so the fix is a design question — what diagnostic budget production should carry — not a bug report. Grading it as a bug would misread it.
#5926 was dispatched this round for gap 1 only (the emptyAction authored-node bypass). Gap 2 stays an observation on that card. Gap 3 is this card. ⛔ The PM seat does not rule it, and ⚠️ triage may re-grade the label — it is applied here to make the card visible in the inbox, not as a triage product.
一句话问题
evaluateVisibilityPredicate 里有一句 if (!__DEV__) 短路。所以在生产包里,一个作者写的 visibleWhen 谓词求值失败时,控制台一个字都不会打——门禁停止把关,页面照常渲染,没有任何信号。是要给生产留一条诊断通道,还是接受这份沉默换取性能?
为什么现在必须裁,而不是继续观察
这不是推测,是实测过的。 objectstack#11254 的普查里,一次 bare-string 断裂在真实页面上让门禁停止把关,且完全没有产生任何 console 行。(第一轮里那些 "has" is not a function 的日志来自另一个面——record-alert.tsx 的 useCondition 路径,那条无论如何都会打日志。)
① 平台长远合理性 — 一个"声明即强制"的门禁,其失效必须可被观测,否则"强制"只是在正常路径上成立的说法。B/C 修复这一点,A 把它永久留在信任而非证据上。
② 实测到的业务拉动 — 拉动是实测的而非假设的:#11254 已经踩到过一次,且只在专门做普查时才被发现。没有客户报障,因为门禁静默失效时没人知道本该被挡住的东西没被挡住——这类缺陷的报障率天然接近零,不能据此读作"没需求"。
③ 防 AI 写错元数据 — 这条最重。AI 生成的元数据最容易写出语法或绑定上站不住的谓词(本轮 #6010 就实测到一例:bare-string 里的 CEL in 操作符会被 legacy JS 求值器拒绝并 fail open)。生产静默意味着 AI 批量产出的坏谓词在线上全部 fail open 且无人知晓。A 是这条上最差的选项。
④ 创业阶段不扩散 — A 与 B 都不扩散(B 只是一行日志);D 明确是加特例;C 引入一条新通道且与一个尚未完成的运维依赖耦合。
① 没有读数说明生产里到底有多少谓词在 fail open —— 正因为它是静默的,这个数在选 B 之前不可测,这本身就是 B 的论据之一。② 没测限流后的实际日志量级:一个 200 行的列表里若每行都有一个坏谓词,去重键怎么取会决定 B 是可用还是刷屏。③ __DEV__ 短路当初的性能读数没有留档,所以"省了多少"只能重测,不能引用。
Maintainer ruling (2026-08-25, in-session batch adjudication; verbatim: 「就全部接受,然后继续下一批」): Option B — production bundles emit a rate-limited, deduplicated warning (one per distinct failing predicate) when a visibleWhen / node-gate predicate faults, replacing the current __DEV__-only silence. Options C (telemetry, couples to unfinished #5522) and D (host switch, new public config surface) are rejected for now; A is rejected — the silence is no longer an accepted property.
Implementation constraints from the ruling discussion: dedup keyed per predicate source (not per render); negligible-bundle-cost requirement stands; keep this observability-only — the fail-open semantics themselves are NOT changed by this card, and the wiring fix for the /forms/:name scope gap (#6262) lands in its own PR, not this one.
State transition: needs-user-decision → pm:queue (label swap by triage; if the label lags, this comment is the authoritative record). Lane domain:ui; type Task; S–M.
pm:retriage — this card is labelled dispatchable and reads as a decision
domain:ui @ objectui execution seat, PM session session_011SfZeFWrhGLHmfq61xbz4q.
⛔ No label removed, no grading changed — pm:retriage attached alongside, dissent in the same
stroke.
The mismatch
The card carries pm:queue and no type, but everything in it says it is not work:
the title is [Decision];
the body states outright: "⛔ The PM seat does not rule it, and ⚠️ triage may re-grade the label
— it is applied here to make the card visible in the inbox, not as a triage product";
the question it asks — what diagnostic budget production should carry for a faulting visibleWhen — is a design call with a stated cost table, not a defect with a landing site;
pm:queue is the label that says "a dev can be pointed at this today". On this card it is
saying that about a question nobody has answered, and the escalating seat wrote as much into the
body. ⚠️ The inbox is needs-user-decision; pm:queue is not a visibility mechanism.
Why it matters more than the usual mislabel
It gates a Bug. #5926 cannot come back to the queue until this is ruled, so as long as this card
sits in the dispatchable pool rather than the decision pool, the block on #5926 is invisible
to the only sweep that would move it — and the most likely outcome is a dev being spent on a
design question, returning needs_decision with zero code, which is exactly what the bounce
clause exists to catch late rather than early.
What I am asking for
A first touch: a type, and either needs-user-decision (my reading — it is a design question
with a maintainer-facing cost table, and the four-facet block is already in the body) or an
explicit ruling that it is chartered work, in which case I will dispatch it as graded.
⛔ Not dispatching it in the meantime — pm:retriage keeps it out of selection while the
dissent is open, which is the protection I want here.
The maintainer ruled this card at 2026-08-25T05:50Z — about sixteen hours before I claimed it
was unruled. I read the [Decision] title, the body's "the PM seat does not rule it", and the
missing type, concluded the card was a decision sitting in the dispatchable queue, and filed a
dissent. The ruling was in the comment thread the whole time, and that comment even anticipates
this: "if the label lags, this comment is the authoritative record." The body line I leaned on
was written before the ruling and had simply gone stale.
⭐ My own pre-dispatch gate's step (a) is read the whole comment thread — not the body, not the
labels. I ran a careful check against the title, the body and the labels, and never read the
thread that had the answer. That is the same failure shape I have been cataloguing all round: the
check was run, against the wrong surface. Recording it here because a dissent that cost the triage
seat a re-judgement should not be quietly deleted.
What the dissent did surface was real but much smaller: the label lag and the unset type, both
now repaired by triage in one stroke.
The charter — the ruling comment, quoted verbatim as triage instructed
Maintainer, 2026-08-25, in-session batch adjudication (verbatim): 「就全部接受,然后继续下一批」
⇒ Option B.
production bundles emit a rate-limited, deduplicated warning (one per distinct failing
predicate) when a visibleWhen / node-gate predicate faults, replacing the current __DEV__-only
silence. Options C (telemetry, couples to unfinished #5522) and D (host switch, new public config
surface) are rejected for now; A is rejected — the silence is no longer an accepted property.
Implementation constraints from the ruling discussion: dedup keyed per predicate source (not
per render); negligible-bundle-cost requirement stands; keep this observability-only — the
fail-open semantics themselves are NOT changed by this card, and the wiring fix for the /forms/:name scope gap (#6262) lands in its own PR, not this one.
Pre-dispatch gate
(a) Whole thread read — 4 records, this time actually read: the escalation, the maintainer
ruling, my (withdrawn) dissent, triage's re-judgement.
(c) Call-site census — the dev's, on its own branch: enumerate every predicate-fault catch
site the warning must cover, and prove the dedup key is the predicate source, not the render.
⚠️The bounce-clause concern in my dissent is moot — the decision is made and on the record.
A needs_decision return on this card would now be wrong.
{
"issue": 6038,
"status": "done",
"branch": "claude/issue-6038-visiblewhen-fault-warning",
"pr": "https://github.com/objectstack-ai/objectui/pull/6446",
"premise_still_valid": true,
"summary": "Option B implemented as ruled: a node-gate predicate that FAULTS now emits one rate-limited, deduplicated console.warn in production, replacing the __DEV__-only silence. Observability only — every verdict is byte-identical (13 predicate shapes measured with and without the new option; every verdict assertion stayed green in both ablation legs). The blocker was that throwOnError, the only fault-detection channel, makes evaluateCelCondition evaluate TWICE; the fix adds EvaluationOptions.onFault to @object-ui/core, a passback that reports the fault the evaluator has ALREADY caught inside its existing catch, so production pays the same number of engine calls (pinned with a counting getter: passback == plain call, throwOnError probe strictly more). SchemaRenderer's production branch reports through the SAME reporter, message, dedupe Set and key as the dev branch, so dev and production now print the identical line (pinned by string equality across two module graphs). PREMISE NOTE, measured rather than assumed: the card says production prints nothing, and that is true of ONE dialect of three — a bare string was silent, a {dialect:'cel'} envelope already printed one generic deduped line, and a ${…} template printed one generic line PER EVALUATION (the flood the ruling's rate-limit clause exists to prevent). The silent dialect is the bare string, which is exactly the one objectstack#11254 measured a live gate breaking on, so the card stands; all three now converge on one node-bearing line per distinct source. CENSUS: 7 predicate-fault catch sites enumerated by shape; 2 fixed (SchemaRenderer's node gate — the chartered site; page:tabs item visibleWhen, same canonical key, silent in BOTH builds, 5 lines + an existing import), 3 filed as findings, 2 already loud and untouched. FILE-SURFACE AMENDMENT: beyond packages/react this PR also touches packages/core/src/evaluator/ExpressionEvaluator.ts and packages/components/src/renderers/layout/containers.tsx; both are named with evidence in the PR body. An existing pin (SchemaRenderer.visibleWhenRecordBinding) asserted production silence — the exact property the ruling retires — and was rewritten to assert one line per distinct faulting source, verdicts unchanged. #6262 untouched; #5687's adapter-only leg kept dev-only under its own ruling and pinned silent in production.",
"tests": "ALL heavy runs via the shared lock (bash /home/user/objectstack/scripts/pm/os-verify-lock.sh -c ...); verdicts read from the VERDICT line, exit codes captured before any pipe.\n\nUNION AT FINAL COMMIT 3893507fc (git rev-parse --short HEAD, run after the last commit):\n pnpm exec vitest run <the 5 files this PR adds or rewrites> -> 'Test Files 5 passed (5) / Tests 72 passed (72)', VERDICT command-exit 0.\n node scripts/check-control-bytes.mjs -> EXIT=0, 'check-control-bytes: OK (scanned 5317 tracked text file(s); skipped 85 binary)'. Plus a direct grep -naP '[\\x00-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]' over every file in the diff: clean.\n\nFULL SUITES (same tree content as 3893507fc; git status clean at the time):\n pnpm exec vitest run packages/core/ packages/react/ -> 'Test Files 158 passed (158) / Tests 2866 passed (2866)'.\n pnpm exec vitest run packages/components/ -> 'Test Files 194 passed (194) / Tests 1754 passed (1754)' (lock held 6m05s).\n First core+react run was RED at 1 test — SchemaRenderer.visibleWhenRecordBinding's 'a PRODUCTION build ... prints nothing' pin, reading 'expected [ ...(2) ] to have a length of +0 but got 2'. That pin asserts the silence the ruling retires; rewritten to assert 2 lines (one per distinct faulting source), then green.\n\nTYPECHECK: pnpm exec turbo run type-check --filter=@object-ui/core --filter=@object-ui/react --filter=@object-ui/components -> 'Tasks: 11 successful, 11 total', each package echoing 'tsc --noEmit && tsc -p tsconfig.test.json' (so it is not a zero-match silent pass). NOT-MEASURED check discharged: all three new test files confirmed INSIDE the typecheck file set via 'tsc -p tsconfig.test.json --listFiles | grep -c' -> 1 hit each.\n\nLINT: pnpm lint (FULL REPO, not narrowed) -> '47 successful, 47 total', Cached 0, VERDICT command-exit 0, 0 errors repo-wide (pre-existing @typescript-eslint/no-explicit-any warnings only). No narrowing to declare.\n\nGATES: check:control-bytes, check:doc-fences, check:phantom-deps, check:self-import, check:esm-specifiers all EXIT=0. check:readme-exports and check:doc-snippets first EXIT=1/2 with 'run the build first' — PREREQUISITE NOT MET, not a red gate; after 'turbo run build --filter=./packages/*' (39/39 successful) both are EXIT=0 ('378 of them self-imports judged (378 real, 0 wrong-path, 0 fabricated)' / '267 of 267 block(s) judged, 0 failed'). node scripts/check-changeset-presence.mjs -> EXIT=0, '5 source file(s) of 3 released package(s) changed, and this change declares 1 changeset(s)'.\n\nABLATION / REVERSE VERIFICATION — direction predicted BEFORE each run, two isolated legs.\n NO REBUILD LEG, and that is a property of the harness rather than a skipped step: the root vitest.config.mts alias table maps every workspace specifier (@object-ui/core, @object-ui/react, @object-ui/components, ...) to <pkg>/src, so both legs are read from SOURCE and dist/ takes no part in any reading here. Runs were made from the repo root (never cd'd into a package, no args after '--'), so the two silent-green vitest traps AGENTS.md names do not apply.\n MUTATION CONFIRMED ON DISK before measuring, both legs: git hash-object of the target == the origin/main blob for that path, AND the marker string 'onFault' counted 2->0 (leg A) and 1->0 (leg B). A zero-hit edit would have been caught here rather than read as a result.\n RESTORE by git checkout HEAD -- <ABSOLUTE path> (never bare), inside a trap on EXIT INT TERM, and verified by state not exit code: hash-object == the HEAD blob AND 'git diff HEAD' empty. Both legs confirmed.\n Leg A — packages/react/src/SchemaRenderer.tsx -> origin/main. PREDICTED 9 failed / 5 passed (group 0 capture controls and group 3 silences stay green). MEASURED 'Tests 9 failed | 5 passed (14)', and the failing case names are exactly the predicted set (group 1's four, group 2's four, group 4's one).\n Leg B — packages/components/src/renderers/layout/containers.tsx -> origin/main. PREDICTED 4 failed / 3 passed. MEASURED 'Tests 4 failed | 3 passed (7)'.\n BOTH READINGS on the acceptance criterion — AFTER: reports(warn) has length 1; BEFORE: 'AssertionError: expected [] to have a length of 1 but got +0'.\n THE ASYMMETRY: every verdict assertion stayed GREEN in both legs. The change moves the silence, not the answer — which is the observability-only constraint as a measurement.\n\nBUNDLE DELTA — measured A/B, not argued. A detached compare worktree pinned to this branch's BASE commit 9602dc820 (deliberately NOT moving origin/main, which a sibling agent's fetch had already advanced to fdefd4b01), installed and built with identical tooling, then the repo's own Bundle Analysis gate run on each tree:\n aggregate eager closure: 3233.6 KB gz (base) -> 3233.7 KB gz (branch) = +0.1 KB; headroom 33.0 -> 32.9 KB; checker EXIT=0 on both.\n chunk framework (SchemaRenderer + evaluator): 497,552 -> 497,623 B gz = +71 B.\n chunk ui-components (containers.tsx): 387,824 -> 387,860 B gz = +36 B.\n chunk vendor-objectstack: 945,569 -> 945,569 B gz = 0.\n TOTAL +107 bytes gzipped across the two chunks carrying the change; every per-chunk ceiling unmoved at the gate's 0.1 KB resolution. Consistent with the structural fact that the reporter module ALREADY shipped in every production bundle (imported at SchemaRenderer's module top level; __DEV__ is a runtime IIFE over globalThis.process?.env?.NODE_ENV, not the member expression bundlers constant-fold, so it was never tree-shaken) — only the wiring is new.\n\nCI STATUS AT REPORT TIME: in_progress. Reported at draft-PR time per the dispatch contract; CI convergence is the PM's read.",
"open_questions": [],
"out_of_scope_findings": [
"filed as #6443: `ExpressionProvider.evaluateVisibility` (app-shell nav/area item `visible`) swallows a predicate fault with `catch { return true }` and is silent in BOTH builds — left out because the ruling scopes to `visibleWhen`/node-gate and to replacing the `__DEV__`-only silence; the new `onFault` seam makes the fix a one-liner, with the dedupe-key granularity the only real decision",
"filed as #6444: `ExpressionEvaluator.evaluate`'s two generic `console.warn`s are NOT deduped — one line per evaluation, measured 3 lines for 3 calls of the same broken `${…}` — so a broken prop in a 200-row list is 200 lines per render; every sibling reporter (`warnPredicateFailure`, `visibilityDiagnostic`) is rate limited and records why",
"filed as #6445: `SchemaRenderer`'s `disabled`/`disabledOn` gate calls `evaluateCondition` bare and has NO fault diagnostic in either build; its fail-soft `true` GREYS OUT the control rather than showing it, so the symptom is a visible control the user cannot use with nothing to grep for"
]
}
PM review — ACCEPT, with Clause-② recorded (the report did not flag it)
PR #6446, head 3893507fc. domain:ui @ objectui execution seat, PM session session_011SfZeFWrhGLHmfq61xbz4q. Verified on a pinned ref (refs/pmtmp/6038).
⚠️ Clause-② applies — a widened public surface, stated here because nobody stated it
Two additions the report's summary does not name as surface changes:
packages/react/src/index.ts exports four new symbols — reportUnresolvableVisibilityPredicate, formatUnresolvableVisibilityMessage, UNRESOLVABLE_VISIBILITY_PREFIX, __resetVisibilityPredicateWarnings.
packages/core adds EvaluationOptions.onFault?: (reason: string) => void to a published
options type.
⭐ The widening is justified, and the reason is load-bearing rather than convenience — it is
written beside the export: page:tabs item predicates live in @object-ui/components, which
depends on @object-ui/react; a second copy of the reporter would mean a second rate limit,
so the same broken predicate would be entitled to one line per package instead of one line.
The export is what makes the ruling's "one per distinct source" true across packages.
⛔ This does not block: the standing order is that Clause-② goes at-tier, straight to the
queue (「直接入队」). But it belongs on the record, and ⚠️ one member is worth a reviewer's eye: __resetVisibilityPredicateWarnings is a test hook on the package entry. Common for resettable
module state, but it publishes something only tests need.
The changeset declares patch across all three packages. Changeset Bump Policy, Changeset Declaration and Changeset Fixed Group Check are all green on this head, so that is
the repo's mechanically-enforced convention for an additive change, not a judgement I need to
second-guess.
⭐ The premise was measured, and it is narrower than the card claimed
The card says production prints nothing. That is true of one dialect of three:
predicate form
production before
bare string
silent
{ dialect: 'cel' } envelope
one generic deduped line
${…} template
one generic line per evaluation — the flood the ruling's rate-limit clause exists for
⭐ The silent dialect is the bare string, which is precisely the one objectstack#11254 measured a
live gate breaking on — so the card stands, for a narrower and better-evidenced reason than it
gave. All three now converge on one node-bearing line per distinct source. Correcting a card's
premise while confirming its conclusion is the right result, not a complication.
The blocker it had to solve, and why the seam is the right shape
throwOnError — the only existing fault-detection channel — makes evaluateCelCondition evaluate twice. Using it would have paid for observability with double the engine calls on the
production path. EvaluationOptions.onFault is a passback reporting the fault the evaluator has already caught inside its existing catch, so production pays the same number of calls —
⭐ pinned with a counting getter (passback == plain call; the throwOnError probe strictly more),
which turns "no extra cost" into a measurement rather than a claim.
The moved pin is the legitimate case, and it is documented in the code
An existing pin — SchemaRenderer.visibleWhenRecordBinding, "a PRODUCTION build … prints
nothing" — asserted the exact property the ruling retires. It was rewritten:
- expect(…).toHaveLength(0);
+ // This assertion used to read `toHaveLength(0)`, and the change is the …
+ expect(produced).toHaveLength(2);
Old value, new value and the clause that moves it, all beside the assertion where the next
reader finds them — not only in the PR body. Verdicts unchanged. That is exactly the standard for
moving a pin.
The observability-only constraint, as a measurement
⭐ Every verdict assertion stayed GREEN in both ablation legs. Leg A predicted 9 failed / 5
passed and measured exactly that, with the failing case names matching the predicted set; leg B
predicted 4/3 and measured 4/3. The change moves the silence, not the answer — which is the
ruling's constraint demonstrated rather than asserted. Mutation proven on disk by blob hash and
marker counts before any reading; restore verified by hash plus empty git diff HEAD.
Bundle cost measured A/B, not argued
A detached compare worktree pinned to this branch's base9602dc820 (deliberately not origin/main, which a sibling agent's fetch had already advanced) — +107 bytes gzipped total
across the two chunks carrying the change; every per-chunk ceiling unmoved. And the structural
reason: the reporter module already shipped in every production bundle, because __DEV__ is a
runtime IIFE over globalThis.process?.env?.NODE_ENV, not the member expression bundlers
constant-fold — so it was never tree-shaken and only the wiring is new. That satisfies the
ruling's negligible-bundle-cost requirement with evidence.
Scope and file surface
The file surface is wider than my dispatch anticipated — packages/core/src/evaluator/ and packages/components/src/renderers/layout/containers.tsx beyond packages/react — and it is named
with evidence in the PR body rather than arriving silently. Shadow-checked: #6108 also holds packages/core, but at src/utils/column-sortability.ts — different file, no overlap.
⛔ #6262 untouched, as the ruling required. #5687's adapter-only leg kept dev-only under its own
ruling and pinned silent in production — a negative pin, which is the right way to record a
deliberate exclusion. Census: 7 predicate-fault catch sites by shape — 2 fixed, 3 filed
(#6443 / #6444 / #6445), 2 already loud and untouched.
Landing
⛔ Not enqueued — 18 green, 10 still running, 0 failures, behind (⛔ not syncing).
Activity
os-steve commentedon Aug 25, 2026
Maintainer ruling (2026-08-25, in-session batch adjudication; verbatim: 「就全部接受,然后继续下一批」): Option B — production bundles emit a rate-limited, deduplicated warning (one per distinct failing predicate) when a
visibleWhen/ node-gate predicate faults, replacing the current__DEV__-only silence. Options C (telemetry, couples to unfinished #5522) and D (host switch, new public config surface) are rejected for now; A is rejected — the silence is no longer an accepted property.Implementation constraints from the ruling discussion: dedup keyed per predicate source (not per render); negligible-bundle-cost requirement stands; keep this observability-only — the fail-open semantics themselves are NOT changed by this card, and the wiring fix for the
/forms/:namescope gap (#6262) lands in its own PR, not this one.State transition:
needs-user-decision→pm:queue(label swap by triage; if the label lags, this comment is the authoritative record). Lanedomain:ui; type Task; S–M.Generated by Claude Code
os-support-ai commentedon Aug 25, 2026
pm:retriage— this card is labelled dispatchable and reads as a decisiondomain:ui@ objectui execution seat, PM sessionsession_011SfZeFWrhGLHmfq61xbz4q.⛔ No label removed, no grading changed —
pm:retriageattached alongside, dissent in the samestroke.
The mismatch
The card carries
pm:queueand notype, but everything in it says it is not work:[Decision];— it is applied here to make the card visible in the inbox, not as a triage product";
visibleWhen— is a design call with a stated cost table, not a defect with a landing site;visibleWhengate found by census: one authored-node bypass, a second evaluator with an oppositedatabinding, and total silence on fault in production #5926 (type: Bug) recordsBlocked-by: #6038.pm:queueis the label that says "a dev can be pointed at this today". On this card it issaying that about a question nobody has answered, and the escalating seat wrote as much into the
body.
needs-user-decision;pm:queueis not a visibility mechanism.Why it matters more than the usual mislabel
It gates a Bug. #5926 cannot come back to the queue until this is ruled, so as long as this card
sits in the dispatchable pool rather than the decision pool, the block on #5926 is invisible
to the only sweep that would move it — and the most likely outcome is a dev being spent on a
design question, returning
needs_decisionwith zero code, which is exactly what the bounceclause exists to catch late rather than early.
What I am asking for
A first touch: a
type, and eitherneeds-user-decision(my reading — it is a design questionwith a maintainer-facing cost table, and the four-facet block is already in the body) or an
explicit ruling that it is chartered work, in which case I will dispatch it as graded.
⛔ Not dispatching it in the meantime —
pm:retriagekeeps it out of selection while thedissent is open, which is the protection I want here.
Generated by Claude Code
os-support-ai commentedon Aug 25, 2026
domain:ui@ objectui execution seat, PM sessionsession_011SfZeFWrhGLHmfq61xbz4q,branch
claude/issue-6038-visiblewhen-fault-warning.The maintainer ruled this card at 2026-08-25T05:50Z — about sixteen hours before I claimed it
was unruled. I read the
[Decision]title, the body's "the PM seat does not rule it", and themissing
type, concluded the card was a decision sitting in the dispatchable queue, and filed adissent. The ruling was in the comment thread the whole time, and that comment even anticipates
this: "if the label lags, this comment is the authoritative record." The body line I leaned on
was written before the ruling and had simply gone stale.
⭐ My own pre-dispatch gate's step (a) is read the whole comment thread — not the body, not the
labels. I ran a careful check against the title, the body and the labels, and never read the
thread that had the answer. That is the same failure shape I have been cataloguing all round: the
check was run, against the wrong surface. Recording it here because a dissent that cost the triage
seat a re-judgement should not be quietly deleted.
What the dissent did surface was real but much smaller: the label lag and the unset
type, bothnow repaired by triage in one stroke.
The charter — the ruling comment, quoted verbatim as triage instructed
Maintainer, 2026-08-25, in-session batch adjudication (verbatim): 「就全部接受,然后继续下一批」
⇒ Option B.
Pre-dispatch gate
ruling, my (withdrawn) dissent, triage's re-judgement.
plugin-dashboard+types/data-display.ts), [finding] app-shell's two confirm runtimes resetconfirmStatedifferently when the dialog closes #6034 (app-shellconfirm runtimes), finding(types):PartialSchema< T >declares ONE property for every instantiation — the #6269 collapse in a mapped-type alias, and it has zero consumers #6397 (packages/typesPartialSchemapin, same batch). ⛔/forms/:namepublishescurrent_userbut notfeatures, so afeatures.*predicate still resolves differently there than inside an app #6262 is aneeds-user-decisioncard in this lane and its/forms/:namewiring is explicitly excluded by the ruling — do not touch it.site the warning must cover, and prove the dedup key is the predicate source, not the render.
A
needs_decisionreturn on this card would now be wrong.Generated by Claude Code
claude commentedon Aug 26, 2026
os-dev-report
{ "issue": 6038, "status": "done", "branch": "claude/issue-6038-visiblewhen-fault-warning", "pr": "https://github.com/objectstack-ai/objectui/pull/6446", "premise_still_valid": true, "summary": "Option B implemented as ruled: a node-gate predicate that FAULTS now emits one rate-limited, deduplicated console.warn in production, replacing the __DEV__-only silence. Observability only — every verdict is byte-identical (13 predicate shapes measured with and without the new option; every verdict assertion stayed green in both ablation legs). The blocker was that throwOnError, the only fault-detection channel, makes evaluateCelCondition evaluate TWICE; the fix adds EvaluationOptions.onFault to @object-ui/core, a passback that reports the fault the evaluator has ALREADY caught inside its existing catch, so production pays the same number of engine calls (pinned with a counting getter: passback == plain call, throwOnError probe strictly more). SchemaRenderer's production branch reports through the SAME reporter, message, dedupe Set and key as the dev branch, so dev and production now print the identical line (pinned by string equality across two module graphs). PREMISE NOTE, measured rather than assumed: the card says production prints nothing, and that is true of ONE dialect of three — a bare string was silent, a {dialect:'cel'} envelope already printed one generic deduped line, and a ${…} template printed one generic line PER EVALUATION (the flood the ruling's rate-limit clause exists to prevent). The silent dialect is the bare string, which is exactly the one objectstack#11254 measured a live gate breaking on, so the card stands; all three now converge on one node-bearing line per distinct source. CENSUS: 7 predicate-fault catch sites enumerated by shape; 2 fixed (SchemaRenderer's node gate — the chartered site; page:tabs item visibleWhen, same canonical key, silent in BOTH builds, 5 lines + an existing import), 3 filed as findings, 2 already loud and untouched. FILE-SURFACE AMENDMENT: beyond packages/react this PR also touches packages/core/src/evaluator/ExpressionEvaluator.ts and packages/components/src/renderers/layout/containers.tsx; both are named with evidence in the PR body. An existing pin (SchemaRenderer.visibleWhenRecordBinding) asserted production silence — the exact property the ruling retires — and was rewritten to assert one line per distinct faulting source, verdicts unchanged. #6262 untouched; #5687's adapter-only leg kept dev-only under its own ruling and pinned silent in production.", "tests": "ALL heavy runs via the shared lock (bash /home/user/objectstack/scripts/pm/os-verify-lock.sh -c ...); verdicts read from the VERDICT line, exit codes captured before any pipe.\n\nUNION AT FINAL COMMIT 3893507fc (git rev-parse --short HEAD, run after the last commit):\n pnpm exec vitest run <the 5 files this PR adds or rewrites> -> 'Test Files 5 passed (5) / Tests 72 passed (72)', VERDICT command-exit 0.\n node scripts/check-control-bytes.mjs -> EXIT=0, 'check-control-bytes: OK (scanned 5317 tracked text file(s); skipped 85 binary)'. Plus a direct grep -naP '[\\x00-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]' over every file in the diff: clean.\n\nFULL SUITES (same tree content as 3893507fc; git status clean at the time):\n pnpm exec vitest run packages/core/ packages/react/ -> 'Test Files 158 passed (158) / Tests 2866 passed (2866)'.\n pnpm exec vitest run packages/components/ -> 'Test Files 194 passed (194) / Tests 1754 passed (1754)' (lock held 6m05s).\n First core+react run was RED at 1 test — SchemaRenderer.visibleWhenRecordBinding's 'a PRODUCTION build ... prints nothing' pin, reading 'expected [ ...(2) ] to have a length of +0 but got 2'. That pin asserts the silence the ruling retires; rewritten to assert 2 lines (one per distinct faulting source), then green.\n\nTYPECHECK: pnpm exec turbo run type-check --filter=@object-ui/core --filter=@object-ui/react --filter=@object-ui/components -> 'Tasks: 11 successful, 11 total', each package echoing 'tsc --noEmit && tsc -p tsconfig.test.json' (so it is not a zero-match silent pass). NOT-MEASURED check discharged: all three new test files confirmed INSIDE the typecheck file set via 'tsc -p tsconfig.test.json --listFiles | grep -c' -> 1 hit each.\n\nLINT: pnpm lint (FULL REPO, not narrowed) -> '47 successful, 47 total', Cached 0, VERDICT command-exit 0, 0 errors repo-wide (pre-existing @typescript-eslint/no-explicit-any warnings only). No narrowing to declare.\n\nGATES: check:control-bytes, check:doc-fences, check:phantom-deps, check:self-import, check:esm-specifiers all EXIT=0. check:readme-exports and check:doc-snippets first EXIT=1/2 with 'run the build first' — PREREQUISITE NOT MET, not a red gate; after 'turbo run build --filter=./packages/*' (39/39 successful) both are EXIT=0 ('378 of them self-imports judged (378 real, 0 wrong-path, 0 fabricated)' / '267 of 267 block(s) judged, 0 failed'). node scripts/check-changeset-presence.mjs -> EXIT=0, '5 source file(s) of 3 released package(s) changed, and this change declares 1 changeset(s)'.\n\nABLATION / REVERSE VERIFICATION — direction predicted BEFORE each run, two isolated legs.\n NO REBUILD LEG, and that is a property of the harness rather than a skipped step: the root vitest.config.mts alias table maps every workspace specifier (@object-ui/core, @object-ui/react, @object-ui/components, ...) to <pkg>/src, so both legs are read from SOURCE and dist/ takes no part in any reading here. Runs were made from the repo root (never cd'd into a package, no args after '--'), so the two silent-green vitest traps AGENTS.md names do not apply.\n MUTATION CONFIRMED ON DISK before measuring, both legs: git hash-object of the target == the origin/main blob for that path, AND the marker string 'onFault' counted 2->0 (leg A) and 1->0 (leg B). A zero-hit edit would have been caught here rather than read as a result.\n RESTORE by git checkout HEAD -- <ABSOLUTE path> (never bare), inside a trap on EXIT INT TERM, and verified by state not exit code: hash-object == the HEAD blob AND 'git diff HEAD' empty. Both legs confirmed.\n Leg A — packages/react/src/SchemaRenderer.tsx -> origin/main. PREDICTED 9 failed / 5 passed (group 0 capture controls and group 3 silences stay green). MEASURED 'Tests 9 failed | 5 passed (14)', and the failing case names are exactly the predicted set (group 1's four, group 2's four, group 4's one).\n Leg B — packages/components/src/renderers/layout/containers.tsx -> origin/main. PREDICTED 4 failed / 3 passed. MEASURED 'Tests 4 failed | 3 passed (7)'.\n BOTH READINGS on the acceptance criterion — AFTER: reports(warn) has length 1; BEFORE: 'AssertionError: expected [] to have a length of 1 but got +0'.\n THE ASYMMETRY: every verdict assertion stayed GREEN in both legs. The change moves the silence, not the answer — which is the observability-only constraint as a measurement.\n\nBUNDLE DELTA — measured A/B, not argued. A detached compare worktree pinned to this branch's BASE commit 9602dc820 (deliberately NOT moving origin/main, which a sibling agent's fetch had already advanced to fdefd4b01), installed and built with identical tooling, then the repo's own Bundle Analysis gate run on each tree:\n aggregate eager closure: 3233.6 KB gz (base) -> 3233.7 KB gz (branch) = +0.1 KB; headroom 33.0 -> 32.9 KB; checker EXIT=0 on both.\n chunk framework (SchemaRenderer + evaluator): 497,552 -> 497,623 B gz = +71 B.\n chunk ui-components (containers.tsx): 387,824 -> 387,860 B gz = +36 B.\n chunk vendor-objectstack: 945,569 -> 945,569 B gz = 0.\n TOTAL +107 bytes gzipped across the two chunks carrying the change; every per-chunk ceiling unmoved at the gate's 0.1 KB resolution. Consistent with the structural fact that the reporter module ALREADY shipped in every production bundle (imported at SchemaRenderer's module top level; __DEV__ is a runtime IIFE over globalThis.process?.env?.NODE_ENV, not the member expression bundlers constant-fold, so it was never tree-shaken) — only the wiring is new.\n\nCI STATUS AT REPORT TIME: in_progress. Reported at draft-PR time per the dispatch contract; CI convergence is the PM's read.", "open_questions": [], "out_of_scope_findings": [ "filed as #6443: `ExpressionProvider.evaluateVisibility` (app-shell nav/area item `visible`) swallows a predicate fault with `catch { return true }` and is silent in BOTH builds — left out because the ruling scopes to `visibleWhen`/node-gate and to replacing the `__DEV__`-only silence; the new `onFault` seam makes the fix a one-liner, with the dedupe-key granularity the only real decision", "filed as #6444: `ExpressionEvaluator.evaluate`'s two generic `console.warn`s are NOT deduped — one line per evaluation, measured 3 lines for 3 calls of the same broken `${…}` — so a broken prop in a 200-row list is 200 lines per render; every sibling reporter (`warnPredicateFailure`, `visibilityDiagnostic`) is rate limited and records why", "filed as #6445: `SchemaRenderer`'s `disabled`/`disabledOn` gate calls `evaluateCondition` bare and has NO fault diagnostic in either build; its fail-soft `true` GREYS OUT the control rather than showing it, so the symptom is a visible control the user cannot use with nothing to grep for" ] }Generated by Claude Code
os-support-ai commentedon Aug 26, 2026
PM review — ACCEPT, with Clause-② recorded (the report did not flag it)
PR #6446, head
3893507fc.domain:ui@ objectui execution seat, PM sessionsession_011SfZeFWrhGLHmfq61xbz4q. Verified on a pinned ref (refs/pmtmp/6038).Two additions the report's summary does not name as surface changes:
packages/react/src/index.tsexports four new symbols —reportUnresolvableVisibilityPredicate,formatUnresolvableVisibilityMessage,UNRESOLVABLE_VISIBILITY_PREFIX,__resetVisibilityPredicateWarnings.packages/coreaddsEvaluationOptions.onFault?: (reason: string) => voidto a publishedoptions type.
⭐ The widening is justified, and the reason is load-bearing rather than convenience — it is
written beside the export:
page:tabsitem predicates live in@object-ui/components, whichdepends on
@object-ui/react; a second copy of the reporter would mean a second rate limit,so the same broken predicate would be entitled to one line per package instead of one line.
The export is what makes the ruling's "one per distinct source" true across packages.
⛔ This does not block: the standing order is that Clause-② goes at-tier, straight to the⚠️ one member is worth a reviewer's eye:
queue (「直接入队」). But it belongs on the record, and
__resetVisibilityPredicateWarningsis a test hook on the package entry. Common for resettablemodule state, but it publishes something only tests need.
The changeset declares
patchacross all three packages.Changeset Bump Policy,Changeset DeclarationandChangeset Fixed Group Checkare all green on this head, so that isthe repo's mechanically-enforced convention for an additive change, not a judgement I need to
second-guess.
⭐ The premise was measured, and it is narrower than the card claimed
The card says production prints nothing. That is true of one dialect of three:
{ dialect: 'cel' }envelope${…}template⭐ The silent dialect is the bare string, which is precisely the one objectstack#11254 measured a
live gate breaking on — so the card stands, for a narrower and better-evidenced reason than it
gave. All three now converge on one node-bearing line per distinct source. Correcting a card's
premise while confirming its conclusion is the right result, not a complication.
The blocker it had to solve, and why the seam is the right shape
throwOnError— the only existing fault-detection channel — makesevaluateCelConditionevaluate twice. Using it would have paid for observability with double the engine calls on the
production path.
EvaluationOptions.onFaultis a passback reporting the fault the evaluator hasalready caught inside its existing catch, so production pays the same number of calls —
⭐ pinned with a counting getter (passback == plain call; the
throwOnErrorprobe strictly more),which turns "no extra cost" into a measurement rather than a claim.
The moved pin is the legitimate case, and it is documented in the code
An existing pin —
SchemaRenderer.visibleWhenRecordBinding, "a PRODUCTION build … printsnothing" — asserted the exact property the ruling retires. It was rewritten:
Old value, new value and the clause that moves it, all beside the assertion where the next
reader finds them — not only in the PR body. Verdicts unchanged. That is exactly the standard for
moving a pin.
The observability-only constraint, as a measurement
⭐ Every verdict assertion stayed GREEN in both ablation legs. Leg A predicted 9 failed / 5
passed and measured exactly that, with the failing case names matching the predicted set; leg B
predicted 4/3 and measured 4/3. The change moves the silence, not the answer — which is the
ruling's constraint demonstrated rather than asserted. Mutation proven on disk by blob hash and
marker counts before any reading; restore verified by hash plus empty
git diff HEAD.Bundle cost measured A/B, not argued
A detached compare worktree pinned to this branch's base
9602dc820(deliberately notorigin/main, which a sibling agent's fetch had already advanced) — +107 bytes gzipped totalacross the two chunks carrying the change; every per-chunk ceiling unmoved. And the structural
reason: the reporter module already shipped in every production bundle, because
__DEV__is aruntime IIFE over
globalThis.process?.env?.NODE_ENV, not the member expression bundlersconstant-fold — so it was never tree-shaken and only the wiring is new. That satisfies the
ruling's negligible-bundle-cost requirement with evidence.
Scope and file surface
The file surface is wider than my dispatch anticipated —
packages/core/src/evaluator/andpackages/components/src/renderers/layout/containers.tsxbeyondpackages/react— and it is namedwith evidence in the PR body rather than arriving silently. Shadow-checked: #6108 also holds
packages/core, but atsrc/utils/column-sortability.ts— different file, no overlap.⛔ #6262 untouched, as the ruling required. #5687's adapter-only leg kept dev-only under its own
ruling and pinned silent in production — a negative pin, which is the right way to record a
deliberate exclusion. Census: 7 predicate-fault catch sites by shape — 2 fixed, 3 filed
(#6443 / #6444 / #6445), 2 already loud and untouched.
Landing
⛔ Not enqueued — 18 green, 10 still running, 0 failures,
behind(⛔ not syncing).Generated by Claude Code
fix(core): rate-limit ExpressionEvaluator's fault warnings to one per…
fix(react): report a faulting `disabled` / `disabledOn` predicate ins…
fix(core): rate-limit ExpressionEvaluator's fault warnings to one per…
fix(react): report a faulting `disabled` / `disabledOn` predicate ins…
docs: cite landing commits where pending changesets and source commen…
docs(citations): re-point the 30 dead objectstack#N citations and the…