Skip to content

fix(lint): one prescription for a bare unquoted word on the right of == (#7696) - #7810

Merged
os-help merged 1 commit into
mainfrom
claude/issue-7696-reconcile-bare-word-prescriptions
Aug 11, 2026
Merged

fix(lint): one prescription for a bare unquoted word on the right of == (#7696)#7810
os-help merged 1 commit into
mainfrom
claude/issue-7696-reconcile-bare-word-prescriptions

Conversation

@os-help

@os-help os-help commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

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:

The rooted reading has no spelling in this position. visibility-bare-identifier told the author to write data.active on the right of ==. That is a dotted chain on the RIGHT, which is predicate-rhs-path-shaped's own error arm — the same gate refuses the fix it prescribes. Measured, not argued: see the controls table row data.type == data.active.

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 (error on a dotted chain, warning on a bare word) is untouched. ⛔ #7214's resolution logic is untouched. ⛔ authoring-rules.ts registration is untouched.

Controls table — what an author sees for status == active and its neighbours

Measured at the runtime publish gate (runRuntimeAuthoringRules({ type: 'view', … })), on origin/main@098b629 vs this branch. FORM = schema-bound metadata form; RTV = runtime view.

# predicate before after
1 FORM data.type == active error visibility-bare-identifier"write record.active"
warning predicate-rhs-path-shaped"write 'active'"
warning predicate-rhs-path-shapedonly finding, names both spellings
2 FORM data.type == label (label is a schema key) error visibility-bare-identifierrecord.label
error predicate-path-unrooteddata.label
warning predicate-rhs-path-shaped'label'three prescriptions
warning predicate-rhs-path-shaped — only finding
3 FORM status == active error visibility-bare-identifier (on status)
warning predicate-rhs-path-shaped
unchanged — left-side bare word still refused
4 FORM data.type == data.label (dotted chain) error predicate-rhs-path-shaped unchanged
5 FORM data.type == 'active' (quoted literal) silent¹ unchanged
6 FORM data.type == data.activethe old error's own prescription error predicate-rhs-path-shaped + error predicate-path-unresolved unchanged — this is the proof of row 1
7 FORM data.tpye == active error visibility-bare-identifier + error predicate-path-unresolved error predicate-path-unresolved only — #7214 untouched on the left
8 RTV record.status == active error visibility-bare-identifierrecord.active unchanged — real CEL, path on the right is legal there
9 RTV record.status == 'active' silent unchanged
10 FORM active == data.type (bare on the left) error visibility-bare-identifier unchanged
11 FORM data.name == active && active (also used elsewhere) error visibility-bare-identifier unchanged
12 FORM data.tags.all(t, t == active) (macro body) error visibility-bare-identifier unchanged — no replacement fires there, so nothing stands down
13 FORM unresolvable schemaId, data.x == active error visibility-bare-identifier warning predicate-rhs-path-shaped
14 FORM unresolvable schemaId, data.x == data.y silent¹ error predicate-rhs-path-shaped — coverage increase

¹ Both also carry a pre-existing warning visibility-root-mislayered on every FORM row — 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) and data.active == 'yes' (silent when the field exists; predicate-path-unresolved when 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 at error — 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 shared schemaIdOf test 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.tspredicate-path-unrooted stands down for that set; the bare arm of predicate-rhs-path-shaped gets the merged message; a site whose schemaId resolves to nothing is now walked with no scope instead of skipped, so the oracle-free right-hand check still runs there.
  • validate-visibility-predicates.tsvisibility-bare-identifier stands down for that set, on schema-bound form sites only, fed through the declared list (the same conservative mechanism namespaceRoots already 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's CLI_ONLY surfaceReason claims coverage the ADR-0094 object authoring gate does not give it — it covers 1 of the block's 13 rules #7576 serialization).
  • Unit tests in both rules' own suites, and a changeset.

Scope expansions, disclosed rather than absorbed

  1. validate-visibility-predicates.ts was edited, though scope named only validate-predicate-path-refs.ts. Unavoidable: visibility-bare-identifier lives there and is the error the card is about. Option 2 explicitly "costs a special case in two rules".
  2. A new file, predicate-rhs-position.ts, to hold the one condition both files must agree on.
  3. The walk widening (row 14) — a coverage increase, and the thing that keeps the stand-down from becoming a silence on that shape.

Not fixed, and not mine — a neighbouring defect found while measuring

At the runtime gate, validateVisibilityPredicates runs with opts.layer at its 'runtime' default for every view, including schema-bound metadata forms. So a correctly-data.-rooted metadata form draws a visibility-root-mislayered advisory telling the author to use record. (the ¹ rows above), and before this PR visibility-bare-identifier prescribed record.active rather than the data.active the 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

gate invocation result
unit tests pnpm --filter @objectstack/lint run test 70 files, 1921 passed
typecheck pnpm --filter @objectstack/lint run typecheck clean
eslint npx eslint <6 changed files> --no-inline-config clean
changeset gates node scripts/check-empty-changeset.mjs, check-adr-0087-registration.mjs, check-changeset-no-major.mjs all ✓
real-app corpus os build in examples/app-showcase and examples/app-crm grep -c over the four rule ids = 0 on both, before and after

CI 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

…`==` (#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
@vercel

vercel Bot commented Aug 11, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 11, 2026 3:32pm

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/lint.

3 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/automation/hook-bodies.mdx (via @objectstack/lint)
  • content/docs/deployment/validating-metadata.mdx (via packages/lint)
  • content/docs/permissions/authorization.mdx (via @objectstack/lint)

1 release-owned page(s) also reference the affected code. These are read-only:

  • content/docs/releases/v17.mdx (via @objectstack/lint)

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 31515105939 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Test Core (1/3) — 失败步骤: Run this shard's tests

    �[41m�[1m FAIL �[22m�[49m src/data/api-methods-batch-conformance.test.ts�[2m > �[22mapiMethods conformance — single-record writes imply batch (#3026)�[2m > �[22mgrants bulk wherever it grants create /
    

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 51 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 在其他 PR 的同类评论里搜同名测试;出现过 ⇒ flaky 实锤,开 issue 修/隔离那条测试。修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 31515158706 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Test Core (1/3) — 失败步骤: Run this shard's tests

    �[41m�[1m FAIL �[22m�[49m src/data/api-methods-batch-conformance.test.ts�[2m > �[22mapiMethods conformance — single-record writes imply batch (#3026)�[2m > �[22mgrants bulk wherever it grants create /
    

历史信号:

  • ⚠️ 本 PR 过去 24h 已在队列失败 1 次(不含本次)。 内容未变而反复失败 ⇒ 高度怀疑 flaky 测试或与同组 PR 的语义冲突,重排不解决。
  • 过去 24h 队列共有 53 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 在其他 PR 的同类评论里搜同名测试;出现过 ⇒ flaky 实锤,开 issue 修/隔离那条测试。修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

os-help commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator Author

Triaged both queue failures (31515105939, 31515158706) — checklist item 2: not this PR, and not flaky either. main is red. Requeuing will burn another full-queue rebuild for nothing.

Evidence.

  • The failing test is packages/spec/src/data/api-methods-batch-conformance.test.ts. It scans packages/**/*.object.ts off disk (deliberately, to avoid inverting the spec → * dependency direction). This PR changes 7 files, all under packages/lint/ plus a changeset: git diff --name-only origin/main...HEAD -- '*.object.ts' 'packages/spec/src/data/api-derivation*'0. The test's entire input set is byte-identical to origin/main.
  • Reproduced locally on this branch, deterministically: sys_api_key: [get, list, update] grants single-record writes but not 'bulk'.
  • The declaration came from 52200b4"fix(platform-objects,plugin-auth): give API-key revoke/restore a working product route (api-key-ui-lifecycle: API keys cannot be revoked through any product route (405) — object declares revoke/restore as PATCH but disables the PATCH method #7727) (fix(platform-objects,plugin-auth): give API-key revoke/restore a working product route #7769)" — which changed apiMethods: ['get', 'list']['get', 'list', 'update'] on sys-api-key.object.ts. That commit is on main, and main has not moved past this PR's base (098b629), so it is live right now.
  • Not flaky: two runs, same test, same single offender. The signal in the bot's "已在队列失败 1 次" line is a red base branch, not nondeterminism.

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 'bulk' to the whitelist, or register sys_api_key in SINGLE_RECORD_WRITE_ONLY with a reason. sys-api-key.object.ts:24-28 argues update is intentional for managedBy objects, so which way it goes is #7727's call, not a guess I should land inside a lint PR.

I'll re-check when a base-branch-recovered notice arrives and merge main in so CI re-runs against the fixed base. Nothing on this PR's own diff is failing.


Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 31516022696 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Test Core (1/3) — 失败步骤: Run this shard's tests

    �[41m�[1m FAIL �[22m�[49m src/data/api-methods-batch-conformance.test.ts�[2m > �[22mapiMethods conformance — single-record writes imply batch (#3026)�[2m > �[22mgrants bulk wherever it grants create /
    

历史信号:

  • ⚠️ 本 PR 过去 24h 已在队列失败 2 次(不含本次)。 内容未变而反复失败 ⇒ 高度怀疑 flaky 测试或与同组 PR 的语义冲突,重排不解决。
  • 过去 24h 队列共有 57 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 在其他 PR 的同类评论里搜同名测试;出现过 ⇒ flaky 实锤,开 issue 修/隔离那条测试。修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 31516763104 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Test Core (1/3) — 失败步骤: Run this shard's tests

    �[41m�[1m FAIL �[22m�[49m src/data/api-methods-batch-conformance.test.ts�[2m > �[22mapiMethods conformance — single-record writes imply batch (#3026)�[2m > �[22mgrants bulk wherever it grants create /
    

历史信号:

  • ⚠️ 本 PR 过去 24h 已在队列失败 3 次(不含本次)。 内容未变而反复失败 ⇒ 高度怀疑 flaky 测试或与同组 PR 的语义冲突,重排不解决。
  • 过去 24h 队列共有 59 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 在其他 PR 的同类评论里搜同名测试;出现过 ⇒ flaky 实锤,开 issue 修/隔离那条测试。修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 31517424557 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Test Core (1/3) — 失败步骤: Run this shard's tests

    �[41m�[1m FAIL �[22m�[49m src/data/api-methods-batch-conformance.test.ts�[2m > �[22mapiMethods conformance — single-record writes imply batch (#3026)�[2m > �[22mgrants bulk wherever it grants create /
    

历史信号:

  • ⚠️ 本 PR 过去 24h 已在队列失败 4 次(不含本次)。 内容未变而反复失败 ⇒ 高度怀疑 flaky 测试或与同组 PR 的语义冲突,重排不解决。
  • 过去 24h 队列共有 64 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 在其他 PR 的同类评论里搜同名测试;出现过 ⇒ flaky 实锤,开 issue 修/隔离那条测试。修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Aug 11, 2026
@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 31518428177 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Test Core (1/3) — 失败步骤: Run this shard's tests

    �[41m�[1m FAIL �[22m�[49m src/data/api-methods-batch-conformance.test.ts�[2m > �[22mapiMethods conformance — single-record writes imply batch (#3026)�[2m > �[22mgrants bulk wherever it grants create /
    

历史信号:

  • ⚠️ 本 PR 过去 24h 已在队列失败 5 次(不含本次)。 内容未变而反复失败 ⇒ 高度怀疑 flaky 测试或与同组 PR 的语义冲突,重排不解决。
  • 过去 24h 队列共有 66 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 在其他 PR 的同类评论里搜同名测试;出现过 ⇒ flaky 实锤,开 issue 修/隔离那条测试。修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

os-help commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator Author

Dequeued (CI_FAILURE) after 6 queue builds, all the same single offender. Triage unchanged from my earlier comment — re-verified just now: origin/main is still 098b629 (has not moved since this PR's base) and sys-api-key.object.ts:262 still reads apiMethods: ['get', 'list', 'update']. Nothing on this PR's diff is failing.

New information — the blocker already has two fix PRs open, both non-draft:

Both take the exemption route rather than adding 'bulk', which matches sys-api-key.object.ts:24-28's argument that the update verb is deliberate for managedBy objects. They overlap, so whoever is sequencing should probably land one and close the other rather than let both race.

⛔ 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 main is green I will merge it in so CI re-runs against the fixed base; this PR then needs a re-queue, which I cannot do myself.


Generated by Claude Code

@os-help
os-help added this pull request to the merge queue Aug 11, 2026
Merged via the queue into main with commit 78ed1f4 Aug 11, 2026
26 checks passed
@os-help
os-help deleted the claude/issue-7696-reconcile-bare-word-prescriptions branch August 11, 2026 19:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

2 participants