Repository navigation
fix(lint): one-line verdicts for the RLS-predicate and sharing-rule enforceability rules; os explain RULE_ID carries their reasoning - #22670
Conversation
…enforceability rules Each finding of the nine rls-predicate-* and sharing-rule-* ids prints one verdict sentence; the reasoning moves into RULE_EXPLANATIONS, so `os explain RULE_ID` prints it. Rule ids, severities, paths, hints and what each rule accepts or refuses are unchanged. Claude-Session: https://claude.ai/code/session_01KNKBCRDJCu5tGy3TEbvtrF Co-authored-by: Claude <noreply@anthropic.com>
…et names every door Text-only: the three message fragments the admission suite pins follow the new verdict wording. The changeset names every door that prints these ids. Claude-Session: https://claude.ai/code/session_01KNKBCRDJCu5tGy3TEbvtrF Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 12 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 6 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 4 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin c50ad0ace0b602edb938200915b3909c5a5f7b35 && git checkout c50ad0ace0b602edb938200915b3909c5a5f7b35
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 36ac4b40e12893633ebd9d23ad3793fa1dc3314e daf3fabf0dba33d7fb8082b4095c884e0ec630a3 && git checkout -B drift-repro 36ac4b40e12893633ebd9d23ad3793fa1dc3314e && git merge --no-ff daf3fabf0dba33d7fb8082b4095c884e0ec630a3
node scripts/docs-audit/affected-docs.mjs --json 36ac4b40e12893633ebd9d23ad3793fa1dc3314e
|
Landing pre-checks at
|
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
messageof 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).pathalready 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"), thecurrent_userkey, the anchor object and itssharingModel.detailis 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 readsRLS 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.RULE_EXPLANATIONS(packages/lint/src/rule-explanations.ts), 9 new entries, soos explain RULE_IDprints it and the CLI'srule:line ends with the pointer for these ids. No CLI source changes:explainPointer()andos explainresolve any key the table holds (packages/cli/test/explain-rule-id.test.tsiterates every key; run below).error),path,hint(thefix: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 amessageexpression, 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) becameholders(column names), andCrossClassComparison.columnsbecamewhy(text vs a number);listHoldingDeclaration()became the booleanholdsListOrObject()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/lintpatch, naming every door that prints the new text (below).Clause-②: nofollows the contract review on slice 1 (its section ②):RULE_EXPLANATIONSentries are data in an existing export.packages/cli/test/rls-policy-authoring-admission.test.ts, three message fragments it pins (the claim's declared rider; thedomain:cliseat reviews it after the fact, as for slice 1).The verdict forms, one each, as the suites fired them (census below):
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:
rule-explanations.ts)consequence()withDROPPED_UNCOMPILABLE/DROPPED_EVERY_REQUEST/DROPPED_FACE_REFUSED,USING_INSERT_CONSEQUENCE,CHECK_SET_QUALIFIER, andreferenceConsequence()'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-variabledroppedClause()/droppedSentence()overDROPPED_OUTCOME("so the policy is dropped and grants no access" / "… admits no write")RLS_UNCOMPILABLE_DROP,RLS_DROPPED_USING,RLS_DROPPED_CHECKrls-predicate-unenforceable,sharing-rule-unlowerable-conditionlistHoldingVerdict()(exported, both rules), closed byREFUSED_COMPARISON(RLS) orCRITERIA_QUERY_REFUSED(sharing)LIST_HOLDING_COMPARISONCROSS_CLASS_SENTENCEplusdescribeCrossClassComparisons(), both rules)crossClassVerdict()(exported, both rules), same closersCROSS_CLASS_COMPARISONskipped, two ids)sharing-rule-unlowerable-condition,-runtime-variable-conditionNEVER_SEEDEDSHARING_RULE_SKIPPEDassertNotInertGrant, typed in both anchor messages)sharing-rule-object-not-shareable,-object-controlled-by-parentSHARING_NOT_ENABLEDSHARING_INERT_GRANTCROSS_CLASS_SENTENCE,describeCrossClassComparisons(),quote(),ENGINE_REFUSED_CONSEQUENCE,HOLDING_SENTENCEandsourceOf()had no caller left and are removed; none is on the package barrel (index.ts), and the package'sexportsmap 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) patchedArray.prototype.pushto record every finding-shaped object (rule+message) of the 9 ids, deduped by (rule, message), with its push site, in every vitest worker. Lengths aremessagealone; the printed line addswhereand:. Rows fromos lint's own printer (commands/lint.ts) are excluded, as in slices 2 and 3.packages/lintsuite 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).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 onlypackages/clitest whose fixtures carryrowLevelSecurityorsharingRulesfor the authoring rules (grep: the other two hits are a fixture probe set foros verifyand amigrate-metae2e that asserts migrated keys); all 9 ids areerror, so a CLI test over an example app that fired one would fail its ownos validate. Slice 3 recorded that a later slice could scope its cli census this way.packages/metadata-protocol/src/protocol.runtime-authoring-gate.test.ts(a consumer that pins this rule's text): 133 passed, fires two ids.rls-predicate-unparseablerls-predicate-unenforceablerls-predicate-unknown-user-variablerls-predicate-unknown-fieldrls-predicate-over-budgetsharing-rule-unlowerable-conditionsharing-rule-object-not-shareablesharing-rule-object-controlled-by-parentsharing-rule-runtime-variable-conditionThe 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 arecommands: ALL; the RLS entry isCLI_AND_RUNTIMEwithruntimeTypes: ['permission'], the sharing entryCLI_ONLY) and the runtime gate (lint/src/runtime-gate.ts,metadata-protocol/src/runtime-authoring-gate.ts):os validate,os build(andos compile, whichos devruns per compile),os lint,os verify,os initscaffold checkrule:line gains theos explainpointer;os validate --jsonerrorsandos build --jsonauthor-timeissuescarry the newmessagepermissionwrites (Studio, REST/meta, MCP)rls-predicate-*ids, all errorsmessageand theOS_ALLOW_UNLINTED_METADATA_WRITESrefusal log linesharing-rule-*idshint; every other rule idEvery row is named in the changeset.
Tests
The rule suites import the rule source; the CLI and runtime-gate suites read the rebuilt
@objectstack/lintdist (marker:which no comparison class spans1 hit each indist/index.jsanddist/index.cjs, the oldlowers, but compares0;rls-predicate-unknown-user-variable1 hit each indist/rule-explanations.jsand.cjs).explainRule(id)exists and still names what the verdict stopped saying, and the RLS file holds the explanation's two written-outcurrent_userkey lists toRESERVED_RLS_MEMBERSHIP_KEYSand to its own per-key runtime-type table (the explanation module imports nothing). The measured-consequence pins (ZERO rows,PermissionDeniedError, thePERMISSION_DENIED/ 403 read leg, the older-runtime fail-open sentence, the boot-skip,buildReadFilterreturningnull, "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,whereand hint pin is unchanged.pnpm --filter @objectstack/lint exec vitest run --maxWorkers=2gave 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 thedist/they wait for in place).pnpm --filter @objectstack/lint run typecheckgave VERDICT command-exit 0;check:test-typecheckOK, 2 files, 6 errors and 2 pinned signatures held.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.tsgave Test Files 4 passed (4), Tests 127 passed (127). The admission file builds a realObjectQLhost, so it is integration-tier; it ran locally because the diff touches it.explain-rule-id.test.tsiterates everyRULE_EXPLANATIONSkey, so all 9 new ids resolve throughos explainandexplainPointer.node bin/run-dev.js explain sharing-rule-object-controlled-by-parentprints the entry.pnpm --filter @objectstack/metadata-protocol exec vitest run --maxWorkers=2 src/protocol.runtime-authoring-gate.test.tsgave 1 file, 133 passed, unchanged: its pin on the engine's sentence after(CODE / STATUS):holds.repoproject (the dispatch's mechanism assumption 3):pnpm --filter @objectstack/spec exec vitest run --project repo --maxWorkers=2gave Test Files 54 passed (54), Tests 915 passed (915); lock VERDICT command-exit 0, atdaf3fabf0d. Two of its tests walkpackages/lint/src:src/shared/retired-key-migrate-sentence.test.ts(theos migrate metasentence scanner; the new text carries no such sentence, grep 0) andsrc/identity/position-delegatable-enforcer.pin.test.ts(exportedsecurity-*rule id constants; this slice declares none). No retired key is named.#plus digits in any printed message or explanation (rule-explanations.test.tsrefuses one in every paragraph; the RLS suite pins the unknown-field message, hint and explanation free of one); the citations stay in//comments.daf3fabf0d, throughscripts/ablation-replace.mjswrap mode under the verify lock, plus a shelltraprestoringHEAD). 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, blobf3c366fe7d8ato689144b7d98a.src/validate-sharing-rule-enforceability.test.tsthen 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 restoref3c366fe7d8a== blob at HEAD,git diff HEADempty,git status --porcelainempty. A first attempt never acquired the lock (exit 99, queue timeout) and mutated nothing (blob unchanged).npx eslint --no-inline-config --format jsonover the 11 changed.tsfiles gave 11 files in the report, 0 errors, 0 warnings.eslint.config.mjsnever enables type-aware linting, so no untouched file's verdict can move. A control-character scan of every changed file found no match.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackwith no paths, on the final headdaf3fabf0d(merge base1b99388505), 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.--rangaveRun 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 atdaf3fabf0d(all exited 0; 14 of those are checker-health--self-testrows, 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-casingandcheck: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-debt357s,check:query-options-erasure206s.Remaining for later slices — 71 ids in
packages/lint, by fileSlice 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-composite480 cli,relationship/master-detail-required462,unique/unscoped-declared-index427,unique/double-declaration402 cli,rollup/non-numeric-aggregand364,relationship/delete-behavior201 clivalidate-flow-trigger-readiness.ts(5):flow-time-relative-descriptor-invalid796,flow-time-relative-descriptor-unroutable532,flow-trigger-unroutable518,flow-api-trigger-secret-missing336,flow-trigger-unknown-event222validate-react-page-props.ts(5):react-chart-drilldown-invalid784,react-chart-aggregate-invalid507,react-chart-field-unprovisioned429,react-block-needs-record-context267,react-page-source-unparseable211validate-ai-agent-authoring.ts(3):default-agent-legacy-alias466,default-agent-outside-roster388,agent-authoring-withdrawn352validate-predicate-path-refs.ts(3):predicate-rhs-path-shaped648,predicate-path-unrooted434,predicate-path-unresolved371validate-searchable-fields.ts(3):searchable-field-unprovisioned512,searchable-field-unsearchable449,searchable-field-unknown295validate-sortable-fields.ts(3):sort-field-unprovisioned844,sort-field-unsortable369,sort-field-unknown287lint-view-refs.ts(2):view-ref-nav-view-missing437,view-key-collision278 clivalidate-approval-approvers.ts(2):approval-approvers-may-resolve-empty451,approval-approver-not-membership-tier272validate-chart-bindings.ts(2):chart-measure-unknown442,chart-axis-not-selected327validate-dashboard-action-refs.ts(2):dashboard-action-route-unresolved242,dashboard-action-target-undefined239validate-dataset-measure-aggregates.ts(2):measure-aggregate-field-type-refused809,dimension-json-stored-field-refused531validate-empty-combinators.ts(2):filter-empty-combinator352,filter-empty-node226validate-list-view-field-refs.ts(2):list-view-field-dotted445,list-view-field-unknown355validate-rule-compilability.ts(2):validation-rule-json-schema-uncompilable517,validation-rule-regex-uncompilable482validate-translation-references.ts(2):translation-target-unknown345,translation-option-key-unknown230lint-flow-credential-literals.ts(1):flow-credential-literal390validate-action-dispatch-contract.ts(1):action-dispatch-contract-mismatch927validate-action-name-refs.ts(1):action-name-undefined409validate-ai-surface-affinity.ts(1):ai-skill-surface-mismatch281validate-ai-tool-references.ts(1):ai-skill-tool-unresolved441validate-capability-references.ts(1):capability-reference-unknown219validate-component-types.ts(1):component-type-unknown807validate-flow-filter-tokens.ts(1):flow-filter-token-unknown278validate-managed-api-methods.ts(1):object/managed-api-method-unaffordable412validate-mapping-target-fields.ts(1):mapping-target-field-unknown466validate-nav-access.ts(1):nav-object-ungranted353validate-nav-object-servability.ts(1):nav-object-unservable528validate-nav-target-refs.ts(1):nav-target-unresolved426validate-object-field-refs.ts(1):object-field-ref-unknown320validate-object-references.ts(1):object-reference-unregistered-platform324validate-org-axis-red-lines.ts(1):org-axis-cross-org-bu-grant365validate-page-visualization-bindings.ts(1):page/visualization-without-binding629validate-preset-comparands.ts(1):filter-preset-comparand669validate-retired-permission-residue.ts(1):permission-retired-lifecycle-residue235validate-rule-schema-formats.ts(1):validation-rule-json-schema-unknown-format1049validate-seed-replay-safety.ts(1):seed-insert-mode-duplicates-on-replay222validate-seed-state-machine.ts(1):seed-value-outside-state-machine320validate-semantic-roles.ts(1):semantic-role-field-unprovisioned291validate-translatable-sections.ts(1):translation-section-name-missing553validate-view-containers.ts(1):view-container-shape290The 26 ids slice 2 fenced, the 9 ids owned by
packages/cli, and theaction-governance.tsboot-log lines stay as PR #22448's body lists them. This slice touched none of them.Acceptance notes
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 acurrent_usercondition, by the anchor'ssharingModel),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.@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-keyedfix:line carries the remedy the engine's later sentences spelled per instance.detailends with a per-instance rewrite after—(for the barecurrent_userroot, "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 unchangedfix:line names the subset and the three traps in general terms. For the TYPE faults nothing is lost: theirfix:line already carries the pasteable rewrite.reviewersholds a list or an object" replaces "wherereviewersis declaredtype: 'lookup',multiple: true"; the explanation names the classes (a structured JSON type such asjsonoraddress, a multi-value field such asmultiselect,tagsor a lookup flaggedmultiple: true) as examples, not as a list held to the spec.repoproject held it 13m35s on the shared box.Generated by Claude Code