Skip to content

fix(lint): one-line verdicts for the RLS-predicate and sharing-rule enforceability rules; os explain RULE_ID carries their reasoning - #22670

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-22161-s2-lint-slice-4
Oct 10, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-22161-s2-lint-slice-4

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

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):

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

…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>
@github-actions github-actions Bot added size/xl documentation Improvements or additions to documentation tests tooling labels Oct 10, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/lint, touching 58 documentable anchor(s).

12 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/api/error-catalog.mdx (via controlled_by_parent (literal, a string literal in anchorFindings))
  • content/docs/data-modeling/objects.mdx (via controlled_by_parent (literal, a string literal in anchorFindings))
  • content/docs/deployment/validating-metadata.mdx (via validateRlsPredicateEnforceability (symbol, a top-level function), validateSharingRuleEnforceability (symbol, a top-level function))
  • content/docs/getting-started/common-patterns.mdx (via controlled_by_parent (literal, a string literal in anchorFindings))
  • content/docs/kernel/runtime-services/sharing-service.mdx (via controlled_by_parent (literal, a string literal in anchorFindings))
  • content/docs/permissions/authorization.mdx (via controlled_by_parent (literal, a string literal in anchorFindings))
  • content/docs/permissions/index.mdx (via controlled_by_parent (literal, a string literal in anchorFindings))
  • content/docs/permissions/permissions-matrix.mdx (via controlled_by_parent (literal, a string literal in anchorFindings))
  • content/docs/permissions/rls.mdx (via controlled_by_parent (literal, a string literal in anchorFindings))
  • content/docs/permissions/sharing-rules.mdx (via controlled_by_parent (literal, a string literal in anchorFindings))
  • content/docs/protocol/kernel/error-handling.mdx (via controlled_by_parent (literal, a string literal in anchorFindings))
  • content/docs/protocol/objectql/security.mdx (via controlled_by_parent (literal, a string literal in anchorFindings))

⛔ 6 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/implementation-status.mdx (via controlled_by_parent (literal, a string literal in anchorFindings))
  • content/docs/releases/v15.mdx (via controlled_by_parent (literal, a string literal in anchorFindings))
  • content/docs/releases/v17/17-1.mdx (via controlled_by_parent (literal, a string literal in anchorFindings))
  • content/docs/releases/v17/17-2.mdx (via controlled_by_parent (literal, a string literal in anchorFindings))
  • content/docs/releases/v17/17-3.mdx (via controlled_by_parent (literal, a string literal in anchorFindings))
  • content/docs/releases/v17/index.mdx (via controlled_by_parent (literal, a string literal in anchorFindings))

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.

What this run could not see
  • 10 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 4 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 36ac4b40e12893633ebd9d23ad3793fa1dc3314e → packageMentionDocs.

Which tree this was computed on

This run read content/docs from c50ad0ace0b602edb938200915b3909c5a5f7b35 — the merge of head daf3fabf0dba33d7fb8082b4095c884e0ec630a3 into base 36ac4b40e12893633ebd9d23ad3793fa1dc3314e, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 36ac4b40e12893633ebd9d23ad3793fa1dc3314e → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Landing pre-checks at daf3fabf0d, by the owning seat: every check green, queued

domain:spec seat 3 (#18883) · zhuangjianguo · session session_01KNKBCRDJCu5tGy3TEbvtrF · 2026-10-10T10:24Z · holder of claim 6095284867 on #22161.

  • The review: the seat's ACCEPT 6096383240 names this head. The diff is message prose only: the one non-message hunk, listHoldingDeclaration → holdsListOrObject, keeps the same truth table at both callers. No contract review is owed, the same as slice 3: slice 1's review 6072212345 ② records RULE_EXPLANATIONS entries as data, Clause-②: no / patch.
  • CI: 34 check-runs: 31 success, 3 skipped, nothing else. check-expected-skips --pr 22670 reads every skip in the roster.
  • Governed: check-governed-merges --pr objectstack-ai/objectstack#22670 reads NOT governed; +968 / −546.
  • Closing keywords: the body carries Part of #22161 alone, and no commit message carries one. The card stays open for its later slices.
  • main drift since the merge base 1b99388505: main (now ee3ae0360d) moved none of the PR's 12 paths. GitHub reports mergeable: true (clean).

pr_ready and automerge_enable follow.


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 10, 2026 10:25
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 10, 2026 10:25
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 10, 2026
Merged via the queue into main with commit 07ee6f6 Oct 10, 2026
36 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-22161-s2-lint-slice-4 branch October 10, 2026 10:56
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/xl tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants