@variance-authority/core 0.5.10 → 0.6.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.
@@ -8,14 +8,15 @@
8
8
  * without touching a disk. `core` performs no I/O (ADR-0006); whoever owns the
9
9
  * disk produces these and hands them over.
10
10
  *
11
- * ## An empty record and an unreadable one are different facts
11
+ * ## An unreadable file contributes what was read of it
12
12
  *
13
- * A file with no imports and a file nothing could parse both produce a record
14
- * with no edges, and treating them alike is the one mistake that turns this from
15
- * a saving into a silent hole: the second one may import anything, including
16
- * whatever just changed. So `unknown` carries the *reason* the edges are not
17
- * known, and every consumer seeds its traversal with those files (ADR-0002 —
18
- * absent is not empty, and here absent is not even absent, it is *unread*).
13
+ * A `require(name)` with a variable for a name, an `import('./' + page)`, a
14
+ * parse that did not finish: the edges a reader could see are recorded, and the
15
+ * one it could not is not guessed at. `unknown` carries the sentence saying why,
16
+ * for whoever reports on the scan. It widens no walk. Seeding every such file as
17
+ * though it had changed turns one computed `require` into a whole run on every
18
+ * diff, forever, and the edge it hides is exactly what a recorded run sees: the
19
+ * module loaded, whatever expression named it.
19
20
  */
20
21
  import { idOf, nodeAt, relationsOf, } from './graph.js';
21
22
  import { dependenciesOf, dependentsOf, trailOf } from './reach.js';
@@ -66,13 +67,11 @@ export function relationsOfFiles(records, options = {}) {
66
67
  return relationsOf({ relations, isolated, unknown, ...(options.shadows === undefined ? {} : { shadows: options.shadows }) });
67
68
  }
68
69
  /**
69
- * What a set of changed files could have moved.
70
+ * What a set of changed files could affect.
70
71
  *
71
- * The seed set is the changed files **and every file whose edges are unknown**,
72
- * which is the rule the whole structure rests on. An unreadable file might import
73
- * the one that changed; seeding it means everything depending on *it* is observed
74
- * too, and the cost is a collection rather than a green run over an unwatched
75
- * surface.
72
+ * The seed set is the changed files and nothing else. A file whose edges could
73
+ * not all be read is walked through the edges that were; the one nobody saw is
74
+ * a recorded run's to answer, not a reason to seed the file on every diff.
76
75
  *
77
76
  * A seed is a path, or a node naming its own kind. The second form is how a
78
77
  * changed *package* enters: a bare string keeps meaning a file, because that is
@@ -85,7 +84,7 @@ export function relationsOfFiles(records, options = {}) {
85
84
  * One breadth-first search, whatever the number of changed files, over the
86
85
  * runtime edges unless the caller names others.
87
86
  */
