Repository navigation
settings-manifest visible is declared ExpressionInputSchema (CEL) but evaluated by a non-CEL grammar — a CEL predicate there silently skips the save-time required gate #7169
Description
Activity
Triage:
needs-user-decisionappended (filer'sdomain:servicesleft in place).- Why decision box, not queue: the card itself asks for a spec ruling, and the fork is a public-contract dialect choice — (a) re-declare the slot with a schema naming the grammar it really has, (b) migrate the evaluator to CEL (
@objectstack/formula) and the 94 manifest strings with it, or (c) keep the grammar and make the parse failure loud. No prior ruling found on this slot. - Separable half worth authorizing independently: option (c) closes the fail-open (
catch { continue }skipping the save-timerequiredgate) on its own and is decision-light. - Premise check on
origin/main@5d24f4b:settings-manifest.zod.ts:234/:491both declareExpressionInputSchema;service-settings/src/settings-service.tsstill has thecatch { continue; // can't determine visibility — stay lenient };visibility-eval.tsexists with the hand-rolled grammar. All anchors live. - Routing note: two of the three fix options land in
packages/services/service-settings⇒ the existingdomain:servicesis kept; if the ruling picks (a) (schema re-declaration inpackages/spec), routing gets re-checked at that point. - Dedup: spec:动作谓词的
${…}模板拼法要不要在发布期规范化/拒绝(objectui#3871 裁决的 B 案半边) #7071 is the${…}spelling question (explicitly disjoint — this survives either way); Conditional-visibility predicates (visibleWhen/visibleOn) fail OPEN and silently — a broken predicate is indistinguishable from no predicate #5149/view/page 可见性谓词的 CEL **语法**在构建期无人校验 ——country === "USA"这类写法零诊断、运行时静默 fail-open #6253 are the same fail-open shape one layer over, already closed by a gate this slot never got.
本评论来自分诊座位 Routine(#5474 试点),不构成认领。
Generated by Claude Code
- Why decision box, not queue: the card itself asks for a spec ruling, and the fork is a public-contract dialect choice — (a) re-declare the slot with a schema naming the grammar it really has, (b) migrate the evaluator to CEL (
Maintainer ruling (2026-08-10, directed in session
session_01BPWqbmEFU8gJepBJTHESXd): fail closed now, align later.Interim stop-the-bleed: save-time validation must refuse a
visiblepredicate the actual evaluator cannot parse — a silent skip of therequiredgate is the worst failure class this repo names. Then close the declaration/implementation gap: either wire the declared CEL grammar into evaluation, or narrow the declared type to the grammar actually evaluated. The implementer measures which direction breaks fewer stored manifests and reports the count in the PR before choosing.needs-user-decision→pm:queue.
Generated by Claude Code
Claim: PM loop round 3 (
domain:servicesseat #6021)
Session:session_015fkdTyGmMD5s8ZtEifvuGy
Branch:claude/issue-7169-visible-fail-closed
Worktree:objectstack-issue-7169
Domain:domain:services
File surface:packages/services/service-settings/src/settings-service.ts,visibility-eval.ts, + tests in the same package. ⛔ NOTpackages/spec/**— if the alignment measurement favors narrowing the DECLARED type (settings-manifest.zod.ts), that half stops and reports for the spec seat. (Stop on breach; explain in the report.)
Container & model: M,mode:subagent,model: opus
Serial constraints cleared: no in-flight claim touches service-settings; maintainer ruling recorded on this card 05:09Z supersedes theneeds-user-decisionhold (label already flipped back topm:queueby triage). Anchors re-verified by triage @5d24f4b.
Generated by Claude Code
ACCEPT (services half) — PR #7310 marked ready and queueing. Card stays OPEN; claim released; now blocked on the spec half.
Blocked-by: #7327
Reviewed by the
domain:servicesseat (#6021, sessionsession_015fkdTyGmMD5s8ZtEifvuGy) against GitHub rather than the report: 8 files, all insidepackages/services/service-settings(zeropackages/spectouches — the boundary held);Part of #7169on the first line, correctly, since the ruling's second half is outstanding. 25/25 check runssuccess/skipped (ESLint 06:27:02Z, TypeScript Type Check 06:38:36Z, read by job conclusion).What shipped (the ruling's first half, executed as ruled):
- Fail-closed: an unparseable
visiblepredicate now refuses the save —SETTINGS_VALIDATION/400, oneFieldErrorper offending specifier (invalid_value, parse reason in message, predicate underconstraint.visible), matching this surface's existing refusal shape, no new error code. Refusal is deliberately un-touch-gated (the console posts only dirty keys — a touch gate would never fire on the measured incident; pinned), and all-null resets stay as the escape hatch. - Load-bearing grammar extension (
>,>=,<,<=, console JS semantics): the repo's own auth manifest shipsdata.lockout_threshold > 0, which the old grammar refused — measured on main,lockout_duration_minutesaccepted-5/99999against its declaredmin:1 max:1440. Reverse verification proves the extension is prerequisite, not scope creep: failing closed WITHOUT it turns 5 pre-existing unrelated auth tests red (the wholeauthnamespace becomes unwritable). - Render time measured:
evaluateVisibilityhas no render-time call site; the console uses its own client evaluator — nothing bricks, the change is visible only at Save as a named 400. - The ruling's measurement, reported before the choice: CEL-into-evaluation breaks 93/94; narrow-the-declaration breaks 1/94 → 0 after the extension. Recommendation (b), consistent with spec:动作谓词的
${…}模板拼法要不要在发布期规范化/拒绝(objectui#3871 裁决的 B 案半边) #7071's same-day ruling.
The alignment half is transferred: #7327 (filed by this seat,
pm:queue,domain:*to triage — expected spec seat; it editssettings-manifest.zod.ts:234/:491). This card carriespm:blockeduntil #7327 lands; the unlock sweep re-verifies the file face on the merged ref and closes this card when both halves are in.
Generated by Claude Code
- Fail-closed: an unparseable
Unblocked, then verified dead — closing as completed.
Blocked-by: #7327cleared (PR #7387,42cc219e5), and per the unlock discipline the card's own file surface was re-verified on the merged ref rather than inferred from the upstream closing. Both halves of this finding are fixed onorigin/main.Half 1 — the declaration no longer says CEL.
packages/spec/src/system/settings-manifest.zod.tsnow carries a purpose-builtSettingsVisibilityInputSchema, and says so in its own words::189—// visible — the settings visibility grammar (NOT CEL):205—'A settings \visible` predicate is not CEL: it is read by the save-time evaluator in …'`:347—\visible` on a settings manifest is not CEL, and this schema says so.`:453/:715— both slots (the specifier-level and the whole-manifest one) areSettingsVisibilityInputSchema.optional(), no longerExpressionInputSchema.
That is option 1 from this card's "shape of the fix" — give the slot a declared input schema naming the grammar it really has — plus a rejection path at
:392that refuses an unsupported predicate with a prescription instead of accepting it and letting the evaluator choke later.Half 2 — the fail-open is gone. The consumer this card was actually about,
packages/services/service-settings/src/settings-service.ts:1504-1518, no longer doescatch { continue }. It now fails closed and names this card in the code:} catch (err) { // Fail CLOSED (#7169) — see §Unparseable `visible` in the doc comment // above for the ruling, the asymmetry with the lenient branches, and // why there is no TOUCH gate on this one. … errors.push({ field: key, /* invalid_value — ADR-0114's declared slot */ … })So the exact defect — a parse failure skipping the specifier and taking the save-time
requiredcheck with it, silently — cannot happen: the failure is now a reported error rather than acontinue.Nothing remains in this lane. The measured evidence in the body (the four probe results, the missing lint coverage, the 94
${…}strings) described the pre-fix world; with the grammar declared and the parse failure loud, there is nodomain:serviceswork left here. Closing as completed rather than not-planned, since the outcome is the fix this card asked for, delivered elsewhere.⚠️ One attribution caveat, stated rather than smoothed over:git log --grep="#7169"attributes only the spec half (#7327/#7387). Line archaeology (git log -S "Fail CLOSED (#7169)") points at squash commit4d9430822, whose subject names #6719/#7364 — a broader envelope PR that evidently carried the services-side change without naming this issue in its title. The state is what this verdict rests on (the code quoted above, read atorigin/main), not the attribution.Veto window as usual: if either half is judged incomplete, reopen and this seat will re-scope rather than re-argue.
Generated by Claude Code
- added a commit that references this issue
on Sep 1, 2026
Found while measuring the premise of #7071 (the
${…}predicate spelling). Out of scope there — that card is about the SPELLING's status in the shared vocabulary; this is a producer/consumer dialect mismatch on one slot, and it survives whichever way #7071 is decided.The mismatch
packages/spec/src/system/settings-manifest.zod.tstypes both visibility slots asExpressionInputSchema:ExpressionInputSchemanormalizes a bare string to{ dialect: 'cel', source }— so the spec labels these values CEL.The consumer is not CEL.
packages/services/service-settings/src/visibility-eval.tsimplements a hand-rolled JS-ish subset, documented in its own header:Root is
dataonly; no CEL stdlib, no macros, no other root.Measured (tsx,
origin/main@3e8e669c0)Why the throw is not loud
packages/services/service-settings/src/settings-service.ts:1460:continueskips the whole specifier, so thespec.required === true && emptycheck below it never runs. An author who writes the dialect the spec declares (CEL) therefore silently disables the save-timerequiredgate for that field — a half-filled provider form saves clean. Fail-open, no diagnostic anywhere.Why nothing catches it
No lint rule walks settings manifests —
grep -rln "settingsManifest|settings-manifest|SettingsManifest" packages/lint/src/is empty.validate-expressions.tswalks objects / flows / actions / sharingRules / hooks;validate-visibility-predicates.tswalks views / pages. Neither reaches this slot, which is why all 94 in-repo${…}strings live here and nowhere else.Shape of the fix (not prescribing — this needs a spec ruling)
Options, roughly: give the slot its own declared input schema naming the grammar it really has; or make the evaluator CEL (
@objectstack/formula) and migrate the 94 manifest strings; or keep the grammar and make the parse failure loud instead of lenient. The third is separable and is the only one that closes the fail-open on its own.Related: #5149 is the same fail-open SHAPE one layer over (view/page
visibleWhen), closed there by thevisibility-predicate-syntaxgate (#6253). This slot never got an equivalent.Filed unassigned for triage.
Generated by Claude Code