@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,638 @@
1
+ //#region src/graph/ir.d.ts
2
+ /** Semver of the Project Graph IR schema itself, independent of package versions. */
3
+ declare const IR_VERSION = "1.0.0";
4
+ /**
5
+ * A module's identity, in the IR's own vocabulary rather than the host's.
6
+ *
7
+ * ```
8
+ * file:<repo-relative-posix-path> source | workspace | excluded
9
+ * pkg:<name> package — the package, not one of its files
10
+ * builtin:<specifier> builtin — always prefixed (builtin:node:fs)
11
+ * virtual:<host>:<opaque> virtual — host-synthesized, host-specific by nature
12
+ * unresolved:<raw-specifier> unresolved
13
+ * ```
14
+ *
15
+ * Producers report host facts; `GraphBuilder` decides identity — the same division
16
+ * {@link ModuleKind} already uses. That is what makes a violation's fingerprint the same under
17
+ * every bundler, which is what makes a baseline file possible at all.
18
+ */
19
+ type ModuleId = string;
20
+ /** The schemes {@link ModuleId} recognises. */
21
+ declare const MODULE_ID_SCHEMES: readonly ["file", "pkg", "builtin", "virtual", "unresolved"];
22
+ type ModuleIdScheme = (typeof MODULE_ID_SCHEMES)[number];
23
+ /**
24
+ * Splits a canonical id into its scheme and body, or null when it carries no known scheme.
25
+ *
26
+ * Null is a legitimate answer, not an error: in-memory graphs (`@archwall/test-utils`, a
27
+ * playground) use bare ids, and every consumer here degrades to treating the id as opaque.
28
+ */
29
+ declare function parseModuleId(id: ModuleId): {
30
+ scheme: ModuleIdScheme;
31
+ body: string;
32
+ } | null;
33
+ /**
34
+ * The id as a human should read it: the path, the package name, the builtin specifier.
35
+ *
36
+ * Used by every reporter and offered to rules as `RuleContext.display`, so that a message names
37
+ * `src/domain/rules.ts` and `react` rather than a scheme-prefixed id — or, as before canonical
38
+ * ids existed, an absolute path from whichever machine produced the graph.
39
+ *
40
+ * `virtual:` keeps its prefix: it is not a path, and the prefix is the only thing that says so.
41
+ */
42
+ declare function displayModuleId(id: ModuleId): string;
43
+ type WellKnownCapability =
44
+ /** `Edge.loc` is populated. */
45
+ "import-locations" |
46
+ /** Dynamic `import()` edges are present and marked `kind: "dynamic"`. */
47
+ "dynamic-imports" |
48
+ /** Every module in the project is present; absence of a module IS evidence. */
49
+ "complete-graph" |
50
+ /** Re-export edges are distinguished from plain imports. */
51
+ "reexport-edges" |
52
+ /**
53
+ * `Edge.rawSpecifier` is what the author wrote, not a copy of the resolved id. A rule
54
+ * that matches on specifiers must require this, or it silently matches nothing on hosts
55
+ * that cannot supply them and reports a clean run rather than an unavailable one.
56
+ */
57
+ "raw-specifiers";
58
+ /**
59
+ * Open union: adapters may declare capabilities core does not know about, and rules may
60
+ * require them, without an IR major. `WellKnownCapability` keeps autocomplete useful for
61
+ * the ones core ships.
62
+ */
63
+ type Capability = WellKnownCapability | (string & {});
64
+ interface HostInfo {
65
+ name: string;
66
+ version: string;
67
+ capabilities: ReadonlySet<Capability>;
68
+ }
69
+ interface SourceLocation {
70
+ file: string;
71
+ /** 1-based */
72
+ line: number;
73
+ /** 0-based */
74
+ column: number;
75
+ }
76
+ type WellKnownEdgeKind = "static" | "dynamic" | "reexport";
77
+ /**
78
+ * Open union so future graph facts (CSS imports, worker edges, type-only edges) arrive
79
+ * additively. Consumers must treat an unrecognised kind as "some dependency exists" —
80
+ * never assume exhaustiveness.
81
+ */
82
+ type EdgeKind = WellKnownEdgeKind | (string & {});
83
+ /**
84
+ * What a module *is*, relative to the project being analysed.
85
+ *
86
+ * - `source` — a first-party file inside the analysed project
87
+ * - `workspace` — a file owned by a *different* package in the same monorepo
88
+ * - `package` — a third-party dependency (node_modules)
89
+ * - `builtin` — a runtime builtin (`node:fs`, `bun:sqlite`, …)
90
+ * - `virtual` — generated by the toolchain; no file on disk
91
+ * - `unresolved` — the specifier could not be resolved to anything
92
+ * - `excluded` — a real project file the config's `exclude` removed from analysis
93
+ *
94
+ * The seven-way split is load-bearing: a purity rule that cannot tell `node:crypto` from
95
+ * `lodash` from `@myorg/shared-kernel` gives the wrong answer for two of the three.
96
+ */
97
+ type ModuleKind = "source" | "workspace" | "package" | "builtin" | "virtual" | "unresolved" | "excluded";
98
+ interface ModuleNode {
99
+ /** Canonical; see {@link ModuleId}. */
100
+ id: ModuleId;
101
+ /**
102
+ * Absolute path, for the kinds that denote a file: `source`, `workspace`, `excluded`.
103
+ *
104
+ * Null for everything else — including `package`, because a dependency is one node
105
+ * (`pkg:react`) rather than one node per file, so there is no single file to name.
106
+ */
107
+ file: string | null;
108
+ kind: ModuleKind;
109
+ /** npm package name, for `kind: "package"`. */
110
+ packageName?: string;
111
+ /** Owning workspace package name, for `kind: "workspace"`. */
112
+ workspace?: string;
113
+ /** Filled by classification, e.g. layer → "features". */
114
+ tags: ReadonlyMap<string, string>;
115
+ }
116
+ /** Code the project owns and can change — including sibling packages in the monorepo. */
117
+ declare const FIRST_PARTY_KINDS: readonly ["source", "workspace"];
118
+ /** A dependency the project does not own: third-party code or a runtime builtin. */
119
+ declare const THIRD_PARTY_KINDS: readonly ["package", "builtin"];
120
+ declare function isFirstParty(kind: ModuleKind): boolean;
121
+ declare function isThirdParty(kind: ModuleKind): boolean;
122
+ interface Edge {
123
+ from: ModuleId;
124
+ to: ModuleId;
125
+ /** What the source wrote: "@/features/auth". */
126
+ rawSpecifier: string;
127
+ /** What it actually is after resolution. */
128
+ resolvedPath: string;
129
+ kind: EdgeKind;
130
+ /** Present only if host capability allows. */
131
+ loc?: SourceLocation;
132
+ }
133
+ type GraphDelivery = "complete" | "progressive";
134
+ interface ProjectGraphInit {
135
+ host: HostInfo;
136
+ /** Default "complete". */
137
+ delivery?: GraphDelivery;
138
+ modules: Iterable<readonly [ModuleId, ModuleNode]> | ReadonlyMap<ModuleId, ModuleNode>;
139
+ edges: readonly Edge[];
140
+ /** Default {@link IR_VERSION}. Adapters should leave this alone. */
141
+ irVersion?: string;
142
+ }
143
+ /**
144
+ * The module graph, as an OPAQUE handle.
145
+ *
146
+ * The backing stores are private and no accessor hands them out. Everything a consumer
147
+ * legitimately needs is a method here or on `GraphQuery`; if something is missing, the fix
148
+ * is to add a method, never to expose the store.
149
+ *
150
+ * That is what keeps the *representation* out of the IR contract: a `ReadonlyMap` plus an
151
+ * `Edge[]` is the current implementation, not the promise.
152
+ */
153
+ declare class ProjectGraph {
154
+ #private;
155
+ readonly irVersion: string;
156
+ readonly host: HostInfo;
157
+ readonly delivery: GraphDelivery;
158
+ private constructor();
159
+ static create(init: ProjectGraphInit): ProjectGraph;
160
+ get moduleCount(): number;
161
+ get edgeCount(): number;
162
+ module(id: ModuleId): ModuleNode | undefined;
163
+ hasModule(id: ModuleId): boolean;
164
+ /** Every module, in graph order. */
165
+ modules(): Iterable<ModuleNode>;
166
+ /** Every module id, in graph order. */
167
+ moduleIds(): Iterable<ModuleId>;
168
+ /** Every edge, in graph order. Never copy this — it is already immutable. */
169
+ edges(): readonly Edge[];
170
+ /**
171
+ * A new graph with replaced stores, same identity fields.
172
+ *
173
+ * @internal Engine and {@link GraphDraft} only. Not part of the IR contract.
174
+ */
175
+ replaceStores(modules: ReadonlyMap<ModuleId, ModuleNode>, edges?: readonly Edge[]): ProjectGraph;
176
+ }
177
+ /**
178
+ * The write surface a {@link GraphTransform} gets.
179
+ *
180
+ * A transform adds, patches, and removes; it never constructs a graph. That is what keeps
181
+ * {@link ProjectGraph} opaque in practice rather than only in principle, and it means a
182
+ * transform cannot drop an IR field it does not know about.
183
+ */
184
+ interface GraphMutation {
185
+ /** Read side, mirroring {@link ProjectGraph}. */
186
+ module(id: ModuleId): ModuleNode | undefined;
187
+ hasModule(id: ModuleId): boolean;
188
+ modules(): Iterable<ModuleNode>;
189
+ edges(): readonly Edge[];
190
+ /** Adds a module, or replaces one with the same id. */
191
+ addModule(node: ModuleNode): void;
192
+ /** Merges fields into an existing module; `tags` merge key-by-key. No-op if absent. */
193
+ patchModule(id: ModuleId, patch: Partial<Omit<ModuleNode, "id" | "tags">> & {
194
+ tags?: Record<string, string>;
195
+ }): void;
196
+ addEdge(edge: Edge): void;
197
+ /** Removes every edge the predicate accepts. */
198
+ removeEdges(predicate: (edge: Edge) => boolean): void;
199
+ }
200
+ /**
201
+ * Copy-on-write {@link GraphMutation} over a {@link ProjectGraph}.
202
+ *
203
+ * A transform that touches nothing costs nothing: the stores are only cloned on the first
204
+ * write, and `commit()` returns the original graph when there were none.
205
+ *
206
+ * @internal
207
+ */
208
+ declare class GraphDraft implements GraphMutation {
209
+ #private;
210
+ constructor(base: ProjectGraph);
211
+ module(id: ModuleId): ModuleNode | undefined;
212
+ hasModule(id: ModuleId): boolean;
213
+ modules(): Iterable<ModuleNode>;
214
+ edges(): readonly Edge[];
215
+ addModule(node: ModuleNode): void;
216
+ patchModule(id: ModuleId, patch: Partial<Omit<ModuleNode, "id" | "tags">> & {
217
+ tags?: Record<string, string>;
218
+ }): void;
219
+ addEdge(edge: Edge): void;
220
+ removeEdges(predicate: (edge: Edge) => boolean): void;
221
+ /** The resulting graph, or the untouched original when nothing was written. */
222
+ commit(): ProjectGraph;
223
+ }
224
+ declare function irMajor(version: string): number;
225
+ declare function assertIrCompatible(graphVersion: string): void;
226
+ //#endregion
227
+ //#region src/contracts/classifier.d.ts
228
+ interface ClassifierContext {
229
+ /**
230
+ * Absolute source root from resolved config. Classifier patterns describe the shape of
231
+ * the source tree, so they are relative to this and never to the repository root.
232
+ */
233
+ sourceRoot: string;
234
+ /**
235
+ * A file's path relative to {@link sourceRoot}, forward-slashed, or null when it lies
236
+ * outside. Every path-based classifier needs exactly this, and none of them should be
237
+ * re-deriving it — guards, slash normalisation and all — in user code.
238
+ */
239
+ relative(file: string): string | null;
240
+ }
241
+ type TagPatch = Record<string, string> | null | undefined | void;
242
+ interface Classifier {
243
+ name: string;
244
+ classify(module: ModuleNode, ctx: ClassifierContext): TagPatch;
245
+ }
246
+ declare function defineClassifier(classifier: Classifier): Classifier;
247
+ //#endregion
248
+ //#region src/violations.d.ts
249
+ /**
250
+ * The ONE severity vocabulary, shared by violations and diagnostics.
251
+ *
252
+ * `info` is available to violations too: a rule may report something worth surfacing that
253
+ * should never gate a build.
254
+ */
255
+ type Severity = "error" | "warn" | "info";
256
+ /**
257
+ * Where a violation is.
258
+ *
259
+ * A tagged union rather than a pair of optional fields, and an ARRAY on the violation
260
+ * rather than one value, because findings are not all edge-shaped. A cycle has no single
261
+ * offending location — it has N of them, and the old model could only name one and had to
262
+ * serialise the rest into the message string. A finding about a package, a directory, or
263
+ * the configuration has no module at all.
264
+ *
265
+ * See docs/adr/0004-violation-locations.md.
266
+ */
267
+ type ViolationLocation = {
268
+ type: "edge";
269
+ edge: Edge;
270
+ } | {
271
+ type: "module";
272
+ module: ModuleId;
273
+ } | {
274
+ type: "path";
275
+ path: string;
276
+ loc?: SourceLocation;
277
+ };
278
+ interface Violation {
279
+ ruleName: string;
280
+ /**
281
+ * Rule *instance* id — what you put in `overrides`, and what every reporter prints.
282
+ * Differs from `ruleName` when the rule came from a preset (`fsd/public-api`) or was
283
+ * given an explicit id; equal to it otherwise.
284
+ */
285
+ ruleId: string;
286
+ severity: Severity;
287
+ /** Rendered human summary. Derived from `messageId` + `data` unless set literally. */
288
+ message: string;
289
+ /**
290
+ * Stable identifier for WHICH of the rule's messages this is, independent of wording.
291
+ * Machine consumers group on this; translators key on it; `ConfiguredRule.message`
292
+ * retargets it.
293
+ */
294
+ messageId?: string;
295
+ /**
296
+ * The values interpolated into the message — and the structured payload a reporter needs
297
+ * in order to do anything other than print English. Without it a consumer wanting the
298
+ * layer names out of a `layer-dependencies` finding has to parse the sentence.
299
+ */
300
+ data?: Readonly<Record<string, string | number>>;
301
+ /**
302
+ * Every place this finding is about, most significant first. Always at least one entry
303
+ * for a rule that reported a location; may be empty for a finding about the run itself.
304
+ */
305
+ locations: readonly ViolationLocation[];
306
+ /** "Why": resolution chain, which constraint, how to fix. */
307
+ explanation?: string;
308
+ /**
309
+ * Stable, machine-independent identity. The same architecture problem on two developers'
310
+ * machines, or under two bundlers, yields the same fingerprint.
311
+ *
312
+ * This is what makes a baseline file possible, and a graph-based linter has no other
313
+ * suppression mechanism available: with no source text there can be no `// archwall-ignore`.
314
+ */
315
+ fingerprint: string;
316
+ }
317
+ interface ViolationInput {
318
+ /** Literal message. Mutually exclusive with `messageId`; one of the two is required. */
319
+ message?: string;
320
+ /** Key into the rule's `meta.messages`. Preferred — it is what `data` interpolates into. */
321
+ messageId?: string;
322
+ data?: Record<string, string | number>;
323
+ /** Convenience for the overwhelmingly common single-edge finding. */
324
+ edge?: Edge;
325
+ /** Convenience for the single-module finding. */
326
+ module?: ModuleId;
327
+ /** Full control, for findings with several locations or a non-module subject. */
328
+ locations?: readonly ViolationLocation[];
329
+ explanation?: string;
330
+ /**
331
+ * Overrides the rule instance's configured severity for this one finding — e.g. a
332
+ * two-module cycle as a warning and a forty-module cycle as an error.
333
+ */
334
+ severity?: Severity;
335
+ /**
336
+ * Explicit identity, for findings whose sameness is not captured by their locations.
337
+ * Order-insensitive: parts are sorted before hashing.
338
+ */
339
+ identity?: readonly string[];
340
+ }
341
+ /** Normalizes the three input spellings into the canonical location list. */
342
+ declare function locationsOf(input: ViolationInput): readonly ViolationLocation[];
343
+ /** The edge a finding is primarily about, when it is about one. */
344
+ declare function primaryEdge(v: Pick<Violation, "locations">): Edge | undefined;
345
+ /** The module a finding is primarily about: an explicit module, else an edge's source. */
346
+ declare function primaryModule(v: Pick<Violation, "locations">): ModuleId | undefined;
347
+ /** Where a finding should be anchored in an editor or in SARIF, when that is knowable. */
348
+ declare function primarySourceLocation(v: Pick<Violation, "locations">): SourceLocation | undefined;
349
+ /**
350
+ * Renders `{placeholder}` templates. Unknown placeholders are left verbatim, so a
351
+ * mis-keyed template is visible in the output rather than silently blank.
352
+ */
353
+ declare function renderMessage(template: string, data: Readonly<Record<string, string | number>> | undefined): string;
354
+ /**
355
+ * Fingerprint scheme version. Bump when the algorithm changes so that a stale baseline
356
+ * ERRORS instead of silently mismatching every entry.
357
+ *
358
+ * `aw3` is the first scheme over canonical module ids
359
+ * (docs/adr/0012-canonical-module-identity.md). Before it, a violation about `react` hashed the
360
+ * host's own id — a resolved `node_modules` path under the CLI, the bare specifier under esbuild
361
+ * — so the same finding fingerprinted differently under two bundlers.
362
+ */
363
+ declare const FINGERPRINT_SCHEME = "aw3";
364
+ /**
365
+ * Identity is (rule instance, offending locations) — deliberately NOT the message, so
366
+ * improving the wording of a rule's output does not invalidate every baseline entry that
367
+ * rule ever produced. `identity` overrides the locations when a rule knows better.
368
+ */
369
+ declare function fingerprintOf(repoRoot: string, ruleId: string, input: Pick<ViolationInput, "edge" | "module" | "locations" | "identity">): string;
370
+ type SeverityCounts = Record<Severity, number>;
371
+ /** One definition of "how many of each", shared by every consumer that needs counts. */
372
+ declare function countBySeverity(violations: readonly {
373
+ severity: Severity;
374
+ }[]): SeverityCounts;
375
+ /**
376
+ * Total order over violations, so two runs of the same analysis produce byte-identical
377
+ * output. Required by baselines, CI diffing, and snapshot tests; without it, ordering
378
+ * follows rule registration order and each rule's internal scan order, which differs
379
+ * between hosts because module insertion order does.
380
+ */
381
+ declare function compareViolations(a: Violation, b: Violation): number;
382
+ //#endregion
383
+ //#region src/contracts/diagnostic.d.ts
384
+ /** Alias, for readability at use sites — it IS {@link Severity}. */
385
+ type DiagnosticSeverity = Severity;
386
+ /**
387
+ * Stable, machine-matchable identifiers. Open union — adapters and third-party rules may
388
+ * emit their own codes; the ones core emits are enumerated by {@link WellKnownDiagnosticCode}.
389
+ */
390
+ type WellKnownDiagnosticCode =
391
+ /** A rule required host capabilities this run cannot provide, and was skipped. */
392
+ "rule-skipped" |
393
+ /** A rule threw. The run continued; that rule produced no results. */
394
+ "rule-failed" |
395
+ /** Classification tagged nothing — almost always a misconfigured `sourceRoot`. */
396
+ "no-modules-classified" |
397
+ /** The project boundary (`include`/`exclude`) matched no source modules. */
398
+ "empty-project" |
399
+ /** A rule's `scope` narrowed the graph to nothing, so the rule could not report anything. */
400
+ "empty-scope" |
401
+ /** A rule's options failed its `optionsSchema` at config time; the rule did not run. */
402
+ "invalid-rule-options" |
403
+ /** The configuration itself is wrong; see the message. The rule or preset was dropped. */
404
+ "invalid-config" |
405
+ /** A configured rule, or one of its options, is deprecated. */
406
+ "rule-deprecated" |
407
+ /** A graph transform threw. The pipeline continued without its contribution. */
408
+ "transform-failed";
409
+ type DiagnosticCode = WellKnownDiagnosticCode | (string & {});
410
+ /**
411
+ * Everything the run wants to say that is *not* a violation of the user's architecture: a
412
+ * rule that could not run, a rule that crashed, a configuration that looks wrong.
413
+ *
414
+ * A first-class channel because the alternative — an exception — is the wrong shape for
415
+ * "one of your forty rules is broken": it destroys the other thirty-nine results.
416
+ */
417
+ interface Diagnostic {
418
+ code: DiagnosticCode;
419
+ severity: DiagnosticSeverity;
420
+ message: string;
421
+ /** The rule instance this concerns, when it concerns one. */
422
+ ruleId?: string;
423
+ /** Structured payload for machine consumers; shape depends on `code`. */
424
+ details?: Readonly<Record<string, unknown>>;
425
+ }
426
+ /** Payload shape for `code: "rule-skipped"`. */
427
+ interface RuleSkippedDetails {
428
+ missingCapabilities: readonly Capability[];
429
+ host: string;
430
+ }
431
+ /** Payload shape for `code: "empty-scope"`. */
432
+ interface EmptyScopeDetails {
433
+ /** The scope as configured, so the message can be acted on without reopening the config. */
434
+ scope: {
435
+ include?: readonly string[];
436
+ exclude?: readonly string[];
437
+ tag?: Record<string, string>;
438
+ };
439
+ /** Modules in the graph before scoping — the denominator that makes "0" meaningful. */
440
+ totalModules: number;
441
+ }
442
+ //#endregion
443
+ //#region src/graph/query.d.ts
444
+ interface ModuleFilter {
445
+ /** ALL entries must match module tags. */
446
+ tag?: Record<string, string>;
447
+ /**
448
+ * Any listed kind matches. `FIRST_PARTY_KINDS` / `THIRD_PARTY_KINDS` cover the two
449
+ * groupings that are actually meaningful.
450
+ */
451
+ moduleKind?: ModuleKind | readonly ModuleKind[];
452
+ packageName?: string;
453
+ }
454
+ interface EdgeFilter {
455
+ kind?: EdgeKind;
456
+ /** Any listed kind matches, applied to the edge's target. */
457
+ toModuleKind?: ModuleKind | readonly ModuleKind[];
458
+ fromTag?: Record<string, string>;
459
+ toTag?: Record<string, string>;
460
+ /** Tag key; keep edge iff BOTH endpoints have the tag and values differ. */
461
+ crossing?: string;
462
+ }
463
+ /**
464
+ * Stable key for a filter, so the engine can bucket rules that want the same slice of the
465
+ * graph and evaluate that slice once for all of them.
466
+ */
467
+ declare function filterKey(filter: EdgeFilter | ModuleFilter | undefined): string;
468
+ /**
469
+ * The adjacency and attribute indexes over one graph, built LAZILY per axis.
470
+ *
471
+ * One index serves every query over a graph, scoped or not: a scope narrows *which results
472
+ * are returned*, and does not change what the graph contains, so it must never rebuild the
473
+ * index of it.
474
+ *
475
+ * Each axis is built on first use. A run whose rules only walk edges never pays for the
476
+ * tag, kind, and package indexes.
477
+ */
478
+ declare class GraphIndex {
479
+ #private;
480
+ constructor(graph: ProjectGraph);
481
+ outOf(id: ModuleId): readonly Edge[];
482
+ into(id: ModuleId): readonly Edge[];
483
+ byTag(key: string, value: string): readonly ModuleId[];
484
+ byKind(kind: ModuleKind): readonly ModuleNode[];
485
+ byPackage(name: string): readonly ModuleNode[];
486
+ }
487
+ /**
488
+ * A set of modules, with the operations a rule actually performs on one.
489
+ *
490
+ * An interface rather than a class: a selection carries the query it came from, and one
491
+ * built by hand against a different graph would answer edge questions about the wrong one.
492
+ * Only {@link GraphQuery} can produce one.
493
+ */
494
+ interface ModuleSelection extends Iterable<ModuleNode> {
495
+ readonly size: number;
496
+ isEmpty(): boolean;
497
+ toArray(): readonly ModuleNode[];
498
+ ids(): ModuleId[];
499
+ forEach(fn: (m: ModuleNode) => void): void;
500
+ /** Chainable: narrows this selection without going back to the graph. */
501
+ filter(predicate: (m: ModuleNode) => boolean): ModuleSelection;
502
+ edgesOut(filter?: EdgeFilter): readonly Edge[];
503
+ /** The mirror of {@link edgesOut}: edges arriving at any module in this selection. */
504
+ edgesIn(filter?: EdgeFilter): readonly Edge[];
505
+ }
506
+ /**
507
+ * The only sanctioned way to read a graph.
508
+ *
509
+ * A scoped query is a VIEW: it shares the underlying {@link GraphIndex} with the query it
510
+ * came from and differs only in which modules it is *about*.
511
+ *
512
+ * ## What scope does, exactly
513
+ *
514
+ * One rule: **an operation is scoped if and only if it ENUMERATES. An operation that answers a
515
+ * question about a module you named is never scoped.**
516
+ *
517
+ * | Operation | Scoped |
518
+ * |---|---|
519
+ * | `modules`, `moduleIds`, `moduleCount`, `edges` | yes — they enumerate |
520
+ * | `module`, `has`, `tagOf` | no — you named the module |
521
+ * | `edgesOutOf`, `edgesInto` | no — you named the module |
522
+ * | `reachableFrom`, `reaching`, `pathBetween` | no — traversal from a named module |
523
+ * | `ModuleSelection.edgesOut` / `edgesIn` | anchored: endpoints in-selection, edges unfiltered |
524
+ * | `RuleContext.compute` | yes — a computation enumerates |
525
+ *
526
+ * The asymmetry is deliberate rather than incidental. A scoped rule must be able to ask what an
527
+ * out-of-scope import target *is*, because an edge leaving the scope is the most interesting
528
+ * thing it can find; hiding the target would turn `layer-dependencies` under a scope from a
529
+ * finding into silence.
530
+ */
531
+ declare class GraphQuery {
532
+ #private;
533
+ constructor(graph: ProjectGraph, index?: GraphIndex, scope?: ReadonlySet<ModuleId>);
534
+ /** A view of the same graph restricted to `scope`, sharing this query's index. */
535
+ scoped(scope: ReadonlySet<ModuleId>): GraphQuery;
536
+ module(id: ModuleId): ModuleNode | undefined;
537
+ /** Modules in scope, or all of them when unscoped. */
538
+ moduleCount(): number;
539
+ /** Every in-scope module id, in graph order. The traversal primitive. */
540
+ moduleIds(): Iterable<ModuleId>;
541
+ /** Whether the graph contains this module at all — distinct from "is it a source file". */
542
+ has(id: ModuleId): boolean;
543
+ tagOf(id: ModuleId, key: string): string | undefined;
544
+ modules(filter?: ModuleFilter): ModuleSelection;
545
+ /**
546
+ * In-scope edges matching `filter`.
547
+ *
548
+ * Returns the graph's own array when nothing narrows it — it is already immutable, so a
549
+ * defensive copy per call would protect nobody and cost a full edge list per rule.
550
+ */
551
+ edges(filter?: EdgeFilter): readonly Edge[];
552
+ edgesOutOf(id: ModuleId): readonly Edge[];
553
+ edgesInto(id: ModuleId): readonly Edge[];
554
+ /**
555
+ * Every module reachable from `id` by following edges, excluding `id` itself unless a
556
+ * cycle leads back to it. Iterative: a 10k-module chain would overflow the stack.
557
+ */
558
+ reachableFrom(id: ModuleId, filter?: EdgeFilter): ReadonlySet<ModuleId>;
559
+ /** The mirror of {@link reachableFrom}: everything that can reach `id`. */
560
+ reaching(id: ModuleId, filter?: EdgeFilter): ReadonlySet<ModuleId>;
561
+ /**
562
+ * Shortest dependency path from `from` to `to`, inclusive of both, or null. BFS, because
563
+ * the useful evidence for "domain reaches infrastructure" is the shortest chain, not
564
+ * whichever one a traversal happened to find first.
565
+ */
566
+ pathBetween(from: ModuleId, to: ModuleId, filter?: EdgeFilter): readonly ModuleId[] | null;
567
+ /** Applies an {@link EdgeFilter} to an edge list. Returns the input when there is none. */
568
+ filterEdges(edges: readonly Edge[], filter?: EdgeFilter): readonly Edge[];
569
+ /** Whether one edge satisfies a filter. The unit the engine's visitor dispatch uses. */
570
+ matchesEdge(e: Edge, filter: EdgeFilter): boolean;
571
+ /** Whether one module satisfies a filter. Paired with {@link matchesEdge}. */
572
+ matchesModule(m: ModuleNode, filter: ModuleFilter): boolean;
573
+ }
574
+ //#endregion
575
+ //#region src/contracts/analysis.d.ts
576
+ /**
577
+ * A memoized derived value over the graph — an SCC decomposition, a reachability table —
578
+ * computed at most once per run and shared by every rule that asks for it.
579
+ *
580
+ * Named `GraphComputation` rather than `Analysis` because "analysis" already meant four
581
+ * other things: `AnalysisResult` (the run's output), `AnalysisStats`, `AnalysisCache`, and
582
+ * `analyze()`, plus a directory called `analysis/`. Four meanings for one word in one
583
+ * codebase is a thing contributors conflate weekly; the run-output family keeps the name
584
+ * and this — the odd one out — gives it up.
585
+ */
586
+ interface GraphComputation<T> {
587
+ name: string;
588
+ compute(graph: GraphQuery): T;
589
+ }
590
+ declare function defineGraphComputation<T>(computation: GraphComputation<T>): GraphComputation<T>;
591
+ //#endregion
592
+ //#region src/contracts/transform.d.ts
593
+ interface TransformContext {
594
+ /** Absolute source root from resolved config. */
595
+ sourceRoot: string;
596
+ /** Absolute repository root from resolved config. */
597
+ repoRoot: string;
598
+ /** A file's path relative to {@link sourceRoot}, or null when it lies outside. */
599
+ relative(file: string): string | null;
600
+ }
601
+ /**
602
+ * A pass that enriches the graph, between the project boundary and classification.
603
+ *
604
+ * The slot a TypeScript type-edge enricher — or any other "add facts the bundler did not
605
+ * give us" pass — lives in. Ordered after the boundary so a transform sees which modules
606
+ * are actually in the project, and before classification so anything it adds gets tagged
607
+ * like everything else. Modules a transform adds are boundary-checked too: the pipeline
608
+ * runs the boundary again over its contributions rather than trusting them.
609
+ *
610
+ * A transform may also declare capabilities it CONTRIBUTES. A rule requiring `type-edges`
611
+ * should run when a transform supplies them, even though no host does — which is the whole
612
+ * reason capabilities are a set rather than a property of the adapter.
613
+ */
614
+ interface GraphTransform {
615
+ name: string;
616
+ /**
617
+ * Capabilities this transform adds to the graph. Declaring one is a promise that the
618
+ * transform actually produced it; rules requiring it will now run.
619
+ */
620
+ provides?: Capability[];
621
+ /**
622
+ * Writes through {@link GraphMutation} rather than returning a new graph.
623
+ *
624
+ * Graph-in/graph-out meant every third-party transform in existence saw and
625
+ * reconstructed the concrete representation, which froze it — and invited the whole
626
+ * class of bug where a transform rebuilds a graph and silently drops a field it did not
627
+ * know about.
628
+ *
629
+ * A transform that throws is isolated the same way a rule is: reported as a diagnostic,
630
+ * its partial writes discarded, and the pipeline continues — one broken enricher must
631
+ * not destroy the whole run.
632
+ */
633
+ transform(graph: GraphMutation, ctx: TransformContext): void;
634
+ }
635
+ declare function defineTransform(transform: GraphTransform): GraphTransform;
636
+ //#endregion
637
+ export { THIRD_PARTY_KINDS as $, primarySourceLocation as A, GraphDelivery as B, ViolationLocation as C, locationsOf as D, fingerprintOf as E, defineClassifier as F, MODULE_ID_SCHEMES as G, GraphMutation as H, Capability as I, ModuleKind as J, ModuleId as K, Edge as L, Classifier as M, ClassifierContext as N, primaryEdge as O, TagPatch as P, SourceLocation as Q, EdgeKind as R, ViolationInput as S, countBySeverity as T, HostInfo as U, GraphDraft as V, IR_VERSION as W, ProjectGraph as X, ModuleNode as Y, ProjectGraphInit as Z, WellKnownDiagnosticCode as _, defineGraphComputation as a, isFirstParty as at, SeverityCounts as b, GraphQuery as c, filterKey as d, WellKnownCapability as et, Diagnostic as f, RuleSkippedDetails as g, EmptyScopeDetails as h, GraphComputation as i, irMajor as it, renderMessage as j, primaryModule as k, ModuleFilter as l, DiagnosticSeverity as m, TransformContext as n, assertIrCompatible as nt, EdgeFilter as o, isThirdParty as ot, DiagnosticCode as p, ModuleIdScheme as q, defineTransform as r, displayModuleId as rt, GraphIndex as s, parseModuleId as st, GraphTransform as t, WellKnownEdgeKind as tt, ModuleSelection as u, FINGERPRINT_SCHEME as v, compareViolations as w, Violation as x, Severity as y, FIRST_PARTY_KINDS as z };
638
+ //# sourceMappingURL=transform-CnUPOO0E.d.mts.map