Skip to content

measure: what record:alert binds, and what still breaks it, at objectui pin 190fbd01d (no source change) - #11245

Closed
os-zhuang wants to merge 1 commit into
mainfrom
claude/issue-9167-record-alert-predicate-measurement
Closed

os-zhuang wants to merge 1 commit into
mainfrom
claude/issue-9167-record-alert-predicate-measurement

Conversation

@os-zhuang

@os-zhuang os-zhuang commented Aug 23, 2026 •

Copy link
Copy Markdown
Contributor

Part of #9167 — measurement only. The diff is empty by construction (a single empty commit so the PR can exist); no file in this repo or in objectui was edited. The card stays open: the authoring change this measurement unblocks is deliberately not applied here and is the PM's to rule on.

TL;DR — four readings, two of them reversing the previous round

  1. ⛔ The strike on steps 2–3 is NOT stale. It reproduces exactly at today's pin. Applying a has() guard in place to properties.visible still turns a correctly-hidden banner into a permanently-visible one, on both pages, with "has" is not a function in the console (D1, D2). Screenshot-confirmed: "Email not verified" rendered directly above the page's own "Email Verified: ✓ Yes" chip. Leave steps 2–3 struck.
  2. ⭐ record:alert now honours the node-level visibleWhen gate — the thing that was measured inert on 2026-08-20. visibleWhen: {cel,'false'} on the banner now HIDES it (E1), where the same probe did nothing before.
  3. ⭐ Node-level visibleWhen binds record and ctx.user, polarity-correct on both pages (F1/F2, G4/G5/G6b) — objectui c86185eb5 observed working through the vendored bundle, not inferred from its title.
  4. ✅ The follow-up the card actually wants is now measured SAFE — but only in the shape "move the predicate to visibleWhen (CEL) and remove it from properties.visible", with has() guards. Every leg green, including forced-sparse and a no-flash check. Not applied; see "What I recommend".

Environment

what value
framework origin/main @ 281069522, dedicated worktree, turbo build → Tasks: 71 successful, 71 total
console bundle rebuilt from the repo's pin: ✓ @objectstack/console dist ready (49388 KB) from objectui@190fbd01d061, with ✓ Bundle canary 'import/jobs' present and ✓ Console bundle carries THIS tree's @objectstack/spec, and only it
pin ancestry verified in this run: c86185eb5237a79d… "Bind record into node-level visibleWhen, and stop visible swallowing it (#5505)", Fri Aug 21 08:09:49 2026 — git merge-base --is-ancestor c86185eb5 190fbd01d → true. The clone is shallow and answers Not a valid object name first; re-run after git fetch --depth=400.
server objectstack dev --ui --seed-admin -p 39167 on its own sqlite file, 48 plugins, 132 seed rows
browser Playwright 1.62.1 driving chromium-1194 from PLAYWRIGHT_BROWSERS_PATH via an executablePath override (the bundled 1.62 expects build 1234; ⛔ playwright install was not run)
method every mutation is a network-response rewrite (page metadata and/or the detail read) against the shipped, unmodified bundle — so each result is an observation of the pinned renderer, not of a tree I built

What the block binds today (question 1)

The console's detail read is GET /api/v1/data/<object>?populate=…&top=1&filter=["id","=",<id>] — no $select — and it projects everything:

object declared fields keys returned only absent declared field nulls materialised as present keys
showcase_task 25 25 __search 9
sys_user 26 25 __search 9

Both gate fields (status, email_verified) are present. So the sparse case is not reachable on the ordinary path — it had to be forced by rewriting the response, which is what the C and G runs do.

On the wire (/api/v1/meta/page, 34 pages served): exactly 2 record:alert blocks exist, the two on this card, and both carry visible as a bare string with no visibleWhen — showcase_task_detail at regions[0].components[1], sys_user_detail at slots.alerts[0]. The two are identical on every dimension tested; the earlier "one is a {dialect:'cel'} envelope" claim stays falsified at this pin. Separately, all 6 authored visibleWhen values served are {dialect:'cel'} envelopes and 0 are bare — i.e. an author writing a bare string into visibleWhen gets CEL for free.

Runs

Every "hidden" verdict is paired with a mount of the same shape that must SHOW, and every run carries a page-render control. patched / keysRemoved are the interception counters proving the rewrite was live.

Baselines — both banners are correct today (unmodified)

