fix(lint): one prescription for a bare unquoted word on the right of == (#7696) - #7810
Conversation
…`==` (#7696) Three publish-time rules fired on one token — a bare word on the right of `==` / `!=` in a metadata-editing form's visibility predicate — and prescribed opposite fixes. `visibility-bare-identifier` (`error`) and, when the word is also a schema key, `predicate-path-unrooted` (`error`) read it as a dropped binding root and asked for `<root>.active`; `predicate-rhs-path-shaped` (`warning`) read it as a literal missing its quotes and asked for `'active'`. The `error`s blocked, and the spelling they asked for is one this same gate refuses: `data.type == data.active` is a path on the RIGHT, which is `predicate-rhs-path-shaped`'s own `error` arm. Obeying the loud finding landed the author on a louder one, over metadata that renders correctly today (objectui#4049). The two root-prescribing rules now stand down for identifiers occurring ONLY as a bare right operand of `==` / `!=` on a schema-bound metadata form (`predicate-rhs-position.ts`), and the surviving advisory names both readings — the quoted spelling and the move-the-path-left spelling — and states that adding the root in place is not a third option. Which reading the author meant is still not decided; inventing an answer is what the contradicting messages were doing. Narrow in all three directions: per identifier (`status == active` keeps its refusal over `status`), per surface (a runtime view goes to real CEL and is untouched), and never a silence (the condition is the same `schemaIdOf` test the replacement rule walks on). A form whose `schemaId` resolves to no schema is now walked with no scope rather than skipped, so the oracle-free right-hand check still runs there — 0 new findings over the shipped `METADATA_FORM_REGISTRY` corpus and both example apps. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VazgUHoW88FzFkGykVzJLM
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
📓 Docs Drift CheckThis PR changes 1 package(s): 3 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
⛔ 1 release-owned page(s) also reference the affected code. These are read-only:
|
⛔ merge queue 构建失败 — 先分诊,再决定要不要重排队列构建 31515105939 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集), 失败的 job(日志抽取,best effort):
历史信号:
分诊清单:
Generated by Claude Code · merge-queue-triage workflow (#4859) |
⛔ merge queue 构建失败 — 先分诊,再决定要不要重排队列构建 31515158706 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集), 失败的 job(日志抽取,best effort):
历史信号:
分诊清单:
Generated by Claude Code · merge-queue-triage workflow (#4859) |
|
Triaged both queue failures (31515105939, 31515158706) — checklist item 2: not this PR, and not flaky either. Evidence.
Already filed — no new issue from me: #7793, #7802, #7824 all report exactly this. Not fixing it here. It is another card's regression in another package, outside this dispatch's single-issue scope, and the test states the fix is a deliberate choice its owner has to make — add I'll re-check when a base-branch-recovered notice arrives and merge Generated by Claude Code |
⛔ merge queue 构建失败 — 先分诊,再决定要不要重排队列构建 31516022696 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集), 失败的 job(日志抽取,best effort):
历史信号:
分诊清单:
Generated by Claude Code · merge-queue-triage workflow (#4859) |
⛔ merge queue 构建失败 — 先分诊,再决定要不要重排队列构建 31516763104 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集), 失败的 job(日志抽取,best effort):
历史信号:
分诊清单:
Generated by Claude Code · merge-queue-triage workflow (#4859) |
⛔ merge queue 构建失败 — 先分诊,再决定要不要重排队列构建 31517424557 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集), 失败的 job(日志抽取,best effort):
历史信号:
分诊清单:
Generated by Claude Code · merge-queue-triage workflow (#4859) |
⛔ merge queue 构建失败 — 先分诊,再决定要不要重排队列构建 31518428177 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集), 失败的 job(日志抽取,best effort):
历史信号:
分诊清单:
Generated by Claude Code · merge-queue-triage workflow (#4859) |
|
Dequeued ( New information — the blocker already has two fix PRs open, both non-draft:
Both take the exemption route rather than adding ⛔ Do not re-queue this PR until one of those lands — the queue will reject it a seventh time and rebuild everything behind it. Once Generated by Claude Code |
Fixes #7696
The decision: option 2, and the evidence that made it cheap
The card offered four shapes and said none was obviously right. I took option 2 — one merged finding for this position, naming both sanctioned spellings — and it turned out not to need the open question ("which reading wins for a bare RHS word") answered at all. What settles the mechanics is narrower and measurable:
So removing the rooted prescription is not choosing a reading — it is deleting a spelling the surface does not have. The
error's own bar ("there is no reading of the metadata under which it was going to work") is false here, because the literal reading works today (objectui#4049). The merged advisory still names both readings and lets the author choose.⛔ The #7659 severity split (
erroron a dotted chain,warningon a bare word) is untouched. ⛔ #7214's resolution logic is untouched. ⛔authoring-rules.tsregistration is untouched.Controls table — what an author sees for
status == activeand its neighboursMeasured at the runtime publish gate (
runRuntimeAuthoringRules({ type: 'view', … })), onorigin/main@098b629vs this branch.FORM= schema-bound metadata form;RTV= runtime view.FORM data.type == activeerrorvisibility-bare-identifier→ "writerecord.active"warningpredicate-rhs-path-shaped→ "write'active'"warningpredicate-rhs-path-shaped— only finding, names both spellingsFORM 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 findingFORM status == activeerrorvisibility-bare-identifier(onstatus)warningpredicate-rhs-path-shapedFORM data.type == data.label(dotted chain)errorpredicate-rhs-path-shapedFORM data.type == 'active'(quoted literal)FORM data.type == data.active— the old error's own prescriptionerrorpredicate-rhs-path-shaped+errorpredicate-path-unresolvedFORM data.tpye == activeerrorvisibility-bare-identifier+errorpredicate-path-unresolvederrorpredicate-path-unresolvedonly — #7214 untouched on the leftRTV record.status == activeerrorvisibility-bare-identifier→record.activeRTV record.status == 'active'FORM active == data.type(bare on the left)errorvisibility-bare-identifierFORM data.name == active && active(also used elsewhere)errorvisibility-bare-identifierFORM data.tags.all(t, t == active)(macro body)errorvisibility-bare-identifierFORMunresolvableschemaId,data.x == activeerrorvisibility-bare-identifierwarningpredicate-rhs-path-shapedFORMunresolvableschemaId,data.x == data.yerrorpredicate-rhs-path-shaped— coverage increase¹ Both also carry a pre-existing
warning visibility-root-mislayeredon everyFORMrow — unrelated, unchanged, and disclosed below.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;predicate-path-unresolvedwhen it does not, which is a new statement about a different mistake, pinned as its own test). The spelling it now warns against is row 6, which is reported aterror— so the warning is not noise.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 surface". Shared rather than duplicated so the rule that reports the position and the rules that stand down for it can never disagree about where it applies.validate-predicate-path-refs.ts—predicate-path-unrootedstands down for that set; the bare arm ofpredicate-rhs-path-shapedgets the merged message; a site whoseschemaIdresolves to nothing is now walked with no scope instead of skipped, so the oracle-free right-hand check still runs there.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).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 ([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).Scope expansions, disclosed rather than absorbed
validate-visibility-predicates.tswas edited, though scope named onlyvalidate-predicate-path-refs.ts. Unavoidable:visibility-bare-identifierlives there and is theerrorthe card is about. Option 2 explicitly "costs a special case in two rules".predicate-rhs-position.ts, to hold the one condition both files must agree on.Not fixed, and not mine — a neighbouring defect found while measuring
At the runtime gate,
validateVisibilityPredicatesruns withopts.layerat its'runtime'default for every view, including schema-bound metadata forms. So a correctly-data.-rooted metadata form draws avisibility-root-mislayeredadvisory telling the author to userecord.(the¹rows above), and before this PRvisibility-bare-identifierprescribedrecord.activerather than thedata.activethe issue body quotes. That is a layer-plumbing question in the gate, not the bare-word pair this card reopened — left untouched, and worth its own card.Gates
pnpm --filter @objectstack/lint run testpnpm --filter @objectstack/lint run typechecknpx eslint <6 changed files> --no-inline-confignode scripts/check-empty-changeset.mjs,check-adr-0087-registration.mjs,check-changeset-no-major.mjsos buildinexamples/app-showcaseandexamples/app-crmgrep -cover the four rule ids = 0 on both, before and afterCI has not been run or waited on — that is the PM's.
🤖 Generated with Claude Code
https://claude.ai/code/session_01VazgUHoW88FzFkGykVzJLM
Generated by Claude Code