@variance-authority/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.
Files changed (260) hide show
  1. package/CHANGELOG.md +79 -0
  2. package/LICENSE +21 -0
  3. package/README.md +169 -0
  4. package/dist/artifact.d.ts +47 -0
  5. package/dist/artifact.js +2 -0
  6. package/dist/artifact.js.map +1 -0
  7. package/dist/attribute/because.d.ts +43 -0
  8. package/dist/attribute/because.js +64 -0
  9. package/dist/attribute/because.js.map +1 -0
  10. package/dist/attribute/boundary.d.ts +191 -0
  11. package/dist/attribute/boundary.js +328 -0
  12. package/dist/attribute/boundary.js.map +1 -0
  13. package/dist/attribute/call-site.d.ts +96 -0
  14. package/dist/attribute/call-site.js +183 -0
  15. package/dist/attribute/call-site.js.map +1 -0
  16. package/dist/attribute/component-hash.d.ts +188 -0
  17. package/dist/attribute/component-hash.js +312 -0
  18. package/dist/attribute/component-hash.js.map +1 -0
  19. package/dist/attribute/composition-fixture.d.ts +18 -0
  20. package/dist/attribute/composition-fixture.js +53 -0
  21. package/dist/attribute/composition-fixture.js.map +1 -0
  22. package/dist/attribute/composition.d.ts +182 -0
  23. package/dist/attribute/composition.js +169 -0
  24. package/dist/attribute/composition.js.map +1 -0
  25. package/dist/attribute/control.d.ts +43 -0
  26. package/dist/attribute/control.js +54 -0
  27. package/dist/attribute/control.js.map +1 -0
  28. package/dist/attribute/coverage.d.ts +99 -0
  29. package/dist/attribute/coverage.js +86 -0
  30. package/dist/attribute/coverage.js.map +1 -0
  31. package/dist/attribute/divergence.d.ts +102 -0
  32. package/dist/attribute/divergence.js +152 -0
  33. package/dist/attribute/divergence.js.map +1 -0
  34. package/dist/attribute/index.d.ts +43 -0
  35. package/dist/attribute/index.js +29 -0
  36. package/dist/attribute/index.js.map +1 -0
  37. package/dist/attribute/instability.d.ts +89 -0
  38. package/dist/attribute/instability.js +183 -0
  39. package/dist/attribute/instability.js.map +1 -0
  40. package/dist/attribute/instances.d.ts +145 -0
  41. package/dist/attribute/instances.js +140 -0
  42. package/dist/attribute/instances.js.map +1 -0
  43. package/dist/attribute/locate.d.ts +22 -0
  44. package/dist/attribute/locate.js +100 -0
  45. package/dist/attribute/locate.js.map +1 -0
  46. package/dist/attribute/mask.d.ts +156 -0
  47. package/dist/attribute/mask.js +201 -0
  48. package/dist/attribute/mask.js.map +1 -0
  49. package/dist/attribute/movement.d.ts +211 -0
  50. package/dist/attribute/movement.js +181 -0
  51. package/dist/attribute/movement.js.map +1 -0
  52. package/dist/attribute/region.d.ts +188 -0
  53. package/dist/attribute/region.js +144 -0
  54. package/dist/attribute/region.js.map +1 -0
  55. package/dist/attribute/source-map.d.ts +98 -0
  56. package/dist/attribute/source-map.js +264 -0
  57. package/dist/attribute/source-map.js.map +1 -0
  58. package/dist/attribute/source.d.ts +77 -0
  59. package/dist/attribute/source.js +95 -0
  60. package/dist/attribute/source.js.map +1 -0
  61. package/dist/attribute/stack.d.ts +80 -0
  62. package/dist/attribute/stack.js +108 -0
  63. package/dist/attribute/stack.js.map +1 -0
  64. package/dist/compare/band.d.ts +85 -0
  65. package/dist/compare/band.js +70 -0
  66. package/dist/compare/band.js.map +1 -0
  67. package/dist/compare/cascade.d.ts +45 -0
  68. package/dist/compare/cascade.js +71 -0
  69. package/dist/compare/cascade.js.map +1 -0
  70. package/dist/compare/derive.d.ts +78 -0
  71. package/dist/compare/derive.js +39 -0
  72. package/dist/compare/derive.js.map +1 -0
  73. package/dist/compare/diff/attribution.d.ts +23 -0
  74. package/dist/compare/diff/attribution.js +244 -0
  75. package/dist/compare/diff/attribution.js.map +1 -0
  76. package/dist/compare/diff/compare-nodes.d.ts +15 -0
  77. package/dist/compare/diff/compare-nodes.js +227 -0
  78. package/dist/compare/diff/compare-nodes.js.map +1 -0
  79. package/dist/compare/diff/components.d.ts +20 -0
  80. package/dist/compare/diff/components.js +76 -0
  81. package/dist/compare/diff/components.js.map +1 -0
  82. package/dist/compare/diff/delta.d.ts +165 -0
  83. package/dist/compare/diff/delta.js +12 -0
  84. package/dist/compare/diff/delta.js.map +1 -0
  85. package/dist/compare/diff/index.d.ts +69 -0
  86. package/dist/compare/diff/index.js +101 -0
  87. package/dist/compare/diff/index.js.map +1 -0
  88. package/dist/compare/diff/match.d.ts +37 -0
  89. package/dist/compare/diff/match.js +167 -0
  90. package/dist/compare/diff/match.js.map +1 -0
  91. package/dist/compare/explain.d.ts +23 -0
  92. package/dist/compare/explain.js +190 -0
  93. package/dist/compare/explain.js.map +1 -0
  94. package/dist/compare/holding-diff.d.ts +25 -0
  95. package/dist/compare/holding-diff.js +74 -0
  96. package/dist/compare/holding-diff.js.map +1 -0
  97. package/dist/compare/impact.d.ts +56 -0
  98. package/dist/compare/impact.js +124 -0
  99. package/dist/compare/impact.js.map +1 -0
  100. package/dist/compare/index.d.ts +30 -0
  101. package/dist/compare/index.js +22 -0
  102. package/dist/compare/index.js.map +1 -0
  103. package/dist/compare/instance.d.ts +30 -0
  104. package/dist/compare/instance.js +145 -0
  105. package/dist/compare/instance.js.map +1 -0
  106. package/dist/compare/observability.d.ts +49 -0
  107. package/dist/compare/observability.js +57 -0
  108. package/dist/compare/observability.js.map +1 -0
  109. package/dist/compare/parting.d.ts +179 -0
  110. package/dist/compare/parting.js +244 -0
  111. package/dist/compare/parting.js.map +1 -0
  112. package/dist/compare/slice.d.ts +74 -0
  113. package/dist/compare/slice.js +79 -0
  114. package/dist/compare/slice.js.map +1 -0
  115. package/dist/compare/value.d.ts +60 -0
  116. package/dist/compare/value.js +111 -0
  117. package/dist/compare/value.js.map +1 -0
  118. package/dist/format/accessibility.d.ts +27 -0
  119. package/dist/format/accessibility.js +17 -0
  120. package/dist/format/accessibility.js.map +1 -0
  121. package/dist/format/canonical.d.ts +38 -0
  122. package/dist/format/canonical.js +65 -0
  123. package/dist/format/canonical.js.map +1 -0
  124. package/dist/format/capture.d.ts +212 -0
  125. package/dist/format/capture.js +2 -0
  126. package/dist/format/capture.js.map +1 -0
  127. package/dist/format/document.d.ts +279 -0
  128. package/dist/format/document.js +65 -0
  129. package/dist/format/document.js.map +1 -0
  130. package/dist/format/environment.d.ts +120 -0
  131. package/dist/format/environment.js +53 -0
  132. package/dist/format/environment.js.map +1 -0
  133. package/dist/format/hash.d.ts +45 -0
  134. package/dist/format/hash.js +41 -0
  135. package/dist/format/hash.js.map +1 -0
  136. package/dist/format/holding.d.ts +134 -0
  137. package/dist/format/holding.js +2 -0
  138. package/dist/format/holding.js.map +1 -0
  139. package/dist/format/index.d.ts +39 -0
  140. package/dist/format/index.js +25 -0
  141. package/dist/format/index.js.map +1 -0
  142. package/dist/format/intervention.d.ts +87 -0
  143. package/dist/format/intervention.js +11 -0
  144. package/dist/format/intervention.js.map +1 -0
  145. package/dist/format/profile.d.ts +59 -0
  146. package/dist/format/profile.js +66 -0
  147. package/dist/format/profile.js.map +1 -0
  148. package/dist/format/provenance.d.ts +179 -0
  149. package/dist/format/provenance.js +222 -0
  150. package/dist/format/provenance.js.map +1 -0
  151. package/dist/format/sha256.d.ts +34 -0
  152. package/dist/format/sha256.js +152 -0
  153. package/dist/format/sha256.js.map +1 -0
  154. package/dist/format/snapshot.d.ts +263 -0
  155. package/dist/format/snapshot.js +2 -0
  156. package/dist/format/snapshot.js.map +1 -0
  157. package/dist/format/stabilize.d.ts +190 -0
  158. package/dist/format/stabilize.js +333 -0
  159. package/dist/format/stabilize.js.map +1 -0
  160. package/dist/format/tier.d.ts +19 -0
  161. package/dist/format/tier.js +11 -0
  162. package/dist/format/tier.js.map +1 -0
  163. package/dist/format/value.d.ts +115 -0
  164. package/dist/format/value.js +182 -0
  165. package/dist/format/value.js.map +1 -0
  166. package/dist/format/wiring.d.ts +115 -0
  167. package/dist/format/wiring.js +60 -0
  168. package/dist/format/wiring.js.map +1 -0
  169. package/dist/index.d.ts +35 -0
  170. package/dist/index.js +35 -0
  171. package/dist/index.js.map +1 -0
  172. package/dist/judge/docket.d.ts +85 -0
  173. package/dist/judge/docket.js +162 -0
  174. package/dist/judge/docket.js.map +1 -0
  175. package/dist/judge/fingerprint.d.ts +53 -0
  176. package/dist/judge/fingerprint.js +144 -0
  177. package/dist/judge/fingerprint.js.map +1 -0
  178. package/dist/judge/ignore.d.ts +206 -0
  179. package/dist/judge/ignore.js +237 -0
  180. package/dist/judge/ignore.js.map +1 -0
  181. package/dist/judge/index.d.ts +44 -0
  182. package/dist/judge/index.js +34 -0
  183. package/dist/judge/index.js.map +1 -0
  184. package/dist/judge/inspect.d.ts +148 -0
  185. package/dist/judge/inspect.js +208 -0
  186. package/dist/judge/inspect.js.map +1 -0
  187. package/dist/judge/intent.d.ts +146 -0
  188. package/dist/judge/intent.js +150 -0
  189. package/dist/judge/intent.js.map +1 -0
  190. package/dist/judge/locale.d.ts +111 -0
  191. package/dist/judge/locale.js +247 -0
  192. package/dist/judge/locale.js.map +1 -0
  193. package/dist/judge/scope.d.ts +41 -0
  194. package/dist/judge/scope.js +47 -0
  195. package/dist/judge/scope.js.map +1 -0
  196. package/dist/judge/sensitivity.d.ts +202 -0
  197. package/dist/judge/sensitivity.js +152 -0
  198. package/dist/judge/sensitivity.js.map +1 -0
  199. package/dist/judge/trail.d.ts +122 -0
  200. package/dist/judge/trail.js +96 -0
  201. package/dist/judge/trail.js.map +1 -0
  202. package/dist/judge/verdict.d.ts +39 -0
  203. package/dist/judge/verdict.js +32 -0
  204. package/dist/judge/verdict.js.map +1 -0
  205. package/dist/plan/index.d.ts +12 -0
  206. package/dist/plan/index.js +11 -0
  207. package/dist/plan/index.js.map +1 -0
  208. package/dist/plan/tool.d.ts +85 -0
  209. package/dist/plan/tool.js +87 -0
  210. package/dist/plan/tool.js.map +1 -0
  211. package/dist/relate/graph.d.ts +190 -0
  212. package/dist/relate/graph.js +234 -0
  213. package/dist/relate/graph.js.map +1 -0
  214. package/dist/relate/index.d.ts +16 -0
  215. package/dist/relate/index.js +16 -0
  216. package/dist/relate/index.js.map +1 -0
  217. package/dist/relate/merkle.d.ts +110 -0
  218. package/dist/relate/merkle.js +246 -0
  219. package/dist/relate/merkle.js.map +1 -0
  220. package/dist/relate/reach.d.ts +65 -0
  221. package/dist/relate/reach.js +111 -0
  222. package/dist/relate/reach.js.map +1 -0
  223. package/dist/relate/records.d.ts +129 -0
  224. package/dist/relate/records.js +112 -0
  225. package/dist/relate/records.js.map +1 -0
  226. package/dist/rules/index.d.ts +19 -0
  227. package/dist/rules/index.js +17 -0
  228. package/dist/rules/index.js.map +1 -0
  229. package/dist/rules/normalize/alias.d.ts +50 -0
  230. package/dist/rules/normalize/alias.js +111 -0
  231. package/dist/rules/normalize/alias.js.map +1 -0
  232. package/dist/rules/normalize/cascade.d.ts +85 -0
  233. package/dist/rules/normalize/cascade.js +324 -0
  234. package/dist/rules/normalize/cascade.js.map +1 -0
  235. package/dist/rules/normalize/color.d.ts +30 -0
  236. package/dist/rules/normalize/color.js +185 -0
  237. package/dist/rules/normalize/color.js.map +1 -0
  238. package/dist/rules/normalize/fixture.d.ts +83 -0
  239. package/dist/rules/normalize/fixture.js +74 -0
  240. package/dist/rules/normalize/fixture.js.map +1 -0
  241. package/dist/rules/normalize/index.d.ts +39 -0
  242. package/dist/rules/normalize/index.js +268 -0
  243. package/dist/rules/normalize/index.js.map +1 -0
  244. package/dist/rules/normalize/project.d.ts +42 -0
  245. package/dist/rules/normalize/project.js +83 -0
  246. package/dist/rules/normalize/project.js.map +1 -0
  247. package/dist/rules/normalize/shorthand.d.ts +38 -0
  248. package/dist/rules/normalize/shorthand.js +388 -0
  249. package/dist/rules/normalize/shorthand.js.map +1 -0
  250. package/dist/rules/normalize/value.d.ts +23 -0
  251. package/dist/rules/normalize/value.js +162 -0
  252. package/dist/rules/normalize/value.js.map +1 -0
  253. package/dist/rules/normalize/wrapper.d.ts +16 -0
  254. package/dist/rules/normalize/wrapper.js +137 -0
  255. package/dist/rules/normalize/wrapper.js.map +1 -0
  256. package/dist/rules/ruleset.d.ts +93 -0
  257. package/dist/rules/ruleset.js +175 -0
  258. package/dist/rules/ruleset.js.map +1 -0
  259. package/mark.svg +30 -0
  260. package/package.json +69 -0
