@sequenceholdings/orm 2.3.0 → 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/dist/authoring.cjs +53 -7
  2. package/dist/authoring.cjs.map +1 -1
  3. package/dist/authoring.d.cts +11 -7
  4. package/dist/authoring.d.ts +11 -7
  5. package/dist/authoring.js +53 -7
  6. package/dist/authoring.js.map +1 -1
  7. package/dist/canonical.cjs +48 -2
  8. package/dist/canonical.cjs.map +1 -1
  9. package/dist/canonical.d.cts +72 -72
  10. package/dist/canonical.d.ts +72 -72
  11. package/dist/canonical.js +48 -2
  12. package/dist/canonical.js.map +1 -1
  13. package/dist/cli.cjs +337 -86
  14. package/dist/cli.cjs.map +1 -1
  15. package/dist/cli.js +337 -86
  16. package/dist/cli.js.map +1 -1
  17. package/dist/client.cjs +1 -1
  18. package/dist/client.cjs.map +1 -1
  19. package/dist/client.d.cts +1 -1
  20. package/dist/client.d.ts +1 -1
  21. package/dist/client.js +1 -1
  22. package/dist/client.js.map +1 -1
  23. package/dist/compiler.cjs +493 -52
  24. package/dist/compiler.cjs.map +1 -1
  25. package/dist/compiler.d.cts +22 -7
  26. package/dist/compiler.d.ts +22 -7
  27. package/dist/compiler.js +493 -53
  28. package/dist/compiler.js.map +1 -1
  29. package/dist/{constants-C6EG6Wq7.d.cts → constants-Dou1RkTo.d.cts} +7 -2
  30. package/dist/{constants-C6EG6Wq7.d.ts → constants-Dou1RkTo.d.ts} +7 -2
  31. package/dist/dynamic-input.d.cts +1 -1
  32. package/dist/dynamic-input.d.ts +1 -1
  33. package/dist/index.cjs +329 -130
  34. package/dist/index.cjs.map +1 -1
  35. package/dist/index.d.cts +66 -58
  36. package/dist/index.d.ts +66 -58
  37. package/dist/index.js +329 -131
  38. package/dist/index.js.map +1 -1
  39. package/dist/ir.cjs +50 -3
  40. package/dist/ir.cjs.map +1 -1
  41. package/dist/ir.d.cts +71 -71
  42. package/dist/ir.d.ts +71 -71
  43. package/dist/ir.js +50 -3
  44. package/dist/ir.js.map +1 -1
  45. package/dist/predicate-v2.cjs +145 -22
  46. package/dist/predicate-v2.cjs.map +1 -1
  47. package/dist/predicate-v2.d.cts +41 -4
  48. package/dist/predicate-v2.d.ts +41 -4
  49. package/dist/predicate-v2.js +145 -22
  50. package/dist/predicate-v2.js.map +1 -1
  51. package/dist/sdl.cjs +53 -3
  52. package/dist/sdl.cjs.map +1 -1
  53. package/dist/sdl.d.cts +3 -1
  54. package/dist/sdl.d.ts +3 -1
  55. package/dist/sdl.js +53 -3
  56. package/dist/sdl.js.map +1 -1
  57. package/package.json +1 -1
  58. package/dist/{definition-DjfC7Lll.d.cts → definition-B99BsOAO.d.cts} +4 -4
  59. package/dist/{definition-DjfC7Lll.d.ts → definition-B99BsOAO.d.ts} +4 -4
@@ -1,7 +1,7 @@
1
1
  import { ClaimKey, PredIr } from './ir.cjs';
2
2
  import { P as PolicyExpression } from './policy-1iZbGnZZ.cjs';
3
3
  import 'zod';
4
- import './constants-C6EG6Wq7.cjs';
4
+ import './constants-Dou1RkTo.cjs';
5
5
 
6
6
  declare const POLICY_COMPARE_OPS: readonly ["=", "!=", "<", "<=", ">", ">="];
7
7
  type PolicyCompareOp = (typeof POLICY_COMPARE_OPS)[number];
@@ -50,6 +50,16 @@ type PolicyNode = {
50
50
  kind: 'exists';
51
51
  path: readonly string[];
52
52
  where: PolicyNode;
53
+ }
54
+ /**
55
+ * `visible(<link-path>)` — a related row exists AND the caller may read it:
56
+ * the final target's own read policy is compiled in as the inner predicate.
57
+ * Lets a write policy say "only on a parent you can see" without restating
58
+ * the parent's rule (and without drifting from it).
59
+ */
60
+ | {
61
+ kind: 'visible';
62
+ path: readonly string[];
53
63
  };
