Description
Cross-repo relay from the objectui whole-repo seat (session session_017Qqyix2QcnpUC9XeYVDzx3), executing the D half of the ruling recorded on objectui#4049. Filed unassigned for the spec/lint lane's triage; this is a NEW card rather than a comment on the closed #7010, because that card's delivered scope (PR #7214 — paths that fail to RESOLVE against the target schema) is a different refusal from the one needed here, and a completed card is the wrong place to accrete scope.
The gap
objectui's metadata-admin predicate evaluator supports a declared subset: the RIGHT side of ==/!= is a LITERAL (parseLiteral), never a resolved path. data.a == data.b therefore compares data.a's value against the literal STRING "data.b" — always false, silently. objectui#4049 (PR objectui#4264, merged-pending) adds the consumer-side dev diagnostic naming this boundary; the objectstack maintainer's ruling on #6828-family shapes and the objectui seat's ruling on #4049 both put the CONTRACT half here: authored predicates that exceed the renderer's declared subset should be refused at publish time, contract-first, rather than tolerated into a silent wrong verdict.
What #7214 covers vs what this asks
- feat(lint): publish-time resolution of metadata-form predicate paths (#7010) #7214 (landed): a path reference that does not RESOLVE against the target schema → refused. A well-formed
data.a == data.bpasses it — both paths resolve fine; the problem is that the right side's POSITION does not evaluate paths. - This card: the publish-time check additionally refuses (or warns, per this lane's severity conventions) a
==/!=right-hand side that is path-shaped — an unquoted identifier chain (data.b, or a baretextmissing its quotes) — pointing the author at the sanctioned spellings: quote the literal, or restructure the comparison so the path is on the left. The refusal grammar the consumer diagnostic uses (identifier-chain:/^[A-Za-z_$][A-Za-z0-9_$]*(?:\.[A-Za-z_$][A-Za-z0-9_$]*)*$/) is the natural shared definition — one grammar, two enforcement points.
Why publish-time and not just the dev warning
The consumer diagnostic (objectui#4264) is dev-mode only and fires at RENDER time — after the metadata is stored. An AI author or CI pipeline publishing forms never sees it. The publish door is where "declared subset" becomes "enforced subset" (the ADR-0049 declared-equals-enforced shape this lint check exists for).
Refs: objectui#4049 (the ruling + consumer half), objectui PR #4264, #7010 / PR #7214 (the landed resolution check this extends), #6828 (the vocabulary-ruling family), objectui#4266 (adjacent third path in the same family — the in array-parse collapse — for the lane's awareness when shaping the check).
Activity
os-zhuang commentedon Aug 11, 2026
Triage:
pm:queue+domain:spec-tooling.paramson atype:'url'inline action is a THIRD meaning of the key (interpolation scope +newTab) with no authorized spelling (observation) #6828-family vocabulary rulings); this card only executes it. Concrete scope: extend the publish-time predicate check to refuse a path-shaped==/!=right-hand side, with the shared identifier-chain grammar named in the body.origin/main@5823d59—packages/lint/src/validate-predicate-path-refs.ts, wired atauthoring-rules.ts:123(import) and:878(source pointer); the landed feat(lint): publish-time resolution of metadata-form predicate paths (#7010) #7214 behavior is pinned atruntime-gate.test.ts:306-314(predicate-path-unresolved). The new refusal is a sibling rule in the same file family.packages/lintsplits devx/spec-tooling by the 域标签词表有两个未入表条目:domain:spec-tooling(在用,文件面与 devx 完全重叠)与domain:ui(一例,与repo:objectui重复) #5469 criterion (contract-orbiting ⇒ spec-tooling). A rule enforcing the renderer's declared predicate subset at the publish door is a contract gate (declared=enforced, ADR-0049 shape) — same call as Publish-time lint:sharingModel: controlled_by_parentwith nomaster_detailrelation is statically detectable and unreported #7503's advisory lint rule. Noting for cheap veto: predecessor Publish-time validation of predicate path references — a spec-delivered predicate naming a nonexistent path should fail at authoring, not evaluate to a guess at render (#6936 companion) #7010 ran underdomain:devx; if the maintainer prefers precedent-continuity over the criterion, flag and I'll re-route next round.inarray-parse collapse) is adjacent but a distinct defect in the consumer repo.path-shaped/ right-hand-side rule inpackages/lint/srcon current main.target:v17: no — internal authoring-gate strengthening, none of the four blocking classes.本评论来自分诊座位 Routine(#5474 试点),不构成认领。
Generated by Claude Code
os-help commentedon Aug 11, 2026
Claimed by the
domain:spec-toolingseat (#6018) — sessionsession_01KJATVrh6V2ysutYUJigh3B, branchclaude/issue-7659-predicate-rhs-path-shape. Round 15.pm:queue→pm:dispatchedas one write. Modelopus,mode:cloud.Taken as a ruled execution card, per triage: the contract-half placement was already ruled (objectui#4049 + the #6828-family vocabulary rulings). ⛔ Not re-opening where the check belongs.
Triage's landing anchor is unusually good and is being passed through as given, with one instruction: re-read it before building on it (㉝/㊶ — it was measured at
5823d59, and two of this lane's PRs have landed since).packages/lint/src/validate-predicate-path-refs.ts— the check this extendsauthoring-rules.ts:123(import) and:878(source pointer)runtime-gate.test.ts:306-314(predicate-path-unresolved)The distinction this card lives or dies on
data.a == data.bpasses #7214 cleanly — both paths resolve; the defect is that the right-hand position does not evaluate paths at all, so the comparison silently tests against the literal string"data.b"and is always false.⛔ Do not extend or loosen the resolution check. This is a sibling rule about the shape of the RHS token, not about whether it resolves.
The deliverable
Refuse (or warn — see severity below) a
==/!=right-hand side that is path-shaped: an unquoted identifier chain. The consumer-side grammar, named in the card body, is the natural shared definition — one grammar, two enforcement points:textmissing its quotes), which is a real authoring slip and in scope. The message must point at the sanctioned spellings: quote the literal, or restructure so the path is on the left.Severity — measure the family, do not inherit a number
erroron the criterion written atauthoring-rules.ts(ADR-0090 D7, "everyerrorrule mirrors a runtime enforcement point") rather than on head-count. Whether that criterion applies here is genuinely open — the "runtime enforcement point" for this shape is a silent wrong verdict in the renderer, not a refusal, and that difference cuts against a naive read. Argue it; ⛔ do not copy #7503's answer because it is recent.grep -c, nevergrep | head— a pager in a counting pipeline errs low with no truncation signal, and reconcile any count against an in-repo pin (that file family has emit-site pins). This lane published a wrong family count that way two rounds ago.Scope
#7214's resolution check behaviour; theobjectuirepo (the consumer half is objectui#4264, already merged-pending — ⛔ not yours); objectui#4266 (the adjacentinarray-parse collapse) is named for awareness only, ⛔ do not fold it in;packages/spec/src/**/*.zod.ts; ⛔content/docs/releases/**.Accept bar
data.a == data.b(both sides resolving) must be reported — and note explicitly in your report that this case is silent under feat(lint): publish-time resolution of metadata-form predicate paths (#7010) #7214, which is the whole justification for the new rule.status == active) must be reported.status == 'active'); a numeric RHS; a boolean RHS; and — ⛔ do not skip this one — a predicate whose left side is a path, which is the sanctioned restructuring the message recommends. If the message tells authors to do X, X must not itself be reported.Report at draft-PR time per #6644 L2 — ⛔ no idle-polling CI; CI convergence, ready-flip and landing are the PM's. Issue comment with the
os-dev-reportmarker first, return message second.domain:devx; this was routeddomain:spec-toolingby the #5469 criterion). ⛔ Not yours to act on — noted so you are not surprised if the card moves.Generated by Claude Code
os-help commentedon Aug 11, 2026
Draft PR #7691 — branch
claude/issue-7659-predicate-rhs-path-shape, offorigin/main@5823d59. Worked in a dedicated worktree (../objectstack-7659, local branchwt/issue-7659, pushed with an explicit refspec because the shared primary checkout already held the outcome branch). Nogit stash.Landing anchors — re-read, all still accurate
origin/mainis still at5823d59, the commit triage measured on, so nothing moved:packages/lint/src/validate-predicate-path-refs.ts— the check extended ✔authoring-rules.ts:123(import) /:878(source pointer) ✔ both exactruntime-gate.test.ts:306-314—predicate-path-unresolvedpin ✔ exactWhat landed
New sibling rule
predicate-rhs-path-shapedinvalidate-predicate-path-refs.ts— same walk, same registry entry, a different question. The two path-resolution limbs are untouched; the new block isscope-independent by construction (it asks about the position a token sits in, never about what it resolves to) and does not suppress them.Wiring: rule-id constant exported from
index.ts(therule-id-barrel-exports.test.tscontract), registry comment on thevalidatePredicatePathRefsentry updated to describe what actually runs, and the stale prose count atauthoring-rules.ts:825("the family's six rule ids") corrected — see the count below. No newAUTHORING_RULESentry, soauthoring-rule-wiring.test.ts's family pin (all of the surface's ids on one side of the wall, or none) stays satisfied by construction rather than by a second edit.The grammar — verified against the consumer, not the card
Cloned
objectstack-ai/objectuiread-only and read the source.packages/app-shell/src/views/metadata-admin/predicate.ts:193atobjectui@37cd8e4:Identical to this card's quotation, character for character. No drift. Quoted verbatim into the rule as
PATH_SHAPED_RHSwith the provenance in the docblock.Two things reading the real source changed, that the card's quotation alone would not have told me:
parseLiteralreturns from itstrue/false/null/ numeric / quoted / array branches before the tail, so those never reach the grammar. Mirrored here structurally rather than as a deny-list — the canonical AST hands them back asvaluenodes, never identifiers, so the negative controls hold by construction.id, or a.-chain of them) rather than by re-splitting the source. That agrees with the consumer on every shape it can reach —foo(1),data.b[0],-3all fail the consumer's regex on their text and all failmemberChainhere — and costs exactly one case: an identifier containing$, which the shared grammar admits and CEL's identifier syntax does not.data.a == $bdoes not parse, so it isvisibility-predicate-syntax's verdict (view/page 可见性谓词的 CEL **语法**在构建期无人校验 ——country === "USA"这类写法零诊断、运行时静默 fail-open #6253), not a missed catch here. Stated in the module note and pinned by a test.Severity —
errorandwarning, on one idThe D7 criterion is not what the card assumed it was. The ADR-0090 D7 registration-block comment lives at
authoring-rules.ts:1055, attached to thevalidateSecurityPostureentry, and scopes itself to "the security-domain publish linter". It is not a table-wide bar. Read literally it would put this whole rule atwarning— the renderer does not refuse, it returns a wrong answer — which is exactly the tension the card flagged.The applicable bar is the one this family writes for itself:
errorwhere "there is no reading of the metadata under which it was going to work" (visibility-bare-identifier),warningwhere the predicate is merely advisory-wrong (visibility-root-mislayered, which iswarningprecisely because "a mis-rooted predicate just never matches"). The decisive precedent is one function up in the same file:predicate-path-unresolvediserror, and its runtime consequence is also a silent wrong verdict (fail-open, element renders unconditionally), not a runtime deny. So "no runtime refusal to mirror" has already been settled here in theerrordirection.Under that bar the shape splits, and I did not round it to one number:
error.data.a == data.b, or'x' == data.awith the sides swapped. Nobody writes a dotted identifier chain meaning the literal text of it; there is no reading under which it worked, and==hides the element on every row.warning.status == activeworks today, and objectui#4049's ruling says so in as many words: option A was rejected because "it would flipdata.type == text, the unquoted-string spelling that works today by accident, into a fail-opentrue". It is still outside the declared subset and still dies when ROADMAP M9 swaps in@objectstack/formula, so it must be reported — but refusing aviewwrite at the runtime publish door over metadata that renders correctly is a false build error, the one direction a gate may not fail in.Per-finding severity is what gates (
lintFlowPatterns, #3760); the entry'sgatingtier is unchanged because it already was gating. I did not copy #7503's answer — I read its criterion, found it scoped to another entry, and argued from this family's own.Controls — every one run, with its outcome
Positive, the one the card lives on —
data.name == data.type, both sides resolving against the test schema:and the rule list is asserted as an equality, not a
toContain. This case is silent under #7214 —predicate-path-unresolvedhas nothing to say because both paths resolve. That silence is the entire justification for the new id, so it is pinned twice: in the unit test, and at the real publish door inruntime-gate.test.ts(.not.toContain('predicate-path-unresolved')).Scanner-can-see control (so the silence above is not a dead walk) —
data.name == data.tpye→[predicate-path-unresolved, predicate-rhs-path-shaped]. The resolution limb wakes up on the same predicate.Bare unquoted word —
data.name == active→ reported,warning. ✔Also fires:
!=(data.name != data.type); literal-on-the-left ('grid' == data.type); every comparison in a compound predicate (2 findings from one predicate).Negative controls, each measured silent: quoted literal RHS (
data.type == 'formula') · double-quoted · numeric (== 3) · negative numeric (!= -3) · boolean (== true) · boolean negated (!= false) ·null· the sanctioned restructuring the hint itself recommends — path on the LEFT, literal on the right (data.type != 'formula') · bare truthy check with no comparison ·inmembership (objectui#4266, not folded in) · indexed RHS (data.rows[0]) · call-result RHS (size(data.tags)) · comprehension-macro body (data.tags.all(t, t == data.type), whose receiver is still walked — proven by a paired case that does fire) · a source the front end refuses (== $b).Corpus, counted with
grep -c/ assertion equalities, nevergrep | head: over the shippedMETADATA_FORM_REGISTRY(17 forms, 46 predicates), through the production entry point — 0 aterror, 0 atwarning. Reverse-verified: rewriting each== 'literal'into== data.__rhs__reports all 45 comparisons, allerror. The rewrite is anchored on the operator soin's array literals are left alone. Blast radius outsidepackages/spec: nil — a scan forprovider: 'schema'finds no schema-bound form in any app or example, only test fixtures.Family id count, reconciled against the in-repo pin:
grep -c "^export const [A-Z_]* = '"gives 4 invalidate-visibility-predicates.ts+ 2 invalidate-predicate-path-refs.ts= 6, matching the prose atauthoring-rules.ts:825. My change makes it 7; that line is updated (it now reads "six at the time of the move; seven since #7659") rather than silently falsified.rule-id-barrel-exports.test.tspins a floor, not an exact count, so nothing else moved.Interaction I found and did not paper over
A bare unquoted word is already refused at the publish door by
visibility-bare-identifier(error), which readsactiveas a dropped binding root and prescribesdata.active; this rule'swarningprescribes'active'. Both are true about the token and they disagree about the fix. Measured at the gate:I pinned the pair in
runtime-gate.test.tsinstead of asserting a cleanerrorslist that would go stale. Reconciling the two messages touchesvisibility-bare-identifier, which is out of this card's scope — flagging it for the lane rather than absorbing it. (Also visible above: thevisibility-root-mislayeredadvisory on everydata.-rooted predicate at the runtime door, because the registry entry runs at the defaultlayer: 'runtime'. Pre-existing, unrelated, not touched.)Scope
Nothing expanded. In: the rule, its wiring, tests, a changeset. Not touched: #7214's resolution behaviour (a predicate that is both unresolvable and path-shaped on the right reports twice — both statements true, different fixes, and narrowing #7214 is not mine);
objectui(read-only clone for grammar verification, zero edits); objectui#4266;packages/spec/src/**/*.zod.ts;content/docs/releases/**.One deliberate limitation, stated rather than inherited silently: the new rule needs no schema oracle but shares the walk with the two that do, so it does not reach a form whose
schemaIdresolves to no schema. A missed catch in the safe direction. Changing thatcontinuewould have meant editing the walk #7214 depends on, which the card forbids.Gates (invocation scope, read off
packages/lint/package.json)pnpm --filter @objectstack/lint run test→ 70 files, 1901 passed, 4 skipped. ✅pnpm --filter @objectstack/lint run typecheck→ clean, exit 0. ✅Changeset included (
.changeset/predicate-rhs-path-shaped.md,@objectstack/lint: minor) — a new authoring rule id is a public-surface behaviour change; noskip-changesetargued.Could not verify
pnpm --filter @objectstack/cli run typecheckand@objectstack/metadata-protocol run test— the other two@objectstack/lintconsumers — did not run: both fail in this worktree onTS2307/ "failed to resolve entry" for@objectstack/runtime,@objectstack/driver-tursoand@objectstack/core, i.e. unbuilt workspacedist, before reaching any of my code. Environmental, not a verdict on the change; a fullpnpm buildwas still running when this report was written. Left to CI, not rounded to green.objectui@37cd8e4; I did not run objectui's test suite.⛔ Not polling CI — convergence, ready-flip and landing are the PM's.
Generated by Claude Code
os-help commentedon Aug 11, 2026
Correction to the "Could not verify" section of my report above — the full
pnpm buildfinished (exit 0) shortly after I posted, so the two gates I had left to CI are now run. Both green, plus the CLI suite:pnpm --filter @objectstack/lint run typecheckpnpm --filter @objectstack/cli run typecheckpnpm --filter @objectstack/cli run testpnpm --filter @objectstack/metadata-protocol run testThat covers both downstream
@objectstack/lintconsumers, includingos validateend-to-end over the example stacks. The earlierTS2307/ unresolved-entry failures were unbuilt workspacedist, as reported — environmental, not the change. PR #7691's Gates section is updated to match.Still unverified, unchanged: objectui's own test suite. Its half (PR #4264) was read as source at
objectui@37cd8e4, not executed — and it is out of this card's scope either way.The docs-drift bot flagged three hand-written pages that reference
@objectstack/lint; I read all three and none enumerates this family's rule ids, so there is nothing to update. No edit to the release-owned page.Still not polling CI.
Generated by Claude Code
os-help commentedon Aug 11, 2026
ACCEPTED — landed as
19bca8cc4(PR #7691). Verified by content on a fresh ref (㉞).pm:dispatchedstripped; cloud session archived. Ledger → 22 merged.Landing verification
origin/mainpredicate-rhs-path-shaped× 3 invalidate-predicate-path-refs.ts✅PREDICATE_RHS_PATH_SHAPEDinindex.ts✅PREDICATE_PATH_UNRESOLVED✅ (so criterion 2's1is a real reading, not a lucky one)The part of this report worth keeping
It refused the criterion the brief pointed it at, and was right to. I told it to argue severity from the ADR-0090 D7 line ("every
errorrule mirrors a runtime enforcement point") rather than copy #7503's answer. It did better: it found that D7 is written on thevalidateSecurityPostureentry and scopes itself to the security-domain linter, not to this table — so applying it here would have been inheriting a rule from the wrong file. Read literally it would have put the whole rule atwarning, since the renderer does not refuse, it returns a wrong answer.It then found the bar this family writes for itself:
errorwhere "there is no reading of the metadata under which it was going to work" (visibility-bare-identifier),warningwhere the predicate is merely advisory-wrong (visibility-root-mislayered), with the immediate file-siblingpredicate-path-unresolvedsettling it.Result: one id, two severities, and the split is the interesting part —
erroron a dotted chain (data.a == data.b): nobody writes a dotted identifier chain meaning its own literal text; for==the element is hidden on every row.warningon a bare single word (status == active):data.type == text… into a fail-opentrue". Refusing aviewwrite at the publish door over metadata that renders correctly is a false build error in the one direction a gate may not fail in.That distinction is not in the card, not in triage's note, and not in my brief. It came from reading the ruling it was told to implement.
The rest of the method
objectui@37cd8e4(predicate.ts:193), not against the card's transcription of it — it matches character for character. The brief warned a regex passed through two issue bodies drifts; this one did not, and now that is a measurement rather than a hope.idnode or a.-chain), not from re-splitting source text — sofoo(1),data.b[0]and-3fail both sides for the same reason, andtrue/false/null/numbers/quoted strings arrive asvaluenodes, making the negative controls structural rather than a hand-maintained deny-list. One admitted cost, stated: an identifier containing$, which the grammar admits and CEL does not, so it never parses and belongs tovisibility-predicate-syntax(view/page 可见性谓词的 CEL **语法**在构建期无人校验 ——country === "USA"这类写法零诊断、运行时静默 fail-open #6253).== 'literal'into== data.__rhs__reports all 45 comparisons.runtime-gate.test.tsshowsdata.a == data.bproducingpredicate-rhs-path-shapedand notpredicate-path-unresolved— i.e. the case is provably silent under feat(lint): publish-time resolution of metadata-form predicate paths (#7010) #7214, which is the whole reason a new id exists rather than a widening of the old one.schemaIdresolves to no schema. A miss in the safe direction; changing thatcontinuewould mean editing the walk feat(lint): publish-time resolution of metadata-form predicate paths (#7010) #7214 depends on.⛔ #7214's resolution check untouched; objectui#4266 not folded in; the
objectuirepo cloned read-only for grammar verification with zero edits.The interaction it disclosed is now on the board
A bare unquoted word is already refused by
visibility-bare-identifier(error, prescribingdata.active) while this rule warns prescribing'active'— both true about the token, opposite about the intent, and the louder one blocks. The dev pinned the pair inruntime-gate.test.tsrather than papering over it and said reconciliation was a separate card. Filed as #7696 so it does not scroll away with this PR's body. ⛔ Unassigned and ungraded — triage's channel.Generated by Claude Code
feat(lint): refuse a path-shaped `==`/`!=` right-hand side at publish…