@intentius/chant 0.124.45 → 0.124.46

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 (190) hide show
  1. package/dist/capabilities/aws.paths +148 -0
  2. package/dist/capabilities/base-mappings.json +122 -0
  3. package/dist/capabilities/catalog.d.ts +89 -0
  4. package/dist/capabilities/catalog.d.ts.map +1 -0
  5. package/dist/capabilities/catalog.js +204 -0
  6. package/dist/capabilities/language.d.ts +103 -0
  7. package/dist/capabilities/language.d.ts.map +1 -0
  8. package/dist/capabilities/language.js +335 -0
  9. package/dist/capabilities/matcher.d.ts +65 -0
  10. package/dist/capabilities/matcher.d.ts.map +1 -0
  11. package/dist/capabilities/matcher.js +299 -0
  12. package/dist/capabilities/view.d.ts +207 -0
  13. package/dist/capabilities/view.d.ts.map +1 -0
  14. package/dist/capabilities/view.js +484 -0
  15. package/dist/cli/commands/lint.d.ts.map +1 -1
  16. package/dist/cli/commands/lint.js +1 -0
  17. package/dist/cli/handlers/declared-graph.d.ts +44 -0
  18. package/dist/cli/handlers/declared-graph.d.ts.map +1 -0
  19. package/dist/cli/handlers/declared-graph.js +87 -0
  20. package/dist/cli/handlers/graph.d.ts +25 -0
  21. package/dist/cli/handlers/graph.d.ts.map +1 -1
  22. package/dist/cli/handlers/graph.js +93 -87
  23. package/dist/cli/handlers/pr-env.d.ts.map +1 -1
  24. package/dist/cli/handlers/pr-env.js +2 -1
  25. package/dist/cli/handlers/search-count.d.ts +89 -0
  26. package/dist/cli/handlers/search-count.d.ts.map +1 -0
  27. package/dist/cli/handlers/search-count.js +132 -0
  28. package/dist/cli/handlers/search.d.ts +358 -6
  29. package/dist/cli/handlers/search.d.ts.map +1 -1
  30. package/dist/cli/handlers/search.js +1065 -155
  31. package/dist/cli/main.d.ts.map +1 -1
  32. package/dist/cli/main.js +23 -3
  33. package/dist/cli/mcp/server.d.ts.map +1 -1
  34. package/dist/cli/mcp/server.js +3 -0
  35. package/dist/cli/mcp/tools/capabilities.d.ts +7 -0
  36. package/dist/cli/mcp/tools/capabilities.d.ts.map +1 -0
  37. package/dist/cli/mcp/tools/capabilities.js +27 -0
  38. package/dist/cli/mcp/workspace-tools.d.ts +3 -1
  39. package/dist/cli/mcp/workspace-tools.d.ts.map +1 -1
  40. package/dist/cli/mcp/workspace-tools.js +47 -2
  41. package/dist/cli/parse-args.d.ts.map +1 -1
  42. package/dist/cli/parse-args.js +64 -17
  43. package/dist/cli/registry.d.ts +22 -0
  44. package/dist/cli/registry.d.ts.map +1 -1
  45. package/dist/components/pr-env-ports.d.ts +29 -0
  46. package/dist/components/pr-env-ports.d.ts.map +1 -0
  47. package/dist/components/pr-env-ports.js +77 -0
  48. package/dist/components/pr-env-run.d.ts +5 -21
  49. package/dist/components/pr-env-run.d.ts.map +1 -1
  50. package/dist/components/pr-env-run.js +6 -70
  51. package/dist/components/pr-environment.d.ts +2 -1
  52. package/dist/components/pr-environment.d.ts.map +1 -1
  53. package/dist/components/pr-environment.js +1 -1
  54. package/dist/config-environments.d.ts +92 -0
  55. package/dist/config-environments.d.ts.map +1 -0
  56. package/dist/config-environments.js +102 -0
  57. package/dist/config.d.ts +24 -84
  58. package/dist/config.d.ts.map +1 -1
  59. package/dist/config.js +9 -96
  60. package/dist/graph-detail.d.ts.map +1 -1
  61. package/dist/graph-detail.js +15 -3
  62. package/dist/graph-effective.d.ts +33 -2
  63. package/dist/graph-effective.d.ts.map +1 -1
  64. package/dist/graph-effective.js +215 -68
  65. package/dist/graph-ir.d.ts +23 -0
  66. package/dist/graph-ir.d.ts.map +1 -1
  67. package/dist/graph-ir.js +130 -0
  68. package/dist/graph-sub-template.d.ts +11 -0
  69. package/dist/graph-sub-template.d.ts.map +1 -0
  70. package/dist/graph-sub-template.js +19 -0
  71. package/dist/lexicon.d.ts +9 -0
  72. package/dist/lexicon.d.ts.map +1 -1
  73. package/dist/lifecycle/observe.d.ts +28 -0
  74. package/dist/lifecycle/observe.d.ts.map +1 -1
  75. package/dist/lifecycle/observe.js +23 -1
  76. package/dist/lint/rule.d.ts +6 -0
  77. package/dist/lint/rule.d.ts.map +1 -1
  78. package/dist/live-endpoint.d.ts +1 -1
  79. package/dist/live-endpoint.d.ts.map +1 -1
  80. package/dist/live-endpoint.js +1 -1
  81. package/dist/observation.d.ts +48 -1
  82. package/dist/observation.d.ts.map +1 -1
  83. package/dist/observation.js +21 -5
  84. package/dist/op/activities/activity-contracts.d.ts +4 -4
  85. package/dist/wave-runtime.mjs +250 -249
  86. package/dist/workspace/capabilities-contract.d.ts +66 -0
  87. package/dist/workspace/capabilities-contract.d.ts.map +1 -0
  88. package/dist/workspace/capabilities-contract.js +107 -0
  89. package/dist/workspace/capabilities.schema.json +229 -0
  90. package/dist/workspace/compose-graph.d.ts +9 -0
  91. package/dist/workspace/compose-graph.d.ts.map +1 -1
  92. package/dist/workspace/compose-graph.js +3 -1
  93. package/dist/workspace/conformance/index.d.ts +1 -1
  94. package/dist/workspace/conformance/index.d.ts.map +1 -1
  95. package/dist/workspace/conformance/index.js +39 -2
  96. package/dist/workspace/graph-cache.d.ts +8 -0
  97. package/dist/workspace/graph-cache.d.ts.map +1 -1
  98. package/dist/workspace/graph-cache.js +27 -3
  99. package/dist/workspace/graph-cli.d.ts +39 -2
  100. package/dist/workspace/graph-cli.d.ts.map +1 -1
  101. package/dist/workspace/graph-cli.js +66 -24
  102. package/dist/workspace/graph.schema.json +148 -0
  103. package/dist/workspace/kind-readers.d.ts +3 -0
  104. package/dist/workspace/kind-readers.d.ts.map +1 -1
  105. package/dist/workspace/kind-readers.js +7 -1
  106. package/dist/workspace/member-commands.d.ts +10 -1
  107. package/dist/workspace/member-commands.d.ts.map +1 -1
  108. package/dist/workspace/member-commands.js +17 -1
  109. package/dist/workspace/nested-graph.d.ts +2 -0
  110. package/dist/workspace/nested-graph.d.ts.map +1 -1
  111. package/dist/workspace/nested-graph.js +32 -1
  112. package/dist/workspace/reason-codes.d.ts +10 -0
  113. package/dist/workspace/reason-codes.d.ts.map +1 -1
  114. package/dist/workspace/reason-codes.js +12 -0
  115. package/dist/workspace/search-cli.d.ts +204 -0
  116. package/dist/workspace/search-cli.d.ts.map +1 -0
  117. package/dist/workspace/search-cli.js +466 -0
  118. package/dist/workspace/search-contract.d.ts +56 -0
  119. package/dist/workspace/search-contract.d.ts.map +1 -0
  120. package/dist/workspace/search-contract.js +70 -0
  121. package/dist/workspace/search.schema.json +822 -0
  122. package/package.json +1 -1
  123. package/src/__snapshots__/okf.test.ts.snap +10 -0
  124. package/src/capabilities/aws.paths +148 -0
  125. package/src/capabilities/base-mappings.json +122 -0
  126. package/src/capabilities/catalog.ts +230 -0
  127. package/src/capabilities/language.test.ts +191 -0
  128. package/src/capabilities/language.ts +373 -0
  129. package/src/capabilities/matcher.ts +315 -0
  130. package/src/capabilities/view-compose.test.ts +76 -0
  131. package/src/capabilities/view.ts +631 -0
  132. package/src/cli/commands/lint.ts +1 -0
  133. package/src/cli/handlers/declared-graph.ts +99 -0
  134. package/src/cli/handlers/graph.test.ts +94 -3
  135. package/src/cli/handlers/graph.ts +123 -96
  136. package/src/cli/handlers/pr-env.ts +1 -2
  137. package/src/cli/handlers/search-build-roots.test.ts +144 -0
  138. package/src/cli/handlers/search-count.test.ts +191 -0
  139. package/src/cli/handlers/search-count.ts +165 -0
  140. package/src/cli/handlers/search-json.test.ts +333 -0
  141. package/src/cli/handlers/search-live.test.ts +387 -0
  142. package/src/cli/handlers/search-paths.test.ts +235 -0
  143. package/src/cli/handlers/search.ts +1312 -166
  144. package/src/cli/main.test.ts +28 -3
  145. package/src/cli/main.ts +23 -3
  146. package/src/cli/mcp/server.test.ts +3 -3
  147. package/src/cli/mcp/server.ts +3 -0
  148. package/src/cli/mcp/tools/capabilities.ts +32 -0
  149. package/src/cli/mcp/workspace-tools.e2e.test.ts +21 -1
  150. package/src/cli/mcp/workspace-tools.ts +47 -2
  151. package/src/cli/parse-args.ts +52 -13
  152. package/src/cli/registry.ts +22 -0
  153. package/src/components/pr-env-ports.ts +89 -0
  154. package/src/components/pr-env-run-bundle.test.ts +54 -0
  155. package/src/components/pr-env-run.ts +5 -76
  156. package/src/components/pr-environment.ts +2 -1
  157. package/src/config-environments.ts +133 -0
  158. package/src/config.ts +37 -124
  159. package/src/graph-detail.ts +15 -3
  160. package/src/graph-effective.ts +226 -75
  161. package/src/graph-ir.test.ts +163 -0
  162. package/src/graph-ir.ts +147 -0
  163. package/src/graph-sub-template.ts +22 -0
  164. package/src/lexicon.ts +10 -0
  165. package/src/lifecycle/observe.test.ts +27 -1
  166. package/src/lifecycle/observe.ts +43 -1
  167. package/src/lint/rule.ts +6 -0
  168. package/src/live-endpoint.ts +1 -1
  169. package/src/observation.test.ts +25 -0
  170. package/src/observation.ts +69 -5
  171. package/src/workspace/capabilities-contract.test.ts +88 -0
  172. package/src/workspace/capabilities-contract.ts +132 -0
  173. package/src/workspace/capabilities.schema.json +229 -0
  174. package/src/workspace/compose-graph.ts +10 -1
  175. package/src/workspace/conformance/conformance.e2e.test.ts +14 -1
  176. package/src/workspace/conformance/index.ts +31 -2
  177. package/src/workspace/graph-cache.ts +27 -3
  178. package/src/workspace/graph-cli.ts +96 -24
  179. package/src/workspace/graph.schema.json +148 -0
  180. package/src/workspace/kind-readers.e2e.test.ts +28 -0
  181. package/src/workspace/kind-readers.test.ts +1 -1
  182. package/src/workspace/kind-readers.ts +7 -1
  183. package/src/workspace/member-commands.ts +22 -1
  184. package/src/workspace/nested-graph.ts +32 -1
  185. package/src/workspace/reason-codes.test.ts +7 -0
  186. package/src/workspace/reason-codes.ts +12 -0
  187. package/src/workspace/search-cli.e2e.test.ts +305 -0
  188. package/src/workspace/search-cli.ts +591 -0
  189. package/src/workspace/search-contract.ts +95 -0
  190. package/src/workspace/search.schema.json +822 -0
@@ -0,0 +1,315 @@
1
+ /**
2
+ * The matcher for capability rules (#4375): evaluates `./language.ts` rules
3
+ * over a `./view.ts` view, lazily, keeping the path that proved each fact.
4
+ *
5
+ * A capability is computed the first time something asks for it, over the
6
+ * whole view, and kept. One that refers to another asks for it first, so
7
+ * negation is stratified by construction: `[!@cap]` reads a set that is
8
+ * already complete. Two capabilities that need each other are refused.
9
+ */
10
+ import type { Alternative, Literal, NodeSpec, Rule, Step, Test, Value } from "./language";
11
+ import type { View, ViewHop } from "./view";
12
+
13
+ /** Why a node is in a set, or a pair in an edge: the hops walked, and the sets it relied on. */
14
+ export interface Evidence {
15
+ /** IR hops in walk order. Each keeps the edge's own direction. */
16
+ hops: ViewHop[];
17
+ /** The `[@cap]` tests the match relied on, each with its own evidence. */
18
+ because: { capability: string; id: string; evidence: Evidence }[];
19
+ }
20
+
21
+ export type Args = Record<string, Literal | undefined>;
22
+
23
+ /** An edge capability: each source's targets, with evidence. */
24
+ export interface Relation {
25
+ out: Map<string, Map<string, Evidence>>;
26
+ in: Map<string, Map<string, Evidence>>;
27
+ }
28
+
29
+ export class CapabilityError extends Error {}
30
+
31
+ const glob = (pattern: string, s: string): boolean =>
32
+ new RegExp(`^${pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".")}$`, "i").test(s);
33
+
34
+ function equal(a: unknown, b: Literal): boolean {
35
+ if (b === null) return a === null || a === undefined;
36
+ if (a === b) return true;
37
+ if (a === null || a === undefined || typeof a === "object") return false;
38
+ return String(a) === String(b);
39
+ }
40
+
41
+ function present(a: unknown): boolean {
42
+ return a !== undefined && a !== null && a !== false && !(Array.isArray(a) && a.length === 0);
43
+ }
44
+
45
+ const reverseHops = (hops: ViewHop[]): ViewHop[] => [...hops].reverse();
46
+
47
+ export class CapabilityEngine {
48
+ private readonly rules = new Map<string, Rule>();
49
+ private readonly sets = new Map<string, Map<string, Evidence>>();
50
+ private readonly relations = new Map<string, Relation>();
51
+ private readonly busy = new Set<string>();
52
+ private readonly out = new Map<string, Map<string, { to: string; hops: ViewHop[] }[]>>();
53
+ private readonly in = new Map<string, Map<string, { to: string; hops: ViewHop[] }[]>>();
54
+ private readonly byLabel = new Map<string, string[]>();
55
+
56
+ constructor(
57
+ readonly view: View,
58
+ rules: Rule[],
59
+ ) {
60
+ for (const r of rules) this.rules.set(r.name, r);
61
+ for (const e of view.edges) {
62
+ this.index(this.out, e.from, e.name, e.to, e.hops);
63
+ this.index(this.in, e.to, e.name, e.from, reverseHops(e.hops));
64
+ }
65
+ for (const n of view.nodes.values()) {
66
+ if (!this.byLabel.has(n.label)) this.byLabel.set(n.label, []);
67
+ this.byLabel.get(n.label)!.push(n.id);
68
+ }
69
+ for (const r of rules) this.check(r);
70
+ }
71
+
72
+ private index(ix: CapabilityEngine["out"], a: string, name: string, b: string, hops: ViewHop[]): void {
73
+ let m = ix.get(a);
74
+ if (!m) ix.set(a, (m = new Map()));
75
+ let l = m.get(name);
76
+ if (!l) m.set(name, (l = []));
77
+ l.push({ to: b, hops });
78
+ }
79
+
80
+ /** Refuse a rule that names a capability that doesn't exist, or uses one in the wrong shape. */
81
+ private check(r: Rule): void {
82
+ const node = (n: NodeSpec, alt: Alternative): void => {
83
+ for (const t of n.tests) {
84
+ if (t.kind !== "cap") continue;
85
+ const target = this.rules.get(t.cap);
86
+ if (!target) throw new CapabilityError(`${r.name} (line ${alt.line}): no capability "${t.cap}"`);
87
+ if (target.vars.length !== 1) throw new CapabilityError(`${r.name} (line ${alt.line}): "@${t.cap}" is an edge, not a set; follow it with ->${t.cap}`);
88
+ }
89
+ };
90
+ for (const alt of r.alts) {
91
+ node(alt.head, alt);
92
+ for (const s of alt.steps) {
93
+ node(s.node, alt);
94
+ for (const n of s.names) {
95
+ const target = this.rules.get(n.name);
96
+ if (target && target.vars.length !== 2) {
97
+ throw new CapabilityError(`${r.name} (line ${alt.line}): "${n.name}" is a set, not an edge; test it with [@${n.name}]`);
98
+ }
99
+ }
100
+ }
101
+ }
102
+ }
103
+
104
+ has(name: string): boolean {
105
+ return this.rules.has(name);
106
+ }
107
+
108
+ /** The capabilities the catalog lists: rules that carry a `## meaning`. */
109
+ publicNames(): string[] {
110
+ return [...this.rules.values()].filter((r) => r.meta.meaning !== undefined).map((r) => r.name);
111
+ }
112
+
113
+ rule(name: string): Rule | undefined {
114
+ return this.rules.get(name);
115
+ }
116
+
117
+ /** Bind positional arguments to a rule's parameters. */
118
+ bind(name: string, positional: (Literal | undefined)[]): Args {
119
+ const r = this.rules.get(name);
120
+ if (!r) throw new CapabilityError(`no capability "${name}"`);
121
+ if (positional.length > r.params.length) {
122
+ throw new CapabilityError(`${name} takes ${r.params.length === 0 ? "no arguments" : `at most ${r.params.length} (${r.params.map((p) => p.name).join(", ")})`}`);
123
+ }
124
+ const args: Args = {};
125
+ r.params.forEach((p, i) => {
126
+ const v = positional[i];
127
+ if (v === undefined && !p.optional) throw new CapabilityError(`${name} needs ${p.name}`);
128
+ args[p.name] = v;
129
+ });
130
+ return args;
131
+ }
132
+
133
+ private key(name: string, args: Args): string {
134
+ return `${name}(${JSON.stringify(Object.entries(args).filter(([, v]) => v !== undefined).sort())})`;
135
+ }
136
+
137
+ /** The nodes in a set capability, with evidence. */
138
+ set(name: string, args: Args = {}): Map<string, Evidence> {
139
+ const r = this.rules.get(name);
140
+ if (!r) throw new CapabilityError(`no capability "${name}"`);
141
+ if (r.vars.length !== 1) throw new CapabilityError(`"${name}" is an edge, not a set`);
142
+ const key = this.key(name, args);
143
+ const cached = this.sets.get(key);
144
+ if (cached) return cached;
145
+ this.enter(key);
146
+ const out = new Map<string, Evidence>();
147
+ for (const alt of r.alts) {
148
+ for (const s of this.starts(alt.head)) {
149
+ this.match(alt, s, args, (env, ev) => {
150
+ const x = env[r.vars[0]] ?? s;
151
+ if (!out.has(x)) out.set(x, ev);
152
+ });
153
+ }
154
+ }
155
+ this.busy.delete(key);
156
+ this.sets.set(key, out);
157
+ return out;
158
+ }
159
+
160
+ /** The pairs in an edge capability, with evidence. */
161
+ relation(name: string, args: Args = {}): Relation {
162
+ const r = this.rules.get(name);
163
+ if (!r) throw new CapabilityError(`no capability "${name}"`);
164
+ if (r.vars.length !== 2) throw new CapabilityError(`"${name}" is a set, not an edge`);
165
+ const key = this.key(name, args);
166
+ const cached = this.relations.get(key);
167
+ if (cached) return cached;
168
+ this.enter(key);
169
+ const rel: Relation = { out: new Map(), in: new Map() };
170
+ for (const alt of r.alts) {
171
+ for (const s of this.starts(alt.head)) {
172
+ this.match(alt, s, args, (env, ev, last) => {
173
+ const a = env[r.vars[0]] ?? s;
174
+ const b = env[r.vars[1]] ?? last;
175
+ if (a === b) return;
176
+ let m = rel.out.get(a);
177
+ if (!m) rel.out.set(a, (m = new Map()));
178
+ if (m.has(b)) return;
179
+ m.set(b, ev);
180
+ let back = rel.in.get(b);
181
+ if (!back) rel.in.set(b, (back = new Map()));
182
+ back.set(a, ev);
183
+ });
184
+ }
185
+ }
186
+ this.busy.delete(key);
187
+ this.relations.set(key, rel);
188
+ return rel;
189
+ }
190
+
191
+ private enter(key: string): void {
192
+ if (this.busy.has(key)) {
193
+ throw new CapabilityError(`capability ${key} refers to itself through ${[...this.busy].join(" -> ")}`);
194
+ }
195
+ this.busy.add(key);
196
+ }
197
+
198
+ private starts(head: NodeSpec): string[] {
199
+ if (!head.labels) return [...this.view.nodes.keys()];
200
+ return head.labels.flatMap((l) => this.byLabel.get(l) ?? []);
201
+ }
202
+
203
+ private resolve(v: Value, args: Args): Literal | undefined {
204
+ return "lit" in v ? v.lit : args[v.param];
205
+ }
206
+
207
+ private test(id: string, t: Test, args: Args, because: Evidence["because"]): boolean {
208
+ const result = ((): boolean => {
209
+ if (t.kind === "cap") {
210
+ const capArgs = this.bind(t.cap, t.args.map((v) => this.resolve(v, args)));
211
+ const ev = this.set(t.cap, capArgs).get(id);
212
+ if (ev && !t.negated) because.push({ capability: t.cap, id, evidence: ev });
213
+ return !!ev;
214
+ }
215
+ const n = this.view.nodes.get(id)!;
216
+ const a = n.attrs[t.attr];
217
+ if (t.op === "present") return present(a);
218
+ const values = t.values.map((v) => this.resolve(v, args));
219
+ // A test on a parameter nobody passed is left out, whatever else it names.
220
+ if (values.some((v) => v === undefined)) return !t.negated;
221
+ const given = values as Literal[];
222
+ const each = Array.isArray(a) ? a : [a];
223
+ switch (t.op) {
224
+ case "=":
225
+ case "in":
226
+ return each.some((x) => given.some((v) => equal(x, v)));
227
+ case "!=":
228
+ return !each.some((x) => equal(x, given[0]));
229
+ case "~":
230
+ return each.some((x) => typeof x === "string" && given.some((v) => typeof v === "string" && (glob(x, v) || glob(v, x))));
231
+ default: {
232
+ const v = Number(given[0]);
233
+ return each.some((x) => {
234
+ const nx = Number(x);
235
+ if (x === null || x === undefined || Number.isNaN(nx) || Number.isNaN(v)) return false;
236
+ return t.op === "<" ? nx < v : t.op === "<=" ? nx <= v : t.op === ">" ? nx > v : nx >= v;
237
+ });
238
+ }
239
+ }
240
+ })();
241
+ return t.negated ? !result : result;
242
+ }
243
+
244
+ private ok(id: string, s: NodeSpec, env: Record<string, string>, args: Args, because: Evidence["because"]): boolean {
245
+ if (s.v && env[s.v] !== undefined && env[s.v] !== id) return false;
246
+ const n = this.view.nodes.get(id);
247
+ if (!n) return false;
248
+ if (s.labels && !s.labels.includes(n.label)) return false;
249
+ return s.tests.every((t) => this.test(id, t, args, because));
250
+ }
251
+
252
+ private hop(id: string, step: Step, args: Args): { to: string; hops: ViewHop[] }[] {
253
+ const out: { to: string; hops: ViewHop[] }[] = [];
254
+ for (const n of step.names) {
255
+ if (this.rules.has(n.name)) {
256
+ const rel = this.relation(n.name, this.bind(n.name, n.args.map((v) => this.resolve(v, args))));
257
+ const m = (step.dir === "out" ? rel.out : rel.in).get(id);
258
+ if (m) for (const [to, ev] of m) out.push({ to, hops: step.dir === "out" ? ev.hops : reverseHops(ev.hops) });
259
+ } else {
260
+ out.push(...((step.dir === "out" ? this.out : this.in).get(id)?.get(n.name) ?? []));
261
+ }
262
+ }
263
+ return out;
264
+ }
265
+
266
+ /** Every node within the step's depth range, by its shortest walk. */
267
+ private walk(id: string, step: Step, args: Args): { to: string; hops: ViewHop[] }[] {
268
+ const res: { to: string; hops: ViewHop[] }[] = [];
269
+ const seen = new Set([id]);
270
+ let frontier = [{ to: id, hops: [] as ViewHop[] }];
271
+ if (step.min === 0) res.push(frontier[0]);
272
+ for (let d = 1; d <= step.max && frontier.length > 0; d++) {
273
+ const next: typeof frontier = [];
274
+ for (const f of frontier) {
275
+ for (const h of this.hop(f.to, step, args)) {
276
+ if (step.max > 1) {
277
+ if (seen.has(h.to)) continue;
278
+ seen.add(h.to);
279
+ }
280
+ const p = { to: h.to, hops: [...f.hops, ...h.hops] };
281
+ next.push(p);
282
+ if (d >= step.min) res.push(p);
283
+ }
284
+ }
285
+ frontier = next;
286
+ }
287
+ return res;
288
+ }
289
+
290
+ private match(
291
+ alt: Alternative,
292
+ start: string,
293
+ args: Args,
294
+ emit: (env: Record<string, string>, ev: Evidence, last: string) => void,
295
+ ): void {
296
+ const because0: Evidence["because"] = [];
297
+ if (!this.ok(start, alt.head, {}, args, because0)) return;
298
+ const env0: Record<string, string> = alt.head.v ? { [alt.head.v]: start } : {};
299
+ const go = (k: number, at: string, env: Record<string, string>, hops: ViewHop[], because: Evidence["because"]): void => {
300
+ if (k === alt.steps.length) return emit(env, { hops, because }, at);
301
+ const st = alt.steps[k];
302
+ if (st.negated) {
303
+ const blocked = this.walk(at, st, args).some((w) => this.ok(w.to, st.node, env, args, []));
304
+ if (!blocked) go(k + 1, at, env, hops, because);
305
+ return;
306
+ }
307
+ for (const w of this.walk(at, st, args)) {
308
+ const b: Evidence["because"] = [];
309
+ if (!this.ok(w.to, st.node, env, args, b)) continue;
310
+ go(k + 1, w.to, st.node.v ? { ...env, [st.node.v]: w.to } : env, [...hops, ...w.hops], [...because, ...b]);
311
+ }
312
+ };
313
+ go(0, start, env0, [], because0);
314
+ }
315
+ }
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Member views composed into one (#4378): each member's chant builds its own
3
+ * view and prints it; the workspace's chant prefixes the ids, joins the
4
+ * views and resolves the ARNs one member names in another.
5
+ */
6
+ import { describe, expect, it } from "vitest";
7
+ import type { GraphIR } from "../graph-ir";
8
+ import { buildView, composeViews, isViewDocument, prefixView, serializeView, type CapabilityPack } from "./view";
9
+
10
+ const pack: CapabilityPack = {
11
+ mapping: {
12
+ lexicon: "test",
13
+ labels: { "T::Policy": "Policy", "T::Bucket": "Bucket" },
14
+ attrs: { Bucket: { name: "BucketName" } },
15
+ documents: { Policy: { Doc: "permission" } },
16
+ },
17
+ };
18
+
19
+ const ir = (nodes: { id: string; kind: string; attrs: Record<string, unknown> }[]): GraphIR =>
20
+ ({ nodes: nodes.map((n) => ({ ...n, lexicon: "test" })), edges: [], groups: {} }) as unknown as GraphIR;
21
+
22
+ const policy = ir([
23
+ {
24
+ id: "pol",
25
+ kind: "T::Policy",
26
+ attrs: {
27
+ Doc: {
28
+ Statement: [
29
+ { Effect: "Allow", Action: "s3:GetObject", Resource: ["arn:aws:s3:::shared/*", "arn:aws:s3:::nowhere"] },
30
+ ],
31
+ },
32
+ },
33
+ },
34
+ ]);
35
+ const bucket = ir([{ id: "bkt", kind: "T::Bucket", attrs: { BucketName: "shared" } }]);
36
+
37
+ /** A member's view as it travels: serialized by the member's chant, read back as JSON. */
38
+ const printed = (g: GraphIR) => JSON.parse(JSON.stringify(serializeView(buildView(g, [pack])))) as unknown;
39
+
40
+ describe("member views", () => {
41
+ it("keeps an ARN that names nothing in the member, and prints a view a reader recognizes", () => {
42
+ const v = buildView(policy, [pack]);
43
+ expect(v.edges.filter((e) => e.name === "resource")).toEqual([]);
44
+ expect(v.unresolved).toEqual([
45
+ { from: "pol#Doc[0].Statement[0]", fromAttr: "Resource[0]", edge: "resource", value: "arn:aws:s3:::shared/*" },
46
+ { from: "pol#Doc[0].Statement[0]", fromAttr: "Resource[1]", edge: "resource", value: "arn:aws:s3:::nowhere" },
47
+ ]);
48
+ expect(isViewDocument(printed(policy))).toBe(true);
49
+ expect(isViewDocument({ ...(printed(policy) as object), version: 2 })).toBe(false);
50
+ expect(isViewDocument(undefined)).toBe(false);
51
+ });
52
+
53
+ it("resolves an ARN inside the member it names, leaving nothing for the composer", () => {
54
+ const both = ir([...policy.nodes, ...bucket.nodes] as never);
55
+ const v = buildView(both, [pack]);
56
+ expect(v.edges.filter((e) => e.name === "resource").map((e) => [e.from, e.to])).toEqual([["pol#Doc[0].Statement[0]", "bkt"]]);
57
+ expect(v.unresolved!.map((u) => u.value)).toEqual(["arn:aws:s3:::nowhere"]);
58
+ });
59
+
60
+ it("composes views under member ids and resolves an ARN across members", () => {
61
+ const a = printed(policy);
62
+ const b = printed(bucket);
63
+ if (!isViewDocument(a) || !isViewDocument(b)) throw new Error("not a view document");
64
+ const v = composeViews([prefixView(a, "platform/"), prefixView(b, "data/")]);
65
+ expect([...v.nodes.keys()].sort()).toEqual(["data/bkt", "platform/pol", "platform/pol#Doc[0].Statement[0]"]);
66
+ expect(v.nodes.get("platform/pol#Doc[0].Statement[0]")!.irId).toBe("platform/pol");
67
+ expect(v.edges.find((e) => e.name === "resource")).toEqual({
68
+ from: "platform/pol#Doc[0].Statement[0]",
69
+ to: "data/bkt",
70
+ name: "resource",
71
+ hops: [{ from: "platform/pol#Doc[0].Statement[0]", fromAttr: "Resource[0]", to: "data/bkt", edge: "resource" }],
72
+ });
73
+ expect(v.edges.find((e) => e.name === "statement")!.hops[0]).toMatchObject({ from: "platform/pol", to: "platform/pol#Doc[0].Statement[0]" });
74
+ expect(v.unresolved!.map((u) => [u.from, u.value])).toEqual([["platform/pol#Doc[0].Statement[0]", "arn:aws:s3:::nowhere"]]);
75
+ });
76
+ });