@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.
- package/CHANGELOG.md +79 -0
- package/LICENSE +21 -0
- package/README.md +169 -0
- package/dist/artifact.d.ts +47 -0
- package/dist/artifact.js +2 -0
- package/dist/artifact.js.map +1 -0
- package/dist/attribute/because.d.ts +43 -0
- package/dist/attribute/because.js +64 -0
- package/dist/attribute/because.js.map +1 -0
- package/dist/attribute/boundary.d.ts +191 -0
- package/dist/attribute/boundary.js +328 -0
- package/dist/attribute/boundary.js.map +1 -0
- package/dist/attribute/call-site.d.ts +96 -0
- package/dist/attribute/call-site.js +183 -0
- package/dist/attribute/call-site.js.map +1 -0
- package/dist/attribute/component-hash.d.ts +188 -0
- package/dist/attribute/component-hash.js +312 -0
- package/dist/attribute/component-hash.js.map +1 -0
- package/dist/attribute/composition-fixture.d.ts +18 -0
- package/dist/attribute/composition-fixture.js +53 -0
- package/dist/attribute/composition-fixture.js.map +1 -0
- package/dist/attribute/composition.d.ts +182 -0
- package/dist/attribute/composition.js +169 -0
- package/dist/attribute/composition.js.map +1 -0
- package/dist/attribute/control.d.ts +43 -0
- package/dist/attribute/control.js +54 -0
- package/dist/attribute/control.js.map +1 -0
- package/dist/attribute/coverage.d.ts +99 -0
- package/dist/attribute/coverage.js +86 -0
- package/dist/attribute/coverage.js.map +1 -0
- package/dist/attribute/divergence.d.ts +102 -0
- package/dist/attribute/divergence.js +152 -0
- package/dist/attribute/divergence.js.map +1 -0
- package/dist/attribute/index.d.ts +43 -0
- package/dist/attribute/index.js +29 -0
- package/dist/attribute/index.js.map +1 -0
- package/dist/attribute/instability.d.ts +89 -0
- package/dist/attribute/instability.js +183 -0
- package/dist/attribute/instability.js.map +1 -0
- package/dist/attribute/instances.d.ts +145 -0
- package/dist/attribute/instances.js +140 -0
- package/dist/attribute/instances.js.map +1 -0
- package/dist/attribute/locate.d.ts +22 -0
- package/dist/attribute/locate.js +100 -0
- package/dist/attribute/locate.js.map +1 -0
- package/dist/attribute/mask.d.ts +156 -0
- package/dist/attribute/mask.js +201 -0
- package/dist/attribute/mask.js.map +1 -0
- package/dist/attribute/movement.d.ts +211 -0
- package/dist/attribute/movement.js +181 -0
- package/dist/attribute/movement.js.map +1 -0
- package/dist/attribute/region.d.ts +188 -0
- package/dist/attribute/region.js +144 -0
- package/dist/attribute/region.js.map +1 -0
- package/dist/attribute/source-map.d.ts +98 -0
- package/dist/attribute/source-map.js +264 -0
- package/dist/attribute/source-map.js.map +1 -0
- package/dist/attribute/source.d.ts +77 -0
- package/dist/attribute/source.js +95 -0
- package/dist/attribute/source.js.map +1 -0
- package/dist/attribute/stack.d.ts +80 -0
- package/dist/attribute/stack.js +108 -0
- package/dist/attribute/stack.js.map +1 -0
- package/dist/compare/band.d.ts +85 -0
- package/dist/compare/band.js +70 -0
- package/dist/compare/band.js.map +1 -0
- package/dist/compare/cascade.d.ts +45 -0
- package/dist/compare/cascade.js +71 -0
- package/dist/compare/cascade.js.map +1 -0
- package/dist/compare/derive.d.ts +78 -0
- package/dist/compare/derive.js +39 -0
- package/dist/compare/derive.js.map +1 -0
- package/dist/compare/diff/attribution.d.ts +23 -0
- package/dist/compare/diff/attribution.js +244 -0
- package/dist/compare/diff/attribution.js.map +1 -0
- package/dist/compare/diff/compare-nodes.d.ts +15 -0
- package/dist/compare/diff/compare-nodes.js +227 -0
- package/dist/compare/diff/compare-nodes.js.map +1 -0
- package/dist/compare/diff/components.d.ts +20 -0
- package/dist/compare/diff/components.js +76 -0
- package/dist/compare/diff/components.js.map +1 -0
- package/dist/compare/diff/delta.d.ts +165 -0
- package/dist/compare/diff/delta.js +12 -0
- package/dist/compare/diff/delta.js.map +1 -0
- package/dist/compare/diff/index.d.ts +69 -0
- package/dist/compare/diff/index.js +101 -0
- package/dist/compare/diff/index.js.map +1 -0
- package/dist/compare/diff/match.d.ts +37 -0
- package/dist/compare/diff/match.js +167 -0
- package/dist/compare/diff/match.js.map +1 -0
- package/dist/compare/explain.d.ts +23 -0
- package/dist/compare/explain.js +190 -0
- package/dist/compare/explain.js.map +1 -0
- package/dist/compare/holding-diff.d.ts +25 -0
- package/dist/compare/holding-diff.js +74 -0
- package/dist/compare/holding-diff.js.map +1 -0
- package/dist/compare/impact.d.ts +56 -0
- package/dist/compare/impact.js +124 -0
- package/dist/compare/impact.js.map +1 -0
- package/dist/compare/index.d.ts +30 -0
- package/dist/compare/index.js +22 -0
- package/dist/compare/index.js.map +1 -0
- package/dist/compare/instance.d.ts +30 -0
- package/dist/compare/instance.js +145 -0
- package/dist/compare/instance.js.map +1 -0
- package/dist/compare/observability.d.ts +49 -0
- package/dist/compare/observability.js +57 -0
- package/dist/compare/observability.js.map +1 -0
- package/dist/compare/parting.d.ts +179 -0
- package/dist/compare/parting.js +244 -0
- package/dist/compare/parting.js.map +1 -0
- package/dist/compare/slice.d.ts +74 -0
- package/dist/compare/slice.js +79 -0
- package/dist/compare/slice.js.map +1 -0
- package/dist/compare/value.d.ts +60 -0
- package/dist/compare/value.js +111 -0
- package/dist/compare/value.js.map +1 -0
- package/dist/format/accessibility.d.ts +27 -0
- package/dist/format/accessibility.js +17 -0
- package/dist/format/accessibility.js.map +1 -0
- package/dist/format/canonical.d.ts +38 -0
- package/dist/format/canonical.js +65 -0
- package/dist/format/canonical.js.map +1 -0
- package/dist/format/capture.d.ts +212 -0
- package/dist/format/capture.js +2 -0
- package/dist/format/capture.js.map +1 -0
- package/dist/format/document.d.ts +279 -0
- package/dist/format/document.js +65 -0
- package/dist/format/document.js.map +1 -0
- package/dist/format/environment.d.ts +120 -0
- package/dist/format/environment.js +53 -0
- package/dist/format/environment.js.map +1 -0
- package/dist/format/hash.d.ts +45 -0
- package/dist/format/hash.js +41 -0
- package/dist/format/hash.js.map +1 -0
- package/dist/format/holding.d.ts +134 -0
- package/dist/format/holding.js +2 -0
- package/dist/format/holding.js.map +1 -0
- package/dist/format/index.d.ts +39 -0
- package/dist/format/index.js +25 -0
- package/dist/format/index.js.map +1 -0
- package/dist/format/intervention.d.ts +87 -0
- package/dist/format/intervention.js +11 -0
- package/dist/format/intervention.js.map +1 -0
- package/dist/format/profile.d.ts +59 -0
- package/dist/format/profile.js +66 -0
- package/dist/format/profile.js.map +1 -0
- package/dist/format/provenance.d.ts +179 -0
- package/dist/format/provenance.js +222 -0
- package/dist/format/provenance.js.map +1 -0
- package/dist/format/sha256.d.ts +34 -0
- package/dist/format/sha256.js +152 -0
- package/dist/format/sha256.js.map +1 -0
- package/dist/format/snapshot.d.ts +263 -0
- package/dist/format/snapshot.js +2 -0
- package/dist/format/snapshot.js.map +1 -0
- package/dist/format/stabilize.d.ts +190 -0
- package/dist/format/stabilize.js +333 -0
- package/dist/format/stabilize.js.map +1 -0
- package/dist/format/tier.d.ts +19 -0
- package/dist/format/tier.js +11 -0
- package/dist/format/tier.js.map +1 -0
- package/dist/format/value.d.ts +115 -0
- package/dist/format/value.js +182 -0
- package/dist/format/value.js.map +1 -0
- package/dist/format/wiring.d.ts +115 -0
- package/dist/format/wiring.js +60 -0
- package/dist/format/wiring.js.map +1 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.js +35 -0
- package/dist/index.js.map +1 -0
- package/dist/judge/docket.d.ts +85 -0
- package/dist/judge/docket.js +162 -0
- package/dist/judge/docket.js.map +1 -0
- package/dist/judge/fingerprint.d.ts +53 -0
- package/dist/judge/fingerprint.js +144 -0
- package/dist/judge/fingerprint.js.map +1 -0
- package/dist/judge/ignore.d.ts +206 -0
- package/dist/judge/ignore.js +237 -0
- package/dist/judge/ignore.js.map +1 -0
- package/dist/judge/index.d.ts +44 -0
- package/dist/judge/index.js +34 -0
- package/dist/judge/index.js.map +1 -0
- package/dist/judge/inspect.d.ts +148 -0
- package/dist/judge/inspect.js +208 -0
- package/dist/judge/inspect.js.map +1 -0
- package/dist/judge/intent.d.ts +146 -0
- package/dist/judge/intent.js +150 -0
- package/dist/judge/intent.js.map +1 -0
- package/dist/judge/locale.d.ts +111 -0
- package/dist/judge/locale.js +247 -0
- package/dist/judge/locale.js.map +1 -0
- package/dist/judge/scope.d.ts +41 -0
- package/dist/judge/scope.js +47 -0
- package/dist/judge/scope.js.map +1 -0
- package/dist/judge/sensitivity.d.ts +202 -0
- package/dist/judge/sensitivity.js +152 -0
- package/dist/judge/sensitivity.js.map +1 -0
- package/dist/judge/trail.d.ts +122 -0
- package/dist/judge/trail.js +96 -0
- package/dist/judge/trail.js.map +1 -0
- package/dist/judge/verdict.d.ts +39 -0
- package/dist/judge/verdict.js +32 -0
- package/dist/judge/verdict.js.map +1 -0
- package/dist/plan/index.d.ts +12 -0
- package/dist/plan/index.js +11 -0
- package/dist/plan/index.js.map +1 -0
- package/dist/plan/tool.d.ts +85 -0
- package/dist/plan/tool.js +87 -0
- package/dist/plan/tool.js.map +1 -0
- package/dist/relate/graph.d.ts +190 -0
- package/dist/relate/graph.js +234 -0
- package/dist/relate/graph.js.map +1 -0
- package/dist/relate/index.d.ts +16 -0
- package/dist/relate/index.js +16 -0
- package/dist/relate/index.js.map +1 -0
- package/dist/relate/merkle.d.ts +110 -0
- package/dist/relate/merkle.js +246 -0
- package/dist/relate/merkle.js.map +1 -0
- package/dist/relate/reach.d.ts +65 -0
- package/dist/relate/reach.js +111 -0
- package/dist/relate/reach.js.map +1 -0
- package/dist/relate/records.d.ts +129 -0
- package/dist/relate/records.js +112 -0
- package/dist/relate/records.js.map +1 -0
- package/dist/rules/index.d.ts +19 -0
- package/dist/rules/index.js +17 -0
- package/dist/rules/index.js.map +1 -0
- package/dist/rules/normalize/alias.d.ts +50 -0
- package/dist/rules/normalize/alias.js +111 -0
- package/dist/rules/normalize/alias.js.map +1 -0
- package/dist/rules/normalize/cascade.d.ts +85 -0
- package/dist/rules/normalize/cascade.js +324 -0
- package/dist/rules/normalize/cascade.js.map +1 -0
- package/dist/rules/normalize/color.d.ts +30 -0
- package/dist/rules/normalize/color.js +185 -0
- package/dist/rules/normalize/color.js.map +1 -0
- package/dist/rules/normalize/fixture.d.ts +83 -0
- package/dist/rules/normalize/fixture.js +74 -0
- package/dist/rules/normalize/fixture.js.map +1 -0
- package/dist/rules/normalize/index.d.ts +39 -0
- package/dist/rules/normalize/index.js +268 -0
- package/dist/rules/normalize/index.js.map +1 -0
- package/dist/rules/normalize/project.d.ts +42 -0
- package/dist/rules/normalize/project.js +83 -0
- package/dist/rules/normalize/project.js.map +1 -0
- package/dist/rules/normalize/shorthand.d.ts +38 -0
- package/dist/rules/normalize/shorthand.js +388 -0
- package/dist/rules/normalize/shorthand.js.map +1 -0
- package/dist/rules/normalize/value.d.ts +23 -0
- package/dist/rules/normalize/value.js +162 -0
- package/dist/rules/normalize/value.js.map +1 -0
- package/dist/rules/normalize/wrapper.d.ts +16 -0
- package/dist/rules/normalize/wrapper.js +137 -0
- package/dist/rules/normalize/wrapper.js.map +1 -0
- package/dist/rules/ruleset.d.ts +93 -0
- package/dist/rules/ruleset.js +175 -0
- package/dist/rules/ruleset.js.map +1 -0
- package/mark.svg +30 -0
- package/package.json +69 -0
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What reaches what, stored so that asking is cheap.
|
|
3
|
+
*
|
|
4
|
+
* Every question this project asks about a change is a reachability question. A
|
|
5
|
+
* diff names files; a subject is made of components; a component is declared in a
|
|
6
|
+
* file that imports other files. *Which subjects could this diff have changed* is
|
|
7
|
+
* one traversal of one graph, and the only reason it is not asked that way today
|
|
8
|
+
* is that there has never been a graph to traverse.
|
|
9
|
+
*
|
|
10
|
+
* ## Why a structure rather than a `Map`
|
|
11
|
+
*
|
|
12
|
+
* The obvious shape is `Map<string, string[]>`, and it is wrong at the size this
|
|
13
|
+
* runs at. A monorepo scan produces tens of thousands of files and several times
|
|
14
|
+
* as many edges; the map holds one array object and one string copy per edge, and
|
|
15
|
+
* a traversal chases a pointer for every step. What replaces it here is the
|
|
16
|
+
* standard answer, and it is standard because nothing has beaten it:
|
|
17
|
+
*
|
|
18
|
+
* - **strings are interned once.** A node is an integer. Comparison is an integer
|
|
19
|
+
* compare, membership is an index into a byte array, and a visited set is a
|
|
20
|
+
* `Uint8Array` rather than a `Set<string>`.
|
|
21
|
+
* - **edges are two typed arrays** in compressed-sparse-row form: `offset` says
|
|
22
|
+
* where a node's row starts, `target` holds the rows end to end. One node's
|
|
23
|
+
* edges are contiguous, so a traversal walks memory forwards instead of
|
|
24
|
+
* following pointers into the heap.
|
|
25
|
+
* - **both directions are materialized.** *What does this depend on* and *what
|
|
26
|
+
* depends on this* are different questions, and a graph answering only the
|
|
27
|
+
* first makes the second a whole-graph scan. The transpose is a counting sort —
|
|
28
|
+
* one pass to count, one to place — so holding it costs `O(n + m)` once instead
|
|
29
|
+
* of `O(n + m)` per query.
|
|
30
|
+
*
|
|
31
|
+
* ## Edges point at what a thing depends on
|
|
32
|
+
*
|
|
33
|
+
* One convention, held everywhere: **`A → B` means A depends on B**, so a change
|
|
34
|
+
* in B may move A. Every affected-set question is therefore a walk against the
|
|
35
|
+
* arrows, which is what `dependentsOf` does, and every *what would I have to read
|
|
36
|
+
* to understand this* question is a walk along them.
|
|
37
|
+
*
|
|
38
|
+
* It decides the direction of the less obvious edges too. A component is
|
|
39
|
+
* `declared-in` the file that declares it — the component depends on the file and
|
|
40
|
+
* not the other way round — so editing that file reaches the component in the
|
|
41
|
+
* same traversal that reaches every importer.
|
|
42
|
+
*
|
|
43
|
+
* ## Nodes are typed, because files are not the last kind
|
|
44
|
+
*
|
|
45
|
+
* This ships with two kinds and the join between them: a `file` graph built from
|
|
46
|
+
* imports, and the `component` nodes the source index already knows how to find.
|
|
47
|
+
* That is deliberate rather than incidental. A component relation — *`TodoFooter`
|
|
48
|
+
* renders `Chip`* — is another edge kind between nodes of another kind in this
|
|
49
|
+
* same structure, and
|
|
50
|
+
* [`composition.md`](../../../../docs/composition.md) already computes exactly
|
|
51
|
+
* that relation from what a run rendered. One graph means the static answer and
|
|
52
|
+
* the rendered answer are joinable rather than adjacent.
|
|
53
|
+
*
|
|
54
|
+
* ## What it is not
|
|
55
|
+
*
|
|
56
|
+
* **Not a build graph.** It has no notion of a task, an output or a cache key,
|
|
57
|
+
* and it does not know what an install produced. `nx` and `turbo` own that layer,
|
|
58
|
+
* and what reads them reads their answer rather than competing with it.
|
|
59
|
+
*
|
|
60
|
+
* **Not a resolver.** Nothing here opens a file or knows what a specifier means.
|
|
61
|
+
* `core` performs no I/O (ADR-0006); the records this folds are somebody else's
|
|
62
|
+
* disk.
|
|
63
|
+
*/
|
|
64
|
+
/** Node kinds, in id order. An id is an index into this, never the word. */
|
|
65
|
+
export const NODE_KINDS = ['file', 'component'];
|
|
66
|
+
/**
|
|
67
|
+
* Edge kinds, in id order.
|
|
68
|
+
*
|
|
69
|
+
* The distinctions are kept because they explain a finding, not because the
|
|
70
|
+
* default traversal narrows on them. `type` is erased by every compiler and can
|
|
71
|
+
* change no rendering, and it is still walked unless a caller says otherwise —
|
|
72
|
+
* for the reason every rule in selection resolves the same way: a subject skipped
|
|
73
|
+
* in error is a green run over an unwatched surface, and a subject observed in
|
|
74
|
+
* error costs a collection.
|
|
75
|
+
*/
|
|
76
|
+
export const EDGE_KINDS = [
|
|
77
|
+
/** `import x from './y'` — a value import. */
|
|
78
|
+
'imports',
|
|
79
|
+
/** `export … from './y'` — an import that also republishes. */
|
|
80
|
+
'reexports',
|
|
81
|
+
/** `import('./y')`, with a specifier that was a literal. */
|
|
82
|
+
'dynamic',
|
|
83
|
+
/** `import type { T } from './y'` — erased before anything renders. */
|
|
84
|
+
'type',
|
|
85
|
+
/** A stylesheet's `@import`, or a `url()` reaching a font or an image. */
|
|
86
|
+
'asset',
|
|
87
|
+
/** A component to the file that declares it. */
|
|
88
|
+
'declared-in',
|
|
89
|
+
];
|
|
90
|
+
/**
|
|
91
|
+
* The lookup key for a node.
|
|
92
|
+
*
|
|
93
|
+
* Runtime only; nothing serializes this, and no code parses it back — the
|
|
94
|
+
* separator only has to be injective, and it is, because a kind is one of two
|
|
95
|
+
* fixed words and neither contains a `:`. A name that contains one is therefore
|
|
96
|
+
* fine.
|
|
97
|
+
*
|
|
98
|
+
* The obvious separator is a NUL, and it is the wrong one: a source file
|
|
99
|
+
* carrying that byte stops being text to `grep`, `file` and `git diff`, which is
|
|
100
|
+
* a search finding nothing looking exactly like a search that returned nothing.
|
|
101
|
+
* `tools/boundaries.check.ts` fails a build over it.
|
|
102
|
+
*/
|
|
103
|
+
export function keyOf(kind, name) {
|
|
104
|
+
return `${kind}:${name}`;
|
|
105
|
+
}
|
|
106
|
+
export function idOf(relations, kind, name) {
|
|
107
|
+
return relations.index.get(keyOf(kind, name));
|
|
108
|
+
}
|
|
109
|
+
export function nodeAt(relations, id) {
|
|
110
|
+
const name = relations.names[id];
|
|
111
|
+
if (name === undefined)
|
|
112
|
+
return undefined;
|
|
113
|
+
return { kind: NODE_KINDS[relations.kinds[id]], name };
|
|
114
|
+
}
|
|
115
|
+
/** Every node of one kind, as ids. Contiguous, because the order is kind-major. */
|
|
116
|
+
export function nodesOfKind(relations, kind) {
|
|
117
|
+
const wanted = NODE_KINDS.indexOf(kind);
|
|
118
|
+
const found = [];
|
|
119
|
+
for (let id = 0; id < relations.kinds.length; id += 1) {
|
|
120
|
+
if (relations.kinds[id] === wanted)
|
|
121
|
+
found.push(id);
|
|
122
|
+
}
|
|
123
|
+
return found;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Fold relations into the queryable form.
|
|
127
|
+
*
|
|
128
|
+
* Deterministic to the byte: nodes are ordered by kind and then by **code unit** —
|
|
129
|
+
* never `localeCompare`, which would make an id a promise about `LANG` — and edges
|
|
130
|
+
* are sorted and deduplicated, so two runs over the same records produce identical
|
|
131
|
+
* arrays and a serialized graph is stable.
|
|
132
|
+
*
|
|
133
|
+
* `O(n log n + m log m)` for the two sorts and `O(n + m)` for everything else. The
|
|
134
|
+
* transpose is a counting sort rather than a second pass over a map, which is why
|
|
135
|
+
* both directions cost one build instead of one per query.
|
|
136
|
+
*/
|
|
137
|
+
export function relationsOf(input) {
|
|
138
|
+
const relations = [...input.relations];
|
|
139
|
+
const unknownNodes = [...(input.unknown ?? [])].map((entry) => Array.isArray(entry) ? entry : [entry, undefined]);
|
|
140
|
+
const seen = new Map();
|
|
141
|
+
const see = (node) => {
|
|
142
|
+
const key = keyOf(node.kind, node.name);
|
|
143
|
+
if (!seen.has(key))
|
|
144
|
+
seen.set(key, node);
|
|
145
|
+
};
|
|
146
|
+
for (const relation of relations) {
|
|
147
|
+
see(relation.from);
|
|
148
|
+
see(relation.to);
|
|
149
|
+
}
|
|
150
|
+
for (const [node] of unknownNodes)
|
|
151
|
+
see(node);
|
|
152
|
+
for (const node of input.isolated ?? [])
|
|
153
|
+
see(node);
|
|
154
|
+
// Kind-major, then code unit. Kind-major keeps one kind's nodes contiguous,
|
|
155
|
+
// which is what lets a caller filter to files without touching a string.
|
|
156
|
+
const ordered = [...seen.values()].sort((a, b) => NODE_KINDS.indexOf(a.kind) - NODE_KINDS.indexOf(b.kind) ||
|
|
157
|
+
(a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
|
158
|
+
const index = new Map();
|
|
159
|
+
const names = Array.from({ length: ordered.length });
|
|
160
|
+
const kinds = new Uint8Array(ordered.length);
|
|
161
|
+
for (const [id, node] of ordered.entries()) {
|
|
162
|
+
index.set(keyOf(node.kind, node.name), id);
|
|
163
|
+
names[id] = node.name;
|
|
164
|
+
kinds[id] = NODE_KINDS.indexOf(node.kind);
|
|
165
|
+
}
|
|
166
|
+
const edges = sortedEdges(relations, index);
|
|
167
|
+
const depends = adjacencyOf(edges, ordered.length, 'from');
|
|
168
|
+
const dependents = adjacencyOf(edges, ordered.length, 'to');
|
|
169
|
+
const unknown = new Uint8Array(ordered.length);
|
|
170
|
+
const reasons = new Map();
|
|
171
|
+
for (const [node, because] of unknownNodes) {
|
|
172
|
+
const id = index.get(keyOf(node.kind, node.name));
|
|
173
|
+
if (id === undefined)
|
|
174
|
+
continue;
|
|
175
|
+
unknown[id] = 1;
|
|
176
|
+
if (because !== undefined)
|
|
177
|
+
reasons.set(id, because);
|
|
178
|
+
}
|
|
179
|
+
return { names, kinds, depends, dependents, unknown, reasons, index };
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Edges as ids, sorted and deduplicated.
|
|
183
|
+
*
|
|
184
|
+
* Sorting the whole list once is what makes every row of *both* adjacencies
|
|
185
|
+
* sorted without sorting any row: each fill walks the list in order, so a row
|
|
186
|
+
* receives its targets in order. Two files related twice — a value import and a
|
|
187
|
+
* type import — stay two edges, because the kinds differ and one of them explains
|
|
188
|
+
* a finding the other does not.
|
|
189
|
+
*/
|
|
190
|
+
function sortedEdges(relations, index) {
|
|
191
|
+
const edges = [];
|
|
192
|
+
for (const relation of relations) {
|
|
193
|
+
const from = index.get(keyOf(relation.from.kind, relation.from.name));
|
|
194
|
+
const to = index.get(keyOf(relation.to.kind, relation.to.name));
|
|
195
|
+
if (from === undefined || to === undefined)
|
|
196
|
+
continue;
|
|
197
|
+
edges.push({ from, to, kind: EDGE_KINDS.indexOf(relation.kind) });
|
|
198
|
+
}
|
|
199
|
+
edges.sort((a, b) => a.from - b.from || a.to - b.to || a.kind - b.kind);
|
|
200
|
+
const unique = [];
|
|
201
|
+
for (const edge of edges) {
|
|
202
|
+
const last = unique[unique.length - 1];
|
|
203
|
+
const same = last !== undefined && last.from === edge.from && last.to === edge.to && last.kind === edge.kind;
|
|
204
|
+
if (!same)
|
|
205
|
+
unique.push(edge);
|
|
206
|
+
}
|
|
207
|
+
return unique;
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* One direction, by counting sort.
|
|
211
|
+
*
|
|
212
|
+
* `by` names the endpoint a row is keyed on: `from` produces *what this depends
|
|
213
|
+
* on*, `to` produces the transpose. Two passes — count the degrees, then place —
|
|
214
|
+
* and no per-node array is ever allocated.
|
|
215
|
+
*/
|
|
216
|
+
function adjacencyOf(edges, nodes, by) {
|
|
217
|
+
const offset = new Uint32Array(nodes + 1);
|
|
218
|
+
for (const edge of edges)
|
|
219
|
+
offset[edge[by] + 1] = offset[edge[by] + 1] + 1;
|
|
220
|
+
for (let id = 0; id < nodes; id += 1)
|
|
221
|
+
offset[id + 1] = offset[id + 1] + offset[id];
|
|
222
|
+
const target = new Uint32Array(edges.length);
|
|
223
|
+
const kind = new Uint8Array(edges.length);
|
|
224
|
+
const cursor = Uint32Array.from(offset.subarray(0, nodes));
|
|
225
|
+
for (const edge of edges) {
|
|
226
|
+
const row = edge[by];
|
|
227
|
+
const at = cursor[row];
|
|
228
|
+
target[at] = by === 'from' ? edge.to : edge.from;
|
|
229
|
+
kind[at] = edge.kind;
|
|
230
|
+
cursor[row] = at + 1;
|
|
231
|
+
}
|
|
232
|
+
return { offset, target, kind };
|
|
233
|
+
}
|
|
234
|
+
//# sourceMappingURL=graph.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"graph.js","sourceRoot":"","sources":["../../src/relate/graph.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * What reaches what, stored so that asking is cheap.\n *\n * Every question this project asks about a change is a reachability question. A\n * diff names files; a subject is made of components; a component is declared in a\n * file that imports other files. *Which subjects could this diff have changed* is\n * one traversal of one graph, and the only reason it is not asked that way today\n * is that there has never been a graph to traverse.\n *\n * ## Why a structure rather than a `Map`\n *\n * The obvious shape is `Map<string, string[]>`, and it is wrong at the size this\n * runs at. A monorepo scan produces tens of thousands of files and several times\n * as many edges; the map holds one array object and one string copy per edge, and\n * a traversal chases a pointer for every step. What replaces it here is the\n * standard answer, and it is standard because nothing has beaten it:\n *\n * - **strings are interned once.** A node is an integer. Comparison is an integer\n * compare, membership is an index into a byte array, and a visited set is a\n * `Uint8Array` rather than a `Set<string>`.\n * - **edges are two typed arrays** in compressed-sparse-row form: `offset` says\n * where a node's row starts, `target` holds the rows end to end. One node's\n * edges are contiguous, so a traversal walks memory forwards instead of\n * following pointers into the heap.\n * - **both directions are materialized.** *What does this depend on* and *what\n * depends on this* are different questions, and a graph answering only the\n * first makes the second a whole-graph scan. The transpose is a counting sort —\n * one pass to count, one to place — so holding it costs `O(n + m)` once instead\n * of `O(n + m)` per query.\n *\n * ## Edges point at what a thing depends on\n *\n * One convention, held everywhere: **`A → B` means A depends on B**, so a change\n * in B may move A. Every affected-set question is therefore a walk against the\n * arrows, which is what `dependentsOf` does, and every *what would I have to read\n * to understand this* question is a walk along them.\n *\n * It decides the direction of the less obvious edges too. A component is\n * `declared-in` the file that declares it — the component depends on the file and\n * not the other way round — so editing that file reaches the component in the\n * same traversal that reaches every importer.\n *\n * ## Nodes are typed, because files are not the last kind\n *\n * This ships with two kinds and the join between them: a `file` graph built from\n * imports, and the `component` nodes the source index already knows how to find.\n * That is deliberate rather than incidental. A component relation — *`TodoFooter`\n * renders `Chip`* — is another edge kind between nodes of another kind in this\n * same structure, and\n * [`composition.md`](../../../../docs/composition.md) already computes exactly\n * that relation from what a run rendered. One graph means the static answer and\n * the rendered answer are joinable rather than adjacent.\n *\n * ## What it is not\n *\n * **Not a build graph.** It has no notion of a task, an output or a cache key,\n * and it does not know what an install produced. `nx` and `turbo` own that layer,\n * and what reads them reads their answer rather than competing with it.\n *\n * **Not a resolver.** Nothing here opens a file or knows what a specifier means.\n * `core` performs no I/O (ADR-0006); the records this folds are somebody else's\n * disk.\n */\n\n/** Node kinds, in id order. An id is an index into this, never the word. */\nexport const NODE_KINDS = ['file', 'component'] as const;\nexport type NodeKind = (typeof NODE_KINDS)[number];\n\n/**\n * Edge kinds, in id order.\n *\n * The distinctions are kept because they explain a finding, not because the\n * default traversal narrows on them. `type` is erased by every compiler and can\n * change no rendering, and it is still walked unless a caller says otherwise —\n * for the reason every rule in selection resolves the same way: a subject skipped\n * in error is a green run over an unwatched surface, and a subject observed in\n * error costs a collection.\n */\nexport const EDGE_KINDS = [\n /** `import x from './y'` — a value import. */\n 'imports',\n /** `export … from './y'` — an import that also republishes. */\n 'reexports',\n /** `import('./y')`, with a specifier that was a literal. */\n 'dynamic',\n /** `import type { T } from './y'` — erased before anything renders. */\n 'type',\n /** A stylesheet's `@import`, or a `url()` reaching a font or an image. */\n 'asset',\n /** A component to the file that declares it. */\n 'declared-in',\n] as const;\nexport type EdgeKind = (typeof EDGE_KINDS)[number];\n\nexport interface Node {\n readonly kind: NodeKind;\n /** Repository-relative for a file; the identifier for a component. */\n readonly name: string;\n}\n\nexport interface Relation {\n readonly from: Node;\n readonly to: Node;\n readonly kind: EdgeKind;\n}\n\n/** An index into `Relations.names`. Valid only against the graph it came from. */\nexport type NodeId = number;\n\n/**\n * One direction of the edge set, compressed-sparse-row.\n *\n * Node `i`'s edges are `target[offset[i] … offset[i + 1])`, and `kind[j]`\n * describes `target[j]`. `offset` holds one more entry than there are nodes, so\n * the last row needs no special case.\n */\nexport interface Adjacency {\n readonly offset: Uint32Array;\n readonly target: Uint32Array;\n readonly kind: Uint8Array;\n}\n\nexport interface Relations {\n /** Node names by id, ordered by kind and then by code unit. */\n readonly names: readonly string[];\n /** The `NODE_KINDS` index of each node. */\n readonly kinds: Uint8Array;\n\n /** What each node depends on. */\n readonly depends: Adjacency;\n /** What depends on each node — the transpose, materialized. */\n readonly dependents: Adjacency;\n\n /**\n * `1` where this node's outgoing edges could not be enumerated.\n *\n * A parse failure, a specifier that was not a literal, a file in a language\n * nothing here reads. It is *not* \"this file has no dependencies\", and the\n * difference is the whole safety argument: a file whose edges are unknown may\n * depend on anything that changed, so selection seeds the traversal with every\n * one of them (ADR-0002 — absent is not empty).\n */\n readonly unknown: Uint8Array;\n\n /**\n * Why, for each of those nodes, in the words whoever read the file used.\n *\n * Sparse — a repository the scan understands carries none of these — and kept\n * apart from the mask because the mask is what the inner loop tests and this is\n * what an operator reads. Without it, a run that observed forty subjects instead\n * of twelve can say *because eight files could not be read* and can never say\n * **which**, which is the difference between a fact and a work item.\n */\n readonly reasons: ReadonlyMap<NodeId, string>;\n\n /** `keyOf` to id — the interning table, kept for lookup. */\n readonly index: ReadonlyMap<string, NodeId>;\n}\n\n/**\n * The lookup key for a node.\n *\n * Runtime only; nothing serializes this, and no code parses it back — the\n * separator only has to be injective, and it is, because a kind is one of two\n * fixed words and neither contains a `:`. A name that contains one is therefore\n * fine.\n *\n * The obvious separator is a NUL, and it is the wrong one: a source file\n * carrying that byte stops being text to `grep`, `file` and `git diff`, which is\n * a search finding nothing looking exactly like a search that returned nothing.\n * `tools/boundaries.check.ts` fails a build over it.\n */\nexport function keyOf(kind: NodeKind, name: string): string {\n return `${kind}:${name}`;\n}\n\nexport function idOf(relations: Relations, kind: NodeKind, name: string): NodeId | undefined {\n return relations.index.get(keyOf(kind, name));\n}\n\nexport function nodeAt(relations: Relations, id: NodeId): Node | undefined {\n const name = relations.names[id];\n if (name === undefined) return undefined;\n return { kind: NODE_KINDS[relations.kinds[id]!]!, name };\n}\n\n/** Every node of one kind, as ids. Contiguous, because the order is kind-major. */\nexport function nodesOfKind(relations: Relations, kind: NodeKind): readonly NodeId[] {\n const wanted = NODE_KINDS.indexOf(kind);\n const found: NodeId[] = [];\n for (let id = 0; id < relations.kinds.length; id += 1) {\n if (relations.kinds[id] === wanted) found.push(id);\n }\n return found;\n}\n\n/**\n * Fold relations into the queryable form.\n *\n * Deterministic to the byte: nodes are ordered by kind and then by **code unit** —\n * never `localeCompare`, which would make an id a promise about `LANG` — and edges\n * are sorted and deduplicated, so two runs over the same records produce identical\n * arrays and a serialized graph is stable.\n *\n * `O(n log n + m log m)` for the two sorts and `O(n + m)` for everything else. The\n * transpose is a counting sort rather than a second pass over a map, which is why\n * both directions cost one build instead of one per query.\n */\nexport function relationsOf(input: {\n readonly relations: Iterable<Relation>;\n /**\n * Nodes whose outgoing edges are not fully known, and must be treated as such.\n *\n * A bare node says *this one is blind*. A pair says why, in whatever words the\n * reader of the file used — and the sentence is the half an operator can act on,\n * so it travels with the fact rather than being reconstructed from it.\n */\n readonly unknown?: Iterable<Node | readonly [Node, string]>;\n /** Nodes with no edges at all, which would otherwise be absent from the graph. */\n readonly isolated?: Iterable<Node>;\n}): Relations {\n const relations = [...input.relations];\n const unknownNodes = [...(input.unknown ?? [])].map((entry) =>\n Array.isArray(entry) ? entry : ([entry, undefined] as const),\n ) as readonly (readonly [Node, string | undefined])[];\n\n const seen = new Map<string, Node>();\n const see = (node: Node): void => {\n const key = keyOf(node.kind, node.name);\n if (!seen.has(key)) seen.set(key, node);\n };\n\n for (const relation of relations) {\n see(relation.from);\n see(relation.to);\n }\n for (const [node] of unknownNodes) see(node);\n for (const node of input.isolated ?? []) see(node);\n\n // Kind-major, then code unit. Kind-major keeps one kind's nodes contiguous,\n // which is what lets a caller filter to files without touching a string.\n const ordered = [...seen.values()].sort(\n (a, b) =>\n NODE_KINDS.indexOf(a.kind) - NODE_KINDS.indexOf(b.kind) ||\n (a.name < b.name ? -1 : a.name > b.name ? 1 : 0),\n );\n\n const index = new Map<string, NodeId>();\n const names: string[] = Array.from<string>({ length: ordered.length });\n const kinds = new Uint8Array(ordered.length);\n\n for (const [id, node] of ordered.entries()) {\n index.set(keyOf(node.kind, node.name), id);\n names[id] = node.name;\n kinds[id] = NODE_KINDS.indexOf(node.kind);\n }\n\n const edges = sortedEdges(relations, index);\n const depends = adjacencyOf(edges, ordered.length, 'from');\n const dependents = adjacencyOf(edges, ordered.length, 'to');\n\n const unknown = new Uint8Array(ordered.length);\n const reasons = new Map<NodeId, string>();\n for (const [node, because] of unknownNodes) {\n const id = index.get(keyOf(node.kind, node.name));\n if (id === undefined) continue;\n\n unknown[id] = 1;\n if (because !== undefined) reasons.set(id, because);\n }\n\n return { names, kinds, depends, dependents, unknown, reasons, index };\n}\n\ninterface Edge {\n readonly from: NodeId;\n readonly to: NodeId;\n readonly kind: number;\n}\n\n/**\n * Edges as ids, sorted and deduplicated.\n *\n * Sorting the whole list once is what makes every row of *both* adjacencies\n * sorted without sorting any row: each fill walks the list in order, so a row\n * receives its targets in order. Two files related twice — a value import and a\n * type import — stay two edges, because the kinds differ and one of them explains\n * a finding the other does not.\n */\nfunction sortedEdges(\n relations: readonly Relation[],\n index: ReadonlyMap<string, NodeId>,\n): readonly Edge[] {\n const edges: Edge[] = [];\n\n for (const relation of relations) {\n const from = index.get(keyOf(relation.from.kind, relation.from.name));\n const to = index.get(keyOf(relation.to.kind, relation.to.name));\n if (from === undefined || to === undefined) continue;\n edges.push({ from, to, kind: EDGE_KINDS.indexOf(relation.kind) });\n }\n\n edges.sort((a, b) => a.from - b.from || a.to - b.to || a.kind - b.kind);\n\n const unique: Edge[] = [];\n for (const edge of edges) {\n const last = unique[unique.length - 1];\n const same =\n last !== undefined && last.from === edge.from && last.to === edge.to && last.kind === edge.kind;\n if (!same) unique.push(edge);\n }\n\n return unique;\n}\n\n/**\n * One direction, by counting sort.\n *\n * `by` names the endpoint a row is keyed on: `from` produces *what this depends\n * on*, `to` produces the transpose. Two passes — count the degrees, then place —\n * and no per-node array is ever allocated.\n */\nfunction adjacencyOf(edges: readonly Edge[], nodes: number, by: 'from' | 'to'): Adjacency {\n const offset = new Uint32Array(nodes + 1);\n for (const edge of edges) offset[edge[by] + 1] = offset[edge[by] + 1]! + 1;\n for (let id = 0; id < nodes; id += 1) offset[id + 1] = offset[id + 1]! + offset[id]!;\n\n const target = new Uint32Array(edges.length);\n const kind = new Uint8Array(edges.length);\n const cursor = Uint32Array.from(offset.subarray(0, nodes));\n\n for (const edge of edges) {\n const row = edge[by];\n const at = cursor[row]!;\n target[at] = by === 'from' ? edge.to : edge.from;\n kind[at] = edge.kind;\n cursor[row] = at + 1;\n }\n\n return { offset, target, kind };\n}\n"]}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `core/relate` — what reaches what.
|
|
3
|
+
*
|
|
4
|
+
* A typed, interned, bidirectional graph and the two traversals over it, plus the
|
|
5
|
+
* file-shaped record the scanner produces and the fold that turns a pile of them
|
|
6
|
+
* into the structure. No I/O, no parser, no resolver: the expensive half belongs
|
|
7
|
+
* to whoever owns the disk, and this is the half that has to be fast and has to
|
|
8
|
+
* be provable without one.
|
|
9
|
+
*
|
|
10
|
+
* See [`docs/selecting.md`](../../../../docs/selecting.md) for what it is for.
|
|
11
|
+
*/
|
|
12
|
+
export { EDGE_KINDS, NODE_KINDS, idOf, keyOf, nodeAt, nodesOfKind, relationsOf, type Adjacency, type EdgeKind, type Node, type NodeId, type NodeKind, type Relation, type Relations, } from './graph.js';
|
|
13
|
+
export { dependenciesOf, dependentsOf, trailOf, type Reach, type ReachOptions, } from './reach.js';
|
|
14
|
+
export { CLOSURE_EDGES, closureOf, driftedBetween, type Closure, type ClosureInput, type Drift, } from './merkle.js';
|
|
15
|
+
export { explain, movedBy, relationsOfFiles, type FileEdge, type FileRecord, type Hole, type Reached, } from './records.js';
|
|
16
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `core/relate` — what reaches what.
|
|
3
|
+
*
|
|
4
|
+
* A typed, interned, bidirectional graph and the two traversals over it, plus the
|
|
5
|
+
* file-shaped record the scanner produces and the fold that turns a pile of them
|
|
6
|
+
* into the structure. No I/O, no parser, no resolver: the expensive half belongs
|
|
7
|
+
* to whoever owns the disk, and this is the half that has to be fast and has to
|
|
8
|
+
* be provable without one.
|
|
9
|
+
*
|
|
10
|
+
* See [`docs/selecting.md`](../../../../docs/selecting.md) for what it is for.
|
|
11
|
+
*/
|
|
12
|
+
export { EDGE_KINDS, NODE_KINDS, idOf, keyOf, nodeAt, nodesOfKind, relationsOf, } from './graph.js';
|
|
13
|
+
export { dependenciesOf, dependentsOf, trailOf, } from './reach.js';
|
|
14
|
+
export { CLOSURE_EDGES, closureOf, driftedBetween, } from './merkle.js';
|
|
15
|
+
export { explain, movedBy, relationsOfFiles, } from './records.js';
|
|
16
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/relate/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EACL,UAAU,EACV,UAAU,EACV,IAAI,EACJ,KAAK,EACL,MAAM,EACN,WAAW,EACX,WAAW,GAQZ,MAAM,YAAY,CAAC;AAEpB,OAAO,EACL,cAAc,EACd,YAAY,EACZ,OAAO,GAGR,MAAM,YAAY,CAAC;AAEpB,OAAO,EACL,aAAa,EACb,SAAS,EACT,cAAc,GAIf,MAAM,aAAa,CAAC;AAErB,OAAO,EACL,OAAO,EACP,OAAO,EACP,gBAAgB,GAKjB,MAAM,cAAc,CAAC","sourcesContent":["/**\n * `core/relate` — what reaches what.\n *\n * A typed, interned, bidirectional graph and the two traversals over it, plus the\n * file-shaped record the scanner produces and the fold that turns a pile of them\n * into the structure. No I/O, no parser, no resolver: the expensive half belongs\n * to whoever owns the disk, and this is the half that has to be fast and has to\n * be provable without one.\n *\n * See [`docs/selecting.md`](../../../../docs/selecting.md) for what it is for.\n */\n\nexport {\n EDGE_KINDS,\n NODE_KINDS,\n idOf,\n keyOf,\n nodeAt,\n nodesOfKind,\n relationsOf,\n type Adjacency,\n type EdgeKind,\n type Node,\n type NodeId,\n type NodeKind,\n type Relation,\n type Relations,\n} from './graph.js';\n\nexport {\n dependenciesOf,\n dependentsOf,\n trailOf,\n type Reach,\n type ReachOptions,\n} from './reach.js';\n\nexport {\n CLOSURE_EDGES,\n closureOf,\n driftedBetween,\n type Closure,\n type ClosureInput,\n type Drift,\n} from './merkle.js';\n\nexport {\n explain,\n movedBy,\n relationsOfFiles,\n type FileEdge,\n type FileRecord,\n type Hole,\n type Reached,\n} from './records.js';\n"]}
|
|
@@ -0,0 +1,110 @@
|
|
|
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 { type Digest } from '../format/hash.js';
|
|
46
|
+
import { type EdgeKind, type Relations } from './graph.js';
|
|
47
|
+
export interface ClosureInput {
|
|
48
|
+
readonly relations: Relations;
|
|
49
|
+
/**
|
|
50
|
+
* Content digest per file, keyed by the file's path.
|
|
51
|
+
*
|
|
52
|
+
* A file with no entry is volatile: nothing here can tell whether it moved.
|
|
53
|
+
* Component nodes take no entry — a component has no bytes of its own, and its
|
|
54
|
+
* content is the file that declares it, which it already depends on.
|
|
55
|
+
*/
|
|
56
|
+
readonly content: ReadonlyMap<string, Digest>;
|
|
57
|
+
/** Restrict the closure to these edge kinds. Every kind by default. */
|
|
58
|
+
readonly through?: Iterable<EdgeKind>;
|
|
59
|
+
}
|
|
60
|
+
export interface Closure {
|
|
61
|
+
/** `keyOf(kind, name)` → the digest of everything that node rests on. */
|
|
62
|
+
readonly digests: ReadonlyMap<string, Digest>;
|
|
63
|
+
/**
|
|
64
|
+
* Nodes whose digest cannot prove sameness, and everything resting on them.
|
|
65
|
+
*
|
|
66
|
+
* Not an error. It is the honest half of the answer, and a caller that ignores
|
|
67
|
+
* it converts a missing input into a subject nobody observed.
|
|
68
|
+
*/
|
|
69
|
+
readonly volatile: ReadonlySet<string>;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Hash every node over its own content and its whole dependency closure.
|
|
73
|
+
*
|
|
74
|
+
* One pass of Tarjan's algorithm and one pass over the components it emits, which
|
|
75
|
+
* is `O(n + m)` — the same cost as the traversal next door, for an answer that
|
|
76
|
+
* survives a rebase.
|
|
77
|
+
*/
|
|
78
|
+
export declare function closureOf(input: ClosureInput): Closure;
|
|
79
|
+
export interface Drift {
|
|
80
|
+
/** Node keys whose closure is not provably the one it was. */
|
|
81
|
+
readonly changed: readonly string[];
|
|
82
|
+
/** The file paths among them. */
|
|
83
|
+
readonly files: readonly string[];
|
|
84
|
+
/** The component names among them. */
|
|
85
|
+
readonly components: readonly string[];
|
|
86
|
+
/** Node keys the earlier closure held and this one does not. */
|
|
87
|
+
readonly gone: readonly string[];
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* What moved between two closures, without consulting a repository.
|
|
91
|
+
*
|
|
92
|
+
* Three ways a node lands in `changed`, and only the first is a difference: its
|
|
93
|
+
* digest differs, it is **new**, or it is **volatile** — the last because a node
|
|
94
|
+
* whose inputs were not fully read has a digest that can match by accident, and a
|
|
95
|
+
* selector that believed it would skip a subject on the strength of a hash over
|
|
96
|
+
* bytes nobody hashed.
|
|
97
|
+
*/
|
|
98
|
+
export declare function driftedBetween(before: Closure, after: Closure): Drift;
|
|
99
|
+
/**
|
|
100
|
+
* The edge kinds a closure walks when the caller does not say.
|
|
101
|
+
*
|
|
102
|
+
* Listed rather than derived from `EDGE_KINDS`, and the difference is the whole
|
|
103
|
+
* point. A digest is a claim about *which inputs* were folded, so widening the
|
|
104
|
+
* fold silently changes every digest that exists — one added edge kind, and the
|
|
105
|
+
* next run finds nothing it can prove unchanged and re-observes the entire suite
|
|
106
|
+
* while reporting a successful narrowing. Adding a kind here is a decision with a
|
|
107
|
+
* cost, and it should read like one.
|
|
108
|
+
*/
|
|
109
|
+
export declare const CLOSURE_EDGES: readonly EdgeKind[];
|
|
110
|
+
//# sourceMappingURL=merkle.d.ts.map
|