88
- export function movedBy(relations, changed, options = {}) {
87
+ export function affectedBy(relations, changed, options = {}) {
89
88
  const seeds = [];
90
89
  const missing = [];
91
90
  for (const entry of changed) {
@@ -96,17 +95,9 @@ export function movedBy(relations, changed, options = {}) {
96
95
  else
97
96
  seeds.push(id);
98
97
  }
99
- const opaque = [];
100
- for (let id = 0; id < relations.unknown.length; id += 1) {
101
- if (relations.unknown[id] !== 1)
102
- continue;
103
- seeds.push(id);
104
- const because = relations.reasons.get(id);
105
- opaque.push({ file: relations.names[id], ...(because === undefined ? {} : { because }) });
106
- }
107
- const reach = unshadowed(relations, seeds, options);
98
+ const traversal = unshadowed(relations, seeds, options);
108
99
  const found = { file: [], component: [], package: [] };
109
- for (const id of reach.reached) {
100
+ for (const id of traversal.nodes) {
110
101
  const node = nodeAt(relations, id);
111
102
  if (node === undefined)
112
103
  continue;
@@ -117,31 +108,30 @@ export function movedBy(relations, changed, options = {}) {
117
108
  components: found.component,
118
109
  packages: found.package,
119
110
  missing: missing.sort(byCodeUnit),
120
- opaque,
121
- shadowed: reach.shadowed,
122
- reach,
111
+ shadowed: traversal.shadowed,
112
+ traversal,
123
113
  };
124
114
  }
125
115
  /**
126
116
  * The walk against the arrows with every file left out whose own shadows cut
127
- * every trail to it, and with nothing reached through such a file.
117
+ * every trail to it, and with nothing visited through such a file.
128
118
  *
129
119
  * The walk records one parent per node, so it cannot say whether *another*
130
120
  * trail avoided the shadows. Asking the other way can: a walk along the arrows
131
121
  * from the file, entering none of its shadows, either finds a seed or does not.
132
- * It runs only for a reached file whose row names a reached node — a mock of
122
+ * It runs only for a visited file whose row names a visited node — a mock of
133
123
  * something the change never touched cuts nothing, and costs nothing. A file
134
124
  * found shadowed is then avoided and the walk repeated, so a component or a
135
- * file reached only through it is not reached either; the repeat ends when a
125
+ * file visited only through it is not visited either; the repeat ends when a
136
126
  * walk finds no new shadowed file, and a row once decided is not asked again.
137
127
  */
138
128
  function unshadowed(relations, seeds, options) {
139
129
  const shadowed = [];
140
130
  const avoid = new Set(options.avoid ?? []);
141
131
  const shadows = options.shadows ?? relations.shadows;
142
- let reach = dependentsOf(relations, seeds, options);
132
+ let traversal = dependentsOf(relations, seeds, options);
143
133
  if (shadows.size === 0)
144
- return { ...reach, shadowed };
134
+ return { ...traversal, shadowed };
145
135
  const seeded = new Uint8Array(relations.names.length);
146
136
  for (const seed of seeds)
147
137
  seeded[seed] = 1;
@@ -150,19 +140,19 @@ function unshadowed(relations, seeds, options) {
150
140
  let found = 0;
151
141
  for (const [name, row] of shadows) {
152
142
  const id = idOf(relations, 'file', name);
153
- if (id === undefined || reach.mask[id] !== 1 || seeded[id] === 1 || decided.has(id))
143
+ if (id === undefined || traversal.mask[id] !== 1 || seeded[id] === 1 || decided.has(id))
154
144
  continue;
155
145
  const cut = [];
156
146
  for (const shadow of row) {
157
147
  const target = idOf(relations, 'file', shadow);
158
- if (target !== undefined && reach.mask[target] === 1)
148
+ if (target !== undefined && traversal.mask[target] === 1)
159
149
  cut.push(target);
160
150
  }
161
151
  if (cut.length === 0)
162
152
  continue;
163
153
  decided.add(id);
164
154
  const forward = dependenciesOf(relations, [id], { ...options, avoid: cut });
165
- if (forward.reached.some((node) => seeded[node] === 1))
155
+ if (forward.nodes.some((node) => seeded[node] === 1))
166
156
  continue;
167
157
  avoid.add(id);
168
158
  shadowed.push(name);
@@ -170,21 +160,21 @@ function unshadowed(relations, seeds, options) {
170
160
  }
171
161
  if (found === 0)
172
162
  break;
173
- reach = dependentsOf(relations, seeds, { ...options, avoid });
163
+ traversal = dependentsOf(relations, seeds, { ...options, avoid });
174
164
  }
175
- return { ...reach, shadowed: shadowed.sort(byCodeUnit) };
165
+ return { ...traversal, shadowed: shadowed.sort(byCodeUnit) };
176
166
  }
177
167
  /**
178
- * How one file or component was reached, as the chain that reached it.
168
+ * Why one file or component is affected: the chain of imports from a changed file to it.
179
169
  *
180
170
  * The sentence a run prints when somebody asks why a subject was observed. Empty
181
- * when the node was not reached at all.
171
+ * when the node is not affected at all.
182
172
  */
183
- export function explain(relations, moved, node) {
173
+ export function explain(relations, affected, node) {
184
174
  const id = idOf(relations, node.kind, node.name);
185
175
  if (id === undefined)
186
176
  return [];
187
- return trailOf(moved.reach, id).map((step) => relations.names[step]);
177
+ return trailOf(affected.traversal, id).map((step) => relations.names[step]);
188
178
  }
189
179
  function byCodeUnit(a, b) {
190
180
  return a < b ? -1 : a > b ? 1 : 0;
@@ -1 +1 @@
1
- {"version":3,"file":"records.js","sourceRoot":"","sources":["../../src/relate/records.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAGH,OAAO,EACL,IAAI,EACJ,MAAM,EACN,WAAW,GAOZ,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,OAAO,EAAiC,MAAM,YAAY,CAAC;AAgFlG,MAAM,IAAI,GAAG,CAAC,IAAY,EAAQ,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC;AAC9D,MAAM,SAAS,GAAG,CAAC,IAAY,EAAQ,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,CAAC,CAAC;AACxE,MAAM,GAAG,GAAG,CAAC,IAAY,EAAQ,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;AAEhE;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,gBAAgB,CAAC,OAA6B,EAAE,OAAO,GAAqB,EAAE;IAC5F,MAAM,SAAS,GAAe,EAAE,CAAC;IACjC,MAAM,QAAQ,GAAW,EAAE,CAAC;IAC5B,MAAM,OAAO,GAAgC,EAAE,CAAC;IAEhD,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;QAC7B,MAAM,IAAI,GAAG,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;QAC/B,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAEpB,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,KAAK,IAAI,EAAE,EAAE,CAAC;YACtC,SAAS,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,EAAE,EAAE,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC;QAC/D,CAAC;QACD,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,QAAQ,IAAI,EAAE,EAAE,CAAC;YACzC,SAAS,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,EAAE,EAAE,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC;QAC9D,CAAC;QACD,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,QAAQ,IAAI,EAAE,EAAE,CAAC;YACzC,SAAS,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,SAAS,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,aAAa,EAAE,CAAC,CAAC;QAC3E,CAAC;QACD,4EAA4E;QAC5E,sEAAsE;QACtE,IAAI,MAAM,CAAC,OAAO,KAAK,SAAS;YAAE,OAAO,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC;IACzE,CAAC;IAED,4EAA4E;IAC5E,uEAAuE;IACvE,4EAA4E;IAC5E,qEAAqE;IACrE,KAAK,MAAM,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,OAAO,CAAC,OAAO,IAAI,EAAE,EAAE,CAAC;QAC/C,SAAS,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,GAAG,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,GAAG,CAAC,EAAE,CAAC,EAAE,IAAI,EAAE,YAAY,EAAE,CAAC,CAAC;IACvE,CAAC;IAED,OAAO,WAAW,CAAC,EAAE,SAAS,EAAE,QAAQ,EAAE,OAAO,EAAE,GAAG,CAAC,OAAO,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC;AAC/H,CAAC;AAkGD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,UAAU,OAAO,CACrB,SAAoB,EACpB,OAAgC,EAChC,OAAO,GAAiB,EAAE;IAE1B,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,MAAM,OAAO,GAAa,EAAE,CAAC;IAE7B,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;QAC5B,MAAM,IAAI,GAAG,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC;QAC7D,MAAM,EAAE,GAAG,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC;QACjD,IAAI,EAAE,KAAK,SAAS;YAAE,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;;YACzC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACtB,CAAC;IAED,MAAM,MAAM,GAAW,EAAE,CAAC;IAC1B,KAAK,IAAI,EAAE,GAAG,CAAC,EAAE,EAAE,GAAG,SAAS,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,IAAI,CAAC,EAAE,CAAC;QACxD,IAAI,SAAS,CAAC,OAAO,CAAC,EAAE,CAAC,KAAK,CAAC;YAAE,SAAS;QAC1C,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QAEf,MAAM,OAAO,GAAG,SAAS,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QAC1C,MAAM,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,SAAS,CAAC,KAAK,CAAC,EAAE,CAAE,EAAE,GAAG,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC;IAC7F,CAAC;IAED,MAAM,KAAK,GAAG,UAAU,CAAC,SAAS,EAAE,KAAK,EAAE,OAAO,CAAC,CAAC;IACpD,MAAM,KAAK,GAA+B,EAAE,IAAI,EAAE,EAAE,EAAE,SAAS,EAAE,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC;IAEnF,KAAK,MAAM,EAAE,IAAI,KAAK,CAAC,OAAO,EAAE,CAAC;QAC/B,MAAM,IAAI,GAAG,MAAM,CAAC,SAAS,EAAE,EAAE,CAAC,CAAC;QACnC,IAAI,IAAI,KAAK,SAAS;YAAE,SAAS;QACjC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACnC,CAAC;IAED,OAAO;QACL,KAAK,EAAE,KAAK,CAAC,IAAI;QACjB,UAAU,EAAE,KAAK,CAAC,SAAS;QAC3B,QAAQ,EAAE,KAAK,CAAC,OAAO;QACvB,OAAO,EAAE,OAAO,CAAC,IAAI,CAAC,UAAU,CAAC;QACjC,MAAM;QACN,QAAQ,EAAE,KAAK,CAAC,QAAQ;QACxB,KAAK;KACN,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,SAAS,UAAU,CACjB,SAAoB,EACpB,KAAwB,EACxB,OAAqB;IAErB,MAAM,QAAQ,GAAa,EAAE,CAAC;IAC9B,MAAM,KAAK,GAAG,IAAI,GAAG,CAAS,OAAO,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC;IACnD,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,IAAI,SAAS,CAAC,OAAO,CAAC;IACrD,IAAI,KAAK,GAAG,YAAY,CAAC,SAAS,EAAE,KAAK,EAAE,OAAO,CAAC,CAAC;IACpD,IAAI,OAAO,CAAC,IAAI,KAAK,CAAC;QAAE,OAAO,EAAE,GAAG,KAAK,EAAE,QAAQ,EAAE,CAAC;IAEtD,MAAM,MAAM,GAAG,IAAI,UAAU,CAAC,SAAS,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;IACtD,KAAK,MAAM,IAAI,IAAI,KAAK;QAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAC3C,MAAM,OAAO,GAAG,IAAI,GAAG,EAAU,CAAC;IAElC,SAAS,CAAC;QACR,IAAI,KAAK,GAAG,CAAC,CAAC;QACd,KAAK,MAAM,CAAC,IAAI,EAAE,GAAG,CAAC,IAAI,OAAO,EAAE,CAAC;YAClC,MAAM,EAAE,GAAG,IAAI,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,CAAC,CAAC;YACzC,IAAI,EAAE,KAAK,SAAS,IAAI,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,KAAK,CAAC,IAAI,MAAM,CAAC,EAAE,CAAC,KAAK,CAAC,IAAI,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;gBAAE,SAAS;YAE9F,MAAM,GAAG,GAAa,EAAE,CAAC;YACzB,KAAK,MAAM,MAAM,IAAI,GAAG,EAAE,CAAC;gBACzB,MAAM,MAAM,GAAG,IAAI,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;gBAC/C,IAAI,MAAM,KAAK,SAAS,IAAI,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC;oBAAE,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;YACzE,CAAC;YACD,IAAI,GAAG,CAAC,MAAM,KAAK,CAAC;gBAAE,SAAS;YAC/B,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;YAEhB,MAAM,OAAO,GAAG,cAAc,CAAC,SAAS,EAAE,CAAC,EAAE,CAAC,EAAE,EAAE,GAAG,OAAO,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,CAAC;YAC5E,IAAI,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;gBAAE,SAAS;YAEjE,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;YACd,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACpB,KAAK,IAAI,CAAC,CAAC;QACb,CAAC;QACD,IAAI,KAAK,KAAK,CAAC;YAAE,MAAM;QACvB,KAAK,GAAG,YAAY,CAAC,SAAS,EAAE,KAAK,EAAE,EAAE,GAAG,OAAO,EAAE,KAAK,EAAE,CAAC,CAAC;IAChE,CAAC;IAED,OAAO,EAAE,GAAG,KAAK,EAAE,QAAQ,EAAE,QAAQ,CAAC,IAAI,CAAC,UAAU,CAAC,EAAE,CAAC;AAC3D,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,OAAO,CAAC,SAAoB,EAAE,KAAc,EAAE,IAAU;IACtE,MAAM,EAAE,GAAG,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC;IACjD,IAAI,EAAE,KAAK,SAAS;QAAE,OAAO,EAAE,CAAC;IAChC,OAAO,OAAO,CAAC,KAAK,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,CAAE,CAAC,CAAC;AACxE,CAAC;AAED,SAAS,UAAU,CAAC,CAAS,EAAE,CAAS;IACtC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AACpC,CAAC","sourcesContent":["/**\n * One file's outgoing edges, and the fold that turns a pile of them into a graph.\n *\n * A record is what reading **one** file produced, and that is the unit on\n * purpose. It makes the expensive half — open, parse, resolve — cacheable by\n * content digest and parallelizable per file, and it leaves the graph a pure\n * function of the records, so the structure can be rebuilt, sharded or merged\n * without touching a disk. `core` performs no I/O (ADR-0006); whoever owns the\n * disk produces these and hands them over.\n *\n * ## An empty record and an unreadable one are different facts\n *\n * A file with no imports and a file nothing could parse both produce a record\n * with no edges, and treating them alike is the one mistake that turns this from\n * a saving into a silent hole: the second one may import anything, including\n * whatever just changed. So `unknown` carries the *reason* the edges are not\n * known, and every consumer seeds its traversal with those files (ADR-0002 —\n * absent is not empty, and here absent is not even absent, it is *unread*).\n */\n\nimport type { Digest } from '../format/hash.js';\nimport {\n idOf,\n nodeAt,\n relationsOf,\n type EdgeKind,\n type Node,\n type NodeId,\n type NodeKind,\n type Relation,\n type Relations,\n} from './graph.js';\nimport { dependenciesOf, dependentsOf, trailOf, type Reach, type ReachOptions } from './reach.js';\n\nexport interface FileEdge {\n /** Repository-relative, already resolved. A specifier is not an edge. */\n readonly to: string;\n readonly kind: EdgeKind;\n}\n\n/**\n * A file's import of something the install provides.\n *\n * The name a specifier asked for, never a version and never a resolution:\n * which copy a resolver handed this importer is unanswerable without\n * reproducing that resolver, and a selector that guessed would skip on the\n * guess. Names over-include, which is the safe direction.\n */\nexport interface PackageEdge {\n /** The package name the specifier asked for — `@mui/material`, not a path. */\n readonly to: string;\n /** The import's own kind, so a `type` import of a package is erased like any other. */\n readonly kind: EdgeKind;\n}\n\nexport interface FileRecord {\n /** Repository-relative, so a graph is portable between machines and CI. */\n readonly file: string;\n\n /**\n * The content digest this record was read from.\n *\n * Two jobs. It lets a second scan skip the parse for a file that has not moved,\n * which is the difference between a scan that costs a second and one nobody\n * leaves enabled. And it is the leaf of the Merkle fold in\n * [`merkle.ts`](./merkle.ts) — a file with no digest is one nothing can prove\n * unchanged, and is marked as such rather than assumed still.\n */\n readonly digest?: Digest;\n\n /** Resolved outgoing edges. Absent and empty mean the same thing here. */\n readonly edges?: readonly FileEdge[];\n\n /** Component names this file declares, from `indexSource` or better. */\n readonly declares?: readonly string[];\n\n /**\n * Packages this file imports, by the name it imports them by.\n *\n * The far end of the line. Nothing resolves these to a path — under pnpm's\n * store or Yarn PnP there may not be one, and under a custom resolver the path\n * would be a lie about a different machine — so the name stands for the\n * dependency, and what the install currently has under that name is a question\n * for the lockfile ([`sense/lock`](../../../sense/src/lock/index.ts)).\n *\n * A file that imports `@mui/material` is moved by `@mui/material` moving; a\n * file that does not, is not. That is the whole rule, and it is why this is\n * read from the imports rather than from a manifest: a `package.json` says what\n * a workspace may use, and the imports say what a file does use.\n */\n readonly packages?: readonly PackageEdge[];\n\n /**\n * Specifiers that were read but did not resolve.\n *\n * Kept for the report rather than for the graph. A bare specifier that failed\n * to resolve is usually a package that is not installed; a **relative** one\n * that failed is a hole, and whoever produced the record says so by also\n * setting `unknown`.\n */\n readonly unresolved?: readonly string[];\n\n /**\n * Why this file's outgoing edges could not be enumerated, when they could not.\n *\n * Present means *this file may depend on anything*. The sentence is carried\n * rather than a flag, because it ends up in the paragraph explaining why a run\n * observed more than the operator expected.\n */\n readonly unknown?: string;\n}\n\nconst file = (name: string): Node => ({ kind: 'file', name });\nconst component = (name: string): Node => ({ kind: 'component', name });\nconst pkg = (name: string): Node => ({ kind: 'package', name });\n\n/**\n * Fold file records into the graph.\n *\n * Every record contributes its own node even when it has no edges, so a changed\n * file that imports nothing is still findable — otherwise the one file in the\n * diff would be missing from the structure that exists to answer questions about\n * it.\n *\n * A declaration becomes an edge from the **component to the file**, which is the\n * direction the convention requires: the component depends on the file that\n * declares it, so one walk against the arrows from a changed file reaches every\n * importer and every component in the same pass.\n */\nexport function relationsOfFiles(records: Iterable<FileRecord>, options: RelationsOptions = {}): Relations {\n const relations: Relation[] = [];\n const isolated: Node[] = [];\n const unknown: (readonly [Node, string])[] = [];\n\n for (const record of records) {\n const from = file(record.file);\n isolated.push(from);\n\n for (const edge of record.edges ?? []) {\n relations.push({ from, to: file(edge.to), kind: edge.kind });\n }\n for (const edge of record.packages ?? []) {\n relations.push({ from, to: pkg(edge.to), kind: edge.kind });\n }\n for (const name of record.declares ?? []) {\n relations.push({ from: component(name), to: from, kind: 'declared-in' });\n }\n // The sentence travels with the node. Reduced to a flag here, the paragraph\n // this record exists to produce could name a count and never a cause.\n if (record.unknown !== undefined) unknown.push([from, record.unknown]);\n }\n\n // The lock's own edges arrive here rather than as records, because they are\n // not a file's reading of anything: one lockfile answers for the whole\n // install, and folding them in beside the imports is what lets a bump three\n // packages deep walk up to the file that imports the one at the top.\n for (const [from, to] of options.depends ?? []) {\n relations.push({ from: pkg(from), to: pkg(to), kind: 'depends-on' });\n }\n\n return relationsOf({ relations, isolated, unknown, ...(options.shadows === undefined ? {} : { shadows: options.shadows }) });\n}\n\nexport interface RelationsOptions {\n /** Per file, the files its run never reaches; see {@link Relations.shadows}. */\n readonly shadows?: ReadonlyMap<string, readonly string[]>;\n\n /**\n * `[dependent, dependency]` pairs between packages, from a lockfile\n * ([`packageRelations`](../../../sense/src/lock/lockfile.ts)).\n *\n * Optional, and a graph without them is not wrong, only shorter-sighted: it\n * answers a change to a package some file imports directly, and says nothing\n * about a change three levels down. Supplying them is what turns *`jsdom`\n * moved* into *every test whose environment is built on it*.\n */\n readonly depends?: Iterable<readonly [string, string]>;\n}\n\nexport interface Hole {\n /** The file whose outgoing edges could not be enumerated. */\n readonly file: string;\n /** Why, in the words of whoever read it. Absent when nobody said. */\n readonly because?: string;\n}\n\n/**\n * What one backwards walk over the file graph found, and what widened it.\n *\n * Every field is part of one answer and none of them stands alone: `files` is\n * the answer, `missing` and `opaque` are the two ways it is not the whole truth,\n * and `reach` is what an explanation is read back out of. A caller that took\n * `files` and dropped the rest would be a caller that cannot say whether the\n * list is narrow because the diff was small or because the scan never ran.\n */\nexport interface Reached {\n /** Files the change could have moved, including the changed files themselves. */\n readonly files: readonly string[];\n /** Components declared in any of them. */\n readonly components: readonly string[];\n\n /**\n * Packages the change could have moved, including the changed packages\n * themselves and everything the install resolved beneath them.\n *\n * Reported rather than used: the answer a caller acts on is in `files`, and\n * this is the trail of package names that got there. Empty for a walk seeded\n * only with files, because a package is downstream of every file that imports\n * it and a walk against the arrows never descends.\n */\n readonly packages: readonly string[];\n\n /**\n * Changed paths the graph does not hold.\n *\n * Never silently ignored. A `README.md` belongs here and means nothing; a\n * source file belongs here only because the scan never reached it, and the\n * caller — which knows where it told the scan to look — is the one that can\n * tell those apart.\n */\n readonly missing: readonly string[];\n\n /**\n * Files seeded because their own edges are unknown, rather than because\n * anything reached them, each with the reason it could not be read.\n *\n * Counted separately so that \"we widened\" never hides inside \"we found\", and\n * carrying its sentence so the widening is a work item rather than a tax: *four\n * files could not be read* is something to live with, and ``src/legacy.js — a\n * computed require()`` is something to fix.\n */\n readonly opaque: readonly Hole[];\n\n /**\n * Files the graph reached and their own shadows cut: every trail from a\n * changed file to each one crossed a module the file replaces for its run.\n * Listed so the report can say a test was left out and by whose word.\n */\n readonly shadowed: readonly string[];\n\n /** The traversal, kept so a caller can ask how any one file was reached. */\n readonly reach: Reach;\n}\n\nexport interface MovedOptions extends ReachOptions {\n /**\n * Per file, the files it takes out of its own graph: what a test mocks.\n *\n * A row is a fact about one file's run and nothing else's, so it is kept\n * beside the records rather than in them — the records say what the text\n * imports, this says what the run never reaches. A file with a row is moved\n * only if some trail from a changed file arrives at it without crossing a\n * file in its row, and what is reached only *through* it goes with it: the\n * component a story declares, the test a setup file's mocks hold for. The\n * graph's own table when absent ({@link Relations.shadows}).\n */\n readonly shadows?: ReadonlyMap<string, readonly string[]>;\n}\n\n/**\n * What a set of changed files could have moved.\n *\n * The seed set is the changed files **and every file whose edges are unknown**,\n * which is the rule the whole structure rests on. An unreadable file might import\n * the one that changed; seeding it means everything depending on *it* is observed\n * too, and the cost is a collection rather than a green run over an unwatched\n * surface.\n *\n * A seed is a path, or a node naming its own kind. The second form is how a\n * changed *package* enters: a bare string keeps meaning a file, because that is\n * what every caller written before packages existed meant by one, and a caller\n * with a lockfile in hand passes `{ kind: 'package', name: 'jsdom' }` into the\n * same walk. There is no second selector, and there is no order between the two\n * — a commit that bumps a dependency and edits a file is one seed set and one\n * traversal.\n *\n * One breadth-first search, whatever the number of changed files, over the\n * runtime edges unless the caller names others.\n */\nexport function movedBy(\n relations: Relations,\n changed: Iterable<string | Node>,\n options: MovedOptions = {},\n): Reached {\n const seeds: NodeId[] = [];\n const missing: string[] = [];\n\n for (const entry of changed) {\n const node = typeof entry === 'string' ? file(entry) : entry;\n const id = idOf(relations, node.kind, node.name);\n if (id === undefined) missing.push(node.name);\n else seeds.push(id);\n }\n\n const opaque: Hole[] = [];\n for (let id = 0; id < relations.unknown.length; id += 1) {\n if (relations.unknown[id] !== 1) continue;\n seeds.push(id);\n\n const because = relations.reasons.get(id);\n opaque.push({ file: relations.names[id]!, ...(because === undefined ? {} : { because }) });\n }\n\n const reach = unshadowed(relations, seeds, options);\n const found: Record<NodeKind, string[]> = { file: [], component: [], package: [] };\n\n for (const id of reach.reached) {\n const node = nodeAt(relations, id);\n if (node === undefined) continue;\n found[node.kind].push(node.name);\n }\n\n return {\n files: found.file,\n components: found.component,\n packages: found.package,\n missing: missing.sort(byCodeUnit),\n opaque,\n shadowed: reach.shadowed,\n reach,\n };\n}\n\n/**\n * The walk against the arrows with every file left out whose own shadows cut\n * every trail to it, and with nothing reached through such a file.\n *\n * The walk records one parent per node, so it cannot say whether *another*\n * trail avoided the shadows. Asking the other way can: a walk along the arrows\n * from the file, entering none of its shadows, either finds a seed or does not.\n * It runs only for a reached file whose row names a reached node — a mock of\n * something the change never touched cuts nothing, and costs nothing. A file\n * found shadowed is then avoided and the walk repeated, so a component or a\n * file reached only through it is not reached either; the repeat ends when a\n * walk finds no new shadowed file, and a row once decided is not asked again.\n */\nfunction unshadowed(\n relations: Relations,\n seeds: readonly NodeId[],\n options: MovedOptions,\n): Reach & { readonly shadowed: readonly string[] } {\n const shadowed: string[] = [];\n const avoid = new Set<NodeId>(options.avoid ?? []);\n const shadows = options.shadows ?? relations.shadows;\n let reach = dependentsOf(relations, seeds, options);\n if (shadows.size === 0) return { ...reach, shadowed };\n\n const seeded = new Uint8Array(relations.names.length);\n for (const seed of seeds) seeded[seed] = 1;\n const decided = new Set<NodeId>();\n\n for (;;) {\n let found = 0;\n for (const [name, row] of shadows) {\n const id = idOf(relations, 'file', name);\n if (id === undefined || reach.mask[id] !== 1 || seeded[id] === 1 || decided.has(id)) continue;\n\n const cut: NodeId[] = [];\n for (const shadow of row) {\n const target = idOf(relations, 'file', shadow);\n if (target !== undefined && reach.mask[target] === 1) cut.push(target);\n }\n if (cut.length === 0) continue;\n decided.add(id);\n\n const forward = dependenciesOf(relations, [id], { ...options, avoid: cut });\n if (forward.reached.some((node) => seeded[node] === 1)) continue;\n\n avoid.add(id);\n shadowed.push(name);\n found += 1;\n }\n if (found === 0) break;\n reach = dependentsOf(relations, seeds, { ...options, avoid });\n }\n\n return { ...reach, shadowed: shadowed.sort(byCodeUnit) };\n}\n\n/**\n * How one file or component was reached, as the chain that reached it.\n *\n * The sentence a run prints when somebody asks why a subject was observed. Empty\n * when the node was not reached at all.\n */\nexport function explain(relations: Relations, moved: Reached, node: Node): readonly string[] {\n const id = idOf(relations, node.kind, node.name);\n if (id === undefined) return [];\n return trailOf(moved.reach, id).map((step) => relations.names[step]!);\n}\n\nfunction byCodeUnit(a: string, b: string): number {\n return a < b ? -1 : a > b ? 1 : 0;\n}\n"]}
1
+ {"version":3,"file":"records.js","sourceRoot":"","sources":["../../src/relate/records.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * One file's outgoing edges, and the fold that turns a pile of them into a graph.\n *\n * A record is what reading **one** file produced, and that is the unit on\n * purpose. It makes the expensive half — open, parse, resolve — cacheable by\n * content digest and parallelizable per file, and it leaves the graph a pure\n * function of the records, so the structure can be rebuilt, sharded or merged\n * without touching a disk. `core` performs no I/O (ADR-0006); whoever owns the\n * disk produces these and hands them over.\n *\n * ## An unreadable file contributes what was read of it\n *\n * A `require(name)` with a variable for a name, an `import('./' + page)`, a\n * parse that did not finish: the edges a reader could see are recorded, and the\n * one it could not is not guessed at. `unknown` carries the sentence saying why,\n * for whoever reports on the scan. It widens no walk. Seeding every such file as\n * though it had changed turns one computed `require` into a whole run on every\n * diff, forever, and the edge it hides is exactly what a recorded run sees: the\n * module loaded, whatever expression named it.\n */\n\nimport type { Digest } from '../format/hash.js';\nimport {\n idOf,\n nodeAt,\n relationsOf,\n type EdgeKind,\n type Node,\n type NodeId,\n type NodeKind,\n type Relation,\n type Relations,\n} from './graph.js';\nimport { dependenciesOf, dependentsOf, trailOf, type Traversal, type TraversalOptions } from './reach.js';\n\nexport interface FileEdge {\n /** Repository-relative, already resolved. A specifier is not an edge. */\n readonly to: string;\n readonly kind: EdgeKind;\n}\n\n/**\n * A file's import of something the install provides.\n *\n * The name a specifier asked for, never a version and never a resolution:\n * which copy a resolver handed this importer is unanswerable without\n * reproducing that resolver, and a selector that guessed would skip on the\n * guess. Names over-include, which is the safe direction.\n */\nexport interface PackageEdge {\n /** The package name the specifier asked for — `@mui/material`, not a path. */\n readonly to: string;\n /** The import's own kind, so a `type` import of a package is erased like any other. */\n readonly kind: EdgeKind;\n}\n\nexport interface FileRecord {\n /** Repository-relative, so a graph is portable between machines and CI. */\n readonly file: string;\n\n /**\n * The content digest this record was read from.\n *\n * Two jobs. It lets a second scan skip the parse for a file that has not changed,\n * which is the difference between a scan that costs a second and one nobody\n * leaves enabled. And it is the leaf of the Merkle fold in\n * [`merkle.ts`](./merkle.ts) — a file with no digest is one nothing can prove\n * unchanged, and is marked as such rather than assumed still.\n */\n readonly digest?: Digest;\n\n /** Resolved outgoing edges. Absent and empty mean the same thing here. */\n readonly edges?: readonly FileEdge[];\n\n /** Component names this file declares, from `indexSource` or better. */\n readonly declares?: readonly string[];\n\n /**\n * Packages this file imports, by the name it imports them by.\n *\n * The far end of the line. Nothing resolves these to a path — under pnpm's\n * store or Yarn PnP there may not be one, and under a custom resolver the path\n * would be a lie about a different machine — so the name stands for the\n * dependency, and what the install currently has under that name is a question\n * for the lockfile ([`sense/lock`](../../../sense/src/lock/index.ts)).\n *\n * A file that imports `@mui/material` is affected when `@mui/material`\n * changes; a file that does not, is not. That is the whole rule, and it is why this is\n * read from the imports rather than from a manifest: a `package.json` says what\n * a workspace may use, and the imports say what a file does use.\n */\n readonly packages?: readonly PackageEdge[];\n\n /**\n * Specifiers that were read but did not resolve.\n *\n * Kept for the report rather than for the graph. A bare specifier that failed\n * to resolve is usually a package that is not installed; a **relative** one\n * that failed is a hole, and whoever produced the record says so by also\n * setting `unknown`.\n */\n readonly unresolved?: readonly string[];\n\n /**\n * Why this file's outgoing edges could not all be enumerated, when they could not.\n *\n * The edges that were read are in `edges`; this names what was not. A walk\n * treats the file like any other, and the edge nobody could read is left to a\n * recorded run, which sees the module load whatever named it.\n */\n readonly unknown?: string;\n}\n\nconst file = (name: string): Node => ({ kind: 'file', name });\nconst component = (name: string): Node => ({ kind: 'component', name });\nconst pkg = (name: string): Node => ({ kind: 'package', name });\n\n/**\n * Fold file records into the graph.\n *\n * Every record contributes its own node even when it has no edges, so a changed\n * file that imports nothing is still findable — otherwise the one file in the\n * diff would be missing from the structure that exists to answer questions about\n * it.\n *\n * A declaration becomes an edge from the **component to the file**, which is the\n * direction the convention requires: the component depends on the file that\n * declares it, so one walk against the arrows from a changed file reaches every\n * importer and every component in the same pass.\n */\nexport function relationsOfFiles(records: Iterable<FileRecord>, options: RelationsOptions = {}): Relations {\n const relations: Relation[] = [];\n const isolated: Node[] = [];\n const unknown: (readonly [Node, string])[] = [];\n\n for (const record of records) {\n const from = file(record.file);\n isolated.push(from);\n\n for (const edge of record.edges ?? []) {\n relations.push({ from, to: file(edge.to), kind: edge.kind });\n }\n for (const edge of record.packages ?? []) {\n relations.push({ from, to: pkg(edge.to), kind: edge.kind });\n }\n for (const name of record.declares ?? []) {\n relations.push({ from: component(name), to: from, kind: 'declared-in' });\n }\n // The sentence travels with the node. Reduced to a flag here, the paragraph\n // this record exists to produce could name a count and never a cause.\n if (record.unknown !== undefined) unknown.push([from, record.unknown]);\n }\n\n // The lock's own edges arrive here rather than as records, because they are\n // not a file's reading of anything: one lockfile answers for the whole\n // install, and folding them in beside the imports is what lets a bump three\n // packages deep walk up to the file that imports the one at the top.\n for (const [from, to] of options.depends ?? []) {\n relations.push({ from: pkg(from), to: pkg(to), kind: 'depends-on' });\n }\n\n return relationsOf({ relations, isolated, unknown, ...(options.shadows === undefined ? {} : { shadows: options.shadows }) });\n}\n\nexport interface RelationsOptions {\n /** Per file, the files its run never reaches; see {@link Relations.shadows}. */\n readonly shadows?: ReadonlyMap<string, readonly string[]>;\n\n /**\n * `[dependent, dependency]` pairs between packages, from a lockfile\n * ([`packageRelations`](../../../sense/src/lock/lockfile.ts)).\n *\n * Optional, and a graph without them is not wrong, only shorter-sighted: it\n * answers a change to a package some file imports directly, and says nothing\n * about a change three levels down. Supplying them is what turns *`jsdom`\n * changed* into *every test whose environment is built on it*.\n */\n readonly depends?: Iterable<readonly [string, string]>;\n}\n\n/**\n * What one backwards walk over the file graph found.\n *\n * Every field is part of one answer and none of them stands alone: `files` is\n * the answer, `missing` is what it could not say anything about, and\n * `traversal` is what an explanation is read back out of. A caller that took\n * `files` and dropped the rest would be a caller that cannot say whether the\n * list is narrow because the diff was small or because the scan never ran.\n */\nexport interface Affected {\n /** Files the change could affect, including the changed files themselves. */\n readonly files: readonly string[];\n /** Components declared in any of them. */\n readonly components: readonly string[];\n\n /**\n * Packages the change could affect, including the changed packages\n * themselves and everything the install resolved beneath them.\n *\n * Reported rather than used: the answer a caller acts on is in `files`, and\n * this is the trail of package names that got there. Empty for a walk seeded\n * only with files, because a package is downstream of every file that imports\n * it and a walk against the arrows never descends.\n */\n readonly packages: readonly string[];\n\n /**\n * Changed paths the graph does not hold.\n *\n * Never silently ignored. A `README.md` belongs here and means nothing; a\n * source file belongs here only because the scan never read it, and the\n * caller — which knows where it told the scan to look — is the one that can\n * tell those apart.\n */\n readonly missing: readonly string[];\n\n /**\n * Files the walk visited and their own shadows cut: every trail from a\n * changed file to each one crossed a module the file replaces for its run.\n * Listed so the report can say a test was left out and by whose word.\n */\n readonly shadowed: readonly string[];\n\n /** The walk itself, kept so a caller can ask why any one file is in the answer. */\n readonly traversal: Traversal;\n}\n\nexport interface AffectedOptions extends TraversalOptions {\n /**\n * Per file, the files it takes out of its own graph: what a test mocks.\n *\n * A row is a fact about one file's run and nothing else's, so it is kept\n * beside the records rather than in them — the records say what the text\n * imports, this says what the run never loads. A file with a row is affected\n * only if some trail from a changed file arrives at it without crossing a\n * file in its row, and what depends on the change only *through* it goes with it: the\n * component a story declares, the test a setup file's mocks hold for. The\n * graph's own table when absent ({@link Relations.shadows}).\n */\n readonly shadows?: ReadonlyMap<string, readonly string[]>;\n}\n\n/**\n * What a set of changed files could affect.\n *\n * The seed set is the changed files and nothing else. A file whose edges could\n * not all be read is walked through the edges that were; the one nobody saw is\n * a recorded run's to answer, not a reason to seed the file on every diff.\n *\n * A seed is a path, or a node naming its own kind. The second form is how a\n * changed *package* enters: a bare string keeps meaning a file, because that is\n * what every caller written before packages existed meant by one, and a caller\n * with a lockfile in hand passes `{ kind: 'package', name: 'jsdom' }` into the\n * same walk. There is no second selector, and there is no order between the two\n * — a commit that bumps a dependency and edits a file is one seed set and one\n * traversal.\n *\n * One breadth-first search, whatever the number of changed files, over the\n * runtime edges unless the caller names others.\n */\nexport function affectedBy(\n relations: Relations,\n changed: Iterable<string | Node>,\n options: AffectedOptions = {},\n): Affected {\n const seeds: NodeId[] = [];\n const missing: string[] = [];\n\n for (const entry of changed) {\n const node = typeof entry === 'string' ? file(entry) : entry;\n const id = idOf(relations, node.kind, node.name);\n if (id === undefined) missing.push(node.name);\n else seeds.push(id);\n }\n\n const traversal = unshadowed(relations, seeds, options);\n const found: Record<NodeKind, string[]> = { file: [], component: [], package: [] };\n\n for (const id of traversal.nodes) {\n const node = nodeAt(relations, id);\n if (node === undefined) continue;\n found[node.kind].push(node.name);\n }\n\n return {\n files: found.file,\n components: found.component,\n packages: found.package,\n missing: missing.sort(byCodeUnit),\n shadowed: traversal.shadowed,\n traversal,\n };\n}\n\n/**\n * The walk against the arrows with every file left out whose own shadows cut\n * every trail to it, and with nothing visited through such a file.\n *\n * The walk records one parent per node, so it cannot say whether *another*\n * trail avoided the shadows. Asking the other way can: a walk along the arrows\n * from the file, entering none of its shadows, either finds a seed or does not.\n * It runs only for a visited file whose row names a visited node — a mock of\n * something the change never touched cuts nothing, and costs nothing. A file\n * found shadowed is then avoided and the walk repeated, so a component or a\n * file visited only through it is not visited either; the repeat ends when a\n * walk finds no new shadowed file, and a row once decided is not asked again.\n */\nfunction unshadowed(\n relations: Relations,\n seeds: readonly NodeId[],\n options: AffectedOptions,\n): Traversal & { readonly shadowed: readonly string[] } {\n const shadowed: string[] = [];\n const avoid = new Set<NodeId>(options.avoid ?? []);\n const shadows = options.shadows ?? relations.shadows;\n let traversal = dependentsOf(relations, seeds, options);\n if (shadows.size === 0) return { ...traversal, shadowed };\n\n const seeded = new Uint8Array(relations.names.length);\n for (const seed of seeds) seeded[seed] = 1;\n const decided = new Set<NodeId>();\n\n for (;;) {\n let found = 0;\n for (const [name, row] of shadows) {\n const id = idOf(relations, 'file', name);\n if (id === undefined || traversal.mask[id] !== 1 || seeded[id] === 1 || decided.has(id)) continue;\n\n const cut: NodeId[] = [];\n for (const shadow of row) {\n const target = idOf(relations, 'file', shadow);\n if (target !== undefined && traversal.mask[target] === 1) cut.push(target);\n }\n if (cut.length === 0) continue;\n decided.add(id);\n\n const forward = dependenciesOf(relations, [id], { ...options, avoid: cut });\n if (forward.nodes.some((node) => seeded[node] === 1)) continue;\n\n avoid.add(id);\n shadowed.push(name);\n found += 1;\n }\n if (found === 0) break;\n traversal = dependentsOf(relations, seeds, { ...options, avoid });\n }\n\n return { ...traversal, shadowed: shadowed.sort(byCodeUnit) };\n}\n\n/**\n * Why one file or component is affected: the chain of imports from a changed file to it.\n *\n * The sentence a run prints when somebody asks why a subject was observed. Empty\n * when the node is not affected at all.\n */\nexport function explain(relations: Relations, affected: Affected, node: Node): readonly string[] {\n const id = idOf(relations, node.kind, node.name);\n if (id === undefined) return [];\n return trailOf(affected.traversal, id).map((step) => relations.names[step]!);\n}\n\nfunction byCodeUnit(a: string, b: string): number {\n return a < b ? -1 : a > b ? 1 : 0;\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@variance-authority/core",
3
- "version": "0.5.10",
3
+ "version": "0.6.0",
4
4
  "description": "The Variance Authority format, rules, comparison, attribution and verdicts. Pure data in, pure data out, no DOM and no I/O.",
5
5
  "keywords": [
6
6
  "variance-authority",