@archwall/core 0.1.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.
@@ -0,0 +1,921 @@
1
+ //#region \0rolldown/runtime.js
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __copyProps = (to, from, except, desc) => {
9
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
10
+ key = keys[i];
11
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
12
+ get: ((k) => from[k]).bind(null, key),
13
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
14
+ });
15
+ }
16
+ return to;
17
+ };
18
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
19
+ value: mod,
20
+ enumerable: true
21
+ }) : target, mod));
22
+ //#endregion
23
+ let node_path = require("node:path");
24
+ node_path = __toESM(node_path, 1);
25
+ let picomatch = require("picomatch");
26
+ picomatch = __toESM(picomatch, 1);
27
+ //#region src/paths.ts
28
+ /** Forward slashes everywhere, so one string form crosses platforms. */
29
+ function normalize(p) {
30
+ return p.replaceAll("\\", "/");
31
+ }
32
+ /**
33
+ * A file's path relative to `root`, forward-slashed, or `null` when it does not lie
34
+ * strictly inside `root`.
35
+ *
36
+ * The one answer to "where is this file, in the terms my patterns are written in".
37
+ * `include`/`exclude`, classifier patterns, `RuleScope.include`, and `require-tag`'s
38
+ * `within` all describe positions in a tree, and they must all agree on what a position
39
+ * is — including on the edge cases: the root itself is not *inside* the root, and a file
40
+ * above it has no position at all.
41
+ *
42
+ * A path that is already relative is taken as relative *to the root* rather than resolved
43
+ * against `process.cwd()`. Real producers emit absolute paths, but in-memory graphs (tests,
44
+ * `@archwall/test-utils`) use bare ids, and resolving those against the working directory
45
+ * would silently place every module outside the project.
46
+ */
47
+ function sourceRelative(root, file) {
48
+ const normalized = normalize(file);
49
+ if (!node_path.isAbsolute(normalized)) return normalized === "" ? null : normalized;
50
+ const rel = normalize(node_path.relative(root, normalized));
51
+ if (rel === "" || rel.startsWith("../") || rel === ".." || node_path.isAbsolute(rel)) return null;
52
+ return rel;
53
+ }
54
+ /**
55
+ * Repository-relative, for anything that leaves the process: violation fingerprints,
56
+ * reporter output, SARIF `artifactLocation.uri`.
57
+ *
58
+ * Absolute paths are the right module identity *inside* a run and wrong in every output,
59
+ * because they make results machine-specific. SARIF in particular is silently useless with
60
+ * absolute URIs: GitHub code scanning cannot associate the result with a repository file.
61
+ *
62
+ * Distinct from {@link sourceRelative} in its failure mode, deliberately: an id outside the
63
+ * root is returned as-is rather than as `null`, because output must always print something,
64
+ * whereas matching must be able to say "not here".
65
+ */
66
+ function toRelative(root, id) {
67
+ const normalized = normalize(id);
68
+ if (!node_path.isAbsolute(normalized)) return normalized;
69
+ return sourceRelative(root, normalized) ?? normalized;
70
+ }
71
+ /**
72
+ * FNV-1a, 64-bit, as 16 lowercase hex chars.
73
+ *
74
+ * Not `node:crypto`: core stays runnable wherever a graph can be built (browser playground,
75
+ * worker, edge runtime), and this hash is used for identity, never for security.
76
+ */
77
+ function stableHash(input) {
78
+ let h1 = 2166136261;
79
+ let h2 = 16777619;
80
+ for (let i = 0; i < input.length; i++) {
81
+ const c = input.charCodeAt(i);
82
+ h1 = Math.imul(h1 ^ c, 16777619) >>> 0;
83
+ h2 = Math.imul(h2 ^ (c << 5 | c >>> 3), 16777619) >>> 0;
84
+ }
85
+ return h1.toString(16).padStart(8, "0") + h2.toString(16).padStart(8, "0");
86
+ }
87
+ /**
88
+ * Joins parts into one hashable string.
89
+ *
90
+ * `\0` rather than a space: parts are paths and specifiers, which may contain spaces, and
91
+ * a delimiter that can occur inside a part makes two different tuples hash identically.
92
+ */
93
+ function hashParts(parts) {
94
+ return stableHash(parts.join("\0"));
95
+ }
96
+ //#endregion
97
+ //#region src/errors.ts
98
+ var ArchWallError = class extends Error {
99
+ constructor(message) {
100
+ super(message);
101
+ this.name = new.target.name;
102
+ }
103
+ };
104
+ var IrVersionMismatchError = class extends ArchWallError {
105
+ graphVersion;
106
+ coreVersion;
107
+ constructor(graphVersion, coreVersion) {
108
+ super(`Incompatible graph IR: adapter produced irVersion ${graphVersion}, but @archwall/core supports ${coreVersion} (majors must match). Upgrade the adapter or core so their IR majors align.`);
109
+ this.graphVersion = graphVersion;
110
+ this.coreVersion = coreVersion;
111
+ }
112
+ };
113
+ //#endregion
114
+ //#region src/graph/ir.ts
115
+ /** Semver of the Project Graph IR schema itself, independent of package versions. */
116
+ const IR_VERSION = "1.0.0";
117
+ /** The schemes {@link ModuleId} recognises. */
118
+ const MODULE_ID_SCHEMES = [
119
+ "file",
120
+ "pkg",
121
+ "builtin",
122
+ "virtual",
123
+ "unresolved"
124
+ ];
125
+ const SCHEME_OF = /^(file|pkg|builtin|virtual|unresolved):/;
126
+ /**
127
+ * Splits a canonical id into its scheme and body, or null when it carries no known scheme.
128
+ *
129
+ * Null is a legitimate answer, not an error: in-memory graphs (`@archwall/test-utils`, a
130
+ * playground) use bare ids, and every consumer here degrades to treating the id as opaque.
131
+ */
132
+ function parseModuleId(id) {
133
+ const m = SCHEME_OF.exec(id);
134
+ if (!m) return null;
135
+ const scheme = m[1];
136
+ return {
137
+ scheme,
138
+ body: id.slice(scheme.length + 1)
139
+ };
140
+ }
141
+ /**
142
+ * The id as a human should read it: the path, the package name, the builtin specifier.
143
+ *
144
+ * Used by every reporter and offered to rules as `RuleContext.display`, so that a message names
145
+ * `src/domain/rules.ts` and `react` rather than a scheme-prefixed id — or, as before canonical
146
+ * ids existed, an absolute path from whichever machine produced the graph.
147
+ *
148
+ * `virtual:` keeps its prefix: it is not a path, and the prefix is the only thing that says so.
149
+ */
150
+ function displayModuleId(id) {
151
+ const parsed = parseModuleId(id);
152
+ if (parsed === null || parsed.scheme === "virtual") return id;
153
+ return parsed.body;
154
+ }
155
+ /** Code the project owns and can change — including sibling packages in the monorepo. */
156
+ const FIRST_PARTY_KINDS = ["source", "workspace"];
157
+ /** A dependency the project does not own: third-party code or a runtime builtin. */
158
+ const THIRD_PARTY_KINDS = ["package", "builtin"];
159
+ function isFirstParty(kind) {
160
+ return kind === "source" || kind === "workspace";
161
+ }
162
+ function isThirdParty(kind) {
163
+ return kind === "package" || kind === "builtin";
164
+ }
165
+ /**
166
+ * The module graph, as an OPAQUE handle.
167
+ *
168
+ * The backing stores are private and no accessor hands them out. Everything a consumer
169
+ * legitimately needs is a method here or on `GraphQuery`; if something is missing, the fix
170
+ * is to add a method, never to expose the store.
171
+ *
172
+ * That is what keeps the *representation* out of the IR contract: a `ReadonlyMap` plus an
173
+ * `Edge[]` is the current implementation, not the promise.
174
+ */
175
+ var ProjectGraph = class ProjectGraph {
176
+ irVersion;
177
+ host;
178
+ delivery;
179
+ #modules;
180
+ #edges;
181
+ constructor(irVersion, host, delivery, modules, edges) {
182
+ this.irVersion = irVersion;
183
+ this.host = host;
184
+ this.delivery = delivery;
185
+ this.#modules = modules;
186
+ this.#edges = edges;
187
+ }
188
+ static create(init) {
189
+ const modules = init.modules instanceof Map ? init.modules : new Map(init.modules);
190
+ return new ProjectGraph(init.irVersion ?? "1.0.0", init.host, init.delivery ?? "complete", modules, init.edges);
191
+ }
192
+ get moduleCount() {
193
+ return this.#modules.size;
194
+ }
195
+ get edgeCount() {
196
+ return this.#edges.length;
197
+ }
198
+ module(id) {
199
+ return this.#modules.get(id);
200
+ }
201
+ hasModule(id) {
202
+ return this.#modules.has(id);
203
+ }
204
+ /** Every module, in graph order. */
205
+ modules() {
206
+ return this.#modules.values();
207
+ }
208
+ /** Every module id, in graph order. */
209
+ moduleIds() {
210
+ return this.#modules.keys();
211
+ }
212
+ /** Every edge, in graph order. Never copy this — it is already immutable. */
213
+ edges() {
214
+ return this.#edges;
215
+ }
216
+ /**
217
+ * A new graph with replaced stores, same identity fields.
218
+ *
219
+ * @internal Engine and {@link GraphDraft} only. Not part of the IR contract.
220
+ */
221
+ replaceStores(modules, edges) {
222
+ return new ProjectGraph(this.irVersion, this.host, this.delivery, modules, edges ?? this.#edges);
223
+ }
224
+ };
225
+ /**
226
+ * Copy-on-write {@link GraphMutation} over a {@link ProjectGraph}.
227
+ *
228
+ * A transform that touches nothing costs nothing: the stores are only cloned on the first
229
+ * write, and `commit()` returns the original graph when there were none.
230
+ *
231
+ * @internal
232
+ */
233
+ var GraphDraft = class {
234
+ #base;
235
+ #modules;
236
+ #edges;
237
+ constructor(base) {
238
+ this.#base = base;
239
+ }
240
+ #mutableModules() {
241
+ if (this.#modules === void 0) {
242
+ this.#modules = /* @__PURE__ */ new Map();
243
+ for (const m of this.#base.modules()) this.#modules.set(m.id, m);
244
+ }
245
+ return this.#modules;
246
+ }
247
+ #mutableEdges() {
248
+ this.#edges ??= [...this.#base.edges()];
249
+ return this.#edges;
250
+ }
251
+ module(id) {
252
+ return this.#modules ? this.#modules.get(id) : this.#base.module(id);
253
+ }
254
+ hasModule(id) {
255
+ return this.#modules ? this.#modules.has(id) : this.#base.hasModule(id);
256
+ }
257
+ modules() {
258
+ return this.#modules ? this.#modules.values() : this.#base.modules();
259
+ }
260
+ edges() {
261
+ return this.#edges ?? this.#base.edges();
262
+ }
263
+ addModule(node) {
264
+ this.#mutableModules().set(node.id, node);
265
+ }
266
+ patchModule(id, patch) {
267
+ const current = this.module(id);
268
+ if (current === void 0) return;
269
+ const { tags: tagPatch, ...rest } = patch;
270
+ const next = {
271
+ ...current,
272
+ ...rest
273
+ };
274
+ if (tagPatch !== void 0) {
275
+ const tags = new Map(current.tags);
276
+ for (const [k, v] of Object.entries(tagPatch)) tags.set(k, v);
277
+ next.tags = tags;
278
+ }
279
+ this.#mutableModules().set(id, next);
280
+ }
281
+ addEdge(edge) {
282
+ this.#mutableEdges().push(edge);
283
+ }
284
+ removeEdges(predicate) {
285
+ const kept = this.edges().filter((e) => !predicate(e));
286
+ if (kept.length !== this.edges().length) this.#edges = kept;
287
+ }
288
+ /** The resulting graph, or the untouched original when nothing was written. */
289
+ commit() {
290
+ if (this.#modules === void 0 && this.#edges === void 0) return this.#base;
291
+ const modules = this.#modules ?? new Map([...this.#base.modules()].map((m) => [m.id, m]));
292
+ return this.#base.replaceStores(modules, this.#edges);
293
+ }
294
+ };
295
+ function irMajor(version) {
296
+ const m = /^(\d+)\./.exec(version);
297
+ if (!m) throw new ArchWallError(`Malformed IR version: "${version}"`);
298
+ return Number(m[1]);
299
+ }
300
+ function assertIrCompatible(graphVersion) {
301
+ if (irMajor(graphVersion) !== irMajor("1.0.0")) throw new IrVersionMismatchError(graphVersion, IR_VERSION);
302
+ }
303
+ //#endregion
304
+ //#region src/analysis/cache.ts
305
+ /**
306
+ * Memoizes graph computations per (computation, view): ten unscoped rules requesting SCCs cost
307
+ * one traversal.
308
+ *
309
+ * The view is part of the key because a computation is an ENUMERATION of the graph, and
310
+ * enumeration is scoped. A cache bound to the root query
311
+ * would hand a rule scoped to `apps/web` the cycles of the whole repository — the rule's
312
+ * `ctx.graph` narrowed and its `ctx.compute` silently not.
313
+ *
314
+ * Rules sharing a scope share the base query object, so they share the entry; the common case
315
+ * (no scope at all) is still one evaluation for everyone.
316
+ */
317
+ var GraphComputationCache = class {
318
+ #memo = /* @__PURE__ */ new Map();
319
+ get(computation, graph) {
320
+ let perView = this.#memo.get(graph);
321
+ if (perView === void 0) {
322
+ perView = /* @__PURE__ */ new Map();
323
+ this.#memo.set(graph, perView);
324
+ }
325
+ const key = computation;
326
+ if (perView.has(key)) return perView.get(key);
327
+ const value = computation.compute(graph);
328
+ perView.set(key, value);
329
+ return value;
330
+ }
331
+ };
332
+ //#endregion
333
+ //#region src/graph/query.ts
334
+ function matchesKind(m, want) {
335
+ return typeof want === "string" ? m.kind === want : want.includes(m.kind);
336
+ }
337
+ function matchesTags(m, want) {
338
+ for (const k of Object.keys(want)) if (m.tags.get(k) !== want[k]) return false;
339
+ return true;
340
+ }
341
+ const NO_EDGES = [];
342
+ /**
343
+ * Stable key for a filter, so the engine can bucket rules that want the same slice of the
344
+ * graph and evaluate that slice once for all of them.
345
+ */
346
+ function filterKey(filter) {
347
+ if (filter === void 0) return "*";
348
+ return JSON.stringify(filter, Object.keys(filter).sort());
349
+ }
350
+ /**
351
+ * The adjacency and attribute indexes over one graph, built LAZILY per axis.
352
+ *
353
+ * One index serves every query over a graph, scoped or not: a scope narrows *which results
354
+ * are returned*, and does not change what the graph contains, so it must never rebuild the
355
+ * index of it.
356
+ *
357
+ * Each axis is built on first use. A run whose rules only walk edges never pays for the
358
+ * tag, kind, and package indexes.
359
+ */
360
+ var GraphIndex = class {
361
+ #graph;
362
+ #out;
363
+ #in;
364
+ #byTag;
365
+ #byKind;
366
+ #byPackage;
367
+ constructor(graph) {
368
+ this.#graph = graph;
369
+ }
370
+ #buildAdjacency() {
371
+ const out = /* @__PURE__ */ new Map();
372
+ const inn = /* @__PURE__ */ new Map();
373
+ for (const e of this.#graph.edges()) {
374
+ const o = out.get(e.from);
375
+ if (o) o.push(e);
376
+ else out.set(e.from, [e]);
377
+ const i = inn.get(e.to);
378
+ if (i) i.push(e);
379
+ else inn.set(e.to, [e]);
380
+ }
381
+ this.#out = out;
382
+ this.#in = inn;
383
+ }
384
+ outOf(id) {
385
+ if (this.#out === void 0) this.#buildAdjacency();
386
+ return this.#out?.get(id) ?? NO_EDGES;
387
+ }
388
+ into(id) {
389
+ if (this.#in === void 0) this.#buildAdjacency();
390
+ return this.#in?.get(id) ?? NO_EDGES;
391
+ }
392
+ byTag(key, value) {
393
+ if (this.#byTag === void 0) {
394
+ const index = /* @__PURE__ */ new Map();
395
+ for (const m of this.#graph.modules()) for (const [k, v] of m.tags) {
396
+ const bucket = `${k}\0${v}`;
397
+ const ids = index.get(bucket);
398
+ if (ids) ids.push(m.id);
399
+ else index.set(bucket, [m.id]);
400
+ }
401
+ this.#byTag = index;
402
+ }
403
+ return this.#byTag.get(`${key}\0${value}`) ?? [];
404
+ }
405
+ byKind(kind) {
406
+ if (this.#byKind === void 0) {
407
+ const index = /* @__PURE__ */ new Map();
408
+ for (const m of this.#graph.modules()) {
409
+ const bucket = index.get(m.kind);
410
+ if (bucket) bucket.push(m);
411
+ else index.set(m.kind, [m]);
412
+ }
413
+ this.#byKind = index;
414
+ }
415
+ return this.#byKind.get(kind) ?? [];
416
+ }
417
+ byPackage(name) {
418
+ if (this.#byPackage === void 0) {
419
+ const index = /* @__PURE__ */ new Map();
420
+ for (const m of this.#graph.modules()) {
421
+ if (m.packageName === void 0) continue;
422
+ const bucket = index.get(m.packageName);
423
+ if (bucket) bucket.push(m);
424
+ else index.set(m.packageName, [m]);
425
+ }
426
+ this.#byPackage = index;
427
+ }
428
+ return this.#byPackage.get(name) ?? [];
429
+ }
430
+ };
431
+ /**
432
+ * The only sanctioned way to read a graph.
433
+ *
434
+ * A scoped query is a VIEW: it shares the underlying {@link GraphIndex} with the query it
435
+ * came from and differs only in which modules it is *about*.
436
+ *
437
+ * ## What scope does, exactly
438
+ *
439
+ * One rule: **an operation is scoped if and only if it ENUMERATES. An operation that answers a
440
+ * question about a module you named is never scoped.**
441
+ *
442
+ * | Operation | Scoped |
443
+ * |---|---|
444
+ * | `modules`, `moduleIds`, `moduleCount`, `edges` | yes — they enumerate |
445
+ * | `module`, `has`, `tagOf` | no — you named the module |
446
+ * | `edgesOutOf`, `edgesInto` | no — you named the module |
447
+ * | `reachableFrom`, `reaching`, `pathBetween` | no — traversal from a named module |
448
+ * | `ModuleSelection.edgesOut` / `edgesIn` | anchored: endpoints in-selection, edges unfiltered |
449
+ * | `RuleContext.compute` | yes — a computation enumerates |
450
+ *
451
+ * The asymmetry is deliberate rather than incidental. A scoped rule must be able to ask what an
452
+ * out-of-scope import target *is*, because an edge leaving the scope is the most interesting
453
+ * thing it can find; hiding the target would turn `layer-dependencies` under a scope from a
454
+ * finding into silence.
455
+ */
456
+ var GraphQuery = class GraphQuery {
457
+ #graph;
458
+ #index;
459
+ /** When present, the ANCHOR set: which modules this view is about. See the class doc. */
460
+ #scope;
461
+ /** Scoped `edges()` is one filter over the whole edge list; do it once, not per call. */
462
+ #scopedEdges;
463
+ constructor(graph, index, scope) {
464
+ this.#graph = graph;
465
+ this.#index = index ?? new GraphIndex(graph);
466
+ this.#scope = scope;
467
+ }
468
+ /** A view of the same graph restricted to `scope`, sharing this query's index. */
469
+ scoped(scope) {
470
+ return new GraphQuery(this.#graph, this.#index, scope);
471
+ }
472
+ module(id) {
473
+ return this.#graph.module(id);
474
+ }
475
+ /** Modules in scope, or all of them when unscoped. */
476
+ moduleCount() {
477
+ return this.#scope ? this.#scope.size : this.#graph.moduleCount;
478
+ }
479
+ /** Every in-scope module id, in graph order. The traversal primitive. */
480
+ moduleIds() {
481
+ if (!this.#scope) return this.#graph.moduleIds();
482
+ const scope = this.#scope;
483
+ return (function* (ids) {
484
+ for (const id of ids) if (scope.has(id)) yield id;
485
+ })(this.#graph.moduleIds());
486
+ }
487
+ /** Whether the graph contains this module at all — distinct from "is it a source file". */
488
+ has(id) {
489
+ return this.#graph.hasModule(id);
490
+ }
491
+ tagOf(id, key) {
492
+ return this.#graph.module(id)?.tags.get(key);
493
+ }
494
+ modules(filter) {
495
+ let candidates;
496
+ const tagEntries = filter?.tag ? Object.entries(filter.tag) : [];
497
+ if (tagEntries.length > 0) {
498
+ const sets = tagEntries.map(([k, v]) => this.#index.byTag(k, v)).sort((a, b) => a.length - b.length);
499
+ const rest = sets.slice(1).map((ids) => new Set(ids));
500
+ const narrowed = [];
501
+ for (const id of sets[0] ?? []) {
502
+ if (!rest.every((s) => s.has(id))) continue;
503
+ const m = this.#graph.module(id);
504
+ if (m !== void 0) narrowed.push(m);
505
+ }
506
+ candidates = narrowed;
507
+ } else if (filter?.packageName !== void 0) candidates = this.#index.byPackage(filter.packageName);
508
+ else if (filter?.moduleKind !== void 0) {
509
+ const kinds = typeof filter.moduleKind === "string" ? [filter.moduleKind] : filter.moduleKind;
510
+ candidates = kinds.length === 1 ? this.#index.byKind(kinds[0]) : kinds.flatMap((k) => this.#index.byKind(k));
511
+ } else candidates = this.#graph.modules();
512
+ const nodes = [];
513
+ for (const m of candidates) {
514
+ if (this.#scope !== void 0 && !this.#scope.has(m.id)) continue;
515
+ if (filter?.moduleKind !== void 0 && !matchesKind(m, filter.moduleKind)) continue;
516
+ if (filter?.packageName !== void 0 && m.packageName !== filter.packageName) continue;
517
+ nodes.push(m);
518
+ }
519
+ return new Selection(this, nodes);
520
+ }
521
+ /**
522
+ * In-scope edges matching `filter`.
523
+ *
524
+ * Returns the graph's own array when nothing narrows it — it is already immutable, so a
525
+ * defensive copy per call would protect nobody and cost a full edge list per rule.
526
+ */
527
+ edges(filter) {
528
+ let all;
529
+ if (this.#scope === void 0) all = this.#graph.edges();
530
+ else {
531
+ const scope = this.#scope;
532
+ this.#scopedEdges ??= this.#graph.edges().filter((e) => scope.has(e.from));
533
+ all = this.#scopedEdges;
534
+ }
535
+ return this.filterEdges(all, filter);
536
+ }
537
+ edgesOutOf(id) {
538
+ return this.#index.outOf(id);
539
+ }
540
+ edgesInto(id) {
541
+ return this.#index.into(id);
542
+ }
543
+ /**
544
+ * Every module reachable from `id` by following edges, excluding `id` itself unless a
545
+ * cycle leads back to it. Iterative: a 10k-module chain would overflow the stack.
546
+ */
547
+ reachableFrom(id, filter) {
548
+ const seen = /* @__PURE__ */ new Set();
549
+ const stack = [id];
550
+ while (stack.length > 0) {
551
+ const current = stack.pop();
552
+ for (const e of this.filterEdges(this.edgesOutOf(current), filter)) {
553
+ if (seen.has(e.to)) continue;
554
+ seen.add(e.to);
555
+ stack.push(e.to);
556
+ }
557
+ }
558
+ return seen;
559
+ }
560
+ /** The mirror of {@link reachableFrom}: everything that can reach `id`. */
561
+ reaching(id, filter) {
562
+ const seen = /* @__PURE__ */ new Set();
563
+ const stack = [id];
564
+ while (stack.length > 0) {
565
+ const current = stack.pop();
566
+ for (const e of this.filterEdges(this.edgesInto(current), filter)) {
567
+ if (seen.has(e.from)) continue;
568
+ seen.add(e.from);
569
+ stack.push(e.from);
570
+ }
571
+ }
572
+ return seen;
573
+ }
574
+ /**
575
+ * Shortest dependency path from `from` to `to`, inclusive of both, or null. BFS, because
576
+ * the useful evidence for "domain reaches infrastructure" is the shortest chain, not
577
+ * whichever one a traversal happened to find first.
578
+ */
579
+ pathBetween(from, to, filter) {
580
+ if (from === to) return [from];
581
+ const previous = /* @__PURE__ */ new Map();
582
+ const queue = [from];
583
+ const seen = /* @__PURE__ */ new Set([from]);
584
+ for (let i = 0; i < queue.length; i++) {
585
+ const current = queue[i];
586
+ for (const e of this.filterEdges(this.edgesOutOf(current), filter)) {
587
+ if (seen.has(e.to)) continue;
588
+ seen.add(e.to);
589
+ previous.set(e.to, current);
590
+ if (e.to === to) {
591
+ const path = [to];
592
+ for (let at = to; previous.has(at);) {
593
+ at = previous.get(at);
594
+ path.push(at);
595
+ }
596
+ return path.reverse();
597
+ }
598
+ queue.push(e.to);
599
+ }
600
+ }
601
+ return null;
602
+ }
603
+ /** Applies an {@link EdgeFilter} to an edge list. Returns the input when there is none. */
604
+ filterEdges(edges, filter) {
605
+ if (!filter) return edges;
606
+ return edges.filter((e) => this.matchesEdge(e, filter));
607
+ }
608
+ /** Whether one edge satisfies a filter. The unit the engine's visitor dispatch uses. */
609
+ matchesEdge(e, filter) {
610
+ if (filter.kind !== void 0 && e.kind !== filter.kind) return false;
611
+ const from = this.#graph.module(e.from);
612
+ const to = this.#graph.module(e.to);
613
+ if (filter.toModuleKind !== void 0 && (!to || !matchesKind(to, filter.toModuleKind))) return false;
614
+ if (filter.fromTag && (!from || !matchesTags(from, filter.fromTag))) return false;
615
+ if (filter.toTag && (!to || !matchesTags(to, filter.toTag))) return false;
616
+ if (filter.crossing !== void 0) {
617
+ const a = from?.tags.get(filter.crossing);
618
+ const b = to?.tags.get(filter.crossing);
619
+ if (a === void 0 || b === void 0 || a === b) return false;
620
+ }
621
+ return true;
622
+ }
623
+ /** Whether one module satisfies a filter. Paired with {@link matchesEdge}. */
624
+ matchesModule(m, filter) {
625
+ if (filter.tag && !matchesTags(m, filter.tag)) return false;
626
+ if (filter.moduleKind !== void 0 && !matchesKind(m, filter.moduleKind)) return false;
627
+ if (filter.packageName !== void 0 && m.packageName !== filter.packageName) return false;
628
+ return true;
629
+ }
630
+ };
631
+ var Selection = class Selection {
632
+ query;
633
+ nodes;
634
+ constructor(query, nodes) {
635
+ this.query = query;
636
+ this.nodes = nodes;
637
+ }
638
+ [Symbol.iterator]() {
639
+ return this.nodes[Symbol.iterator]();
640
+ }
641
+ get size() {
642
+ return this.nodes.length;
643
+ }
644
+ isEmpty() {
645
+ return this.nodes.length === 0;
646
+ }
647
+ toArray() {
648
+ return this.nodes;
649
+ }
650
+ ids() {
651
+ return this.nodes.map((m) => m.id);
652
+ }
653
+ forEach(fn) {
654
+ this.nodes.forEach(fn);
655
+ }
656
+ filter(predicate) {
657
+ return new Selection(this.query, this.nodes.filter(predicate));
658
+ }
659
+ edgesOut(filter) {
660
+ const all = this.nodes.flatMap((m) => this.query.edgesOutOf(m.id));
661
+ return this.query.filterEdges(all, filter);
662
+ }
663
+ edgesIn(filter) {
664
+ const all = this.nodes.flatMap((m) => this.query.edgesInto(m.id));
665
+ return this.query.filterEdges(all, filter);
666
+ }
667
+ };
668
+ //#endregion
669
+ //#region src/engine/prepare.ts
670
+ /**
671
+ * The one pipeline: project boundary → transforms → boundary again → classification.
672
+ *
673
+ * The boundary belongs to the ENGINE, not to producers: producers are the component that
674
+ * varies, so anything that must be identical across hosts cannot live in them. Producers
675
+ * over-collect; the engine trims.
676
+ *
677
+ * It runs twice because a transform may ADD modules, and those must be bounded exactly as
678
+ * if a producer had supplied them. Running it again is safe because it is idempotent — it
679
+ * only ever re-kinds `source` → `excluded`. With no transforms configured, boundary and
680
+ * classification are one fused pass over the modules.
681
+ *
682
+ * Excluded modules are re-kinded, never deleted. An edge *into* an excluded file still says
683
+ * something true about the architecture, and deleting the node would silently rewrite the
684
+ * graph's shape (a cycle through a test helper would vanish).
685
+ */
686
+ function prepareGraph(graph, config, transforms, classifiers) {
687
+ const diagnostics = [];
688
+ const provided = [];
689
+ let current = graph;
690
+ if (transforms.length > 0) {
691
+ current = boundaryAndClassify(current, config, []);
692
+ const ctx = {
693
+ sourceRoot: config.sourceRoot,
694
+ repoRoot: config.repoRoot,
695
+ relative: (file) => sourceRelative(config.sourceRoot, file)
696
+ };
697
+ for (const t of transforms) {
698
+ const draft = new GraphDraft(current);
699
+ try {
700
+ t.transform(draft, ctx);
701
+ current = draft.commit();
702
+ for (const c of t.provides ?? []) provided.push(c);
703
+ } catch (err) {
704
+ diagnostics.push({
705
+ code: "transform-failed",
706
+ severity: "error",
707
+ message: `Graph transform "${t.name}" threw and was skipped: ${err instanceof Error ? err.message : String(err)}`,
708
+ ...err instanceof Error && err.stack !== void 0 ? { details: { stack: err.stack } } : {}
709
+ });
710
+ }
711
+ }
712
+ }
713
+ return {
714
+ graph: boundaryAndClassify(current, config, classifiers),
715
+ diagnostics,
716
+ provided
717
+ };
718
+ }
719
+ /**
720
+ * One pass over the modules applying both the boundary and classification.
721
+ *
722
+ * They do not interact — the boundary decides `kind` from the file path alone, and
723
+ * classification decides `tags` from the node — so one pass serves both, halving the
724
+ * per-run allocation. At 50k modules a second pass would mean another 50k node copies and
725
+ * 50k tag Maps on every watch rebuild.
726
+ *
727
+ * Two further allocations are avoided: a module whose kind is unchanged AND that no
728
+ * classifier tagged is passed through by reference, and the tag map is cloned only when a
729
+ * classifier actually contributes something.
730
+ */
731
+ function boundaryAndClassify(graph, config, classifiers) {
732
+ const isIncluded = (0, picomatch.default)(config.include, { dot: true });
733
+ const isExcluded = (0, picomatch.default)(config.exclude, { dot: true });
734
+ const ctx = {
735
+ sourceRoot: config.sourceRoot,
736
+ relative: (file) => sourceRelative(config.sourceRoot, file)
737
+ };
738
+ const modules = /* @__PURE__ */ new Map();
739
+ for (const m of graph.modules()) {
740
+ let node = m;
741
+ if (m.kind === "source" && m.file !== null) {
742
+ const rel = sourceRelative(config.sourceRoot, m.file);
743
+ if (rel === null || !isIncluded(rel) || isExcluded(rel)) node = {
744
+ ...m,
745
+ kind: "excluded"
746
+ };
747
+ }
748
+ let tags;
749
+ for (const classifier of classifiers) {
750
+ const patch = classifier.classify(node, ctx);
751
+ if (!patch) continue;
752
+ tags ??= new Map(node.tags);
753
+ for (const [k, v] of Object.entries(patch)) tags.set(k, v);
754
+ }
755
+ modules.set(m.id, tags === void 0 ? node : {
756
+ ...node,
757
+ tags
758
+ });
759
+ }
760
+ return graph.replaceStores(modules);
761
+ }
762
+ /**
763
+ * The project boundary on its own, for callers that want kinds settled without tagging.
764
+ * One implementation, shared with {@link prepareGraph}.
765
+ */
766
+ function applyProjectBoundary(graph, config) {
767
+ return boundaryAndClassify(graph, config, []);
768
+ }
769
+ //#endregion
770
+ Object.defineProperty(exports, "ArchWallError", {
771
+ enumerable: true,
772
+ get: function() {
773
+ return ArchWallError;
774
+ }
775
+ });
776
+ Object.defineProperty(exports, "FIRST_PARTY_KINDS", {
777
+ enumerable: true,
778
+ get: function() {
779
+ return FIRST_PARTY_KINDS;
780
+ }
781
+ });
782
+ Object.defineProperty(exports, "GraphComputationCache", {
783
+ enumerable: true,
784
+ get: function() {
785
+ return GraphComputationCache;
786
+ }
787
+ });
788
+ Object.defineProperty(exports, "GraphDraft", {
789
+ enumerable: true,
790
+ get: function() {
791
+ return GraphDraft;
792
+ }
793
+ });
794
+ Object.defineProperty(exports, "GraphIndex", {
795
+ enumerable: true,
796
+ get: function() {
797
+ return GraphIndex;
798
+ }
799
+ });
800
+ Object.defineProperty(exports, "GraphQuery", {
801
+ enumerable: true,
802
+ get: function() {
803
+ return GraphQuery;
804
+ }
805
+ });
806
+ Object.defineProperty(exports, "IR_VERSION", {
807
+ enumerable: true,
808
+ get: function() {
809
+ return IR_VERSION;
810
+ }
811
+ });
812
+ Object.defineProperty(exports, "IrVersionMismatchError", {
813
+ enumerable: true,
814
+ get: function() {
815
+ return IrVersionMismatchError;
816
+ }
817
+ });
818
+ Object.defineProperty(exports, "MODULE_ID_SCHEMES", {
819
+ enumerable: true,
820
+ get: function() {
821
+ return MODULE_ID_SCHEMES;
822
+ }
823
+ });
824
+ Object.defineProperty(exports, "ProjectGraph", {
825
+ enumerable: true,
826
+ get: function() {
827
+ return ProjectGraph;
828
+ }
829
+ });
830
+ Object.defineProperty(exports, "THIRD_PARTY_KINDS", {
831
+ enumerable: true,
832
+ get: function() {
833
+ return THIRD_PARTY_KINDS;
834
+ }
835
+ });
836
+ Object.defineProperty(exports, "__toESM", {
837
+ enumerable: true,
838
+ get: function() {
839
+ return __toESM;
840
+ }
841
+ });
842
+ Object.defineProperty(exports, "applyProjectBoundary", {
843
+ enumerable: true,
844
+ get: function() {
845
+ return applyProjectBoundary;
846
+ }
847
+ });
848
+ Object.defineProperty(exports, "assertIrCompatible", {
849
+ enumerable: true,
850
+ get: function() {
851
+ return assertIrCompatible;
852
+ }
853
+ });
854
+ Object.defineProperty(exports, "displayModuleId", {
855
+ enumerable: true,
856
+ get: function() {
857
+ return displayModuleId;
858
+ }
859
+ });
860
+ Object.defineProperty(exports, "filterKey", {
861
+ enumerable: true,
862
+ get: function() {
863
+ return filterKey;
864
+ }
865
+ });
866
+ Object.defineProperty(exports, "hashParts", {
867
+ enumerable: true,
868
+ get: function() {
869
+ return hashParts;
870
+ }
871
+ });
872
+ Object.defineProperty(exports, "irMajor", {
873
+ enumerable: true,
874
+ get: function() {
875
+ return irMajor;
876
+ }
877
+ });
878
+ Object.defineProperty(exports, "isFirstParty", {
879
+ enumerable: true,
880
+ get: function() {
881
+ return isFirstParty;
882
+ }
883
+ });
884
+ Object.defineProperty(exports, "isThirdParty", {
885
+ enumerable: true,
886
+ get: function() {
887
+ return isThirdParty;
888
+ }
889
+ });
890
+ Object.defineProperty(exports, "parseModuleId", {
891
+ enumerable: true,
892
+ get: function() {
893
+ return parseModuleId;
894
+ }
895
+ });
896
+ Object.defineProperty(exports, "prepareGraph", {
897
+ enumerable: true,
898
+ get: function() {
899
+ return prepareGraph;
900
+ }
901
+ });
902
+ Object.defineProperty(exports, "sourceRelative", {
903
+ enumerable: true,
904
+ get: function() {
905
+ return sourceRelative;
906
+ }
907
+ });
908
+ Object.defineProperty(exports, "stableHash", {
909
+ enumerable: true,
910
+ get: function() {
911
+ return stableHash;
912
+ }
913
+ });
914
+ Object.defineProperty(exports, "toRelative", {
915
+ enumerable: true,
916
+ get: function() {
917
+ return toRelative;
918
+ }
919
+ });
920
+
921
+ //# sourceMappingURL=prepare-C1FfL8Qd.cjs.map