@@ -0,0 +1,246 @@
1
+ /**
2
+ * The graph, hashed the way a build system hashes an action's inputs.
3
+ *
4
+ * Reachability answers *what could this diff have moved*. It needs a diff, which
5
+ * means it needs `git`, a ref that exists, a checkout deep enough to contain it,
6
+ * and the assumption that the ref is where this branch actually diverged. Every
7
+ * one of those is a thing CI gets wrong, and each failure narrows a run.
8
+ *
9
+ * A Merkle digest asks a different question and needs none of them: **is this
10
+ * subject's entire input closure byte-identical to the one that produced the
11
+ * baseline?** One digest per node, over the node's own content and the digests of
12
+ * everything it rests on — the same construction `bazel` uses to key an action,
13
+ * and the same idea as this project's own content addressing, applied one level
14
+ * out from the document to the source that produced it.
15
+ *
16
+ * What the difference buys:
17
+ *
18
+ * | situation | reachability | closure digest |
19
+ * |---|---|---|
20
+ * | a change, then a revert | both commits are in the diff, so it widens | identical digest, nothing runs |
21
+ * | rebase, squash, branch switch | the merge base moves and the diff with it | unaffected — no ref is consulted |
22
+ * | a shallow clone with no merge base | cannot answer; runs everything | unaffected |
23
+ * | a dependency changed two hops away | reached, and observed | different digest, and observed |
24
+ * | *why* a subject is being observed | a chain of files | a digest that differs |
25
+ *
26
+ * Neither replaces the other. The trail is the explanation, and the digest is the
27
+ * proof; a run wants the digest to decide and the trail to justify.
28
+ *
29
+ * ## Cycles
30
+ *
31
+ * A dependency graph has them and a Merkle tree cannot. Strongly connected
32
+ * components are condensed and hashed as a unit, so every file in a cycle carries
33
+ * the same digest — which is exactly the truth about a cycle: no member of one can
34
+ * be called unchanged while another moved.
35
+ *
36
+ * ## The digest that must never lie
37
+ *
38
+ * A digest says *these inputs are the same*. A file whose content was not supplied,
39
+ * or whose own imports could not be read, breaks that claim — its closure may have
40
+ * moved with no digest in this structure changing. Those nodes are marked
41
+ * **volatile**, the mark propagates to everything that rests on them, and a
42
+ * volatile node is treated as changed however its digest compares. A cache that
43
+ * cannot be trusted must not be silently trusted.
44
+ */
45
+ import { digestCombine, digestString } from '../format/hash.js';
46
+ import { EDGE_KINDS, NODE_KINDS, keyOf, } from './graph.js';
47
+ /**
48
+ * Hash every node over its own content and its whole dependency closure.
49
+ *
50
+ * One pass of Tarjan's algorithm and one pass over the components it emits, which
51
+ * is `O(n + m)` — the same cost as the traversal next door, for an answer that
52
+ * survives a rebase.
53
+ */
54
+ export function closureOf(input) {
55
+ const { relations, content } = input;
56
+ const allowed = allowedKinds(input.through);
57
+ const count = relations.names.length;
58
+ const { component, order } = condense(relations, allowed);
59
+ const digests = new Map();
60
+ const unstable = new Set();
61
+ // One digest per component, so every member of a cycle carries the same one.
62
+ const byComponent = [];
63
+ const shakyComponent = [];
64
+ // Tarjan emits a component only after everything it can reach, so a dependency
65
+ // digest is always already computed by the time it is read here.
66
+ for (const [id, members] of order.entries()) {
67
+ const terms = [];
68
+ const outward = new Set();
69
+ let shaky = false;
70
+ for (const node of members) {
71
+ const kind = NODE_KINDS[relations.kinds[node]];
72
+ const name = relations.names[node];
73
+ const own = kind === 'file' ? content.get(name) : SETTLED;
74
+ if (own === undefined || relations.unknown[node] === 1)
75
+ shaky = true;
76
+ terms.push(`${kind}\u0000${name}\u0000${own ?? UNREAD}`);
77
+ const { offset, target, kind: edgeKind } = relations.depends;
78
+ for (let at = offset[node]; at < offset[node + 1]; at += 1) {
79
+ if (allowed[edgeKind[at]] !== 1)
80
+ continue;
81
+ const other = target[at];
82
+ if (component[other] === id)
83
+ continue;
84
+ outward.add(`${EDGE_KINDS[edgeKind[at]]}\u0000${byComponent[component[other]]}`);
85
+ if (shakyComponent[component[other]] === true)
86
+ shaky = true;
87
+ }
88
+ }
89
+ byComponent.push(digestCombine('relate.closure', [
90
+ digestString([...terms].sort(byCodeUnit).join('')),
91
+ digestString([...outward].sort(byCodeUnit).join('')),
92
+ ]));
93
+ shakyComponent.push(shaky);
94
+ }
95
+ for (let node = 0; node < count; node += 1) {
96
+ const key = keyOf(NODE_KINDS[relations.kinds[node]], relations.names[node]);
97
+ digests.set(key, byComponent[component[node]]);
98
+ if (shakyComponent[component[node]] === true)
99
+ unstable.add(key);
100
+ }
101
+ return { digests, volatile: unstable };
102
+ }
103
+ /** The term a node with content contributes when the content is not known. */
104
+ const UNREAD = '(unread)';
105
+ /** The term a node that has no content of its own contributes. */
106
+ const SETTLED = '(declared)';
107
+ /**
108
+ * What moved between two closures, without consulting a repository.
109
+ *
110
+ * Three ways a node lands in `changed`, and only the first is a difference: its
111
+ * digest differs, it is **new**, or it is **volatile** — the last because a node
112
+ * whose inputs were not fully read has a digest that can match by accident, and a
113
+ * selector that believed it would skip a subject on the strength of a hash over
114
+ * bytes nobody hashed.
115
+ */
116
+ export function driftedBetween(before, after) {
117
+ const changed = [];
118
+ for (const [key, digest] of after.digests) {
119
+ const was = before.digests.get(key);
120
+ if (was === digest && !after.volatile.has(key) && !before.volatile.has(key))
121
+ continue;
122
+ changed.push(key);
123
+ }
124
+ const gone = [...before.digests.keys()].filter((key) => !after.digests.has(key));
125
+ return {
126
+ changed: changed.sort(byCodeUnit),
127
+ files: named(changed, 'file'),
128
+ components: named(changed, 'component'),
129
+ gone: gone.sort(byCodeUnit),
130
+ };
131
+ }
132
+ function named(keys, kind) {
133
+ const prefix = `${kind}:`;
134
+ return keys
135
+ .filter((key) => key.startsWith(prefix))
136
+ .map((key) => key.slice(prefix.length))
137
+ .sort(byCodeUnit);
138
+ }
139
+ /**
140
+ * Tarjan's strongly connected components, iteratively.
141
+ *
142
+ * Iterative because the recursion depth is the longest dependency chain in the
143
+ * repository, and a deep monorepo would overflow a call stack on a structure whose
144
+ * whole point is to be cheap. The emission order is the useful half: a component
145
+ * appears only once everything it can reach has, which is the order a Merkle fold
146
+ * needs and costs nothing extra to get.
147
+ */
148
+ function condense(relations, allowed) {
149
+ const count = relations.names.length;
150
+ const { offset, target, kind } = relations.depends;
151
+ const index = new Int32Array(count).fill(-1);
152
+ const low = new Int32Array(count);
153
+ const onStack = new Uint8Array(count);
154
+ const component = new Int32Array(count).fill(-1);
155
+ const pending = [];
156
+ const order = [];
157
+ // Frames as two parallel arrays: the node, and how far through its edges it is.
158
+ const frameNode = [];
159
+ const frameEdge = [];
160
+ let counter = 0;
161
+ for (let root = 0; root < count; root += 1) {
162
+ if (index[root] !== -1)
163
+ continue;
164
+ index[root] = counter;
165
+ low[root] = counter;
166
+ counter += 1;
167
+ pending.push(root);
168
+ onStack[root] = 1;
169
+ frameNode.push(root);
170
+ frameEdge.push(offset[root]);
171
+ while (frameNode.length > 0) {
172
+ const node = frameNode[frameNode.length - 1];
173
+ const at = frameEdge[frameEdge.length - 1];
174
+ if (at < offset[node + 1]) {
175
+ frameEdge[frameEdge.length - 1] = at + 1;
176
+ if (allowed[kind[at]] !== 1)
177
+ continue;
178
+ const next = target[at];
179
+ if (index[next] === -1) {
180
+ index[next] = counter;
181
+ low[next] = counter;
182
+ counter += 1;
183
+ pending.push(next);
184
+ onStack[next] = 1;
185
+ frameNode.push(next);
186
+ frameEdge.push(offset[next]);
187
+ }
188
+ else if (onStack[next] === 1) {
189
+ if (index[next] < low[node])
190
+ low[node] = index[next];
191
+ }
192
+ continue;
193
+ }
194
+ frameNode.pop();
195
+ frameEdge.pop();
196
+ const parent = frameNode[frameNode.length - 1];
197
+ if (parent !== undefined && low[node] < low[parent])
198
+ low[parent] = low[node];
199
+ if (low[node] !== index[node])
200
+ continue;
201
+ const members = [];
202
+ for (;;) {
203
+ const member = pending.pop();
204
+ onStack[member] = 0;
205
+ component[member] = order.length;
206
+ members.push(member);
207
+ if (member === node)
208
+ break;
209
+ }
210
+ order.push(members.sort((a, b) => a - b));
211
+ }
212
+ }
213
+ return { component, order };
214
+ }
215
+ /**
216
+ * The edge kinds a closure walks when the caller does not say.
217
+ *
218
+ * Listed rather than derived from `EDGE_KINDS`, and the difference is the whole
219
+ * point. A digest is a claim about *which inputs* were folded, so widening the
220
+ * fold silently changes every digest that exists — one added edge kind, and the
221
+ * next run finds nothing it can prove unchanged and re-observes the entire suite
222
+ * while reporting a successful narrowing. Adding a kind here is a decision with a
223
+ * cost, and it should read like one.
224
+ */
225
+ export const CLOSURE_EDGES = [
226
+ 'imports',
227
+ 'reexports',
228
+ 'dynamic',
229
+ 'type',
230
+ 'asset',
231
+ 'declared-in',
232
+ ];
233
+ /** Edge kinds as a lookup, so the inner loop tests a byte. */
234
+ function allowedKinds(through) {
235
+ const allowed = new Uint8Array(EDGE_KINDS.length);
236
+ for (const kind of through ?? CLOSURE_EDGES) {
237
+ const at = EDGE_KINDS.indexOf(kind);
238
+ if (at !== -1)
239
+ allowed[at] = 1;
240
+ }
241
+ return allowed;
242
+ }
243
+ function byCodeUnit(a, b) {
244
+ return a < b ? -1 : a > b ? 1 : 0;
245
+ }
246
+ //# sourceMappingURL=merkle.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"merkle.js","sourceRoot":"","sources":["../../src/relate/merkle.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The graph, hashed the way a build system hashes an action's inputs.\n *\n * Reachability answers *what could this diff have moved*. It needs a diff, which\n * means it needs `git`, a ref that exists, a checkout deep enough to contain it,\n * and the assumption that the ref is where this branch actually diverged. Every\n * one of those is a thing CI gets wrong, and each failure narrows a run.\n *\n * A Merkle digest asks a different question and needs none of them: **is this\n * subject's entire input closure byte-identical to the one that produced the\n * baseline?** One digest per node, over the node's own content and the digests of\n * everything it rests on — the same construction `bazel` uses to key an action,\n * and the same idea as this project's own content addressing, applied one level\n * out from the document to the source that produced it.\n *\n * What the difference buys:\n *\n * | situation | reachability | closure digest |\n * |---|---|---|\n * | a change, then a revert | both commits are in the diff, so it widens | identical digest, nothing runs |\n * | rebase, squash, branch switch | the merge base moves and the diff with it | unaffected — no ref is consulted |\n * | a shallow clone with no merge base | cannot answer; runs everything | unaffected |\n * | a dependency changed two hops away | reached, and observed | different digest, and observed |\n * | *why* a subject is being observed | a chain of files | a digest that differs |\n *\n * Neither replaces the other. The trail is the explanation, and the digest is the\n * proof; a run wants the digest to decide and the trail to justify.\n *\n * ## Cycles\n *\n * A dependency graph has them and a Merkle tree cannot. Strongly connected\n * components are condensed and hashed as a unit, so every file in a cycle carries\n * the same digest — which is exactly the truth about a cycle: no member of one can\n * be called unchanged while another moved.\n *\n * ## The digest that must never lie\n *\n * A digest says *these inputs are the same*. A file whose content was not supplied,\n * or whose own imports could not be read, breaks that claim — its closure may have\n * moved with no digest in this structure changing. Those nodes are marked\n * **volatile**, the mark propagates to everything that rests on them, and a\n * volatile node is treated as changed however its digest compares. A cache that\n * cannot be trusted must not be silently trusted.\n */\n\nimport { digestCombine, digestString, type Digest } from '../format/hash.js';\nimport {\n EDGE_KINDS,\n NODE_KINDS,\n keyOf,\n type EdgeKind,\n type NodeId,\n type Relations,\n} from './graph.js';\n\nexport interface ClosureInput {\n readonly relations: Relations;\n\n /**\n * Content digest per file, keyed by the file's path.\n *\n * A file with no entry is volatile: nothing here can tell whether it moved.\n * Component nodes take no entry — a component has no bytes of its own, and its\n * content is the file that declares it, which it already depends on.\n */\n readonly content: ReadonlyMap<string, Digest>;\n\n /** Restrict the closure to these edge kinds. Every kind by default. */\n readonly through?: Iterable<EdgeKind>;\n}\n\nexport interface Closure {\n /** `keyOf(kind, name)` → the digest of everything that node rests on. */\n readonly digests: ReadonlyMap<string, Digest>;\n\n /**\n * Nodes whose digest cannot prove sameness, and everything resting on them.\n *\n * Not an error. It is the honest half of the answer, and a caller that ignores\n * it converts a missing input into a subject nobody observed.\n */\n readonly volatile: ReadonlySet<string>;\n}\n\n/**\n * Hash every node over its own content and its whole dependency closure.\n *\n * One pass of Tarjan's algorithm and one pass over the components it emits, which\n * is `O(n + m)` — the same cost as the traversal next door, for an answer that\n * survives a rebase.\n */\nexport function closureOf(input: ClosureInput): Closure {\n const { relations, content } = input;\n const allowed = allowedKinds(input.through);\n const count = relations.names.length;\n\n const { component, order } = condense(relations, allowed);\n const digests = new Map<string, Digest>();\n const unstable = new Set<string>();\n\n // One digest per component, so every member of a cycle carries the same one.\n const byComponent: Digest[] = [];\n const shakyComponent: boolean[] = [];\n\n // Tarjan emits a component only after everything it can reach, so a dependency\n // digest is always already computed by the time it is read here.\n for (const [id, members] of order.entries()) {\n const terms: string[] = [];\n const outward = new Set<string>();\n let shaky = false;\n\n for (const node of members) {\n const kind = NODE_KINDS[relations.kinds[node]!]!;\n const name = relations.names[node]!;\n const own = kind === 'file' ? content.get(name) : SETTLED;\n\n if (own === undefined || relations.unknown[node] === 1) shaky = true;\n terms.push(`${kind}\\u0000${name}\\u0000${own ?? UNREAD}`);\n\n const { offset, target, kind: edgeKind } = relations.depends;\n for (let at = offset[node]!; at < offset[node + 1]!; at += 1) {\n if (allowed[edgeKind[at]!] !== 1) continue;\n\n const other = target[at]!;\n if (component[other] === id) continue;\n\n outward.add(`${EDGE_KINDS[edgeKind[at]!]!}\\u0000${byComponent[component[other]!]!}`);\n if (shakyComponent[component[other]!] === true) shaky = true;\n }\n }\n\n byComponent.push(\n digestCombine('relate.closure', [\n digestString([...terms].sort(byCodeUnit).join('\u0001')),\n digestString([...outward].sort(byCodeUnit).join('\u0001')),\n ]),\n );\n shakyComponent.push(shaky);\n }\n\n for (let node = 0; node < count; node += 1) {\n const key = keyOf(NODE_KINDS[relations.kinds[node]!]!, relations.names[node]!);\n digests.set(key, byComponent[component[node]!]!);\n if (shakyComponent[component[node]!] === true) unstable.add(key);\n }\n\n return { digests, volatile: unstable };\n}\n\n/** The term a node with content contributes when the content is not known. */\nconst UNREAD = '(unread)';\n\n/** The term a node that has no content of its own contributes. */\nconst SETTLED = '(declared)';\n\nexport interface Drift {\n /** Node keys whose closure is not provably the one it was. */\n readonly changed: readonly string[];\n /** The file paths among them. */\n readonly files: readonly string[];\n /** The component names among them. */\n readonly components: readonly string[];\n /** Node keys the earlier closure held and this one does not. */\n readonly gone: readonly string[];\n}\n\n/**\n * What moved between two closures, without consulting a repository.\n *\n * Three ways a node lands in `changed`, and only the first is a difference: its\n * digest differs, it is **new**, or it is **volatile** — the last because a node\n * whose inputs were not fully read has a digest that can match by accident, and a\n * selector that believed it would skip a subject on the strength of a hash over\n * bytes nobody hashed.\n */\nexport function driftedBetween(before: Closure, after: Closure): Drift {\n const changed: string[] = [];\n\n for (const [key, digest] of after.digests) {\n const was = before.digests.get(key);\n if (was === digest && !after.volatile.has(key) && !before.volatile.has(key)) continue;\n changed.push(key);\n }\n\n const gone = [...before.digests.keys()].filter((key) => !after.digests.has(key));\n\n return {\n changed: changed.sort(byCodeUnit),\n files: named(changed, 'file'),\n components: named(changed, 'component'),\n gone: gone.sort(byCodeUnit),\n };\n}\n\nfunction named(keys: readonly string[], kind: 'file' | 'component'): readonly string[] {\n const prefix = `${kind}:`;\n\n return keys\n .filter((key) => key.startsWith(prefix))\n .map((key) => key.slice(prefix.length))\n .sort(byCodeUnit);\n}\n\ninterface Condensation {\n /** Component index per node. */\n readonly component: Int32Array;\n /** Components in emission order — every one after everything it reaches. */\n readonly order: readonly (readonly NodeId[])[];\n}\n\n/**\n * Tarjan's strongly connected components, iteratively.\n *\n * Iterative because the recursion depth is the longest dependency chain in the\n * repository, and a deep monorepo would overflow a call stack on a structure whose\n * whole point is to be cheap. The emission order is the useful half: a component\n * appears only once everything it can reach has, which is the order a Merkle fold\n * needs and costs nothing extra to get.\n */\nfunction condense(relations: Relations, allowed: Uint8Array): Condensation {\n const count = relations.names.length;\n const { offset, target, kind } = relations.depends;\n\n const index = new Int32Array(count).fill(-1);\n const low = new Int32Array(count);\n const onStack = new Uint8Array(count);\n const component = new Int32Array(count).fill(-1);\n\n const pending: NodeId[] = [];\n const order: NodeId[][] = [];\n\n // Frames as two parallel arrays: the node, and how far through its edges it is.\n const frameNode: number[] = [];\n const frameEdge: number[] = [];\n let counter = 0;\n\n for (let root = 0; root < count; root += 1) {\n if (index[root] !== -1) continue;\n\n index[root] = counter;\n low[root] = counter;\n counter += 1;\n pending.push(root);\n onStack[root] = 1;\n frameNode.push(root);\n frameEdge.push(offset[root]!);\n\n while (frameNode.length > 0) {\n const node = frameNode[frameNode.length - 1]!;\n const at = frameEdge[frameEdge.length - 1]!;\n\n if (at < offset[node + 1]!) {\n frameEdge[frameEdge.length - 1] = at + 1;\n if (allowed[kind[at]!] !== 1) continue;\n\n const next = target[at]!;\n if (index[next] === -1) {\n index[next] = counter;\n low[next] = counter;\n counter += 1;\n pending.push(next);\n onStack[next] = 1;\n frameNode.push(next);\n frameEdge.push(offset[next]!);\n } else if (onStack[next] === 1) {\n if (index[next]! < low[node]!) low[node] = index[next]!;\n }\n continue;\n }\n\n frameNode.pop();\n frameEdge.pop();\n\n const parent = frameNode[frameNode.length - 1];\n if (parent !== undefined && low[node]! < low[parent]!) low[parent] = low[node]!;\n\n if (low[node] !== index[node]) continue;\n\n const members: NodeId[] = [];\n for (;;) {\n const member = pending.pop()!;\n onStack[member] = 0;\n component[member] = order.length;\n members.push(member);\n if (member === node) break;\n }\n order.push(members.sort((a, b) => a - b));\n }\n }\n\n return { component, order };\n}\n\n/**\n * The edge kinds a closure walks when the caller does not say.\n *\n * Listed rather than derived from `EDGE_KINDS`, and the difference is the whole\n * point. A digest is a claim about *which inputs* were folded, so widening the\n * fold silently changes every digest that exists — one added edge kind, and the\n * next run finds nothing it can prove unchanged and re-observes the entire suite\n * while reporting a successful narrowing. Adding a kind here is a decision with a\n * cost, and it should read like one.\n */\nexport const CLOSURE_EDGES: readonly EdgeKind[] = [\n 'imports',\n 'reexports',\n 'dynamic',\n 'type',\n 'asset',\n 'declared-in',\n];\n\n/** Edge kinds as a lookup, so the inner loop tests a byte. */\nfunction allowedKinds(through: Iterable<EdgeKind> | undefined): Uint8Array {\n const allowed = new Uint8Array(EDGE_KINDS.length);\n\n for (const kind of through ?? CLOSURE_EDGES) {\n const at = EDGE_KINDS.indexOf(kind);\n if (at !== -1) allowed[at] = 1;\n }\n\n return allowed;\n}\n\nfunction byCodeUnit(a: string, b: string): number {\n return a < b ? -1 : a > b ? 1 : 0;\n}\n"]}
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Walking the graph, and being able to say why the walk arrived.
3
+ *
4
+ * Two questions, one traversal each way:
5
+ *
6
+ * - **`dependentsOf`** — against the arrows. *What could this change have moved?*
7
+ * This is the one selection asks, and it is asked once per run over the whole
8
+ * seed set rather than once per seed, because a breadth-first search from many
9
+ * sources costs the same as one from a single source.
10
+ * - **`dependenciesOf`** — along them. *What does this rest on?* The cache-key
11
+ * question, and the one a reviewer asks about a file they have not read.
12
+ *
13
+ * ## The trail is not an extra
14
+ *
15
+ * Both return the path each node was reached by, and that is load-bearing rather
16
+ * than a convenience. A selector that says *observe 41 of 300* is asking to be
17
+ * trusted; a selector that says *`Button` is affected because
18
+ * `src/tokens.css` → `src/button.css` → `src/Button.tsx`* has shown its work, and
19
+ * the operator can see the edge that is wrong when one is.
20
+ *
21
+ * Recording it costs one `Int32Array` and nothing in the inner loop — the parent
22
+ * is written exactly where the visited mark already is.
23
+ *
24
+ * ## Cycles
25
+ *
26
+ * Import graphs have them, routinely and legitimately. Nothing here treats a
27
+ * cycle as an error: the visited mask means every node is expanded once, so a
28
+ * cycle terminates the same way any revisit does.
29
+ */
30
+ import { type EdgeKind, type NodeId, type Relations } from './graph.js';
31
+ export interface Reach {
32
+ /** `1` where the node was reached, including the seeds themselves. */
33
+ readonly mask: Uint8Array;
34
+ /**
35
+ * The node that reached each one, or `-1` for a seed and for anything
36
+ * unreached. Following it lands on a seed, which is what `trailOf` walks.
37
+ */
38
+ readonly via: Int32Array;
39
+ /** Reached nodes in id order, which is kind-major and then code unit. */
40
+ readonly reached: readonly NodeId[];
41
+ }
42
+ export interface ReachOptions {
43
+ /**
44
+ * Edge kinds to walk. Every kind, when absent.
45
+ *
46
+ * The default is deliberately the widest one. Narrowing here is how a caller
47
+ * says *a type-only import cannot have moved a pixel*, which is true and is
48
+ * still not the default, because the cost of being wrong about it is a green
49
+ * run over a surface nobody looked at.
50
+ */
51
+ readonly through?: Iterable<EdgeKind>;
52
+ }
53
+ /** What depends on these nodes, transitively. Against the arrows. */
54
+ export declare function dependentsOf(relations: Relations, seeds: Iterable<NodeId>, options?: ReachOptions): Reach;
55
+ /** What these nodes depend on, transitively. Along the arrows. */
56
+ export declare function dependenciesOf(relations: Relations, seeds: Iterable<NodeId>, options?: ReachOptions): Reach;
57
+ /**
58
+ * The chain from a reached node back to the seed that reached it.
59
+ *
60
+ * In arrival order — seed first, `id` last — because that is the direction the
61
+ * sentence reads: *the token file, then the stylesheet, then the component*.
62
+ * Empty when the node was never reached.
63
+ */
64
+ export declare function trailOf(reach: Reach, id: NodeId): readonly NodeId[];
65
+ //# sourceMappingURL=reach.d.ts.map
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Walking the graph, and being able to say why the walk arrived.
3
+ *
4
+ * Two questions, one traversal each way:
5
+ *
6
+ * - **`dependentsOf`** — against the arrows. *What could this change have moved?*
7
+ * This is the one selection asks, and it is asked once per run over the whole
8
+ * seed set rather than once per seed, because a breadth-first search from many
9
+ * sources costs the same as one from a single source.
10
+ * - **`dependenciesOf`** — along them. *What does this rest on?* The cache-key
11
+ * question, and the one a reviewer asks about a file they have not read.
12
+ *
13
+ * ## The trail is not an extra
14
+ *
15
+ * Both return the path each node was reached by, and that is load-bearing rather
16
+ * than a convenience. A selector that says *observe 41 of 300* is asking to be
17
+ * trusted; a selector that says *`Button` is affected because
18
+ * `src/tokens.css` → `src/button.css` → `src/Button.tsx`* has shown its work, and
19
+ * the operator can see the edge that is wrong when one is.
20
+ *
21
+ * Recording it costs one `Int32Array` and nothing in the inner loop — the parent
22
+ * is written exactly where the visited mark already is.
23
+ *
24
+ * ## Cycles
25
+ *
26
+ * Import graphs have them, routinely and legitimately. Nothing here treats a
27
+ * cycle as an error: the visited mask means every node is expanded once, so a
28
+ * cycle terminates the same way any revisit does.
29
+ */
30
+ import { EDGE_KINDS } from './graph.js';
31
+ /** What depends on these nodes, transitively. Against the arrows. */
32
+ export function dependentsOf(relations, seeds, options = {}) {
33
+ return search(relations.dependents, relations.names.length, seeds, options);
34
+ }
35
+ /** What these nodes depend on, transitively. Along the arrows. */
36
+ export function dependenciesOf(relations, seeds, options = {}) {
37
+ return search(relations.depends, relations.names.length, seeds, options);
38
+ }
39
+ /**
40
+ * The chain from a reached node back to the seed that reached it.
41
+ *
42
+ * In arrival order — seed first, `id` last — because that is the direction the
43
+ * sentence reads: *the token file, then the stylesheet, then the component*.
44
+ * Empty when the node was never reached.
45
+ */
46
+ export function trailOf(reach, id) {
47
+ if (reach.mask[id] !== 1)
48
+ return [];
49
+ const trail = [];
50
+ for (let at = id; at !== -1; at = reach.via[at])
51
+ trail.push(at);
52
+ return trail.reverse();
53
+ }
54
+ /**
55
+ * Breadth-first over one direction, from every seed at once.
56
+ *
57
+ * A plain array as the queue with a moving head rather than `shift()`, which is
58
+ * `O(n)` per call in every engine that does not special-case it and turns a
59
+ * linear traversal quadratic on the graphs this exists for.
60
+ *
61
+ * Breadth-first rather than depth-first for the trail's sake: the parent
62
+ * recorded is then the one on a *shortest* path, so the explanation a run prints
63
+ * is the shortest true one rather than whichever the stack happened to unwind.
64
+ */
65
+ function search(adjacency, nodes, seeds, options) {
66
+ const mask = new Uint8Array(nodes);
67
+ const via = new Int32Array(nodes).fill(-1);
68
+ const queue = [];
69
+ for (const seed of seeds) {
70
+ if (seed < 0 || seed >= nodes || mask[seed] === 1)
71
+ continue;
72
+ mask[seed] = 1;
73
+ queue.push(seed);
74
+ }
75
+ const allowed = allowedKinds(options.through);
76
+ for (let head = 0; head < queue.length; head += 1) {
77
+ const node = queue[head];
78
+ const end = adjacency.offset[node + 1];
79
+ for (let at = adjacency.offset[node]; at < end; at += 1) {
80
+ if (allowed !== undefined && allowed[adjacency.kind[at]] !== 1)
81
+ continue;
82
+ const next = adjacency.target[at];
83
+ if (mask[next] === 1)
84
+ continue;
85
+ mask[next] = 1;
86
+ via[next] = node;
87
+ queue.push(next);
88
+ }
89
+ }
90
+ // `queue` is in visit order, which depends on seed order. The result is in id
91
+ // order instead, so a report built from it is byte-stable whatever order the
92
+ // caller listed its changed files in.
93
+ const reached = [];
94
+ for (let id = 0; id < nodes; id += 1)
95
+ if (mask[id] === 1)
96
+ reached.push(id);
97
+ return { mask, via, reached };
98
+ }
99
+ /** A kind filter as a byte lookup, or `undefined` when every kind is walked. */
100
+ function allowedKinds(through) {
101
+ if (through === undefined)
102
+ return undefined;
103
+ const allowed = new Uint8Array(EDGE_KINDS.length);
104
+ for (const kind of through) {
105
+ const at = EDGE_KINDS.indexOf(kind);
106
+ if (at !== -1)
107
+ allowed[at] = 1;
108
+ }
109
+ return allowed;
110
+ }
111
+ //# sourceMappingURL=reach.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reach.js","sourceRoot":"","sources":["../../src/relate/reach.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,OAAO,EAAE,UAAU,EAA8D,MAAM,YAAY,CAAC;AA0BpG,qEAAqE;AACrE,MAAM,UAAU,YAAY,CAC1B,SAAoB,EACpB,KAAuB,EACvB,OAAO,GAAiB,EAAE;IAE1B,OAAO,MAAM,CAAC,SAAS,CAAC,UAAU,EAAE,SAAS,CAAC,KAAK,CAAC,MAAM,EAAE,KAAK,EAAE,OAAO,CAAC,CAAC;AAC9E,CAAC;AAED,kEAAkE;AAClE,MAAM,UAAU,cAAc,CAC5B,SAAoB,EACpB,KAAuB,EACvB,OAAO,GAAiB,EAAE;IAE1B,OAAO,MAAM,CAAC,SAAS,CAAC,OAAO,EAAE,SAAS,CAAC,KAAK,CAAC,MAAM,EAAE,KAAK,EAAE,OAAO,CAAC,CAAC;AAC3E,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,OAAO,CAAC,KAAY,EAAE,EAAU;IAC9C,IAAI,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IAEpC,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,KAAK,IAAI,EAAE,GAAW,EAAE,EAAE,EAAE,KAAK,CAAC,CAAC,EAAE,EAAE,GAAG,KAAK,CAAC,GAAG,CAAC,EAAE,CAAE;QAAE,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACzE,OAAO,KAAK,CAAC,OAAO,EAAE,CAAC;AACzB,CAAC;AAED;;;;;;;;;;GAUG;AACH,SAAS,MAAM,CACb,SAAoB,EACpB,KAAa,EACb,KAAuB,EACvB,OAAqB;IAErB,MAAM,IAAI,GAAG,IAAI,UAAU,CAAC,KAAK,CAAC,CAAC;IACnC,MAAM,GAAG,GAAG,IAAI,UAAU,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;IAC3C,MAAM,KAAK,GAAa,EAAE,CAAC;IAE3B,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,IAAI,IAAI,GAAG,CAAC,IAAI,IAAI,IAAI,KAAK,IAAI,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC;YAAE,SAAS;QAC5D,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACf,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACnB,CAAC;IAED,MAAM,OAAO,GAAG,YAAY,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;IAE9C,KAAK,IAAI,IAAI,GAAG,CAAC,EAAE,IAAI,GAAG,KAAK,CAAC,MAAM,EAAE,IAAI,IAAI,CAAC,EAAE,CAAC;QAClD,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAE,CAAC;QAC1B,MAAM,GAAG,GAAG,SAAS,CAAC,MAAM,CAAC,IAAI,GAAG,CAAC,CAAE,CAAC;QAExC,KAAK,IAAI,EAAE,GAAG,SAAS,CAAC,MAAM,CAAC,IAAI,CAAE,EAAE,EAAE,GAAG,GAAG,EAAE,EAAE,IAAI,CAAC,EAAE,CAAC;YACzD,IAAI,OAAO,KAAK,SAAS,IAAI,OAAO,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,CAAE,CAAC,KAAK,CAAC;gBAAE,SAAS;YAE1E,MAAM,IAAI,GAAG,SAAS,CAAC,MAAM,CAAC,EAAE,CAAE,CAAC;YACnC,IAAI,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC;gBAAE,SAAS;YAE/B,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;YACf,GAAG,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;YACjB,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACnB,CAAC;IACH,CAAC;IAED,8EAA8E;IAC9E,6EAA6E;IAC7E,sCAAsC;IACtC,MAAM,OAAO,GAAa,EAAE,CAAC;IAC7B,KAAK,IAAI,EAAE,GAAG,CAAC,EAAE,EAAE,GAAG,KAAK,EAAE,EAAE,IAAI,CAAC;QAAE,IAAI,IAAI,CAAC,EAAE,CAAC,KAAK,CAAC;YAAE,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAE3E,OAAO,EAAE,IAAI,EAAE,GAAG,EAAE,OAAO,EAAE,CAAC;AAChC,CAAC;AAED,gFAAgF;AAChF,SAAS,YAAY,CAAC,OAAuC;IAC3D,IAAI,OAAO,KAAK,SAAS;QAAE,OAAO,SAAS,CAAC;IAE5C,MAAM,OAAO,GAAG,IAAI,UAAU,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC;IAClD,KAAK,MAAM,IAAI,IAAI,OAAO,EAAE,CAAC;QAC3B,MAAM,EAAE,GAAG,UAAU,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;QACpC,IAAI,EAAE,KAAK,CAAC,CAAC;YAAE,OAAO,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC;IACjC,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC","sourcesContent":["/**\n * Walking the graph, and being able to say why the walk arrived.\n *\n * Two questions, one traversal each way:\n *\n * - **`dependentsOf`** — against the arrows. *What could this change have moved?*\n * This is the one selection asks, and it is asked once per run over the whole\n * seed set rather than once per seed, because a breadth-first search from many\n * sources costs the same as one from a single source.\n * - **`dependenciesOf`** — along them. *What does this rest on?* The cache-key\n * question, and the one a reviewer asks about a file they have not read.\n *\n * ## The trail is not an extra\n *\n * Both return the path each node was reached by, and that is load-bearing rather\n * than a convenience. A selector that says *observe 41 of 300* is asking to be\n * trusted; a selector that says *`Button` is affected because\n * `src/tokens.css` → `src/button.css` → `src/Button.tsx`* has shown its work, and\n * the operator can see the edge that is wrong when one is.\n *\n * Recording it costs one `Int32Array` and nothing in the inner loop — the parent\n * is written exactly where the visited mark already is.\n *\n * ## Cycles\n *\n * Import graphs have them, routinely and legitimately. Nothing here treats a\n * cycle as an error: the visited mask means every node is expanded once, so a\n * cycle terminates the same way any revisit does.\n */\n\nimport { EDGE_KINDS, type Adjacency, type EdgeKind, type NodeId, type Relations } from './graph.js';\n\nexport interface Reach {\n /** `1` where the node was reached, including the seeds themselves. */\n readonly mask: Uint8Array;\n /**\n * The node that reached each one, or `-1` for a seed and for anything\n * unreached. Following it lands on a seed, which is what `trailOf` walks.\n */\n readonly via: Int32Array;\n /** Reached nodes in id order, which is kind-major and then code unit. */\n readonly reached: readonly NodeId[];\n}\n\nexport interface ReachOptions {\n /**\n * Edge kinds to walk. Every kind, when absent.\n *\n * The default is deliberately the widest one. Narrowing here is how a caller\n * says *a type-only import cannot have moved a pixel*, which is true and is\n * still not the default, because the cost of being wrong about it is a green\n * run over a surface nobody looked at.\n */\n readonly through?: Iterable<EdgeKind>;\n}\n\n/** What depends on these nodes, transitively. Against the arrows. */\nexport function dependentsOf(\n relations: Relations,\n seeds: Iterable<NodeId>,\n options: ReachOptions = {},\n): Reach {\n return search(relations.dependents, relations.names.length, seeds, options);\n}\n\n/** What these nodes depend on, transitively. Along the arrows. */\nexport function dependenciesOf(\n relations: Relations,\n seeds: Iterable<NodeId>,\n options: ReachOptions = {},\n): Reach {\n return search(relations.depends, relations.names.length, seeds, options);\n}\n\n/**\n * The chain from a reached node back to the seed that reached it.\n *\n * In arrival order — seed first, `id` last — because that is the direction the\n * sentence reads: *the token file, then the stylesheet, then the component*.\n * Empty when the node was never reached.\n */\nexport function trailOf(reach: Reach, id: NodeId): readonly NodeId[] {\n if (reach.mask[id] !== 1) return [];\n\n const trail: NodeId[] = [];\n for (let at: number = id; at !== -1; at = reach.via[at]!) trail.push(at);\n return trail.reverse();\n}\n\n/**\n * Breadth-first over one direction, from every seed at once.\n *\n * A plain array as the queue with a moving head rather than `shift()`, which is\n * `O(n)` per call in every engine that does not special-case it and turns a\n * linear traversal quadratic on the graphs this exists for.\n *\n * Breadth-first rather than depth-first for the trail's sake: the parent\n * recorded is then the one on a *shortest* path, so the explanation a run prints\n * is the shortest true one rather than whichever the stack happened to unwind.\n */\nfunction search(\n adjacency: Adjacency,\n nodes: number,\n seeds: Iterable<NodeId>,\n options: ReachOptions,\n): Reach {\n const mask = new Uint8Array(nodes);\n const via = new Int32Array(nodes).fill(-1);\n const queue: NodeId[] = [];\n\n for (const seed of seeds) {\n if (seed < 0 || seed >= nodes || mask[seed] === 1) continue;\n mask[seed] = 1;\n queue.push(seed);\n }\n\n const allowed = allowedKinds(options.through);\n\n for (let head = 0; head < queue.length; head += 1) {\n const node = queue[head]!;\n const end = adjacency.offset[node + 1]!;\n\n for (let at = adjacency.offset[node]!; at < end; at += 1) {\n if (allowed !== undefined && allowed[adjacency.kind[at]!] !== 1) continue;\n\n const next = adjacency.target[at]!;\n if (mask[next] === 1) continue;\n\n mask[next] = 1;\n via[next] = node;\n queue.push(next);\n }\n }\n\n // `queue` is in visit order, which depends on seed order. The result is in id\n // order instead, so a report built from it is byte-stable whatever order the\n // caller listed its changed files in.\n const reached: NodeId[] = [];\n for (let id = 0; id < nodes; id += 1) if (mask[id] === 1) reached.push(id);\n\n return { mask, via, reached };\n}\n\n/** A kind filter as a byte lookup, or `undefined` when every kind is walked. */\nfunction allowedKinds(through: Iterable<EdgeKind> | undefined): Uint8Array | undefined {\n if (through === undefined) return undefined;\n\n const allowed = new Uint8Array(EDGE_KINDS.length);\n for (const kind of through) {\n const at = EDGE_KINDS.indexOf(kind);\n if (at !== -1) allowed[at] = 1;\n }\n return allowed;\n}\n"]}
@@ -0,0 +1,129 @@
1
+ /**
2
+ * One file's outgoing edges, and the fold that turns a pile of them into a graph.
3
+ *
4
+ * A record is what reading **one** file produced, and that is the unit on
5
+ * purpose. It makes the expensive half — open, parse, resolve — cacheable by
6
+ * content digest and parallelizable per file, and it leaves the graph a pure
7
+ * function of the records, so the structure can be rebuilt, sharded or merged
8
+ * without touching a disk. `core` performs no I/O (ADR-0006); whoever owns the
9
+ * disk produces these and hands them over.
10
+ *
11
+ * ## An empty record and an unreadable one are different facts
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*).
19
+ */
20
+ import type { Digest } from '../format/hash.js';
21
+ import { type EdgeKind, type Node, type Relations } from './graph.js';
22
+ import { type Reach } from './reach.js';
23
+ export interface FileEdge {
24
+ /** Repository-relative, already resolved. A specifier is not an edge. */
25
+ readonly to: string;
26
+ readonly kind: EdgeKind;
27
+ }
28
+ export interface FileRecord {
29
+ /** Repository-relative, so a graph is portable between machines and CI. */
30
+ readonly file: string;
31
+ /**
32
+ * The content digest this record was read from.
33
+ *
34
+ * Two jobs. It lets a second scan skip the parse for a file that has not moved,
35
+ * which is the difference between a scan that costs a second and one nobody
36
+ * leaves enabled. And it is the leaf of the Merkle fold in
37
+ * [`merkle.ts`](./merkle.ts) — a file with no digest is one nothing can prove
38
+ * unchanged, and is marked as such rather than assumed still.
39
+ */
40
+ readonly digest?: Digest;
41
+ /** Resolved outgoing edges. Absent and empty mean the same thing here. */
42
+ readonly edges?: readonly FileEdge[];
43
+ /** Component names this file declares, from `indexSource` or better. */
44
+ readonly declares?: readonly string[];
45
+ /**
46
+ * Specifiers that were read but did not resolve.
47
+ *
48
+ * Kept for the report rather than for the graph. A bare specifier that failed
49
+ * to resolve is usually a package that is not installed; a **relative** one
50
+ * that failed is a hole, and whoever produced the record says so by also
51
+ * setting `unknown`.
52
+ */
53
+ readonly unresolved?: readonly string[];
54
+ /**
55
+ * Why this file's outgoing edges could not be enumerated, when they could not.
56
+ *
57
+ * Present means *this file may depend on anything*. The sentence is carried
58
+ * rather than a flag, because it ends up in the paragraph explaining why a run
59
+ * observed more than the operator expected.
60
+ */
61
+ readonly unknown?: string;
62
+ }
63
+ /**
64
+ * Fold file records into the graph.
65
+ *
66
+ * Every record contributes its own node even when it has no edges, so a changed
67
+ * file that imports nothing is still findable — otherwise the one file in the
68
+ * diff would be missing from the structure that exists to answer questions about
69
+ * it.
70
+ *
71
+ * A declaration becomes an edge from the **component to the file**, which is the
72
+ * direction the convention requires: the component depends on the file that
73
+ * declares it, so one walk against the arrows from a changed file reaches every
74
+ * importer and every component in the same pass.
75
+ */
76
+ export declare function relationsOfFiles(records: Iterable<FileRecord>): Relations;
77
+ export interface Hole {
78
+ /** The file whose outgoing edges could not be enumerated. */
79
+ readonly file: string;
80
+ /** Why, in the words of whoever read it. Absent when nobody said. */
81
+ readonly because?: string;
82
+ }
83
+ export interface Reached {
84
+ /** Files the change could have moved, including the changed files themselves. */
85
+ readonly files: readonly string[];
86
+ /** Components declared in any of them. */
87
+ readonly components: readonly string[];
88
+ /**
89
+ * Changed paths the graph does not hold.
90
+ *
91
+ * Never silently ignored. A `README.md` belongs here and means nothing; a
92
+ * source file belongs here only because the scan never reached it, and the
93
+ * caller — which knows where it told the scan to look — is the one that can
94
+ * tell those apart.
95
+ */
96
+ readonly missing: readonly string[];
97
+ /**
98
+ * Files seeded because their own edges are unknown, rather than because
99
+ * anything reached them, each with the reason it could not be read.
100
+ *
101
+ * Counted separately so that "we widened" never hides inside "we found", and
102
+ * carrying its sentence so the widening is a work item rather than a tax: *four
103
+ * files could not be read* is something to live with, and ``src/legacy.js — a
104
+ * computed require()`` is something to fix.
105
+ */
106
+ readonly opaque: readonly Hole[];
107
+ /** The traversal, kept so a caller can ask how any one file was reached. */
108
+ readonly reach: Reach;
109
+ }
110
+ /**
111
+ * What a set of changed files could have moved.
112
+ *
113
+ * The seed set is the changed files **and every file whose edges are unknown**,
114
+ * which is the rule the whole structure rests on. An unreadable file might import
115
+ * the one that changed; seeding it means everything depending on *it* is observed
116
+ * too, and the cost is a collection rather than a green run over an unwatched
117
+ * surface.
118
+ *
119
+ * One breadth-first search, whatever the number of changed files.
120
+ */
121
+ export declare function movedBy(relations: Relations, changed: Iterable<string>): Reached;
122
+ /**
123
+ * How one file or component was reached, as the chain that reached it.
124
+ *
125
+ * The sentence a run prints when somebody asks why a subject was observed. Empty
126
+ * when the node was not reached at all.
127
+ */
128
+ export declare function explain(relations: Relations, moved: Reached, node: Node): readonly string[];
129
+ //# sourceMappingURL=records.d.ts.map