run page / row verdict expected
A1 task in_review VISIBLE VISIBLE ✅
A2 task todo hidden hidden ✅
B1 sys_user self, email_verified=false VISIBLE VISIBLE ✅
B2 sys_user self, email_verified forced true (15 keys set) hidden hidden ✅

Fault direction on properties.visible (question 2) — fail-SOFT, observed

run probe verdict
H1 visible: "MARKER9167NOSUCHIDENT == 1" on a todo task VISIBLE — console: Failed to evaluate expression: ${MARKER9167NOSUCHIDENT == 1} … is not defined

An unevaluable properties.visible leaves the banner rendered. objectui#4807's reading — 「fail-soft 使作者门控的横幅永远显示」 — holds at this pin.

Forced-sparse, unguarded — still false, still hidden

run probe verdict
C1 task in_review, status stripped from the read (6 keys removed) hidden ✅
C2 sys_user self, email_verified stripped (2 keys removed) hidden ✅

Bare strings run on objectui's legacy JS evaluator, where a missing key is undefined and undefined == 'in_review' is plain false. The #8990 absent-key fault class still does not reach this surface. The card title's attribution stays falsified.

⛔ The struck step 2, executed literally — still harmful

run probe verdict expected
D1 todo task, visible: "has(record.status) && record.status == 'in_review'" VISIBLE hidden ❌
D2 verified user, visible: "has(record.id) && has(record.email_verified) && …" VISIBLE hidden ❌

Console on both: Failed to evaluate expression … "has" is not a function. Fail-soft turns that throw into a shown banner. D2's screenshot is the "Email not verified" banner sitting above "Email Verified: ✓ Yes". Steps 2–3 stay struck; nothing about c86185eb5 touched this path.

⭐ The node-level visibleWhen gate — inert on 2026-08-20, live now

run probe verdict reads as
E1 in_review task (baseline VISIBLE) + visibleWhen: {cel,'false'} hidden the gate is honoured — the 2026-08-20 probe of this exact shape changed nothing
E2 todo task, visible dropped + visibleWhen: {cel,'true'} VISIBLE paired mount: the gate can SHOW, so E1 is the gate and not a dead block
F1 todo, visible dropped + visibleWhen: {cel,"record.status == 'in_review'"} hidden record binds into the node gate
F2 in_review, same VISIBLE polarity-correct pair
E3 todo, visibleWhen: {cel,'true'} and the authored visible left in place hidden the two gates compose as AND — a visibleWhen does not rescue a false properties.visible

E3 is the operationally important one: a migration must remove properties.visible, not just add visibleWhen beside it.

has() on the visibleWhen/CEL face — works, and is load-bearing

run probe verdict
G1 in_review, visibleWhen: has(record.status) && record.status == 'in_review' VISIBLE ✅
G2 same + status stripped from the read (5 keys removed) hidden ✅ — the guard holds
G3 same but unguarded + status stripped (4 keys removed) VISIBLE — [object-ui] A conditional predicate failed to evaluate and was treated as its safe default (true) … Reason: [runtime] No such key: status
G4 verified user, guarded visibleWhen, visible dropped hidden ✅
G5 unverified self, same predicate VISIBLE ✅
G6b another user's record, same predicate (control: that page's own title) hidden ✅
I1 todo, guarded visibleWhen only, polled every 150 ms for 8 s never visible — no flash
I2 polling control: unmodified in_review page hidden → VISIBLE at 1414 ms — the poller does see banners

G3 is the warning label on the whole migration: on the CEL face a missing key is a fault, and that face is fail-soft too — so moving a predicate to visibleWhen without has() re-arms exactly the bug this card feared, on a surface that previously could not have it. G4/G5/G6b show ctx.user binds there as well (a fault on that half would have fail-softed G4 and G6b to VISIBLE; both are hidden).

