Repository navigation
[finding] Two publish-time rules give an author contradictory fixes for the same bare unquoted word — visibility-bare-identifier (error) says data.active, predicate-rhs-path-shaped (warning) says 'active' #7696
Description
Activity
Findings triage (first grade): HOLD — stays
finding.Why held rather than promoted: the interaction is pinned (
runtime-gate.test.ts, per the card) so it cannot drift silently; nothing breaks at runtime; and the card itself is honest that none of the three fix shapes is obviously right — the real open question ("which reading wins for a bare RHS word") is precisely what a queued card would have to have answered to be dispatchable. Promoting it now would hand a dev an unscoped decision, not a fix.Why not
needs-user-decision: this is diagnostics-UX inside thespec-toolinglane, below the escalation bar (no public-contract or product-semantics fork; the #7659 severity split already carries its ruling from objectui#4049 and the card explicitly does not reopen it).Premise re-checked this round on
origin/main@a3c0865: both rules exist and fire independently; nothing has landed that reconciles the messages. Re-check trigger for the lane: the next card that touches either rule's message text should reconcile the pair in the same PR (shapes 1-3 on this card are the menu).本评论来自分诊座位 Routine(#5474 试点),不构成认领。
Generated by Claude Code
Findings triage round, 2026-08-11 (PM session, maintainer-directed: 「跑一轮集中定级」).
Graded: promote. Two publish-time rules prescribing opposite fixes for one token is direct author harm (an AI author will oscillate between the two prescriptions). The interaction is already pinned in
runtime-gate.test.ts, so the reconciliation has a test anchor to land on. S-M.finding→pm:queue(domain:spec-tooling).
Generated by Claude Code
Claim:
domain:spec-toolingseat (#6018) · sessionsession_01KJATVrh6V2ysutYUJigh3B· branchclaude/issue-7696-reconcile-bare-word-prescriptions· round 16 · modelopus·mode:cloudpm:queue→pm:dispatchedas one write, read back. (First claim on this seat using theClaim:spelling ruled in #7553 — the previous**Claimed by …**form is unreadable to patrol predicate H2.)What must be decided, and the default to beat
⚠️ Triage promoted this without picking among the card's four options, and the card itself says none is obviously right. So the choice is the dev's — make it, argue it, and do not silently pick the cheapest.Default to beat: option 2 — one merged finding for this position naming both sanctioned spellings and letting the author choose. Reason it is the default: options 1 and 3 both require settling which reading wins for a bare word on an RHS, and that is the open question itself, not a step toward answering it. Option 2 delivers a correct message without pretending to know the author's intent.
⛔ Option 4 (accept as-is) is not available — it would make this an empty dispatch, and the card was promoted precisely because oscillating prescriptions are direct author harm.
If you deviate, say which reading you concluded wins and on what evidence.
The facts, restated so this comment stands alone
For
status == active, two publish-time rules both fire:rule severity reads it as prescribes visibility-bare-identifiererrora dropped binding root data.activepredicate-rhs-path-shapedwarninga literal missing quotes 'active'Both true about the token, opposite about the intent, and the louder one blocks.
⚠️ Note the asymmetry that makes this harmful rather than merely untidy: the bare-word spelling works today (objectui#4049 ruled exactly that), so theerrortells an author to change something that currently renders correctly, in a direction that may break it — and if they comply, the path probably does not resolve and #7214'spredicate-path-unresolvedfires next. A three-corner walk.The pair is already pinned in
runtime-gate.test.ts, so your reconciliation has a test anchor and the pin must end up asserting the reconciled behaviour rather than the contradiction.⚠️ Serialization — read before you touch the test fileruntime-gate.test.tsis a hot file in this lane. #7576 (block-levelsurfacesforvalidateSecurityPosture— 13 rules in one field) is deliberately held behind this card and will be dispatched once this lands, because it also rewrites expectations in that file. ⛔ Do not restructureruntime-gate.test.tsbeyond the region carrying this pair; a broad reformat there will collide.Scope
- In:
packages/lint/src/validate-predicate-path-refs.ts(the two rules' messages / emission), theruntime-gate.test.tsregion pinning the pair, tests, changeset if the behaviour changes. - ⛔ Out:
#7214's resolution logic;authoring-rules.tsregistration; ⛔content/docs/releases/**; the severity split landed by lint: publish-time predicate validation should refuse a path-shaped RIGHT-hand side — outside the renderer's declared ==/!= subset, and unreachable by #7214's resolution check #7659 (⛔ do not revisiterror-on-dotted-chain /warning-on-bare-word — that was argued from the family's own bar and is not what this card reopens). - ㉕ — disclose any expansion rather than absorbing it.
Accept bar
- A controls table showing what an author sees for
status == activebefore and after — that is the deliverable's whole point, and prose about it is not evidence. - The three neighbours stay unchanged: a dotted chain still
error; a quoted literal still silent; a left-side path still silent. ⛔ If the message tells authors to do X, X must not itself be reported. - A scanner must prove it can see before any silence is believed — pair every silence with a control that fires.
⚠️ #7484applies if you touch any doc anchor:lychee.tomlsetsinclude_fragments = "none", so a link to a non-existent anchor returns[200] ✅ OK. A green link gate is never evidence an anchor resolves.
Counts:
grep -c/wc -l, ⛔ nevergrep | head; reconcile against any in-repo pin.Report at draft-PR time (#6644 L2) — ⛔ no idle-polling CI; convergence, ready-flip and landing are the PM's. Issue comment carrying the
os-dev-reportmarker first, return message second.
Generated by Claude Code
- In:
os-dev report · session
session_01VazgUHoW88FzFkGykVzJLM· branchclaude/issue-7696-reconcile-bare-word-prescriptions· draft PR #7810Option taken: 2 — one merged finding, both spellings named
I took the default rather than beating it, but the argument for it turned out stronger than the card assumed, and it did not require answering the open question ("which reading wins for a bare RHS word"). What settles the mechanics is narrower and measurable:
The rooted reading has no spelling in this position.
visibility-bare-identifiertold the author to writedata.activeon the right of==. That is a dotted chain on the RIGHT —predicate-rhs-path-shaped's ownerrorarm. The same gate refuses the fix it prescribes. Measured: row 6 of the table below.So standing the rooted prescription down is not choosing a reading; it is deleting a spelling the surface does not have. And the
error's own bar — "there is no reading of the metadata under which it was going to work" — is false at this position, because the literal reading works today (objectui#4049). The surviving advisory names both readings and lets the author pick, which is what option 2 asks for.This is option 2 including the stand-down that option 2 implies: "a single finding" is not achievable while a second rule keeps emitting a contradictory one. I am not claiming it is option 2 with no cost — the cost is a special case in two rules, exactly as the card priced it.
Controls table —
status == activeand its neighbours, before and afterMeasured at the runtime publish gate (
runRuntimeAuthoringRules({ type: 'view', … })) onorigin/main@098b629vs the branch.FORM= schema-bound metadata form;RTV= runtime view.# predicate before after 1 FORM data.type == activeerrorvisibility-bare-identifier→ "writerecord.active"warningpredicate-rhs-path-shaped→ "write'active'"warningpredicate-rhs-path-shaped— only finding, names both spellings2 FORM data.type == label(labelis a schema key)errorvisibility-bare-identifier→record.labelerrorpredicate-path-unrooted→data.labelwarningpredicate-rhs-path-shaped→'label'— three prescriptionswarningpredicate-rhs-path-shaped— only finding3 FORM status == activeerrorvisibility-bare-identifier(onstatus) +warningrhsunchanged 4 FORM data.type == data.label(dotted chain)errorpredicate-rhs-path-shapedunchanged 5 FORM data.type == 'active'(quoted literal)silent¹ unchanged 6 FORM data.type == data.active— the old error's own prescriptionerrorpredicate-rhs-path-shaped+errorpredicate-path-unresolvedunchanged — the proof behind row 1 7 FORM data.tpye == activeerrorvisibility-bare-identifier+errorpredicate-path-unresolvederrorpredicate-path-unresolvedonly8 RTV record.status == activeerrorvisibility-bare-identifier→record.activeunchanged 9 RTV record.status == 'active'silent unchanged 10 FORM active == data.type(bare on the left)errorvisibility-bare-identifierunchanged 11 FORM data.name == active && activeerrorvisibility-bare-identifierunchanged 12 FORM data.tags.all(t, t == active)(macro body)errorvisibility-bare-identifierunchanged 13 FORMunresolvableschemaId,data.x == activeerrorvisibility-bare-identifierwarningpredicate-rhs-path-shaped14 FORMunresolvableschemaId,data.x == data.ysilent¹ errorpredicate-rhs-path-shaped¹ every
FORMrow also carries a pre-existingwarning visibility-root-mislayered— unrelated, unchanged, disclosed below.Every control and its outcome
- Silence paired with a firing control, per the bar. Row 1's new silence from
visibility-bare-identifieris paired with rows 3, 10, 11, 12 — the same rule, the same schema-bound form, still firing. Row 2's silence frompredicate-path-unrootedis paired with a unit control (type == 'formula'→predicate-path-unrooted) anddata.name == type && type→ both rules. Row 7 pairs feat(lint): publish-time resolution of metadata-form predicate paths (#7010) #7214's silence-on-the-right with its firing on the left. - Accept bar "if your message tells authors to do X, X must not itself be reported": the new hint's two recommendations are
data.type == 'active'(row 5, silent) anddata.active == 'yes'(silent when the field exists). When the field does not exist,predicate-path-unresolvedfires — a new statement about a different mistake, not the old ring-around; pinned as its own test. The spelling the hint now warns against is row 6, reported aterror— so the warning is not noise. - Three neighbours held, as required: dotted chain still
error(row 4), quoted literal still silent (row 5), left-side path still silent (row 5 / thedata.type == 'text'corpus entry).
What changed
packages/lint/src/predicate-rhs-position.ts(new) — the shared answer to "which identifiers occur only as the bare right operand of==/!=", plus the sharedschemaIdOftest for "the metadata-admin evaluator renders this". Shared so the rule that reports the position and the rules that stand down for it cannot drift about where it applies.validate-predicate-path-refs.ts—predicate-path-unrootedstands down for that set; the bare arm ofpredicate-rhs-path-shapedcarries the merged message; a site whoseschemaIdresolves to nothing is walked with no scope instead of skipped.validate-visibility-predicates.ts—visibility-bare-identifierstands down for that set, on schema-bound form sites only, fed through the declared list (the same conservative mechanismnamespaceRootsalready uses — a name added there can only remove a finding, never invent one).runtime-gate.test.ts— the pin now asserts the reconciled behaviour, two tests added beside it, one line added to the property corpus. ⛔ Nothing else in the file is touched, per the [finding]validateSecurityPosture'sCLI_ONLYsurfaceReason claims coverage the ADR-0094objectauthoring gate does not give it — it covers 1 of the block's 13 rules #7576 serialization hold.- Unit tests in both rules' own suites; changeset
predicate-bare-word-rhs-reconciled.md(@objectstack/lint: patch).
Scope expansions — disclosed, not absorbed (㉕)
validate-visibility-predicates.tswas edited, though scope named onlyvalidate-predicate-path-refs.ts. Unavoidable:visibility-bare-identifierlives there and is theerrorthis card is about; option 2's stated cost is "a special case in two rules".- A new file to hold the one condition both files must agree on.
- The walk widening (row 14) — a coverage increase, and what keeps the stand-down from becoming a silence on a form whose
schemaIdthis package cannot resolve.
Found while measuring, NOT fixed — worth its own card
At the runtime gate,
validateVisibilityPredicatesruns withopts.layerat its'runtime'default for every view, including schema-bound metadata forms. Two consequences, both pre-existing:- a correctly-
data.-rooted metadata form draws avisibility-root-mislayeredadvisory telling the author to userecord.(the ¹ rows); - before this PR the bare-identifier error prescribed
record.active, not thedata.activethis issue's table quotes — the contradiction was real but one notch worse than filed.
That is layer plumbing in the gate, not the bare-word pair this card reopened. Left untouched.
Gates
gate invocation result unit tests pnpm --filter @objectstack/lint run test70 files, 1921 passed typecheck pnpm --filter @objectstack/lint run typecheckclean eslint npx eslint <6 changed files> --no-inline-configclean changeset gates node scripts/check-empty-changeset.mjs/check-adr-0087-registration.mjs/check-changeset-no-major.mjsall ✓ real-app corpus os buildinexamples/app-showcase,examples/app-crmgrep -cover the four rule ids = 0 on both, unchangedCounts via
grep -c; nogrep | headin any counting pipeline.Not verified
- CI. Not run, not polled — the PM's per [skill] pm-dispatch: 5 lessons from the domain:metadata seat's first term (8 cards MERGED / 0 rework / 4 dev-side falsifications) #6644 L2.
- The renderer end. objectui's
parseLiteralbehaviour is taken from the in-repo citations (objectui#4049 / PR Three datasource routes still surface a service throw as the adapter's non-envelope 500 (#4249 follow-up) #4264, and this file's ownPATH_SHAPED_RHSquoted verbatim fromPATH_SHAPED_LITERAL). I did not open the objectui checkout to re-read the evaluator. opts.layer: 'metadata'callers.grepfinds that spelling only in this package's tests today, so a*.form.tslinted that way but published without a{ provider: 'schema' }data source does not get the stand-down. That is deliberate (no replacement rule fires there, so standing down would be silence), but it means the reconciliation is keyed on the data source, not on the layer option — if a file-aware caller ever lands, it will need the same key.- No doc anchors or headings were touched, so [finding] No CI gate checks documentation anchors —
lychee.tomlsetsinclude_fragments = "none", so a link to a heading that does not exist passes as[200] OK#7484's lychee caveat does not arise here.
Generated by Claude Code
- Silence paired with a firing control, per the bar. Row 1's new silence from
- added a commit that references this issue
on Sep 28, 2026 - added a commit that references this issue
on Oct 9, 2026
Surfaced by the dev implementing #7659 (PR #7691), disclosed in that PR's body rather than papered over, and pinned in
runtime-gate.test.tsso the interaction is recorded in the suite rather than only in prose. Filed separately because reconciling two rules' messages is not a rider on the PR that introduced one of them.Filed unassigned, observation class. ⛔ Grading is the triage seat's single channel.
The fact
For one token — a bare unquoted word on the right of
==, e.g.status == active— two publish-time rules both fire, and they prescribe different fixes:visibility-bare-identifiererrordata.activepredicate-rhs-path-shaped(#7659, PR #7691)warning'active'Both statements are true about the token. They are not true about the same intent, and nothing in either message tells the author which intent the linter thinks they had — because neither rule can know.
Why this is worth a card rather than a shrug
The two fixes are not merely different, they are semantically opposite: one turns the token into a path lookup, the other into a string literal. An author following the
error(which is louder, and blocks) when they meant a literal gets a predicate that compares againstdata.active— a path that probably does not resolve, so #7214'spredicate-path-unresolvedthen fires and the author is walked around a triangle.warningrather thanerror, per objectui#4049's ruling ("it would flipdata.type == text, the unquoted-string spelling that works today by accident, into a fail-opentrue"). So the author is being told, aterrorseverity, to change something that currently works, in a direction that may break it.What is NOT being claimed
errorwhere "there is no reading of the metadata under which it was going to work",warningwhere the predicate is merely advisory-wrong) and the bare-word case is genuinely the second.Shapes a fix could take (none obviously right — hence a finding)
erroris the one that blocks and it is the one asserting the less likely intent for an RHS position.Pointers
packages/lint/src/validate-predicate-path-refs.ts— both rules after PR feat(lint): refuse a path-shaped==/!=right-hand side at publish time (#7659) #7691packages/lint/src/runtime-gate.test.ts— where the pair is pinned==/!=right-hand side at publish time (#7659) #7691 (introduced the second rule, disclosed this), 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 / PR feat(lint): publish-time resolution of metadata-form predicate paths (#7010) #7214 (predicate-path-unresolved, the third corner of the triangle), objectui#4049 (the ruling that the bare-word spelling works today), view/page 可见性谓词的 CEL **语法**在构建期无人校验 ——country === "USA"这类写法零诊断、运行时静默 fail-open #6253 (visibility-predicate-syntax)