54
64
  interface ParsedPolicy {
55
65
  readonly source: string;
@@ -63,7 +73,7 @@ declare class PolicyParseError extends Error {
63
73
  readonly name = "PolicyParseError";
64
74
  constructor(message: string, position: number);
65
75
  }
66
- type PolicyCompileErrorCode = 'unknown-table' | 'unknown-column' | 'unknown-link' | 'not-to-one' | 'link-depth-exceeded' | 'unsupported-comparison' | 'invalid-null-comparison' | 'when-not-claims-only' | 'claim-type-mismatch';
76
+ type PolicyCompileErrorCode = 'unknown-table' | 'unknown-column' | 'unknown-link' | 'not-to-one' | 'link-depth-exceeded' | 'unsupported-comparison' | 'invalid-null-comparison' | 'when-not-claims-only' | 'claim-type-mismatch' | 'visible-unresolvable' | 'visible-cycle';
67
77
  /**
68
78
  * Compile-time failure — the parsed policy references something the table
69
79
  * doesn't declare, or uses a shape the v2.0 IR cannot represent. Carries
@@ -96,6 +106,11 @@ declare function parsePolicy(source: string): ParsedPolicy;
96
106
  * - variant `when` guards are claims-only: capability()/role() under
97
107
  * and/or/not. (claim() comparisons in `when` are not representable in the
98
108
  * v2.0 IR — a targeted error says so.)
109
+ * - `visible(<link-path>)` lowers to `exists(<link-path>, <target read policy>)`,
110
+ * compiled from the target's own read policy expression at the combined
111
+ * depth. A read policy that reaches itself again through visible() is a
112
+ * cycle and refused; a target with no read policy is readable to everyone
113
+ * who may use the namespace, so visibility reduces to existence.
99
114
  */
100
115
 
101
116
  declare const POLICY_MAX_LINK_DEPTH = 3;
@@ -120,6 +135,19 @@ interface PolicyTableInfo {
120
135
  /** `{ tables }` keyed by table name — TableMetaIr's record is assignable. */
121
136
  interface PolicyResolver {
122
137
  readonly tables: Record<string, PolicyTableInfo>;
138
+ /**
139
+ * A table's read policy expression, for `visible()`. Return `undefined` when
140
+ * the table has none (readable to every namespace user). A resolver without
141
+ * this callback cannot compile `visible()`: it is refused, never guessed.
142
+ */
143
+ readonly readPolicy?: (table: string) => PolicyExpression | undefined;
144
+ /**
145
+ * The namespace that owns a table's policies, in a composed namespace. A
146
+ * visible() expansion of an imported table's read policy evaluates its
147
+ * capability() and role() checks in that namespace, not the importer's.
148
+ * Omitted (or undefined for a table) leaves them unscoped.
149
+ */
150
+ readonly policyNamespace?: (table: string) => string | undefined;
123
151
  }
124
152
  interface CompilePredicateInput {
125
153
  /** DSL source text, or an already-parsed policy. */
@@ -131,11 +159,20 @@ interface CompilePredicateInput {
131
159
  declare function compilePredicateV2({ expression, table, resolver, maxLinkDepth }: CompilePredicateInput): PredIr;
132
160
  /** Variant `when` guards may only test claims: capability()/role() under and/or/not. */
133
161
  declare function assertClaimsOnlyWhen(parsed: ParsedPolicy): void;
162
+ /** Which of a table's policies is being compiled. */
163
+ type PolicyAccess = 'read' | 'write' | 'insert' | 'update';
134
164
  interface CompilePolicyInput {
135
165
  expression: PolicyExpression;
136
166
  table: string;
137
167
  resolver: PolicyResolver;
138
168
  maxLinkDepth?: number;
169
+ /**
170
+ * Default 'read'. A read policy that reaches its own table through visible()
171
+ * is a cycle; a write policy that follows a self-link (a subtask written
172
+ * under a parent the caller can read) only expands the read policy, so it
173
+ * does not start the cycle check at its own table.
174
+ */
175
+ access?: PolicyAccess;
139
176
  }
140
177
  /**
141
178
  * Policy expression (string or `{default, variants}`) → one PredIr under §4c
@@ -146,6 +183,6 @@ interface CompilePolicyInput {
146
183
  * Granting a role/capability only ever widens access; `allow: true`
147
184
  * short-circuits its branch to the guard alone.
148
185
  */
149
- declare function compilePolicy({ expression, table, resolver, maxLinkDepth }: CompilePolicyInput): PredIr;
186
+ declare function compilePolicy({ expression, table, resolver, maxLinkDepth, access }: CompilePolicyInput): PredIr;
150
187
 
151
- export { type CompilePolicyInput, type CompilePredicateInput, POLICY_COMPARE_OPS, POLICY_MAX_LINK_DEPTH, type ParsedPolicy, type PolicyColumnInfo, type PolicyCompareOp, PolicyCompileError, type PolicyCompileErrorCode, type PolicyLinkInfo, type PolicyNode, type PolicyOperand, PolicyParseError, type PolicyResolver, type PolicyTableInfo, assertClaimsOnlyWhen, compilePolicy, compilePredicateV2, parsePolicy, walkPolicy };
188
+ export { type CompilePolicyInput, type CompilePredicateInput, POLICY_COMPARE_OPS, POLICY_MAX_LINK_DEPTH, type ParsedPolicy, type PolicyAccess, type PolicyColumnInfo, type PolicyCompareOp, PolicyCompileError, type PolicyCompileErrorCode, type PolicyLinkInfo, type PolicyNode, type PolicyOperand, PolicyParseError, type PolicyResolver, type PolicyTableInfo, assertClaimsOnlyWhen, compilePolicy, compilePredicateV2, parsePolicy, walkPolicy };
@@ -1,7 +1,7 @@
1
1
  import { ClaimKey, PredIr } from './ir.js';
2
2
  import { P as PolicyExpression } from './policy-1iZbGnZZ.js';
3
3
  import 'zod';
4
- import './constants-C6EG6Wq7.js';
4
+ import './constants-Dou1RkTo.js';
5
5
 
6
6
  declare const POLICY_COMPARE_OPS: readonly ["=", "!=", "<", "<=", ">", ">="];
7
7
  type PolicyCompareOp = (typeof POLICY_COMPARE_OPS)[number];
@@ -50,6 +50,16 @@ type PolicyNode = {
50
50
  kind: 'exists';
51
51
  path: readonly string[];
52
52
  where: PolicyNode;
53
+ }
54
+ /**
55
+ * `visible(<link-path>)` — a related row exists AND the caller may read it:
56
+ * the final target's own read policy is compiled in as the inner predicate.
57
+ * Lets a write policy say "only on a parent you can see" without restating
58
+ * the parent's rule (and without drifting from it).
59
+ */
60
+ | {
61
+ kind: 'visible';
62
+ path: readonly string[];
53
63
  };
54
64
  interface ParsedPolicy {
55
65
  readonly source: string;
@@ -63,7 +73,7 @@ declare class PolicyParseError extends Error {
63
73
  readonly name = "PolicyParseError";
64
74
  constructor(message: string, position: number);
65
75
  }
66
- type PolicyCompileErrorCode = 'unknown-table' | 'unknown-column' | 'unknown-link' | 'not-to-one' | 'link-depth-exceeded' | 'unsupported-comparison' | 'invalid-null-comparison' | 'when-not-claims-only' | 'claim-type-mismatch';
76
+ type PolicyCompileErrorCode = 'unknown-table' | 'unknown-column' | 'unknown-link' | 'not-to-one' | 'link-depth-exceeded' | 'unsupported-comparison' | 'invalid-null-comparison' | 'when-not-claims-only' | 'claim-type-mismatch' | 'visible-unresolvable' | 'visible-cycle';
67
77
  /**
68
78
  * Compile-time failure — the parsed policy references something the table
69
79
  * doesn't declare, or uses a shape the v2.0 IR cannot represent. Carries
@@ -96,6 +106,11 @@ declare function parsePolicy(source: string): ParsedPolicy;
96
106
  * - variant `when` guards are claims-only: capability()/role() under
97
107
  * and/or/not. (claim() comparisons in `when` are not representable in the
98
108
  * v2.0 IR — a targeted error says so.)
109
+ * - `visible(<link-path>)` lowers to `exists(<link-path>, <target read policy>)`,
110
+ * compiled from the target's own read policy expression at the combined
111
+ * depth. A read policy that reaches itself again through visible() is a
112
+ * cycle and refused; a target with no read policy is readable to everyone
113
+ * who may use the namespace, so visibility reduces to existence.
99
114
  */
100
115
 
101
116
  declare const POLICY_MAX_LINK_DEPTH = 3;
@@ -120,6 +135,19 @@ interface PolicyTableInfo {
120
135
  /** `{ tables }` keyed by table name — TableMetaIr's record is assignable. */
121
136
  interface PolicyResolver {
122
137
  readonly tables: Record<string, PolicyTableInfo>;
138
+ /**
139
+ * A table's read policy expression, for `visible()`. Return `undefined` when
140
+ * the table has none (readable to every namespace user). A resolver without
141
+ * this callback cannot compile `visible()`: it is refused, never guessed.
142
+ */
143
+ readonly readPolicy?: (table: string) => PolicyExpression | undefined;
144
+ /**
145
+ * The namespace that owns a table's policies, in a composed namespace. A
146
+ * visible() expansion of an imported table's read policy evaluates its
147
+ * capability() and role() checks in that namespace, not the importer's.
148
+ * Omitted (or undefined for a table) leaves them unscoped.
149
+ */
150
+ readonly policyNamespace?: (table: string) => string | undefined;
123
151
  }
124
152
  interface CompilePredicateInput {
125
153
  /** DSL source text, or an already-parsed policy. */
@@ -131,11 +159,20 @@ interface CompilePredicateInput {
131
159
  declare function compilePredicateV2({ expression, table, resolver, maxLinkDepth }: CompilePredicateInput): PredIr;
132
160
  /** Variant `when` guards may only test claims: capability()/role() under and/or/not. */
133
161
  declare function assertClaimsOnlyWhen(parsed: ParsedPolicy): void;
162
+ /** Which of a table's policies is being compiled. */
163
+ type PolicyAccess = 'read' | 'write' | 'insert' | 'update';
134
164
  interface CompilePolicyInput {
135
165
  expression: PolicyExpression;
136
166
  table: string;
137
167
  resolver: PolicyResolver;
138
168
  maxLinkDepth?: number;
169
+ /**
170
+ * Default 'read'. A read policy that reaches its own table through visible()
171
+ * is a cycle; a write policy that follows a self-link (a subtask written
172
+ * under a parent the caller can read) only expands the read policy, so it
173
+ * does not start the cycle check at its own table.
174
+ */
175
+ access?: PolicyAccess;
139
176
  }
140
177
  /**
141
178
  * Policy expression (string or `{default, variants}`) → one PredIr under §4c
@@ -146,6 +183,6 @@ interface CompilePolicyInput {
146
183
  * Granting a role/capability only ever widens access; `allow: true`
147
184
  * short-circuits its branch to the guard alone.
148
185
  */
149
- declare function compilePolicy({ expression, table, resolver, maxLinkDepth }: CompilePolicyInput): PredIr;
186
+ declare function compilePolicy({ expression, table, resolver, maxLinkDepth, access }: CompilePolicyInput): PredIr;
150
187
 
151
- export { type CompilePolicyInput, type CompilePredicateInput, POLICY_COMPARE_OPS, POLICY_MAX_LINK_DEPTH, type ParsedPolicy, type PolicyColumnInfo, type PolicyCompareOp, PolicyCompileError, type PolicyCompileErrorCode, type PolicyLinkInfo, type PolicyNode, type PolicyOperand, PolicyParseError, type PolicyResolver, type PolicyTableInfo, assertClaimsOnlyWhen, compilePolicy, compilePredicateV2, parsePolicy, walkPolicy };
188
+ export { type CompilePolicyInput, type CompilePredicateInput, POLICY_COMPARE_OPS, POLICY_MAX_LINK_DEPTH, type ParsedPolicy, type PolicyAccess, type PolicyColumnInfo, type PolicyCompareOp, PolicyCompileError, type PolicyCompileErrorCode, type PolicyLinkInfo, type PolicyNode, type PolicyOperand, PolicyParseError, type PolicyResolver, type PolicyTableInfo, assertClaimsOnlyWhen, compilePolicy, compilePredicateV2, parsePolicy, walkPolicy };
@@ -29,9 +29,11 @@ var PolicyCompileError = class extends Error {
29
29
  };
30
30
 
31
31
  // src/constants.ts
32
+ var NAMESPACE_PATTERN = /^(?!(?:events|native_sources)$)[a-z][a-z0-9_]{0,40}$/;
32
33
  var COLUMN_IDENTIFIER_PATTERN = /^_?[a-z][a-z0-9_]*$/;
33
34
  var MAX_IDENTIFIER_LENGTH = 63;
34
35
  var CAPABILITY_KEY_PATTERN = /^(?:org\/|app\/[a-z][a-z0-9-]{0,63}\/)?[a-z0-9-]{1,64}$/;
36
+ var MAX_PERSISTED_READ_ROWS = 50001;
35
37
  var QUALIFIED_TABLE_PATTERN = /^[a-z][a-z0-9_]{0,62}\.[a-z][a-z0-9_]{0,62}$/;
36
38
  var CELL_PARTS = ["at", "by", "rev", "source", "conflicts"];
37
39
  var CELL_PART_SUFFIX = {
@@ -381,6 +383,7 @@ function validateResolvedView({ view, tables, views = [] }) {
381
383
  function validateSource({ scope, source, table }) {
382
384
  const { fail } = scope;
383
385
  if (source.kind === "etl" && table.source === void 0) fail(`etl source '${source.name}' must reference a synced table`);
386
+ if (source.kind === "etl" && table.primaryKey?.length === 0) fail(`etl source '${source.name}' cannot use an unkeyed synced table for resolved row identity`);
384
387
  if (source.kind !== "etl" && (table.internal !== true || table.cellMetadata !== true)) {
385
388
  fail(`${source.kind} source '${source.name}' must reference an internal table with cell metadata`);
386
389
  }
@@ -672,7 +675,7 @@ var canonicalTableSchema = z.object({
672
675
  /** Synced/reflected read-only tables (`source: schema.table`) — v1 semantics. */
673
676
  source: z.string().regex(QUALIFIED_TABLE_PATTERN).optional(),
674
677
  columns: z.array(canonicalColumnSchema).default([]),
675
- primaryKey: z.array(columnIdentifier).min(1).max(8).optional(),
678
+ primaryKey: z.array(columnIdentifier).max(8).optional(),
676
679
  links: z.array(canonicalLinkSchema).default([]),
677
680
  indexes: z.array(canonicalIndexSchema).default([]),
678
681
  /** Omitted by old definitions, preserving their existing content hashes. */
@@ -708,6 +711,29 @@ var canonicalTableSchema = z.object({
708
711
  * data plane. Omitted otherwise.
709
712
  */
710
713
  materializes: identifier2.optional()
714
+ }).superRefine((table, ctx) => {
715
+ if (table.primaryKey?.length !== 0) return;
716
+ const fail = (message, path = ["primaryKey"]) => ctx.addIssue({ code: "custom", path, message: `unkeyed table '${table.table}' ${message}` });
717
+ if (table.source === void 0 || table.columns.length === 0) {
718
+ fail("requires a typed synced source");
719
+ }
720
+ if ([table.policies.write, table.policies.insert, table.policies.update].some((policy) => policy !== void 0)) {
721
+ fail("is read-only and cannot declare write policies", ["policies"]);
722
+ }
723
+ const ownedCollections = [table.indexes, table.actions, table.checks, table.foreignKeys, table.cowMutations];
724
+ const ownedMetadata = [table.cellMetadata, table.internal, table.materializes, table.renamedFrom];
725
+ if (ownedCollections.some((items) => items !== void 0 && items.length > 0) || ownedMetadata.some((value) => value !== void 0)) {
726
+ fail("cannot declare owned storage, constraints or identity operations");
727
+ }
728
+ for (const [index, column] of table.columns.entries()) {
729
+ const ownedColumnMetadata = [column.default, column.defaultFn, column.onUpdateFn, column.renamedFrom, column.cast, column.cell];
730
+ if (column.unique || ownedColumnMetadata.some((value) => value !== void 0)) {
731
+ fail("cannot declare column uniqueness, mutation defaults or migration metadata", ["columns", index]);
732
+ }
733
+ }
734
+ for (const [index, link] of table.links.entries()) {
735
+ if (link.enforce || link.kind === "toMany") fail("cannot provide an identity for this link", ["links", index]);
736
+ }
711
737
  });
712
738
  var MAX_VIEW_QUERY = 1e5;
713
739
  var MAX_RESOLVED_VIEW_QUERY = 5e6;
@@ -764,6 +790,26 @@ var canonicalViewSchema = z.object({
764
790
  }
765
791
  }
766
792
  });
793
+ function validateUnkeyedLinkIdentities({ definition, ctx }) {
794
+ const tables = new Map(definition.tables.map((table) => [table.table, table]));
795
+ const relations = [
796
+ ...definition.tables.map((table, index) => ({ kind: "tables", index, links: table.links })),
797
+ ...(definition.views ?? []).map((view, index) => ({ kind: "views", index, links: view.links }))
798
+ ];
799
+ for (const relation of relations) {
800
+ for (const [linkIndex, link] of relation.links.entries()) {
801
+ if (link.kind !== "toOne" || link.namespace !== void 0 && link.namespace !== definition.namespace) continue;
802
+ const target = link.target === void 0 ? void 0 : tables.get(link.target);
803
+ if (target?.source !== void 0 && target.primaryKey?.length === 0) {
804
+ ctx.addIssue({
805
+ code: "custom",
806
+ path: [relation.kind, relation.index, "links", linkIndex],
807
+ message: `link '${link.name}' cannot use unkeyed table '${target.table}' as a to-one identity`
808
+ });
809
+ }
810
+ }
811
+ }
812
+ }
767
813
  var CELL_PART_TYPES = {
768
814
  at: "timestamptz",
769
815
  by: "text",
@@ -827,7 +873,7 @@ var exportGateSchema = z.union([z.literal(true), z.object({ capability: z.string
827
873
  var relationExportSchema = z.object({ read: exportGateSchema, write: exportGateSchema.optional() }).strict();
828
874
  var localDefinitionSchema = z.object({
829
875
  definition_format: z.literal(CANONICAL_FORMAT),
830
- namespace: identifier2.max(41),
876
+ namespace: identifier2.max(41).regex(NAMESPACE_PATTERN, "invalid or reserved ORM namespace"),
831
877
  description: z.string().max(2e3).optional(),
832
878
  enums: z.array(canonicalEnumSchema).default([]),
833
879
  tables: z.array(canonicalTableSchema).min(0).max(500),
@@ -861,6 +907,7 @@ var localDefinitionSchema = z.object({
861
907
  }
862
908
  }
863
909
  const fkTables = new Map(definition.tables.map((table) => [table.table, table]));
910
+ validateUnkeyedLinkIdentities({ definition, ctx });
864
911
  const relations = /* @__PURE__ */ new Set([...fkTables.keys(), ...(definition.views ?? []).map((view) => view.view)]);
865
912
  for (const name of Object.keys(definition.exports ?? {})) {
866
913
  if (!relations.has(name)) ctx.addIssue({ code: "custom", path: ["exports", name], message: `export '${name}' is not a local object` });
@@ -1124,7 +1171,7 @@ var limitIrSchema = z.object({
1124
1171
  variable: z.string().optional(),
1125
1172
  value: z.number().int().positive().optional(),
1126
1173
  default: z.number().int().positive(),
1127
- max: z.number().int().positive()
1174
+ max: z.number().int().positive().max(MAX_PERSISTED_READ_ROWS)
1128
1175
  });
1129
1176
  var selectionIrSchema = z.lazy(
1130
1177
  () => z.object({
@@ -1640,6 +1687,7 @@ var Parser2 = class {
1640
1687
  return node;
1641
1688
  }
1642
1689
  if (this.peekKeyword("exists")) return this.parseExists();
1690
+ if (this.peekKeyword("visible") && this.tokens[this.index + 1]?.kind === "lparen") return this.parseVisible();
1643
1691
  return this.parseComparison();
1644
1692
  }
1645
1693
  parseBooleanCall(fn) {
@@ -1659,6 +1707,13 @@ var Parser2 = class {
1659
1707
  this.expect("rparen", "')' closing exists(...)");
1660
1708
  return { kind: "exists", path, where };
1661
1709
  }
1710
+ parseVisible() {
1711
+ this.advance();
1712
+ this.expect("lparen", "'(' after visible");
1713
+ const path = this.parsePath("a link path in visible(...)");
1714
+ this.expect("rparen", "')' closing visible(...)");
1715
+ return { kind: "visible", path };
1716
+ }
1662
1717
  parseComparison() {
1663
1718
  const left = this.parseOperand();
1664
1719
  const operator = this.expect("operator", "a comparison operator");
@@ -1774,6 +1829,24 @@ function parsePolicy(source) {
1774
1829
 
1775
1830
  // src/predicate-v2/compile.ts
1776
1831
  var POLICY_MAX_LINK_DEPTH = 3;
1832
+ function scopeToNamespace(pred, namespace) {
1833
+ switch (pred.kind) {
1834
+ case "capability":
1835
+ case "role":
1836
+ return pred.namespace ? pred : { ...pred, namespace };
1837
+ case "and":
1838
+ case "or":
1839
+ return { ...pred, operands: pred.operands.map((operand) => scopeToNamespace(operand, namespace)) };
1840
+ case "not":
1841
+ return { ...pred, operand: scopeToNamespace(pred.operand, namespace) };
1842
+ case "exists":
1843
+ return { ...pred, where: scopeToNamespace(pred.where, namespace) };
1844
+ case "aggPredicate":
1845
+ return pred.where ? { ...pred, where: scopeToNamespace(pred.where, namespace) } : pred;
1846
+ default:
1847
+ return pred;
1848
+ }
1849
+ }
1777
1850
  var CMP_BY_OP = {
1778
1851
  "=": "eq",
1779
1852
  "!=": "neq",
@@ -1807,7 +1880,7 @@ function resolveLink(info, ref) {
1807
1880
  function columnCandidates(info) {
1808
1881
  return [...info.columns.map((c) => c.name), ...info.links.map((l) => l.name)];
1809
1882
  }
1810
- var PolicyCompiler = class {
1883
+ var PolicyCompiler = class _PolicyCompiler {
1811
1884
  constructor(ctx) {
1812
1885
  this.ctx = ctx;
1813
1886
  }
@@ -1820,13 +1893,15 @@ var PolicyCompiler = class {
1820
1893
  return { kind: node.kind, operands: node.operands.map((operand) => this.compile(operand, table, depth)) };
1821
1894
  case "not":
1822
1895
  assertNoNegatedExternalClaim(node.operand);
1823
- return { kind: "not", operand: this.compile(node.operand, table, depth) };
1896
+ return { kind: "not", operand: new _PolicyCompiler({ ...this.ctx, negated: true }).compile(node.operand, table, depth) };
1824
1897
  case "capability":
1825
1898
  return { kind: "capability", key: node.key };
1826
1899
  case "role":
1827
1900
  return { kind: "role", key: node.key };
1828
1901
  case "exists":
1829
1902
  return this.compileExists(node.path, node.where, table, depth);
1903
+ case "visible":
1904
+ return this.compileVisible(node.path, table, depth);
1830
1905
  case "compare":
1831
1906
  return this.compileCompare(node, table, depth);
1832
1907
  }
@@ -1840,6 +1915,36 @@ var PolicyCompiler = class {
1840
1915
  }
1841
1916
  return pred;
1842
1917
  }
1918
+ /** `visible(path)` → `exists(path, <final target's read policy>)`. */
1919
+ compileVisible(path, table, depth) {
1920
+ const { links, finalTable } = this.resolveLinkPath(path, table, depth, { toOneOnly: false });
1921
+ const readPolicy = this.ctx.resolver.readPolicy;
1922
+ if (!readPolicy) {
1923
+ throw new PolicyCompileError(
1924
+ `visible(${path.join(".")}) needs the target's read policy, which this compile context does not provide`,
1925
+ "visible-unresolvable"
1926
+ );
1927
+ }
1928
+ if (this.ctx.visiting.includes(finalTable)) {
1929
+ throw new PolicyCompileError(
1930
+ `visible(${path.join(".")}) on '${table}' reaches '${finalTable}' whose read policy is already being expanded (${[...this.ctx.visiting, finalTable].join(" \u2192 ")})`,
1931
+ "visible-cycle"
1932
+ );
1933
+ }
1934
+ const expression = readPolicy(finalTable);
1935
+ let pred = expression === void 0 ? { kind: "const", value: true } : compilePolicyAt({
1936
+ expression,
1937
+ table: finalTable,
1938
+ ctx: { ...this.ctx, visiting: [...this.ctx.visiting, finalTable] },
1939
+ depth: depth + links.length
1940
+ });
1941
+ const owner = this.ctx.resolver.policyNamespace?.(finalTable);
1942
+ if (owner) pred = scopeToNamespace(pred, owner);
1943
+ for (let i = links.length - 1; i >= 0; i -= 1) {
1944
+ pred = { kind: "exists", link: links[i].name, where: pred };
1945
+ }
1946
+ return pred;
1947
+ }
1843
1948
  resolveLinkPath(path, table, depth, { toOneOnly }) {
1844
1949
  const links = [];
1845
1950
  let current = table;
@@ -1900,6 +2005,7 @@ var PolicyCompiler = class {
1900
2005
  );
1901
2006
  }
1902
2007
  if (valueSide.kind === "claim" && isExternalPolicyClaimKey(valueSide.key)) {
2008
+ if (this.ctx.negated) throw negatedExternalClaim(valueSide.key);
1903
2009
  assertExternalClaimComparison({ op, key: valueSide.key, column: info.columns.find((c) => c.name === column) });
1904
2010
  }
1905
2011
  let cmp = this.buildCmp(op, column, valueSide);
@@ -1937,22 +2043,30 @@ function assertExternalClaimComparison({ op, key, column }) {
1937
2043
  );
1938
2044
  }
1939
2045
  }
2046
+ function negatedExternalClaim(key) {
2047
+ return new PolicyCompileError(
2048
+ `claim('${key}') cannot appear under not(): a negated borrower comparison admits other borrowers' rows`,
2049
+ "unsupported-comparison"
2050
+ );
2051
+ }
1940
2052
  function assertNoNegatedExternalClaim(operand) {
1941
2053
  walkPolicy(operand, (node) => {
1942
2054
  if (node.kind !== "compare") return;
1943
2055
  const claim = [node.left, node.right].find((side) => side.kind === "claim" && isExternalPolicyClaimKey(side.key));
1944
- if (claim?.kind === "claim") {
1945
- throw new PolicyCompileError(
1946
- `claim('${claim.key}') cannot appear under not(): a negated borrower comparison admits other borrowers' rows`,
1947
- "unsupported-comparison"
1948
- );
1949
- }
2056
+ if (claim?.kind === "claim") throw negatedExternalClaim(claim.key);
1950
2057
  });
1951
2058
  }
1952
2059
  function compilePredicateV2({ expression, table, resolver, maxLinkDepth }) {
2060
+ return compilePredicateAt({
2061
+ expression,
2062
+ table,
2063
+ depth: 0,
2064
+ ctx: { resolver, maxLinkDepth: maxLinkDepth ?? POLICY_MAX_LINK_DEPTH, visiting: [table] }
2065
+ });
2066
+ }
2067
+ function compilePredicateAt({ expression, table, ctx, depth }) {
1953
2068
  const parsed = typeof expression === "string" ? parsePolicy(expression) : expression;
1954
- const compiler = new PolicyCompiler({ resolver, maxLinkDepth: maxLinkDepth ?? POLICY_MAX_LINK_DEPTH });
1955
- return compiler.compile(parsed.ast, table, 0);
2069
+ return new PolicyCompiler(ctx).compile(parsed.ast, table, depth);
1956
2070
  }
1957
2071
  function assertClaimsOnlyWhen(parsed) {
1958
2072
  walkPolicy(parsed.ast, (node) => {
@@ -1969,25 +2083,34 @@ function assertClaimsOnlyWhen(parsed) {
1969
2083
  "when-not-claims-only"
1970
2084
  );
1971
2085
  }
1972
- if (node.kind === "exists") {
2086
+ if (node.kind === "exists" || node.kind === "visible") {
1973
2087
  throw new PolicyCompileError(
1974
- "variant 'when' guards are claims-only \u2014 exists() is not allowed",
2088
+ `variant 'when' guards are claims-only \u2014 ${node.kind}() is not allowed`,
1975
2089
  "when-not-claims-only"
1976
2090
  );
1977
2091
  }
1978
2092
  });
1979
2093
  }
1980
- function compilePolicy({ expression, table, resolver, maxLinkDepth }) {
1981
- if (typeof expression === "string") {
1982
- return compilePredicateV2({ expression, table, resolver, maxLinkDepth });
1983
- }
1984
- const defaultPred = compilePredicateV2({ expression: expression.default, table, resolver, maxLinkDepth });
2094
+ function initialVisiting({ table, access }) {
2095
+ return (access ?? "read") === "read" ? [table] : [];
2096
+ }
2097
+ function compilePolicy({ expression, table, resolver, maxLinkDepth, access }) {
2098
+ return compilePolicyAt({
2099
+ expression,
2100
+ table,
2101
+ depth: 0,
2102
+ ctx: { resolver, maxLinkDepth: maxLinkDepth ?? POLICY_MAX_LINK_DEPTH, visiting: initialVisiting({ table, access }) }
2103
+ });
2104
+ }
2105
+ function compilePolicyAt({ expression, table, ctx, depth }) {
2106
+ if (typeof expression === "string") return compilePredicateAt({ expression, table, ctx, depth });
2107
+ const defaultPred = compilePredicateAt({ expression: expression.default, table, ctx, depth });
1985
2108
  if (expression.variants.length === 0) return defaultPred;
1986
2109
  const branches = expression.variants.map((variant) => {
1987
2110
  const parsedWhen = parsePolicy(variant.when);
1988
2111
  assertClaimsOnlyWhen(parsedWhen);
1989
- const when = compilePredicateV2({ expression: parsedWhen, table, resolver, maxLinkDepth });
1990
- const grant = variant.allow === true ? { kind: "const", value: true } : compilePredicateV2({ expression: variant.policy, table, resolver, maxLinkDepth });
2112
+ const when = compilePredicateAt({ expression: parsedWhen, table, ctx, depth });
2113
+ const grant = variant.allow === true ? { kind: "const", value: true } : compilePredicateAt({ expression: variant.policy, table, ctx, depth });
1991
2114
  return { kind: "and", operands: [when, grant] };
1992
2115
  });
1993
2116
  return { kind: "or", operands: [defaultPred, ...branches] };