Controls, stated (zero-hit discipline)

  • Every negative reading is paired: A1↔A2, B1↔B2, E1↔E2, F1↔F2, G1↔G2, G4↔G5↔G6b, I1↔I2.
  • Every run asserts a page-render control string from that page. The first G6 run failed its control (it used the other page's title) and was discarded and re-run as G6b rather than reported.
  • Interception counters are reported per run: metaPatched=1 proves the metadata rewrite reached the record:alert node; keysRemoved/keysSet prove the payload rewrite touched real rows. A zero there would have invalidated the run.
  • Banner detection is via [role="status"] / [role="alert"] node text, cross-checked against a body-text detector, and it is demonstrated live in both directions in the same session.

What I recommend the PM rule

Do not lift the strike; retire it and replace it. Steps 2–3 as written stay wrong forever — they target properties.visible, whose evaluator has no has() and never will. What changed is that a different, better remedy became available, and it is now measured green end to end:

On both pages, move the predicate off properties.visible and onto the component's declared visibleWhen, adding has() guards, and delete the visible key in the same edit.

  • examples/app-showcase/src/ui/pages/task-detail.page.ts:51 → visibleWhen: "has(record.status) && record.status == 'in_review'"
  • packages/platform-objects/src/pages/sys-user.page.ts:79 → visibleWhen: "has(record.id) && has(record.email_verified) && record.id == ctx.user.id && record.email_verified == false"

Four axes, as the escalation form requires:

  • Real business need — weak, and I will not inflate it. Zero users hit this today: the detail read projects every declared column on both objects, and the legacy evaluator answers the sparse case false, which is the correct verdict anyway. The pull is authoring coherence, not a live bug.
  • Long-term soundness — this is where it wins. It removes a dialect fork instead of adding one: the same declared slot, the same CEL engine, the same has() semantics every other predicate face was migrated to. It is also the downstream half the maintainer's 2026-08-20 ruling 「A 让 record:alert 认 visibleWhen(推荐)」 already ordered; the upstream half has landed and the pin carries it.
  • Hard to get wrong for AI-written metadata — decisive, and it still runs backwards today. An author who writes the sloppy bare visible: 'record.x == 1' gets a silently legacy-evaluated predicate that happens to work; an author who writes the guarded CEL form the platform teaches everywhere else gets a thrown predicate and a permanently-visible banner (D1/D2). Moving to the declared slot makes the correct-looking input the one that works, and a genuine fault there is at least loud (G3's warning names the key).
  • Startup scope — cheap and bounded. Two authored lines, no spec change, no new key, no renderer change; the whole authored surface is exactly the 2 blocks the wire census found.

⚠️ Two conditions I would attach to that follow-up, both measured here rather than assumed: (a) the visible key must be deleted, not left beside the new gate (E3); (b) the has() guards are mandatory on the CEL face — unguarded there is strictly worse than today (G3).

Limits of this run

  • Measured as the seeded platform admin, on one read shape, on the two pages this card names. No block-registry census (that was objectui#5401's deliverable, and it is no longer open).
  • email_verified=true could not be written through the data API (PERMISSION_DENIED … editable fields: name, image, ADR-0092), so the verified-user state is produced by rewriting the response — the same mechanism as every other probe here.
  • Read from source, used only to explain observations and never as evidence for them: record-alert.tsx, predicateInput.ts, and the pinned record-alert.visibleWhen.evidence.test.tsx header at objectui 190fbd01d.
  • No gate union to report: the diff is empty, so node scripts/pm/dispatch-gates.mjs derives nothing — "this branch changes nothing against 'origin/main' (merge base 2810695) — nothing to derive." Reporting a green gate sha here would be reporting the green of a tree nobody changed. git status --short is empty; the tree measured is 281069522.

Generated by Claude Code

Measurement-only branch. The diff is intentionally empty: this card asked for
a reading, not a rewrite, and the reading is carried in the PR body.

Measured at objectui pin 190fbd01d (which contains c86185eb5) against the real
console: properties.visible is still legacy-JS and fail-soft, an in-place has()
guard still breaks both banners, and node-level visibleWhen is now honoured on
record:alert and binds record.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RfyXxZ2WPjcjhuXpiQQc3y
@claude claude Bot added skip-changeset PR has no user-facing published change; bypasses the changeset gate and removed skip-changeset PR has no user-facing published change; bypasses the changeset gate labels Aug 23, 2026

Copy link
Copy Markdown
Contributor Author

Closing: this PR carried no source change (0-file diff, one empty commit), and the measurement report it existed to deliver now lives on #9167 — see the report comment there. The Check Changeset red is the arithmetic of a 0-file PR and was deliberately left alone: no changeset, no skip-changeset label (one I had added before the revised instruction arrived has been removed), no manufactured diff. The branch claude/issue-9167-record-alert-predicate-measurement is left in place rather than deleted.


Generated by Claude Code


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants