@archwall/core 0.1.0 → 0.2.1

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.
@@ -54,7 +54,16 @@ type WellKnownCapability =
54
54
  * that matches on specifiers must require this, or it silently matches nothing on hosts
55
55
  * that cannot supply them and reports a clean run rather than an unavailable one.
56
56
  */
57
- "raw-specifiers";
57
+ "raw-specifiers" |
58
+ /**
59
+ * Type-only edges are PRESENT and carry `attributes.typeOnly`. See {@link EdgeAttributes}.
60
+ *
61
+ * A rule that treats type-only imports differently must require this. Without it, a host
62
+ * that erases type imports (every bundler does) is indistinguishable from one where the
63
+ * code genuinely has no type imports — and "no `attributes.typeOnly` anywhere" would be
64
+ * read as "nothing is type-only" rather than "nobody asked".
65
+ */
66
+ "type-only-edges";
58
67
  /**
59
68
  * Open union: adapters may declare capabilities core does not know about, and rules may
60
69
  * require them, without an IR major. `WellKnownCapability` keeps autocomplete useful for
@@ -75,11 +84,44 @@ interface SourceLocation {
75
84
  }
76
85
  type WellKnownEdgeKind = "static" | "dynamic" | "reexport";
77
86
  /**
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.
87
+ * Open union so future graph facts (CSS imports, worker edges) arrive additively. Consumers
88
+ * must treat an unrecognised kind as "some dependency exists" — never assume exhaustiveness.
89
+ *
90
+ * `kind` answers ONE question — roughly "what syntax produced this edge" — and deliberately
91
+ * keeps answering only that. Everything else an edge might be belongs in
92
+ * {@link EdgeAttributes}; see the note there for why.
81
93
  */
82
94
  type EdgeKind = WellKnownEdgeKind | (string & {});
95
+ /**
96
+ * Orthogonal facts about an edge, as an OPEN bag.
97
+ *
98
+ * `kind` is one enum, but the domain is not one-dimensional. `export type * from "./x"` is
99
+ * *both* a re-export and type-only; a dynamic import of a barrel is both dynamic and a
100
+ * re-export. Modelling those as one enum forces every producer to pick a winner, and forces
101
+ * every consumer to guess which axis the winner came from.
102
+ *
103
+ * So the axes split: `kind` keeps the syntactic one, and everything else lands here, where it
104
+ * composes. A bag rather than named fields because the next axis is always unknown — import
105
+ * attributes (`with { type: "json" }`), worker edges, CSS edges, `require` vs `import`
106
+ * interop — and each one arriving as a new top-level `Edge` field would be a new IR major.
107
+ *
108
+ * **Absent means "the host did not say", never "false".** That distinction is the whole
109
+ * reason {@link WellKnownCapability} has `type-only-edges`: a bundler that erased type imports
110
+ * before ArchWall saw them reports nothing here, and a rule must be able to tell that apart
111
+ * from a codebase with no type imports in it.
112
+ */
113
+ interface EdgeAttributes {
114
+ /**
115
+ * The import is erased at compile time (`import type`, `export type`, or an
116
+ * `import { type X }` specifier where every named binding is type-only).
117
+ *
118
+ * Requires the `type-only-edges` capability to be meaningful. `true` or absent — never
119
+ * `false`, so that a producer cannot accidentally assert the negative it does not know.
120
+ */
121
+ typeOnly?: true;
122
+ /** Third parties and future core versions extend here without an IR major. */
123
+ [key: string]: string | true | undefined;
124
+ }
83
125
  /**
84
126
  * What a module *is*, relative to the project being analysed.
85
127
  *
@@ -129,6 +171,8 @@ interface Edge {
129
171
  kind: EdgeKind;
130
172
  /** Present only if host capability allows. */
131
173
  loc?: SourceLocation;
174
+ /** Orthogonal facts; see {@link EdgeAttributes}. Absent when the host reported none. */
175
+ attributes?: EdgeAttributes;
132
176
  }
133
177
  type GraphDelivery = "complete" | "progressive";
134
178
  interface ProjectGraphInit {
@@ -139,6 +183,14 @@ interface ProjectGraphInit {
139
183
  edges: readonly Edge[];
140
184
  /** Default {@link IR_VERSION}. Adapters should leave this alone. */
141
185
  irVersion?: string;
186
+ /**
187
+ * Opaque identity for this graph. Defaults to a fresh process-local number.
188
+ *
189
+ * The contract is one-directional and deliberately weak: **equal revisions mean the same
190
+ * graph; unequal revisions mean nothing.** Set it explicitly only if you can guarantee the
191
+ * first half — a producer that content-hashes its inputs, for instance.
192
+ */
193
+ revision?: number;
142
194
  }
143
195
  /**
144
196
  * The module graph, as an OPAQUE handle.
@@ -155,6 +207,12 @@ declare class ProjectGraph {
155
207
  readonly irVersion: string;
156
208
  readonly host: HostInfo;
157
209
  readonly delivery: GraphDelivery;
210
+ /**
211
+ * See {@link ProjectGraphInit.revision}. Preserved across {@link replaceStores}, because a
212
+ * derived graph is a deterministic function of this one and the config that derived it —
213
+ * so a cache keyed on `(revision, configKey)` stays sound through the prepare pipeline.
214
+ */
215
+ readonly revision: number;
158
216
  private constructor();
159
217
  static create(init: ProjectGraphInit): ProjectGraph;
160
218
  get moduleCount(): number;
@@ -261,8 +319,6 @@ type Severity = "error" | "warn" | "info";
261
319
  * offending location — it has N of them, and the old model could only name one and had to
262
320
  * serialise the rest into the message string. A finding about a package, a directory, or
263
321
  * the configuration has no module at all.
264
- *
265
- * See docs/adr/0004-violation-locations.md.
266
322
  */
267
323
  type ViolationLocation = {
268
324
  type: "edge";
@@ -355,8 +411,7 @@ declare function renderMessage(template: string, data: Readonly<Record<string, s
355
411
  * Fingerprint scheme version. Bump when the algorithm changes so that a stale baseline
356
412
  * ERRORS instead of silently mismatching every entry.
357
413
  *
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
414
+ * `aw3` is the first scheme over canonical module ids. Before it, a violation about `react` hashed the
360
415
  * host's own id — a resolved `node_modules` path under the CLI, the bare specifier under esbuild
361
416
  * — so the same finding fingerprinted differently under two bundlers.
362
417
  */
@@ -405,7 +460,24 @@ type WellKnownDiagnosticCode =
405
460
  /** A configured rule, or one of its options, is deprecated. */
406
461
  "rule-deprecated" |
407
462
  /** A graph transform threw. The pipeline continued without its contribution. */
408
- "transform-failed";
463
+ "transform-failed" |
464
+ /**
465
+ * Files are inside the project boundary that the producer cannot read, so they are absent
466
+ * from the graph entirely — not excluded, not unresolved, just invisible.
467
+ *
468
+ * Only a producer that ENUMERATES a directory tree can detect this, which in practice means
469
+ * the CLI: a bundler adapter is handed a graph whose membership its compiler already decided.
470
+ */
471
+ "unscannable-files" |
472
+ /**
473
+ * The baseline lists violations this run did not produce — they were fixed, or the code
474
+ * they were about is gone.
475
+ *
476
+ * Worth saying out loud rather than tolerating: a baseline that is never pruned stops being
477
+ * "debt we accepted" and becomes a permanent hole, and the stale entry will silently
478
+ * re-suppress the finding if it ever comes back.
479
+ */
480
+ "baseline-stale";
409
481
  type DiagnosticCode = WellKnownDiagnosticCode | (string & {});
410
482
  /**
411
483
  * Everything the run wants to say that is *not* a violation of the user's architecture: a
@@ -428,6 +500,15 @@ interface RuleSkippedDetails {
428
500
  missingCapabilities: readonly Capability[];
429
501
  host: string;
430
502
  }
503
+ /** Payload shape for `code: "unscannable-files"`. */
504
+ interface UnscannableFilesDetails {
505
+ /** How many in-boundary files the producer could not read. */
506
+ count: number;
507
+ /** Distinct extensions, most common first — what to act on. */
508
+ extensions: readonly string[];
509
+ /** A bounded sample of repo-relative paths, for a message a human can follow. */
510
+ sample: readonly string[];
511
+ }
431
512
  /** Payload shape for `code: "empty-scope"`. */
432
513
  interface EmptyScopeDetails {
433
514
  /** The scope as configured, so the message can be acted on without reopening the config. */
@@ -459,6 +540,16 @@ interface EdgeFilter {
459
540
  toTag?: Record<string, string>;
460
541
  /** Tag key; keep edge iff BOTH endpoints have the tag and values differ. */
461
542
  crossing?: string;
543
+ /**
544
+ * Selects on {@link EdgeAttributes}. `true` requires the attribute present; `false` requires
545
+ * it ABSENT; a string requires that exact value.
546
+ *
547
+ * `false` and "absent" are the same test on purpose — attributes are never stored as `false`
548
+ * (see {@link EdgeAttributes}), so "not type-only" and "nobody said" are indistinguishable
549
+ * *here* by construction. A rule that must tell them apart declares the corresponding
550
+ * capability and gets skipped loudly instead, which is the only honest answer.
551
+ */
552
+ attributes?: Readonly<Record<string, string | boolean>>;
462
553
  }
463
554
  /**
464
555
  * Stable key for a filter, so the engine can bucket rules that want the same slice of the
@@ -504,7 +595,41 @@ interface ModuleSelection extends Iterable<ModuleNode> {
504
595
  edgesIn(filter?: EdgeFilter): readonly Edge[];
505
596
  }
506
597
  /**
507
- * The only sanctioned way to read a graph.
598
+ * The read surface a rule gets and the type it should name.
599
+ *
600
+ * An INTERFACE rather than the class, because the two are different promises. What ArchWall
601
+ * owes a rule author is a set of questions that can be asked about a graph; what it must stay
602
+ * free to change is how those questions are answered. Naming the class in `RuleContext` fused
603
+ * the two: the concrete implementation became observable via `instanceof`, a test double became
604
+ * impossible to supply, and an interned or columnar store became a breaking change rather than
605
+ * an optimisation.
606
+ *
607
+ * {@link GraphQuery} is the only implementation core ships, and it lives in
608
+ * `@archwall/core/internal`. Rules never construct one — they are handed one — so nothing is
609
+ * taken away by that; a rule author who needs one for a TEST gets it from
610
+ * `@archwall/test-utils`, which is the supported way to build a graph by hand.
611
+ *
612
+ * See {@link GraphQuery} for what scope does to each of these operations.
613
+ */
614
+ interface GraphView {
615
+ module(id: ModuleId): ModuleNode | undefined;
616
+ moduleCount(): number;
617
+ moduleIds(): Iterable<ModuleId>;
618
+ has(id: ModuleId): boolean;
619
+ tagOf(id: ModuleId, key: string): string | undefined;
620
+ modules(filter?: ModuleFilter): ModuleSelection;
621
+ edges(filter?: EdgeFilter): readonly Edge[];
622
+ edgesOutOf(id: ModuleId): readonly Edge[];
623
+ edgesInto(id: ModuleId): readonly Edge[];
624
+ reachableFrom(id: ModuleId, filter?: EdgeFilter): ReadonlySet<ModuleId>;
625
+ reaching(id: ModuleId, filter?: EdgeFilter): ReadonlySet<ModuleId>;
626
+ pathBetween(from: ModuleId, to: ModuleId, filter?: EdgeFilter): readonly ModuleId[] | null;
627
+ filterEdges(edges: readonly Edge[], filter?: EdgeFilter): readonly Edge[];
628
+ matchesEdge(e: Edge, filter: EdgeFilter): boolean;
629
+ matchesModule(m: ModuleNode, filter: ModuleFilter): boolean;
630
+ }
631
+ /**
632
+ * The only sanctioned way to read a graph; the sole implementation of {@link GraphView}.
508
633
  *
509
634
  * A scoped query is a VIEW: it shares the underlying {@link GraphIndex} with the query it
510
635
  * came from and differs only in which modules it is *about*.
@@ -528,7 +653,7 @@ interface ModuleSelection extends Iterable<ModuleNode> {
528
653
  * thing it can find; hiding the target would turn `layer-dependencies` under a scope from a
529
654
  * finding into silence.
530
655
  */
531
- declare class GraphQuery {
656
+ declare class GraphQuery implements GraphView {
532
657
  #private;
533
658
  constructor(graph: ProjectGraph, index?: GraphIndex, scope?: ReadonlySet<ModuleId>);
534
659
  /** A view of the same graph restricted to `scope`, sharing this query's index. */
@@ -585,7 +710,7 @@ declare class GraphQuery {
585
710
  */
586
711
  interface GraphComputation<T> {
587
712
  name: string;
588
- compute(graph: GraphQuery): T;
713
+ compute(graph: GraphView): T;
589
714
  }
590
715
  declare function defineGraphComputation<T>(computation: GraphComputation<T>): GraphComputation<T>;
591
716
  //#endregion
@@ -634,5 +759,5 @@ interface GraphTransform {
634
759
  }
635
760
  declare function defineTransform(transform: GraphTransform): GraphTransform;
636
761
  //#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
762
+ export { ProjectGraph as $, primaryEdge as A, EdgeAttributes as B, Violation as C, countBySeverity as D, compareViolations as E, ClassifierContext as F, GraphMutation as G, FIRST_PARTY_KINDS as H, TagPatch as I, MODULE_ID_SCHEMES as J, HostInfo as K, defineClassifier as L, primarySourceLocation as M, renderMessage as N, fingerprintOf as O, Classifier as P, ModuleNode as Q, Capability as R, SeverityCounts as S, ViolationLocation as T, GraphDelivery as U, EdgeKind as V, GraphDraft as W, ModuleIdScheme as X, ModuleId as Y, ModuleKind as Z, RuleSkippedDetails as _, defineGraphComputation as a, assertIrCompatible as at, FINGERPRINT_SCHEME as b, GraphQuery as c, isFirstParty as ct, ModuleSelection as d, ProjectGraphInit as et, filterKey as f, EmptyScopeDetails as g, DiagnosticSeverity as h, GraphComputation as i, WellKnownEdgeKind as it, primaryModule as j, locationsOf as k, GraphView as l, isThirdParty as lt, DiagnosticCode as m, TransformContext as n, THIRD_PARTY_KINDS as nt, EdgeFilter as o, displayModuleId as ot, Diagnostic as p, IR_VERSION as q, defineTransform as r, WellKnownCapability as rt, GraphIndex as s, irMajor as st, GraphTransform as t, SourceLocation as tt, ModuleFilter as u, parseModuleId as ut, UnscannableFilesDetails as v, ViolationInput as w, Severity as x, WellKnownDiagnosticCode as y, Edge as z };
763
+ //# sourceMappingURL=transform-CzxyWbUC.d.cts.map
@@ -54,7 +54,16 @@ type WellKnownCapability =
54
54
  * that matches on specifiers must require this, or it silently matches nothing on hosts
55
55
  * that cannot supply them and reports a clean run rather than an unavailable one.
56
56
  */
57
- "raw-specifiers";
57
+ "raw-specifiers" |
58
+ /**
59
+ * Type-only edges are PRESENT and carry `attributes.typeOnly`. See {@link EdgeAttributes}.
60
+ *
61
+ * A rule that treats type-only imports differently must require this. Without it, a host
62
+ * that erases type imports (every bundler does) is indistinguishable from one where the
63
+ * code genuinely has no type imports — and "no `attributes.typeOnly` anywhere" would be
64
+ * read as "nothing is type-only" rather than "nobody asked".
65
+ */
66
+ "type-only-edges";
58
67
  /**
59
68
  * Open union: adapters may declare capabilities core does not know about, and rules may
60
69
  * require them, without an IR major. `WellKnownCapability` keeps autocomplete useful for
@@ -75,11 +84,44 @@ interface SourceLocation {
75
84
  }
76
85
  type WellKnownEdgeKind = "static" | "dynamic" | "reexport";
77
86
  /**
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.
87
+ * Open union so future graph facts (CSS imports, worker edges) arrive additively. Consumers
88
+ * must treat an unrecognised kind as "some dependency exists" — never assume exhaustiveness.
89
+ *
90
+ * `kind` answers ONE question — roughly "what syntax produced this edge" — and deliberately
91
+ * keeps answering only that. Everything else an edge might be belongs in
92
+ * {@link EdgeAttributes}; see the note there for why.
81
93
  */
82
94
  type EdgeKind = WellKnownEdgeKind | (string & {});
95
+ /**
96
+ * Orthogonal facts about an edge, as an OPEN bag.
97
+ *
98
+ * `kind` is one enum, but the domain is not one-dimensional. `export type * from "./x"` is
99
+ * *both* a re-export and type-only; a dynamic import of a barrel is both dynamic and a
100
+ * re-export. Modelling those as one enum forces every producer to pick a winner, and forces
101
+ * every consumer to guess which axis the winner came from.
102
+ *
103
+ * So the axes split: `kind` keeps the syntactic one, and everything else lands here, where it
104
+ * composes. A bag rather than named fields because the next axis is always unknown — import
105
+ * attributes (`with { type: "json" }`), worker edges, CSS edges, `require` vs `import`
106
+ * interop — and each one arriving as a new top-level `Edge` field would be a new IR major.
107
+ *
108
+ * **Absent means "the host did not say", never "false".** That distinction is the whole
109
+ * reason {@link WellKnownCapability} has `type-only-edges`: a bundler that erased type imports
110
+ * before ArchWall saw them reports nothing here, and a rule must be able to tell that apart
111
+ * from a codebase with no type imports in it.
112
+ */
113
+ interface EdgeAttributes {
114
+ /**
115
+ * The import is erased at compile time (`import type`, `export type`, or an
116
+ * `import { type X }` specifier where every named binding is type-only).
117
+ *
118
+ * Requires the `type-only-edges` capability to be meaningful. `true` or absent — never
119
+ * `false`, so that a producer cannot accidentally assert the negative it does not know.
120
+ */
121
+ typeOnly?: true;
122
+ /** Third parties and future core versions extend here without an IR major. */
123
+ [key: string]: string | true | undefined;
124
+ }
83
125
  /**
84
126
  * What a module *is*, relative to the project being analysed.
85
127
  *
@@ -129,6 +171,8 @@ interface Edge {
129
171
  kind: EdgeKind;
130
172
  /** Present only if host capability allows. */
131
173
  loc?: SourceLocation;
174
+ /** Orthogonal facts; see {@link EdgeAttributes}. Absent when the host reported none. */
175
+ attributes?: EdgeAttributes;
132
176
  }
133
177
  type GraphDelivery = "complete" | "progressive";
134
178
  interface ProjectGraphInit {
@@ -139,6 +183,14 @@ interface ProjectGraphInit {
139
183
  edges: readonly Edge[];
140
184
  /** Default {@link IR_VERSION}. Adapters should leave this alone. */
141
185
  irVersion?: string;
186
+ /**
187
+ * Opaque identity for this graph. Defaults to a fresh process-local number.
188
+ *
189
+ * The contract is one-directional and deliberately weak: **equal revisions mean the same
190
+ * graph; unequal revisions mean nothing.** Set it explicitly only if you can guarantee the
191
+ * first half — a producer that content-hashes its inputs, for instance.
192
+ */
193
+ revision?: number;
142
194
  }
143
195
  /**
144
196
  * The module graph, as an OPAQUE handle.
@@ -155,6 +207,12 @@ declare class ProjectGraph {
155
207
  readonly irVersion: string;
156
208
  readonly host: HostInfo;
157
209
  readonly delivery: GraphDelivery;
210
+ /**
211
+ * See {@link ProjectGraphInit.revision}. Preserved across {@link replaceStores}, because a
212
+ * derived graph is a deterministic function of this one and the config that derived it —
213
+ * so a cache keyed on `(revision, configKey)` stays sound through the prepare pipeline.
214
+ */
215
+ readonly revision: number;
158
216
  private constructor();
159
217
  static create(init: ProjectGraphInit): ProjectGraph;
160
218
  get moduleCount(): number;
@@ -261,8 +319,6 @@ type Severity = "error" | "warn" | "info";
261
319
  * offending location — it has N of them, and the old model could only name one and had to
262
320
  * serialise the rest into the message string. A finding about a package, a directory, or
263
321
  * the configuration has no module at all.
264
- *
265
- * See docs/adr/0004-violation-locations.md.
266
322
  */
267
323
  type ViolationLocation = {
268
324
  type: "edge";
@@ -355,8 +411,7 @@ declare function renderMessage(template: string, data: Readonly<Record<string, s
355
411
  * Fingerprint scheme version. Bump when the algorithm changes so that a stale baseline
356
412
  * ERRORS instead of silently mismatching every entry.
357
413
  *
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
414
+ * `aw3` is the first scheme over canonical module ids. Before it, a violation about `react` hashed the
360
415
  * host's own id — a resolved `node_modules` path under the CLI, the bare specifier under esbuild
361
416
  * — so the same finding fingerprinted differently under two bundlers.
362
417
  */
@@ -405,7 +460,24 @@ type WellKnownDiagnosticCode =
405
460
  /** A configured rule, or one of its options, is deprecated. */
406
461
  "rule-deprecated" |
407
462
  /** A graph transform threw. The pipeline continued without its contribution. */
408
- "transform-failed";
463
+ "transform-failed" |
464
+ /**
465
+ * Files are inside the project boundary that the producer cannot read, so they are absent
466
+ * from the graph entirely — not excluded, not unresolved, just invisible.
467
+ *
468
+ * Only a producer that ENUMERATES a directory tree can detect this, which in practice means
469
+ * the CLI: a bundler adapter is handed a graph whose membership its compiler already decided.
470
+ */
471
+ "unscannable-files" |
472
+ /**
473
+ * The baseline lists violations this run did not produce — they were fixed, or the code
474
+ * they were about is gone.
475
+ *
476
+ * Worth saying out loud rather than tolerating: a baseline that is never pruned stops being
477
+ * "debt we accepted" and becomes a permanent hole, and the stale entry will silently
478
+ * re-suppress the finding if it ever comes back.
479
+ */
480
+ "baseline-stale";
409
481
  type DiagnosticCode = WellKnownDiagnosticCode | (string & {});
410
482
  /**
411
483
  * Everything the run wants to say that is *not* a violation of the user's architecture: a
@@ -428,6 +500,15 @@ interface RuleSkippedDetails {
428
500
  missingCapabilities: readonly Capability[];
429
501
  host: string;
430
502
  }
503
+ /** Payload shape for `code: "unscannable-files"`. */
504
+ interface UnscannableFilesDetails {
505
+ /** How many in-boundary files the producer could not read. */
506
+ count: number;
507
+ /** Distinct extensions, most common first — what to act on. */
508
+ extensions: readonly string[];
509
+ /** A bounded sample of repo-relative paths, for a message a human can follow. */
510
+ sample: readonly string[];
511
+ }
431
512
  /** Payload shape for `code: "empty-scope"`. */
432
513
  interface EmptyScopeDetails {
433
514
  /** The scope as configured, so the message can be acted on without reopening the config. */
@@ -459,6 +540,16 @@ interface EdgeFilter {
459
540
  toTag?: Record<string, string>;
460
541
  /** Tag key; keep edge iff BOTH endpoints have the tag and values differ. */
461
542
  crossing?: string;
543
+ /**
544
+ * Selects on {@link EdgeAttributes}. `true` requires the attribute present; `false` requires
545
+ * it ABSENT; a string requires that exact value.
546
+ *
547
+ * `false` and "absent" are the same test on purpose — attributes are never stored as `false`
548
+ * (see {@link EdgeAttributes}), so "not type-only" and "nobody said" are indistinguishable
549
+ * *here* by construction. A rule that must tell them apart declares the corresponding
550
+ * capability and gets skipped loudly instead, which is the only honest answer.
551
+ */
552
+ attributes?: Readonly<Record<string, string | boolean>>;
462
553
  }
463
554
  /**
464
555
  * Stable key for a filter, so the engine can bucket rules that want the same slice of the
@@ -504,7 +595,41 @@ interface ModuleSelection extends Iterable<ModuleNode> {
504
595
  edgesIn(filter?: EdgeFilter): readonly Edge[];
505
596
  }
506
597
  /**
507
- * The only sanctioned way to read a graph.
598
+ * The read surface a rule gets and the type it should name.
599
+ *
600
+ * An INTERFACE rather than the class, because the two are different promises. What ArchWall
601
+ * owes a rule author is a set of questions that can be asked about a graph; what it must stay
602
+ * free to change is how those questions are answered. Naming the class in `RuleContext` fused
603
+ * the two: the concrete implementation became observable via `instanceof`, a test double became
604
+ * impossible to supply, and an interned or columnar store became a breaking change rather than
605
+ * an optimisation.
606
+ *
607
+ * {@link GraphQuery} is the only implementation core ships, and it lives in
608
+ * `@archwall/core/internal`. Rules never construct one — they are handed one — so nothing is
609
+ * taken away by that; a rule author who needs one for a TEST gets it from
610
+ * `@archwall/test-utils`, which is the supported way to build a graph by hand.
611
+ *
612
+ * See {@link GraphQuery} for what scope does to each of these operations.
613
+ */
614
+ interface GraphView {
615
+ module(id: ModuleId): ModuleNode | undefined;
616
+ moduleCount(): number;
617
+ moduleIds(): Iterable<ModuleId>;
618
+ has(id: ModuleId): boolean;
619
+ tagOf(id: ModuleId, key: string): string | undefined;
620
+ modules(filter?: ModuleFilter): ModuleSelection;
621
+ edges(filter?: EdgeFilter): readonly Edge[];
622
+ edgesOutOf(id: ModuleId): readonly Edge[];
623
+ edgesInto(id: ModuleId): readonly Edge[];
624
+ reachableFrom(id: ModuleId, filter?: EdgeFilter): ReadonlySet<ModuleId>;
625
+ reaching(id: ModuleId, filter?: EdgeFilter): ReadonlySet<ModuleId>;
626
+ pathBetween(from: ModuleId, to: ModuleId, filter?: EdgeFilter): readonly ModuleId[] | null;
627
+ filterEdges(edges: readonly Edge[], filter?: EdgeFilter): readonly Edge[];
628
+ matchesEdge(e: Edge, filter: EdgeFilter): boolean;
629
+ matchesModule(m: ModuleNode, filter: ModuleFilter): boolean;
630
+ }
631
+ /**
632
+ * The only sanctioned way to read a graph; the sole implementation of {@link GraphView}.
508
633
  *
509
634
  * A scoped query is a VIEW: it shares the underlying {@link GraphIndex} with the query it
510
635
  * came from and differs only in which modules it is *about*.
@@ -528,7 +653,7 @@ interface ModuleSelection extends Iterable<ModuleNode> {
528
653
  * thing it can find; hiding the target would turn `layer-dependencies` under a scope from a
529
654
  * finding into silence.
530
655
  */
531
- declare class GraphQuery {
656
+ declare class GraphQuery implements GraphView {
532
657
  #private;
533
658
  constructor(graph: ProjectGraph, index?: GraphIndex, scope?: ReadonlySet<ModuleId>);
534
659
  /** A view of the same graph restricted to `scope`, sharing this query's index. */
@@ -585,7 +710,7 @@ declare class GraphQuery {
585
710
  */
586
711
  interface GraphComputation<T> {
587
712
  name: string;
588
- compute(graph: GraphQuery): T;
713
+ compute(graph: GraphView): T;
589
714
  }
590
715
  declare function defineGraphComputation<T>(computation: GraphComputation<T>): GraphComputation<T>;
591
716
  //#endregion
@@ -634,5 +759,5 @@ interface GraphTransform {
634
759
  }
635
760
  declare function defineTransform(transform: GraphTransform): GraphTransform;
636
761
  //#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.cts.map
762
+ export { ProjectGraph as $, primaryEdge as A, EdgeAttributes as B, Violation as C, countBySeverity as D, compareViolations as E, ClassifierContext as F, GraphMutation as G, FIRST_PARTY_KINDS as H, TagPatch as I, MODULE_ID_SCHEMES as J, HostInfo as K, defineClassifier as L, primarySourceLocation as M, renderMessage as N, fingerprintOf as O, Classifier as P, ModuleNode as Q, Capability as R, SeverityCounts as S, ViolationLocation as T, GraphDelivery as U, EdgeKind as V, GraphDraft as W, ModuleIdScheme as X, ModuleId as Y, ModuleKind as Z, RuleSkippedDetails as _, defineGraphComputation as a, assertIrCompatible as at, FINGERPRINT_SCHEME as b, GraphQuery as c, isFirstParty as ct, ModuleSelection as d, ProjectGraphInit as et, filterKey as f, EmptyScopeDetails as g, DiagnosticSeverity as h, GraphComputation as i, WellKnownEdgeKind as it, primaryModule as j, locationsOf as k, GraphView as l, isThirdParty as lt, DiagnosticCode as m, TransformContext as n, THIRD_PARTY_KINDS as nt, EdgeFilter as o, displayModuleId as ot, Diagnostic as p, IR_VERSION as q, defineTransform as r, WellKnownCapability as rt, GraphIndex as s, irMajor as st, GraphTransform as t, SourceLocation as tt, ModuleFilter as u, parseModuleId as ut, UnscannableFilesDetails as v, ViolationInput as w, Severity as x, WellKnownDiagnosticCode as y, Edge as z };
763
+ //# sourceMappingURL=transform-CzxyWbUC.d.mts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@archwall/core",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "description": "Graph IR, analysis engine, and rule/preset/reporter contracts for ArchWall.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -27,8 +27,14 @@
27
27
  }
28
28
  },
29
29
  "exports": {
30
- ".": "./src/index.ts",
31
- "./internal": "./src/internal.ts",
30
+ ".": {
31
+ "import": "./dist/index.mjs",
32
+ "require": "./dist/index.cjs"
33
+ },
34
+ "./internal": {
35
+ "import": "./dist/internal.mjs",
36
+ "require": "./dist/internal.cjs"
37
+ },
32
38
  "./package.json": "./package.json"
33
39
  },
34
40
  "publishConfig": {
@@ -51,4 +57,4 @@
51
57
  "devDependencies": {
52
58
  "@types/picomatch": "^4.0.3"
53
59
  }
54
- }
60
+ }