Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .changeset/22161-lint-slice-4-one-line.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
"@objectstack/lint": patch
---

fix(lint): the RLS-predicate and sharing-rule findings print one verdict line, and `os explain <rule-id>` carries their reasoning

Clause-②: no

- **Shorter verdicts.** Each finding of these 9 rule ids now prints a `message` of one verdict sentence. Every finding the rules' own test suites, the CLI's RLS admission suite and the runtime publish gate's suite fire is at most 195 characters; before, the longest of each ran from 492 to 2,056 characters. The ids:
- RLS policies (`permissions[].rowLevelSecurity[].using` / `.check`): `rls-predicate-unparseable`, `rls-predicate-unenforceable`, `rls-predicate-unknown-user-variable`, `rls-predicate-unknown-field`, `rls-predicate-over-budget`;
- sharing rules (`sharingRules[]`): `sharing-rule-unlowerable-condition`, `sharing-rule-object-not-shareable`, `sharing-rule-object-controlled-by-parent`, `sharing-rule-runtime-variable-condition`.

A finding no longer echoes the predicate or the condition: its `path` names the clause, and the predicate is author text of any length (the old message quoted up to 200 characters of it, or all of it). Each verdict names what is wrong and closes on what it costs: an RLS policy the runtime drops ends "so the policy is dropped and grants no access" (a `using`) or "… and admits no write" (a `check`); a field compared with a field that holds a list or an object, or across comparison classes, quotes the first such comparison, names the column or the two classes, counts the rest, and ends on the `INVALID_FILTER / 400` refusal; a sharing condition that does not lower ends "so the rule is never seeded and grants nothing"; an anchor object a grant cannot widen names its `sharingModel` and the `SHARING_NOT_ENABLED` refusal. A quoted refusal — the compiler's, the platform's shared filter check's, or the engine's (`RLS using (CODE / STATUS): …`) — is quoted up to its own suggested rewrite, or to its first sentence. The `fix` (the CLI's `fix:` line, the runtime issue's `hint`), every rule id, severity and `path`, and what each rule accepts or refuses are unchanged. A tool that matched the old message text should match on `rule` and `path` instead.
- **`os explain <rule-id>` takes these 9 ids**, for example `os explain rls-predicate-unenforceable`. It prints the reasoning the verdicts no longer carry: what `RLSCompiler` does with a policy it cannot compile (the deny sentinel and its ZERO rows, the OR with other policies, the INSERT check a `using` doubles as, the `check` set), the one WARN line each drop leaves, which `current_user` keys the kernel resolves and with which type, why a column that holds a list or an object — or two columns of different comparison classes — cannot be compared, what the engine's own filter admission refuses and what that costs analytics, and, for sharing rules, why a condition is compiled once at boot, what an unlowerable or refused condition leaves behind, and why a grant on a public or `controlled_by_parent` object is refused. The paragraphs the RLS and sharing rules share are one text, printed under every id they explain. The `rule:` line under each of these findings now ends with `` — `os explain <rule-id>` for … ``. The no-argument listing and its `--json` `rules` array list the 9 ids, and so does the unknown-id error's `Rules with an explanation:` line. `RULE_EXPLANATIONS` in `@objectstack/lint` gains the 9 entries.
- **Where the new text prints.** On the CLI, all 9 ids: `os validate`, `os build` (and `os compile`, which `os dev` runs on every compile), `os lint`, `os verify` and `os init`'s scaffold check print the new `message` on the text face, and `os validate --json` and `os build --json` carry it in their `errors` and author-time `issues`. At the runtime publish gate (Studio, REST `/meta`, MCP), a `permission` write carries the five `rls-predicate-*` ids, all errors: the 422 issue's `message` and the refusal log line under `OS_ALLOW_UNLINTED_METADATA_WRITES` change. Each issue's `hint` is unchanged.
- **Never at the runtime gate:** the four `sharing-rule-*` ids. The rule is registered for the CLI doors only (no write type dispatches it at the publish gate), so they speak only on the CLI doors above.
7 changes: 3 additions & 4 deletions packages/cli/test/rls-policy-authoring-admission.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,8 +208,7 @@ describe('RLS read-scope admission at authoring time — fifteen classes, two do
if (row.engineCode) {
// The engine's own verdict, quoted with its code — never withheld at
// authoring time, where the text is the author's own.
expect(cli[0].message).toContain(`(${row.engineCode} / 400): `);
expect(cli[0].message).toContain('lowers, but the engine refuses to run the lowered filter on "deal"');
expect(cli[0].message.startsWith(`RLS using (${row.engineCode} / 400): `)).toBe(true);
}
});
}
Expand Down Expand Up @@ -276,7 +275,7 @@ describe('a field compared with a json / multiple field is refused at both doors
expect(cli.map((f) => ({ severity: f.severity, rule: f.rule, path: f.path }))).toEqual([
{ severity: 'error', rule: UNENFORCEABLE, path: `permissions[0].rowLevelSecurity[0].${row.clause}` },
]);
expect(cli[0].message).toContain('lowers, but compares a field with a field that holds a list or an object');
expect(cli[0].message).toContain('holds a list or an object, so ');

expect(saved.accepted).toBe(false);
expect({ code: saved.code, status: saved.status }).toEqual({ code: 'INVALID_METADATA', status: 422 });
Expand Down Expand Up @@ -333,7 +332,7 @@ describe('a field compared with a field of another comparison class is refused a
expect(cli.map((f) => ({ severity: f.severity, rule: f.rule, path: f.path }))).toEqual([
{ severity: 'error', rule: UNENFORCEABLE, path: `permissions[0].rowLevelSecurity[0].${row.clause}` },
]);
expect(cli[0].message).toContain('lowers, but compares two fields that share no comparison class');
expect(cli[0].message).toContain('which no comparison class spans (');

expect(saved.accepted).toBe(false);
expect({ code: saved.code, status: saved.status }).toEqual({ code: 'INVALID_METADATA', status: 422 });
Expand Down
Loading
Loading