Repository navigation
Commit 07ee6f6
fix(lint): one-line verdicts for the RLS-predicate and sharing-rule enforceability rules;
Part of #22161
Clause-②: no
Stage 2 of the card, slice 4: the 9 rule ids of the RLS-predicate and
sharing-rule enforceability rules in `packages/lint`, two whole source
files. The card stays open for the later slices listed under "Remaining"
below.
## What changes
- **One verdict line per finding.** Each finding of the 9 ids prints a
`message` of one verdict sentence. Every finding the rules' own suites
fire is now 194 characters or fewer, the CLI's RLS admission suite 195
or fewer, and the runtime publish gate's suite 140 (the longest of each
id was 492 to 2,056 before).
- **The verdict no longer echoes the predicate or the condition.** The
finding's `path` already names the clause
(`permissions[i].rowLevelSecurity[j].using` / `.check`,
`sharingRules[i].condition`), and the predicate is author text of any
length: the old message quoted up to 200 characters of an RLS predicate,
and all of an unparseable one or a sharing condition. Every verdict
still names the offending part: the compiler's, the shared filter
check's or the engine's own refusal, the comparison and its columns or
classes, the field (with the platform's "did you mean"), the
`current_user` key, the anchor object and its `sharingModel`.
- **A quoted refusal is quoted to its verdict.** The compiler's `detail`
is quoted up to the first ` — ` (where its own suggested rewrite begins:
`refusalVerdict()`); the shared filter check's and the engine's message
to the first sentence (`firstSentence()`, already used for the shared
check), then to the first ` — `. An engine verdict reads `RLS using
(CODE / STATUS): ENGINE_SENTENCE`, so the runtime publish gate's
existing pin that the message carries the engine's sentence after its
code and status still holds unchanged.
- **The reasoning moves to `RULE_EXPLANATIONS`**
(`packages/lint/src/rule-explanations.ts`), 9 new entries, so `os
explain RULE_ID` prints it and the CLI's `rule:` line ends with the
pointer for these ids. No CLI source changes: `explainPointer()` and `os
explain` resolve any key the table holds
(`packages/cli/test/explain-rule-id.test.ts` iterates every key; run
below).
- **Nothing else moves.** Rule ids, severities (all 9 are `error`),
`path`, `hint` (the `fix:` line) and what each rule accepts or refuses
are unchanged. No condition, branch, dedupe key or skip moved; every
hunk in the two rule files is a `message` expression, a comment, an
import, or a shared verdict helper. Two internal (non-barrel) helper
shapes changed with the text they fed: `ListHoldingComparison.columns`
(declaration sentences) became `holders` (column names), and
`CrossClassComparison.columns` became `why` (`text vs a number`);
`listHoldingDeclaration()` became the boolean `holdsListOrObject()` with
the same truth table, and it still gates the cross-class arm exactly as
before.
- `.changeset/22161-lint-slice-4-one-line.md`: `@objectstack/lint`
`patch`, naming every door that prints the new text (below). `Clause-②:
no` follows the contract review on slice 1 (its section ②):
`RULE_EXPLANATIONS` entries are data in an existing export.
- **Rider, text-only:**
`packages/cli/test/rls-policy-authoring-admission.test.ts`, three
message fragments it pins (the claim's declared rider; the `domain:cli`
seat reviews it after the fact, as for slice 1).
The verdict forms, one each, as the suites fired them (census below):
```text
RLS using is not lowerable (`==` compares one value, but its comparand is a list literal), so the policy is dropped and grants no access
RLS using does not parse as CEL, even after the legacy SQL bridge (Unexpected character: 'D'), so the policy is dropped and grants no access
RLS using is valid CEL but overruns the parse bound maxListElements (platform limit 64) (Exceeded maxListElements (64)), so the policy is dropped and grants no access
RLS using: `current_user.org_user_ids` is a membership set, a list on every request, used where one value belongs, so the policy is dropped and grants no access
RLS using lowers to a refused comparand (Operator "$in" on field "status" does not accept null as a list member (at using.$not.status.$in[1])), so the policy is dropped and grants no access
RLS using compares `record.status != record.tags`, where `tags` holds a list or an object (and 1 more), so the SQL drivers refuse every read it scopes (INVALID_FILTER / 400)
RLS check compares `record.status != record.amount`, which no comparison class spans (text vs a number), so the write check refuses the writes it judges (INVALID_FILTER / 400)
RLS using (INVALID_FILTER / 400): find('deal'): filter on 'close_date' compares a declared date field against "soon" at where.close_date.$gt, which is not a date value this platform can interpret
RLS using predicate field "is_private_nope" is not a field on object "crm_opportunity". Did you mean "is_private"? The policy is dropped and grants no access.
RLS using reads `current_user.nope`, which nothing pre-resolves, in a scalar position no request can ever fill, so the policy is dropped and grants no access
condition is not lowerable (unsupported operator "call"), so the rule is never seeded and grants nothing
condition compares `record.status != record.amount`, which no comparison class spans (text vs a number), so its criteria query is refused (INVALID_FILTER / 400) and it grants nothing
condition reads a runtime variable (variable "current_user.id" is not resolvable), so the rule is never seeded and grants nothing
anchor object "sys_audit_entry" declares no sharingModel and is a system object, which resolves to public, so the grant is refused (SHARING_NOT_ENABLED) and the rule restricts nothing
anchor object "crm_opportunity" declares sharingModel 'controlled_by_parent', so its access is derived from its master and the grant is refused (SHARING_NOT_ENABLED)
```
## Shared prose: written once (the dispatch's mechanism assumption 2)
It holds: the two files shared four families of sentences, and the RLS
file typed its drop consequence per arm. Each family now lives in one
place for the verdict and one place for the explanation, and every id
that uses it references it:
| shared sentence | ids | verdict clause (rule files) | explanation
(`rule-explanations.ts`) |
|---|---|---|---|
| what a dropped RLS policy does, per clause (was `consequence()` with
`DROPPED_UNCOMPILABLE` / `DROPPED_EVERY_REQUEST` /
`DROPPED_FACE_REFUSED`, `USING_INSERT_CONSEQUENCE`,
`CHECK_SET_QUALIFIER`, and `referenceConsequence()`'s field and variable
halves, which restated the same drop) | `rls-predicate-unparseable`,
`-over-budget`, `-unenforceable` (shape, type and shared-check arms),
`-unknown-field`, `-unknown-user-variable` | `droppedClause()` /
`droppedSentence()` over `DROPPED_OUTCOME` ("so the policy is dropped
and grants no access" / "… admits no write") | `RLS_UNCOMPILABLE_DROP`,
`RLS_DROPPED_USING`, `RLS_DROPPED_CHECK` |
| a field compared with a field that holds a list or an object (the
class sentence typed in both files, plus the per-surface consequences) |
`rls-predicate-unenforceable`, `sharing-rule-unlowerable-condition` |
`listHoldingVerdict()` (exported, both rules), closed by
`REFUSED_COMPARISON` (RLS) or `CRITERIA_QUERY_REFUSED` (sharing) |
`LIST_HOLDING_COMPARISON` |
| a comparison across comparison classes (was the exported
`CROSS_CLASS_SENTENCE` plus `describeCrossClassComparisons()`, both
rules) | the same two | `crossClassVerdict()` (exported, both rules),
same closers | `CROSS_CLASS_COMPARISON` |
| the sharing seeder's skip (was `skipped`, two ids) |
`sharing-rule-unlowerable-condition`, `-runtime-variable-condition` |
`NEVER_SEEDED` | `SHARING_RULE_SKIPPED` |
| the inert-grant refusal (`assertNotInertGrant`, typed in both anchor
messages) | `sharing-rule-object-not-shareable`,
`-object-controlled-by-parent` | each verdict names
`SHARING_NOT_ENABLED` | `SHARING_INERT_GRANT` |
`CROSS_CLASS_SENTENCE`, `describeCrossClassComparisons()`, `quote()`,
`ENGINE_REFUSED_CONSEQUENCE`, `HOLDING_SENTENCE` and `sourceOf()` had no
caller left and are removed; none is on the package barrel (`index.ts`),
and the package's `exports` map has no subpath that reaches them.
`CROSS_CLASS_REMEDY` (a hint) stays. The measurement notes in the
removed docblocks moved to the explanation constants' docblocks.
## Census (taken first, before any edit, at the base `1b99388505`)
Method: a scratch preload (`NODE_OPTIONS=--import`, never committed)
patched `Array.prototype.push` to record every finding-shaped object
(`rule` + `message`) of the 9 ids, deduped by (rule, message), with its
push site, in every vitest worker. Lengths are `message` alone; the
printed line adds `where` and `: `. Rows from `os lint`'s own printer
(`commands/lint.ts`) are excluded, as in slices 2 and 3.
- **`packages/lint` suite** at the base: 134 files, 6,117 passed and 5
skipped. All 9 ids fired, every one over 200, from 16 push sites (the
variants).
- **CLI, scoped (declared narrowing):**
`test/rls-policy-authoring-admission.test.ts`, against the base's lint
dist after the CLI closure build: 39 passed. It fires two ids. Scope:
the only `packages/cli` test whose fixtures carry `rowLevelSecurity` or
`sharingRules` for the authoring rules (grep: the other two hits are a
fixture probe set for `os verify` and a `migrate-meta` e2e that asserts
migrated keys); all 9 ids are `error`, so a CLI test over an example app
that fired one would fail its own `os validate`. Slice 3 recorded that a
later slice could scope its cli census this way.
- **Runtime publish gate:**
`packages/metadata-protocol/src/protocol.runtime-authoring-gate.test.ts`
(a consumer that pins this rule's text): 133 passed, fires two ids.
- **No id dropped:** every id is over 200 on some variant.
| rule id | severity | lint suite before: messages · longest | lint
suite after: messages · range | cli admission suite before → after |
runtime-gate suite before → after |
|---|---|---|---|---|---|
| `rls-predicate-unparseable` | error | 5 · 2,056 | 4 · 135–146 | — |
1,068 → 140 |
| `rls-predicate-unenforceable` | error | 369 · 1,679 | 312 · 41–194 |
1,616 → 195 | 477 → 140 |
| `rls-predicate-unknown-user-variable` | error | 7 · 1,544 | 6 ·
157–167 | 1,519 → 157 | — |
| `rls-predicate-unknown-field` | error | 34 · 1,399 | 21 · 109–158 | —
| — |
| `rls-predicate-over-budget` | error | 4 · 1,259 | 4 · 152–166 | — | —
|
| `sharing-rule-unlowerable-condition` | error | 92 · 1,093 | 60 ·
100–192 | — | — |
| `sharing-rule-object-not-shareable` | error | 3 · 774 | 3 · 140–183 |
— | — |
| `sharing-rule-object-controlled-by-parent` | error | 1 · 660 | 1 · 165
| — | — |
| `sharing-rule-runtime-variable-condition` | error | 3 · 492 | 1 · 129
| — | — |
The message counts fall because messages that differed only in the
echoed predicate or condition are now one message. No variant was lost:
all 16 push sites fire before and after (per site, before → after: the
shape arm 21 → 13 messages, the type-fault arm 23 → 10, the shared-check
arm 14 → 14, the list-holding arm 76 → 53, the cross-class arm 229 →
216, the engine arm 6 → 6, and so on for each id). The 41 is a
hint-by-code case whose stub engine answers "refused.".
## Doors that print the new text
Read from the registry (`authoring-rules.ts`: both entries are
`commands: ALL`; the RLS entry is `CLI_AND_RUNTIME` with `runtimeTypes:
['permission']`, the sharing entry `CLI_ONLY`) and the runtime gate
(`lint/src/runtime-gate.ts`,
`metadata-protocol/src/runtime-authoring-gate.ts`):
| door | ids | what changes |
|---|---|---|
| `os validate`, `os build` (and `os compile`, which `os dev` runs per
compile), `os lint`, `os verify`, `os init` scaffold check | all 9 | the
text-face verdict line; the `rule:` line gains the `os explain` pointer;
`os validate --json` `errors` and `os build --json` author-time `issues`
carry the new `message` |
| runtime publish gate, `permission` writes (Studio, REST `/meta`, MCP)
| the 5 `rls-predicate-*` ids, all errors | the 422 issue `message` and
the `OS_ALLOW_UNLINTED_METADATA_WRITES` refusal log line |
| never at the runtime gate | the 4 `sharing-rule-*` ids | the rule is
registered for the CLI doors only (its registry entry records the
publish-gate crossing as pending) |
| unchanged | every `hint`; every other rule id | |
Every row is named in the changeset.
## Tests
The rule suites import the rule source; the CLI and runtime-gate suites
read the rebuilt `@objectstack/lint` dist (marker: `which no comparison
class spans` 1 hit each in `dist/index.js` and `dist/index.cjs`, the old
`lowers, but compares` 0; `rls-predicate-unknown-user-variable` 1 hit
each in `dist/rule-explanations.js` and `.cjs`).
- **Each rule's own suites pin the new shape**, as in slices 2 and 3.
All seven test files of the two rules wrap their rule import and record
every finding their cases fire. A final case in each holds every
recorded verdict to one line of at most 200 characters, behind a
coverage control (the two main files: each of their ids fired; the five
arm files: their arm's verdict fired). A second block in each main file
pins, per id, that `explainRule(id)` exists and still names what the
verdict stopped saying, and the RLS file holds the explanation's two
written-out `current_user` key lists to `RESERVED_RLS_MEMBERSHIP_KEYS`
and to its own per-key runtime-type table (the explanation module
imports nothing). The measured-consequence pins (ZERO rows,
`PermissionDeniedError`, the `PERMISSION_DENIED` / 403 read leg, the
older-runtime fail-open sentence, the boot-skip, `buildReadFilter`
returning `null`, "share the master record instead") moved from the
message to the explanation. The "naming the declaration" cases now name
the column. Every refusal assertion, rule id, severity, `path`, `where`
and hint pin is unchanged.
- **Lint suite:** `pnpm --filter @objectstack/lint exec vitest run
--maxWorkers=2` gave Test Files 134 passed (134), Tests 6,146 passed
(6,146); lock VERDICT command-exit 0 (the 24 new cases; the 5 tests
skipped at the base ran after the CLI closure build put the `dist/` they
wait for in place).
- **Lint typecheck:** `pnpm --filter @objectstack/lint run typecheck`
gave VERDICT command-exit 0; `check:test-typecheck` OK, 2 files, 6
errors and 2 pinned signatures held.
- **CLI**, against the rebuilt lint dist: `pnpm --filter
@objectstack/cli exec vitest run --maxWorkers=2
test/rls-policy-authoring-admission.test.ts test/explain-rule-id.test.ts
test/commands.test.ts test/validate-build-gate-parity.test.ts` gave Test
Files 4 passed (4), Tests 127 passed (127). The admission file builds a
real `ObjectQL` host, so it is integration-tier; it ran locally because
the diff touches it. `explain-rule-id.test.ts` iterates every
`RULE_EXPLANATIONS` key, so all 9 new ids resolve through `os explain`
and `explainPointer`. `node bin/run-dev.js explain
sharing-rule-object-controlled-by-parent` prints the entry.
- **Runtime gate:** `pnpm --filter @objectstack/metadata-protocol exec
vitest run --maxWorkers=2 src/protocol.runtime-authoring-gate.test.ts`
gave 1 file, 133 passed, unchanged: its pin on the engine's sentence
after `(CODE / STATUS): ` holds.
- **Spec `repo` project (the dispatch's mechanism assumption 3):** `pnpm
--filter @objectstack/spec exec vitest run --project repo
--maxWorkers=2` gave Test Files 54 passed (54), Tests 915 passed (915);
lock VERDICT command-exit 0, at `daf3fabf0d`. Two of its tests walk
`packages/lint/src`: `src/shared/retired-key-migrate-sentence.test.ts`
(the `os migrate meta` sentence scanner; the new text carries no such
sentence, grep 0) and
`src/identity/position-delegatable-enforcer.pin.test.ts` (exported
`security-*` rule id constants; this slice declares none). No retired
key is named.
- **Tracker ids (mechanism assumption 4):** no `#` plus digits in any
printed message or explanation (`rule-explanations.test.ts` refuses one
in every paragraph; the RLS suite pins the unknown-field message, hint
and explanation free of one); the citations stay in `//` comments.
- **Ablation** (one-shot, from the committed state `daf3fabf0d`, through
`scripts/ablation-replace.mjs` wrap mode under the verify lock, plus a
shell `trap` restoring `HEAD`). The tests import the rule source, so
there is no dist leg. `sharing-rule-object-controlled-by-parent`'s
pre-slice message was restored verbatim through the anchor of its new
message: anchor x1 to x0, blob `f3c366fe7d8a` to `689144b7d98a`.
`src/validate-sharing-rule-enforceability.test.ts` then gave Test Files
1 failed (1), Tests 1 failed | 55 passed (56). The one red case was the
bound pin, "every verdict the cases above fired is one line of at most
200 characters" (the restored verdict, 660 characters). Restored: blob
after restore `f3c366fe7d8a` == blob at HEAD, `git diff HEAD` empty,
`git status --porcelain` empty. A first attempt never acquired the lock
(exit 99, queue timeout) and mutated nothing (blob unchanged).
- **ESLint, narrowed:** `npx eslint --no-inline-config --format json`
over the 11 changed `.ts` files gave 11 files in the report, 0 errors, 0
warnings. `eslint.config.mjs` never enables type-aware linting, so no
untouched file's verdict can move. A control-character scan of every
changed file found no match.
- **Gates:** `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` with no paths, on the final head
`daf3fabf0d` (merge base `1b99388505`), derived 63 commands. I ran 62 of
them sequentially, each with its own log and its exit code captured
before any pipe: all 62 exited 0. The 63rd, `pnpm
check:dual-build-cjs-loads`, is NOT MEASURED, reason: the dispatch
forbids the whole-workspace build it needs. `--ran` gave `Run
reconciliation — 63 derived, 62 run, 0 NOT-MEASURED, 1 UNRUN` (the one
above). I also ran the 52-family "Artifact rosters" block the same run
prints, 49 of them at `daf3fabf0d` (all exited 0; 14 of those are
checker-health `--self-test` rows, which grade a checker and not this
diff, and four keep their roster in a directory this diff touches:
`check-changeset-fixed`, `check:authz-resolver`,
`check:error-code-casing` and `check:filter-alias-parity`, all green).
The three PR-context guards (`check-closing-target-claim`,
`check-partof-closing-keyword`, `check-single-claim-paths`) need this
PR's number and body, so they run against this PR after it opens; their
readings are in the report on the card. The long ones on the shared box:
`check:type-check-debt` 357s, `check:query-options-erasure` 206s.
## Remaining for later slices — 71 ids in `packages/lint`, by file
Slice 3's list (PR #22548) minus this slice's two files and 9 ids. The
lengths are slice 2's (PR #22448, census at `05c7c3fa3b`; "cli" marks a
length only the cli suite reached), not re-measured here.
- `data-model-rules.ts` (6): `unique/legacy-organization-composite` 480
cli, `relationship/master-detail-required` 462,
`unique/unscoped-declared-index` 427, `unique/double-declaration` 402
cli, `rollup/non-numeric-aggregand` 364, `relationship/delete-behavior`
201 cli
- `validate-flow-trigger-readiness.ts` (5):
`flow-time-relative-descriptor-invalid` 796,
`flow-time-relative-descriptor-unroutable` 532,
`flow-trigger-unroutable` 518, `flow-api-trigger-secret-missing` 336,
`flow-trigger-unknown-event` 222
- `validate-react-page-props.ts` (5): `react-chart-drilldown-invalid`
784, `react-chart-aggregate-invalid` 507,
`react-chart-field-unprovisioned` 429,
`react-block-needs-record-context` 267, `react-page-source-unparseable`
211
- `validate-ai-agent-authoring.ts` (3): `default-agent-legacy-alias`
466, `default-agent-outside-roster` 388, `agent-authoring-withdrawn` 352
- `validate-predicate-path-refs.ts` (3): `predicate-rhs-path-shaped`
648, `predicate-path-unrooted` 434, `predicate-path-unresolved` 371
- `validate-searchable-fields.ts` (3): `searchable-field-unprovisioned`
512, `searchable-field-unsearchable` 449, `searchable-field-unknown` 295
- `validate-sortable-fields.ts` (3): `sort-field-unprovisioned` 844,
`sort-field-unsortable` 369, `sort-field-unknown` 287
- `lint-view-refs.ts` (2): `view-ref-nav-view-missing` 437,
`view-key-collision` 278 cli
- `validate-approval-approvers.ts` (2):
`approval-approvers-may-resolve-empty` 451,
`approval-approver-not-membership-tier` 272
- `validate-chart-bindings.ts` (2): `chart-measure-unknown` 442,
`chart-axis-not-selected` 327
- `validate-dashboard-action-refs.ts` (2):
`dashboard-action-route-unresolved` 242,
`dashboard-action-target-undefined` 239
- `validate-dataset-measure-aggregates.ts` (2):
`measure-aggregate-field-type-refused` 809,
`dimension-json-stored-field-refused` 531
- `validate-empty-combinators.ts` (2): `filter-empty-combinator` 352,
`filter-empty-node` 226
- `validate-list-view-field-refs.ts` (2): `list-view-field-dotted` 445,
`list-view-field-unknown` 355
- `validate-rule-compilability.ts` (2):
`validation-rule-json-schema-uncompilable` 517,
`validation-rule-regex-uncompilable` 482
- `validate-translation-references.ts` (2): `translation-target-unknown`
345, `translation-option-key-unknown` 230
- `lint-flow-credential-literals.ts` (1): `flow-credential-literal` 390
- `validate-action-dispatch-contract.ts` (1):
`action-dispatch-contract-mismatch` 927
- `validate-action-name-refs.ts` (1): `action-name-undefined` 409
- `validate-ai-surface-affinity.ts` (1): `ai-skill-surface-mismatch` 281
- `validate-ai-tool-references.ts` (1): `ai-skill-tool-unresolved` 441
- `validate-capability-references.ts` (1):
`capability-reference-unknown` 219
- `validate-component-types.ts` (1): `component-type-unknown` 807
- `validate-flow-filter-tokens.ts` (1): `flow-filter-token-unknown` 278
- `validate-managed-api-methods.ts` (1):
`object/managed-api-method-unaffordable` 412
- `validate-mapping-target-fields.ts` (1):
`mapping-target-field-unknown` 466
- `validate-nav-access.ts` (1): `nav-object-ungranted` 353
- `validate-nav-object-servability.ts` (1): `nav-object-unservable` 528
- `validate-nav-target-refs.ts` (1): `nav-target-unresolved` 426
- `validate-object-field-refs.ts` (1): `object-field-ref-unknown` 320
- `validate-object-references.ts` (1):
`object-reference-unregistered-platform` 324
- `validate-org-axis-red-lines.ts` (1): `org-axis-cross-org-bu-grant`
365
- `validate-page-visualization-bindings.ts` (1):
`page/visualization-without-binding` 629
- `validate-preset-comparands.ts` (1): `filter-preset-comparand` 669
- `validate-retired-permission-residue.ts` (1):
`permission-retired-lifecycle-residue` 235
- `validate-rule-schema-formats.ts` (1):
`validation-rule-json-schema-unknown-format` 1049
- `validate-seed-replay-safety.ts` (1):
`seed-insert-mode-duplicates-on-replay` 222
- `validate-seed-state-machine.ts` (1):
`seed-value-outside-state-machine` 320
- `validate-semantic-roles.ts` (1): `semantic-role-field-unprovisioned`
291
- `validate-translatable-sections.ts` (1):
`translation-section-name-missing` 553
- `validate-view-containers.ts` (1): `view-container-shape` 290
The 26 ids slice 2 fenced, the 9 ids owned by `packages/cli`, and the
`action-governance.ts` boot-log lines stay as PR #22448's body lists
them. This slice touched none of them.
## Acceptance notes
- **The `fix:` lines are unchanged and several stay long.** The dispatch
holds hints as they are. `sharing-rule-runtime-variable-condition`'s
hint runs to 2,280 characters (which mechanism replaces a `current_user`
condition, by the anchor's `sharingModel`),
`rls-predicate-unknown-field`'s to 668 on the suite's fixture, and the
over-budget, shape and type-fault hints to several hundred. Whether
hints get the same one-line budget is the card's call, as slices 2 and 3
noted.
- **The quoted engine sentence is the engine's.**
`@objectstack/objectql`'s refusals run to several sentences (up to 728
characters in the admission suite); the verdict quotes the first, up to
its first ` — `. That first sentence carries the engine's own location
suffix (`at where.close_date.$gt`), and the longest one the suites fire
leaves the verdict at 195. An engine sentence that is longer, or longer
field names, can pass 200. The code-keyed `fix:` line carries the remedy
the engine's later sentences spelled per instance.
- **The compiler's suggested rewrite is no longer in the shape
verdict.** For a few refusals the compiler's `detail` ends with a
per-instance rewrite after ` — ` (for the bare `current_user` root,
"compare one of its keys instead, e.g. `record.owner_id ==
current_user.id` …"; for a list literal, "spell 'one of these' as …").
The verdict keeps the half before it; the unchanged `fix:` line names
the subset and the three traps in general terms. For the TYPE faults
nothing is lost: their `fix:` line already carries the pasteable
rewrite.
- **The list-holding verdict names the column, not its declaration.**
"where `reviewers` holds a list or an object" replaces "where
`reviewers` is declared `type: 'lookup'`, `multiple: true`"; the
explanation names the classes (a structured JSON type such as `json` or
`address`, a multi-value field such as `multiselect`, `tags` or a lookup
flagged `multiple: true`) as examples, not as a list held to the spec.
- **Process:** the CLI closure build waited 8m45s and held the shared
lock 7m28s; the spec `repo` project held it 13m35s on the shared box.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01KNKBCRDJCu5tGy3TEbvtrF)_
---------
Co-authored-by: Claude <noreply@anthropic.com>os explain RULE_ID carries their reasoning (#22670)1 parent e194ab4 commit 07ee6f6
12 files changed
Lines changed: 968 additions & 546 deletions
File tree
- .changeset
- packages
- cli/test
- lint/src
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
208 | 208 | | |
209 | 209 | | |
210 | 210 | | |
211 | | - | |
212 | | - | |
| 211 | + | |
213 | 212 | | |
214 | 213 | | |
215 | 214 | | |
| |||
276 | 275 | | |
277 | 276 | | |
278 | 277 | | |
279 | | - | |
| 278 | + | |
280 | 279 | | |
281 | 280 | | |
282 | 281 | | |
| |||
333 | 332 | | |
334 | 333 | | |
335 | 334 | | |
336 | | - | |
| 335 | + | |
337 | 336 | | |
338 | 337 | | |
339 | 338 | | |
| |||
0 commit comments