@variance-authority/core 0.1.1 → 0.2.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 (81) hide show
  1. package/CHANGELOG.md +190 -0
  2. package/README.md +51 -9
  3. package/dist/attribute/closure.d.ts +105 -0
  4. package/dist/attribute/closure.js +180 -0
  5. package/dist/attribute/closure.js.map +1 -0
  6. package/dist/attribute/composition.d.ts +5 -3
  7. package/dist/attribute/composition.js.map +1 -1
  8. package/dist/attribute/divergence.js +7 -1
  9. package/dist/attribute/divergence.js.map +1 -1
  10. package/dist/attribute/index.d.ts +5 -1
  11. package/dist/attribute/index.js +3 -1
  12. package/dist/attribute/index.js.map +1 -1
  13. package/dist/attribute/lexicon.d.ts +88 -0
  14. package/dist/attribute/lexicon.js +150 -0
  15. package/dist/attribute/lexicon.js.map +1 -0
  16. package/dist/attribute/source.d.ts +19 -2
  17. package/dist/attribute/source.js +12 -0
  18. package/dist/attribute/source.js.map +1 -1
  19. package/dist/compare/derive.d.ts +17 -0
  20. package/dist/compare/derive.js.map +1 -1
  21. package/dist/compare/diff/index.d.ts +25 -1
  22. package/dist/compare/diff/index.js +8 -5
  23. package/dist/compare/diff/index.js.map +1 -1
  24. package/dist/compare/explain.d.ts +1 -1
  25. package/dist/compare/explain.js +15 -8
  26. package/dist/compare/explain.js.map +1 -1
  27. package/dist/compare/index.d.ts +2 -2
  28. package/dist/compare/index.js +1 -1
  29. package/dist/compare/index.js.map +1 -1
  30. package/dist/compare/observability.d.ts +25 -1
  31. package/dist/compare/observability.js +54 -0
  32. package/dist/compare/observability.js.map +1 -1
  33. package/dist/compare/parting.d.ts +24 -2
  34. package/dist/compare/parting.js +14 -9
  35. package/dist/compare/parting.js.map +1 -1
  36. package/dist/compare/slice.d.ts +15 -2
  37. package/dist/compare/slice.js +6 -2
  38. package/dist/compare/slice.js.map +1 -1
  39. package/dist/format/capture.d.ts +15 -0
  40. package/dist/format/capture.js +30 -1
  41. package/dist/format/capture.js.map +1 -1
  42. package/dist/format/document.d.ts +65 -5
  43. package/dist/format/document.js +34 -0
  44. package/dist/format/document.js.map +1 -1
  45. package/dist/format/hash.d.ts +26 -0
  46. package/dist/format/hash.js +41 -0
  47. package/dist/format/hash.js.map +1 -1
  48. package/dist/format/index.d.ts +4 -3
  49. package/dist/format/index.js +3 -2
  50. package/dist/format/index.js.map +1 -1
  51. package/dist/format/sha256.d.ts +10 -2
  52. package/dist/format/sha256.js +42 -9
  53. package/dist/format/sha256.js.map +1 -1
  54. package/dist/format/stabilize.d.ts +15 -0
  55. package/dist/format/stabilize.js +22 -1
  56. package/dist/format/stabilize.js.map +1 -1
  57. package/dist/index.d.ts +3 -11
  58. package/dist/index.js +3 -11
  59. package/dist/index.js.map +1 -1
  60. package/dist/relate/graph.d.ts +26 -6
  61. package/dist/relate/graph.js +14 -7
  62. package/dist/relate/graph.js.map +1 -1
  63. package/dist/relate/index.d.ts +2 -2
  64. package/dist/relate/index.js +1 -1
  65. package/dist/relate/index.js.map +1 -1
  66. package/dist/relate/merkle.d.ts +8 -5
  67. package/dist/relate/merkle.js +8 -6
  68. package/dist/relate/merkle.js.map +1 -1
  69. package/dist/relate/reach.d.ts +19 -5
  70. package/dist/relate/reach.js +10 -8
  71. package/dist/relate/reach.js.map +1 -1
  72. package/dist/relate/records.d.ts +29 -4
  73. package/dist/relate/records.js +60 -7
  74. package/dist/relate/records.js.map +1 -1
  75. package/dist/segment/index.d.ts +123 -0
  76. package/dist/segment/index.js +265 -0
  77. package/dist/segment/index.js.map +1 -0
  78. package/dist/share/index.d.ts +148 -0
  79. package/dist/share/index.js +190 -0
  80. package/dist/share/index.js.map +1 -0
  81. package/package.json +10 -2
@@ -28,6 +28,11 @@
28
28
  * cycle terminates the same way any revisit does.
29
29
  */
30
30
  import { type EdgeKind, type NodeId, type Relations } from './graph.js';
31
+ /**
32
+ * What one walk from a set of seeds reached: a mark over every node, the node
33
+ * each one was reached through, and the reached ids in the order they were
34
+ * found. `trailOf` reads the chain from any reached node back to its seed.
35
+ */
31
36
  export interface Reach {
32
37
  /** `1` where the node was reached, including the seeds themselves. */
33
38
  readonly mask: Uint8Array;
@@ -41,14 +46,23 @@ export interface Reach {
41
46
  }
42
47
  export interface ReachOptions {
43
48
  /**
44
- * Edge kinds to walk. Every kind, when absent.
49
+ * Edge kinds to walk. `RUNTIME_EDGES` when absent: every kind but `type`.
45
50
  *
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.
51
+ * A type-only import is erased before anything runs, so a change behind it
52
+ * moves no test and no pixel, and walking it selects work nothing can fail.
53
+ * A caller asking a question about source rather than about a runtime passes
54
+ * `EDGE_KINDS`, or any narrower list, and gets exactly those.
50
55
  */
51
56
  readonly through?: Iterable<EdgeKind>;
57
+ /**
58
+ * Nodes the walk never enters, seeds included.
59
+ *
60
+ * A module a test file mocks is replaced for that file's whole run, so a
61
+ * question asked from the file's point of view is asked of a graph with the
62
+ * module taken out — not one edge, the node. Marking it visited before the
63
+ * walk starts is exactly that removal, at no cost in the inner loop.
64
+ */
65
+ readonly avoid?: Iterable<NodeId>;
52
66
  }
53
67
  /** What depends on these nodes, transitively. Against the arrows. */
54
68
  export declare function dependentsOf(relations: Relations, seeds: Iterable<NodeId>, options?: ReachOptions): Reach;
@@ -27,7 +27,7 @@
27
27
  * cycle as an error: the visited mask means every node is expanded once, so a
28
28
  * cycle terminates the same way any revisit does.
29
29
  */
30
- import { EDGE_KINDS } from './graph.js';
30
+ import { EDGE_KINDS, RUNTIME_EDGES } from './graph.js';
31
31
  /** What depends on these nodes, transitively. Against the arrows. */
32
32
  export function dependentsOf(relations, seeds, options = {}) {
33
33
  return search(relations.dependents, relations.names.length, seeds, options);
@@ -66,8 +66,12 @@ function search(adjacency, nodes, seeds, options) {
66
66
  const mask = new Uint8Array(nodes);
67
67
  const via = new Int32Array(nodes).fill(-1);
68
68
  const queue = [];
69
+ const avoided = new Uint8Array(nodes);
70
+ for (const id of options.avoid ?? [])
71
+ if (id >= 0 && id < nodes)
72
+ avoided[id] = 1;
69
73
  for (const seed of seeds) {
70
- if (seed < 0 || seed >= nodes || mask[seed] === 1)
74
+ if (seed < 0 || seed >= nodes || mask[seed] === 1 || avoided[seed] === 1)
71
75
  continue;
72
76
  mask[seed] = 1;
73
77
  queue.push(seed);
@@ -77,10 +81,10 @@ function search(adjacency, nodes, seeds, options) {
77
81
  const node = queue[head];
78
82
  const end = adjacency.offset[node + 1];
79
83
  for (let at = adjacency.offset[node]; at < end; at += 1) {
80
- if (allowed !== undefined && allowed[adjacency.kind[at]] !== 1)
84
+ if (allowed[adjacency.kind[at]] !== 1)
81
85
  continue;
82
86
  const next = adjacency.target[at];
83
- if (mask[next] === 1)
87
+ if (mask[next] === 1 || avoided[next] === 1)
84
88
  continue;
85
89
  mask[next] = 1;
86
90
  via[next] = node;
@@ -96,12 +100,10 @@ function search(adjacency, nodes, seeds, options) {
96
100
  reached.push(id);
97
101
  return { mask, via, reached };
98
102
  }
99
- /** A kind filter as a byte lookup, or `undefined` when every kind is walked. */
103
+ /** A kind filter as a byte lookup. */
100
104
  function allowedKinds(through) {
101
- if (through === undefined)
102
- return undefined;
103
105
  const allowed = new Uint8Array(EDGE_KINDS.length);
104
- for (const kind of through) {
106
+ for (const kind of through ?? RUNTIME_EDGES) {
105
107
  const at = EDGE_KINDS.indexOf(kind);
106
108
  if (at !== -1)
107
109
  allowed[at] = 1;
@@ -1 +1 @@
1
- {"version":3,"file":"reach.js","sourceRoot":"","sources":["../../src/relate/reach.ts"],"names":[],"mappings":"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","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"]}
1
+ {"version":3,"file":"reach.js","sourceRoot":"","sources":["../../src/relate/reach.ts"],"names":[],"mappings":"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","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, RUNTIME_EDGES, type Adjacency, type EdgeKind, type NodeId, type Relations } from './graph.js';\n\n/**\n * What one walk from a set of seeds reached: a mark over every node, the node\n * each one was reached through, and the reached ids in the order they were\n * found. `trailOf` reads the chain from any reached node back to its seed.\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. `RUNTIME_EDGES` when absent: every kind but `type`.\n *\n * A type-only import is erased before anything runs, so a change behind it\n * moves no test and no pixel, and walking it selects work nothing can fail.\n * A caller asking a question about source rather than about a runtime passes\n * `EDGE_KINDS`, or any narrower list, and gets exactly those.\n */\n readonly through?: Iterable<EdgeKind>;\n /**\n * Nodes the walk never enters, seeds included.\n *\n * A module a test file mocks is replaced for that file's whole run, so a\n * question asked from the file's point of view is asked of a graph with the\n * module taken out — not one edge, the node. Marking it visited before the\n * walk starts is exactly that removal, at no cost in the inner loop.\n */\n readonly avoid?: Iterable<NodeId>;\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 const avoided = new Uint8Array(nodes);\n for (const id of options.avoid ?? []) if (id >= 0 && id < nodes) avoided[id] = 1;\n\n for (const seed of seeds) {\n if (seed < 0 || seed >= nodes || mask[seed] === 1 || avoided[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[adjacency.kind[at]!] !== 1) continue;\n\n const next = adjacency.target[at]!;\n if (mask[next] === 1 || avoided[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. */\nfunction allowedKinds(through: Iterable<EdgeKind> | undefined): Uint8Array {\n const allowed = new Uint8Array(EDGE_KINDS.length);\n for (const kind of through ?? RUNTIME_EDGES) {\n const at = EDGE_KINDS.indexOf(kind);\n if (at !== -1) allowed[at] = 1;\n }\n return allowed;\n}\n"]}
@@ -19,7 +19,7 @@
19
19
  */
20
20
  import type { Digest } from '../format/hash.js';
21
21
  import { type EdgeKind, type Node, type Relations } from './graph.js';
22
- import { type Reach } from './reach.js';
22
+ import { type Reach, type ReachOptions } from './reach.js';
23
23
  export interface FileEdge {
24
24
  /** Repository-relative, already resolved. A specifier is not an edge. */
25
25
  readonly to: string;
@@ -73,7 +73,11 @@ export interface FileRecord {
73
73
  * declares it, so one walk against the arrows from a changed file reaches every
74
74
  * importer and every component in the same pass.
75
75
  */
76
- export declare function relationsOfFiles(records: Iterable<FileRecord>): Relations;
76
+ export declare function relationsOfFiles(records: Iterable<FileRecord>, options?: RelationsOptions): Relations;
77
+ export interface RelationsOptions {
78
+ /** Per file, the files its run never reaches; see {@link Relations.shadows}. */
79
+ readonly shadows?: ReadonlyMap<string, readonly string[]>;
80
+ }
77
81
  export interface Hole {
78
82
  /** The file whose outgoing edges could not be enumerated. */
79
83
  readonly file: string;
@@ -104,9 +108,29 @@ export interface Reached {
104
108
  * computed require()`` is something to fix.
105
109
  */
106
110
  readonly opaque: readonly Hole[];
111
+ /**
112
+ * Files the graph reached and their own shadows cut: every trail from a
113
+ * changed file to each one crossed a module the file replaces for its run.
114
+ * Listed so the report can say a test was left out and by whose word.
115
+ */
116
+ readonly shadowed: readonly string[];
107
117
  /** The traversal, kept so a caller can ask how any one file was reached. */
108
118
  readonly reach: Reach;
109
119
  }
120
+ export interface MovedOptions extends ReachOptions {
121
+ /**
122
+ * Per file, the files it takes out of its own graph: what a test mocks.
123
+ *
124
+ * A row is a fact about one file's run and nothing else's, so it is kept
125
+ * beside the records rather than in them — the records say what the text
126
+ * imports, this says what the run never reaches. A file with a row is moved
127
+ * only if some trail from a changed file arrives at it without crossing a
128
+ * file in its row, and what is reached only *through* it goes with it: the
129
+ * component a story declares, the test a setup file's mocks hold for. The
130
+ * graph's own table when absent ({@link Relations.shadows}).
131
+ */
132
+ readonly shadows?: ReadonlyMap<string, readonly string[]>;
133
+ }
110
134
  /**
111
135
  * What a set of changed files could have moved.
112
136
  *
@@ -116,9 +140,10 @@ export interface Reached {
116
140
  * too, and the cost is a collection rather than a green run over an unwatched
117
141
  * surface.
118
142
  *
119
- * One breadth-first search, whatever the number of changed files.
143
+ * One breadth-first search, whatever the number of changed files, over the
144
+ * runtime edges unless the caller names others.
120
145
  */
121
- export declare function movedBy(relations: Relations, changed: Iterable<string>): Reached;
146
+ export declare function movedBy(relations: Relations, changed: Iterable<string>, options?: MovedOptions): Reached;
122
147
  /**
123
148
  * How one file or component was reached, as the chain that reached it.
124
149
  *
@@ -18,7 +18,7 @@
18
18
  * absent is not empty, and here absent is not even absent, it is *unread*).
19
19
  */
20
20
  import { idOf, nodeAt, relationsOf, } from './graph.js';
21
- import { dependentsOf, trailOf } from './reach.js';
21
+ import { dependenciesOf, dependentsOf, trailOf } from './reach.js';
22
22
  const file = (name) => ({ kind: 'file', name });
23
23
  const component = (name) => ({ kind: 'component', name });
24
24
  /**
@@ -34,7 +34,7 @@ const component = (name) => ({ kind: 'component', name });
34
34
  * declares it, so one walk against the arrows from a changed file reaches every
35
35
  * importer and every component in the same pass.
36
36
  */
37
- export function relationsOfFiles(records) {
37
+ export function relationsOfFiles(records, options = {}) {
38
38
  const relations = [];
39
39
  const isolated = [];
40
40
  const unknown = [];
@@ -52,7 +52,7 @@ export function relationsOfFiles(records) {
52
52
  if (record.unknown !== undefined)
53
53
  unknown.push([from, record.unknown]);
54
54
  }
55
- return relationsOf({ relations, isolated, unknown });
55
+ return relationsOf({ relations, isolated, unknown, ...(options.shadows === undefined ? {} : { shadows: options.shadows }) });
56
56
  }
57
57
  /**
58
58
  * What a set of changed files could have moved.
@@ -63,9 +63,10 @@ export function relationsOfFiles(records) {
63
63
  * too, and the cost is a collection rather than a green run over an unwatched
64
64
  * surface.
65
65
  *
66
- * One breadth-first search, whatever the number of changed files.
66
+ * One breadth-first search, whatever the number of changed files, over the
67
+ * runtime edges unless the caller names others.
67
68
  */
68
- export function movedBy(relations, changed) {
69
+ export function movedBy(relations, changed, options = {}) {
69
70
  const seeds = [];
70
71
  const missing = [];
71
72
  for (const path of changed) {
@@ -83,7 +84,7 @@ export function movedBy(relations, changed) {
83
84
  const because = relations.reasons.get(id);
84
85
  opaque.push({ file: relations.names[id], ...(because === undefined ? {} : { because }) });
85
86
  }
86
- const reach = dependentsOf(relations, seeds);
87
+ const reach = unshadowed(relations, seeds, options);
87
88
  const files = [];
88
89
  const components = [];
89
90
  for (const id of reach.reached) {
@@ -92,7 +93,59 @@ export function movedBy(relations, changed) {
92
93
  continue;
93
94
  (node.kind === 'file' ? files : components).push(node.name);
94
95
  }
95
- return { files, components, missing: missing.sort(byCodeUnit), opaque, reach };
96
+ return { files, components, missing: missing.sort(byCodeUnit), opaque, shadowed: reach.shadowed, reach };
97
+ }
98
+ /**
99
+ * The walk against the arrows with every file left out whose own shadows cut
100
+ * every trail to it, and with nothing reached through such a file.
101
+ *
102
+ * The walk records one parent per node, so it cannot say whether *another*
103
+ * trail avoided the shadows. Asking the other way can: a walk along the arrows
104
+ * from the file, entering none of its shadows, either finds a seed or does not.
105
+ * It runs only for a reached file whose row names a reached node — a mock of
106
+ * something the change never touched cuts nothing, and costs nothing. A file
107
+ * found shadowed is then avoided and the walk repeated, so a component or a
108
+ * file reached only through it is not reached either; the repeat ends when a
109
+ * walk finds no new shadowed file, and a row once decided is not asked again.
110
+ */
111
+ function unshadowed(relations, seeds, options) {
112
+ const shadowed = [];
113
+ const avoid = new Set(options.avoid ?? []);
114
+ const shadows = options.shadows ?? relations.shadows;
115
+ let reach = dependentsOf(relations, seeds, options);
116
+ if (shadows.size === 0)
117
+ return { ...reach, shadowed };
118
+ const seeded = new Uint8Array(relations.names.length);
119
+ for (const seed of seeds)
120
+ seeded[seed] = 1;
121
+ const decided = new Set();
122
+ for (;;) {
123
+ let found = 0;
124
+ for (const [name, row] of shadows) {
125
+ const id = idOf(relations, 'file', name);
126
+ if (id === undefined || reach.mask[id] !== 1 || seeded[id] === 1 || decided.has(id))
127
+ continue;
128
+ const cut = [];
129
+ for (const shadow of row) {
130
+ const target = idOf(relations, 'file', shadow);
131
+ if (target !== undefined && reach.mask[target] === 1)
132
+ cut.push(target);
133
+ }
134
+ if (cut.length === 0)
135
+ continue;
136
+ decided.add(id);
137
+ const forward = dependenciesOf(relations, [id], { ...options, avoid: cut });
138
+ if (forward.reached.some((node) => seeded[node] === 1))
139
+ continue;
140
+ avoid.add(id);
141
+ shadowed.push(name);
142
+ found += 1;
143
+ }
144
+ if (found === 0)
145
+ break;
146
+ reach = dependentsOf(relations, seeds, { ...options, avoid });
147
+ }
148
+ return { ...reach, shadowed: shadowed.sort(byCodeUnit) };
96
149
  }
97
150
  /**
98
151
  * How one file or component was reached, as the chain that reached it.
@@ -1 +1 @@
1
- {"version":3,"file":"records.js","sourceRoot":"","sources":["../../src/relate/records.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * One file's outgoing edges, and the fold that turns a pile of them into a graph.\n *\n * A record is what reading **one** file produced, and that is the unit on\n * purpose. It makes the expensive half — open, parse, resolve — cacheable by\n * content digest and parallelizable per file, and it leaves the graph a pure\n * function of the records, so the structure can be rebuilt, sharded or merged\n * without touching a disk. `core` performs no I/O (ADR-0006); whoever owns the\n * disk produces these and hands them over.\n *\n * ## An empty record and an unreadable one are different facts\n *\n * A file with no imports and a file nothing could parse both produce a record\n * with no edges, and treating them alike is the one mistake that turns this from\n * a saving into a silent hole: the second one may import anything, including\n * whatever just changed. So `unknown` carries the *reason* the edges are not\n * known, and every consumer seeds its traversal with those files (ADR-0002 —\n * absent is not empty, and here absent is not even absent, it is *unread*).\n */\n\nimport type { Digest } from '../format/hash.js';\nimport {\n idOf,\n nodeAt,\n relationsOf,\n type EdgeKind,\n type Node,\n type NodeId,\n type Relation,\n type Relations,\n} from './graph.js';\nimport { dependentsOf, trailOf, type Reach } from './reach.js';\n\nexport interface FileEdge {\n /** Repository-relative, already resolved. A specifier is not an edge. */\n readonly to: string;\n readonly kind: EdgeKind;\n}\n\nexport interface FileRecord {\n /** Repository-relative, so a graph is portable between machines and CI. */\n readonly file: string;\n\n /**\n * The content digest this record was read from.\n *\n * Two jobs. It lets a second scan skip the parse for a file that has not moved,\n * which is the difference between a scan that costs a second and one nobody\n * leaves enabled. And it is the leaf of the Merkle fold in\n * [`merkle.ts`](./merkle.ts) — a file with no digest is one nothing can prove\n * unchanged, and is marked as such rather than assumed still.\n */\n readonly digest?: Digest;\n\n /** Resolved outgoing edges. Absent and empty mean the same thing here. */\n readonly edges?: readonly FileEdge[];\n\n /** Component names this file declares, from `indexSource` or better. */\n readonly declares?: readonly string[];\n\n /**\n * Specifiers that were read but did not resolve.\n *\n * Kept for the report rather than for the graph. A bare specifier that failed\n * to resolve is usually a package that is not installed; a **relative** one\n * that failed is a hole, and whoever produced the record says so by also\n * setting `unknown`.\n */\n readonly unresolved?: readonly string[];\n\n /**\n * Why this file's outgoing edges could not be enumerated, when they could not.\n *\n * Present means *this file may depend on anything*. The sentence is carried\n * rather than a flag, because it ends up in the paragraph explaining why a run\n * observed more than the operator expected.\n */\n readonly unknown?: string;\n}\n\nconst file = (name: string): Node => ({ kind: 'file', name });\nconst component = (name: string): Node => ({ kind: 'component', name });\n\n/**\n * Fold file records into the graph.\n *\n * Every record contributes its own node even when it has no edges, so a changed\n * file that imports nothing is still findable — otherwise the one file in the\n * diff would be missing from the structure that exists to answer questions about\n * it.\n *\n * A declaration becomes an edge from the **component to the file**, which is the\n * direction the convention requires: the component depends on the file that\n * declares it, so one walk against the arrows from a changed file reaches every\n * importer and every component in the same pass.\n */\nexport function relationsOfFiles(records: Iterable<FileRecord>): Relations {\n const relations: Relation[] = [];\n const isolated: Node[] = [];\n const unknown: (readonly [Node, string])[] = [];\n\n for (const record of records) {\n const from = file(record.file);\n isolated.push(from);\n\n for (const edge of record.edges ?? []) {\n relations.push({ from, to: file(edge.to), kind: edge.kind });\n }\n for (const name of record.declares ?? []) {\n relations.push({ from: component(name), to: from, kind: 'declared-in' });\n }\n // The sentence travels with the node. Reduced to a flag here, the paragraph\n // this record exists to produce could name a count and never a cause.\n if (record.unknown !== undefined) unknown.push([from, record.unknown]);\n }\n\n return relationsOf({ relations, isolated, unknown });\n}\n\nexport interface Hole {\n /** The file whose outgoing edges could not be enumerated. */\n readonly file: string;\n /** Why, in the words of whoever read it. Absent when nobody said. */\n readonly because?: string;\n}\n\nexport interface Reached {\n /** Files the change could have moved, including the changed files themselves. */\n readonly files: readonly string[];\n /** Components declared in any of them. */\n readonly components: readonly string[];\n\n /**\n * Changed paths the graph does not hold.\n *\n * Never silently ignored. A `README.md` belongs here and means nothing; a\n * source file belongs here only because the scan never reached it, and the\n * caller — which knows where it told the scan to look — is the one that can\n * tell those apart.\n */\n readonly missing: readonly string[];\n\n /**\n * Files seeded because their own edges are unknown, rather than because\n * anything reached them, each with the reason it could not be read.\n *\n * Counted separately so that \"we widened\" never hides inside \"we found\", and\n * carrying its sentence so the widening is a work item rather than a tax: *four\n * files could not be read* is something to live with, and ``src/legacy.js — a\n * computed require()`` is something to fix.\n */\n readonly opaque: readonly Hole[];\n\n /** The traversal, kept so a caller can ask how any one file was reached. */\n readonly reach: Reach;\n}\n\n/**\n * What a set of changed files could have moved.\n *\n * The seed set is the changed files **and every file whose edges are unknown**,\n * which is the rule the whole structure rests on. An unreadable file might import\n * the one that changed; seeding it means everything depending on *it* is observed\n * too, and the cost is a collection rather than a green run over an unwatched\n * surface.\n *\n * One breadth-first search, whatever the number of changed files.\n */\nexport function movedBy(relations: Relations, changed: Iterable<string>): Reached {\n const seeds: NodeId[] = [];\n const missing: string[] = [];\n\n for (const path of changed) {\n const id = idOf(relations, 'file', path);\n if (id === undefined) missing.push(path);\n else seeds.push(id);\n }\n\n const opaque: Hole[] = [];\n for (let id = 0; id < relations.unknown.length; id += 1) {\n if (relations.unknown[id] !== 1) continue;\n seeds.push(id);\n\n const because = relations.reasons.get(id);\n opaque.push({ file: relations.names[id]!, ...(because === undefined ? {} : { because }) });\n }\n\n const reach = dependentsOf(relations, seeds);\n const files: string[] = [];\n const components: string[] = [];\n\n for (const id of reach.reached) {\n const node = nodeAt(relations, id);\n if (node === undefined) continue;\n (node.kind === 'file' ? files : components).push(node.name);\n }\n\n return { files, components, missing: missing.sort(byCodeUnit), opaque, reach };\n}\n\n/**\n * How one file or component was reached, as the chain that reached it.\n *\n * The sentence a run prints when somebody asks why a subject was observed. Empty\n * when the node was not reached at all.\n */\nexport function explain(relations: Relations, moved: Reached, node: Node): readonly string[] {\n const id = idOf(relations, node.kind, node.name);\n if (id === undefined) return [];\n return trailOf(moved.reach, id).map((step) => relations.names[step]!);\n}\n\nfunction byCodeUnit(a: string, b: string): number {\n return a < b ? -1 : a > b ? 1 : 0;\n}\n"]}
1
+ {"version":3,"file":"records.js","sourceRoot":"","sources":["../../src/relate/records.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * One file's outgoing edges, and the fold that turns a pile of them into a graph.\n *\n * A record is what reading **one** file produced, and that is the unit on\n * purpose. It makes the expensive half — open, parse, resolve — cacheable by\n * content digest and parallelizable per file, and it leaves the graph a pure\n * function of the records, so the structure can be rebuilt, sharded or merged\n * without touching a disk. `core` performs no I/O (ADR-0006); whoever owns the\n * disk produces these and hands them over.\n *\n * ## An empty record and an unreadable one are different facts\n *\n * A file with no imports and a file nothing could parse both produce a record\n * with no edges, and treating them alike is the one mistake that turns this from\n * a saving into a silent hole: the second one may import anything, including\n * whatever just changed. So `unknown` carries the *reason* the edges are not\n * known, and every consumer seeds its traversal with those files (ADR-0002 —\n * absent is not empty, and here absent is not even absent, it is *unread*).\n */\n\nimport type { Digest } from '../format/hash.js';\nimport {\n idOf,\n nodeAt,\n relationsOf,\n type EdgeKind,\n type Node,\n type NodeId,\n type Relation,\n type Relations,\n} from './graph.js';\nimport { dependenciesOf, dependentsOf, trailOf, type Reach, type ReachOptions } from './reach.js';\n\nexport interface FileEdge {\n /** Repository-relative, already resolved. A specifier is not an edge. */\n readonly to: string;\n readonly kind: EdgeKind;\n}\n\nexport interface FileRecord {\n /** Repository-relative, so a graph is portable between machines and CI. */\n readonly file: string;\n\n /**\n * The content digest this record was read from.\n *\n * Two jobs. It lets a second scan skip the parse for a file that has not moved,\n * which is the difference between a scan that costs a second and one nobody\n * leaves enabled. And it is the leaf of the Merkle fold in\n * [`merkle.ts`](./merkle.ts) — a file with no digest is one nothing can prove\n * unchanged, and is marked as such rather than assumed still.\n */\n readonly digest?: Digest;\n\n /** Resolved outgoing edges. Absent and empty mean the same thing here. */\n readonly edges?: readonly FileEdge[];\n\n /** Component names this file declares, from `indexSource` or better. */\n readonly declares?: readonly string[];\n\n /**\n * Specifiers that were read but did not resolve.\n *\n * Kept for the report rather than for the graph. A bare specifier that failed\n * to resolve is usually a package that is not installed; a **relative** one\n * that failed is a hole, and whoever produced the record says so by also\n * setting `unknown`.\n */\n readonly unresolved?: readonly string[];\n\n /**\n * Why this file's outgoing edges could not be enumerated, when they could not.\n *\n * Present means *this file may depend on anything*. The sentence is carried\n * rather than a flag, because it ends up in the paragraph explaining why a run\n * observed more than the operator expected.\n */\n readonly unknown?: string;\n}\n\nconst file = (name: string): Node => ({ kind: 'file', name });\nconst component = (name: string): Node => ({ kind: 'component', name });\n\n/**\n * Fold file records into the graph.\n *\n * Every record contributes its own node even when it has no edges, so a changed\n * file that imports nothing is still findable — otherwise the one file in the\n * diff would be missing from the structure that exists to answer questions about\n * it.\n *\n * A declaration becomes an edge from the **component to the file**, which is the\n * direction the convention requires: the component depends on the file that\n * declares it, so one walk against the arrows from a changed file reaches every\n * importer and every component in the same pass.\n */\nexport function relationsOfFiles(records: Iterable<FileRecord>, options: RelationsOptions = {}): Relations {\n const relations: Relation[] = [];\n const isolated: Node[] = [];\n const unknown: (readonly [Node, string])[] = [];\n\n for (const record of records) {\n const from = file(record.file);\n isolated.push(from);\n\n for (const edge of record.edges ?? []) {\n relations.push({ from, to: file(edge.to), kind: edge.kind });\n }\n for (const name of record.declares ?? []) {\n relations.push({ from: component(name), to: from, kind: 'declared-in' });\n }\n // The sentence travels with the node. Reduced to a flag here, the paragraph\n // this record exists to produce could name a count and never a cause.\n if (record.unknown !== undefined) unknown.push([from, record.unknown]);\n }\n\n return relationsOf({ relations, isolated, unknown, ...(options.shadows === undefined ? {} : { shadows: options.shadows }) });\n}\n\nexport interface RelationsOptions {\n /** Per file, the files its run never reaches; see {@link Relations.shadows}. */\n readonly shadows?: ReadonlyMap<string, readonly string[]>;\n}\n\nexport interface Hole {\n /** The file whose outgoing edges could not be enumerated. */\n readonly file: string;\n /** Why, in the words of whoever read it. Absent when nobody said. */\n readonly because?: string;\n}\n\nexport interface Reached {\n /** Files the change could have moved, including the changed files themselves. */\n readonly files: readonly string[];\n /** Components declared in any of them. */\n readonly components: readonly string[];\n\n /**\n * Changed paths the graph does not hold.\n *\n * Never silently ignored. A `README.md` belongs here and means nothing; a\n * source file belongs here only because the scan never reached it, and the\n * caller — which knows where it told the scan to look — is the one that can\n * tell those apart.\n */\n readonly missing: readonly string[];\n\n /**\n * Files seeded because their own edges are unknown, rather than because\n * anything reached them, each with the reason it could not be read.\n *\n * Counted separately so that \"we widened\" never hides inside \"we found\", and\n * carrying its sentence so the widening is a work item rather than a tax: *four\n * files could not be read* is something to live with, and ``src/legacy.js — a\n * computed require()`` is something to fix.\n */\n readonly opaque: readonly Hole[];\n\n /**\n * Files the graph reached and their own shadows cut: every trail from a\n * changed file to each one crossed a module the file replaces for its run.\n * Listed so the report can say a test was left out and by whose word.\n */\n readonly shadowed: readonly string[];\n\n /** The traversal, kept so a caller can ask how any one file was reached. */\n readonly reach: Reach;\n}\n\nexport interface MovedOptions extends ReachOptions {\n /**\n * Per file, the files it takes out of its own graph: what a test mocks.\n *\n * A row is a fact about one file's run and nothing else's, so it is kept\n * beside the records rather than in them — the records say what the text\n * imports, this says what the run never reaches. A file with a row is moved\n * only if some trail from a changed file arrives at it without crossing a\n * file in its row, and what is reached only *through* it goes with it: the\n * component a story declares, the test a setup file's mocks hold for. The\n * graph's own table when absent ({@link Relations.shadows}).\n */\n readonly shadows?: ReadonlyMap<string, readonly string[]>;\n}\n\n/**\n * What a set of changed files could have moved.\n *\n * The seed set is the changed files **and every file whose edges are unknown**,\n * which is the rule the whole structure rests on. An unreadable file might import\n * the one that changed; seeding it means everything depending on *it* is observed\n * too, and the cost is a collection rather than a green run over an unwatched\n * surface.\n *\n * One breadth-first search, whatever the number of changed files, over the\n * runtime edges unless the caller names others.\n */\nexport function movedBy(\n relations: Relations,\n changed: Iterable<string>,\n options: MovedOptions = {},\n): Reached {\n const seeds: NodeId[] = [];\n const missing: string[] = [];\n\n for (const path of changed) {\n const id = idOf(relations, 'file', path);\n if (id === undefined) missing.push(path);\n else seeds.push(id);\n }\n\n const opaque: Hole[] = [];\n for (let id = 0; id < relations.unknown.length; id += 1) {\n if (relations.unknown[id] !== 1) continue;\n seeds.push(id);\n\n const because = relations.reasons.get(id);\n opaque.push({ file: relations.names[id]!, ...(because === undefined ? {} : { because }) });\n }\n\n const reach = unshadowed(relations, seeds, options);\n const files: string[] = [];\n const components: string[] = [];\n\n for (const id of reach.reached) {\n const node = nodeAt(relations, id);\n if (node === undefined) continue;\n (node.kind === 'file' ? files : components).push(node.name);\n }\n\n return { files, components, missing: missing.sort(byCodeUnit), opaque, shadowed: reach.shadowed, reach };\n}\n\n/**\n * The walk against the arrows with every file left out whose own shadows cut\n * every trail to it, and with nothing reached through such a file.\n *\n * The walk records one parent per node, so it cannot say whether *another*\n * trail avoided the shadows. Asking the other way can: a walk along the arrows\n * from the file, entering none of its shadows, either finds a seed or does not.\n * It runs only for a reached file whose row names a reached node — a mock of\n * something the change never touched cuts nothing, and costs nothing. A file\n * found shadowed is then avoided and the walk repeated, so a component or a\n * file reached only through it is not reached either; the repeat ends when a\n * walk finds no new shadowed file, and a row once decided is not asked again.\n */\nfunction unshadowed(\n relations: Relations,\n seeds: readonly NodeId[],\n options: MovedOptions,\n): Reach & { readonly shadowed: readonly string[] } {\n const shadowed: string[] = [];\n const avoid = new Set<NodeId>(options.avoid ?? []);\n const shadows = options.shadows ?? relations.shadows;\n let reach = dependentsOf(relations, seeds, options);\n if (shadows.size === 0) return { ...reach, shadowed };\n\n const seeded = new Uint8Array(relations.names.length);\n for (const seed of seeds) seeded[seed] = 1;\n const decided = new Set<NodeId>();\n\n for (;;) {\n let found = 0;\n for (const [name, row] of shadows) {\n const id = idOf(relations, 'file', name);\n if (id === undefined || reach.mask[id] !== 1 || seeded[id] === 1 || decided.has(id)) continue;\n\n const cut: NodeId[] = [];\n for (const shadow of row) {\n const target = idOf(relations, 'file', shadow);\n if (target !== undefined && reach.mask[target] === 1) cut.push(target);\n }\n if (cut.length === 0) continue;\n decided.add(id);\n\n const forward = dependenciesOf(relations, [id], { ...options, avoid: cut });\n if (forward.reached.some((node) => seeded[node] === 1)) continue;\n\n avoid.add(id);\n shadowed.push(name);\n found += 1;\n }\n if (found === 0) break;\n reach = dependentsOf(relations, seeds, { ...options, avoid });\n }\n\n return { ...reach, shadowed: shadowed.sort(byCodeUnit) };\n}\n\n/**\n * How one file or component was reached, as the chain that reached it.\n *\n * The sentence a run prints when somebody asks why a subject was observed. Empty\n * when the node was not reached at all.\n */\nexport function explain(relations: Relations, moved: Reached, node: Node): readonly string[] {\n const id = idOf(relations, node.kind, node.name);\n if (id === undefined) return [];\n return trailOf(moved.reach, id).map((step) => relations.names[step]!);\n}\n\nfunction byCodeUnit(a: string, b: string): number {\n return a < b ? -1 : a > b ? 1 : 0;\n}\n"]}
@@ -0,0 +1,123 @@
1
+ /**
2
+ * `@variance-authority/core/segment` — a column of facts, as bytes.
3
+ *
4
+ * Two things in this repository already had to be written down and read back on
5
+ * another machine: the source index a scan reuses, and now the suite index a run
6
+ * leaves behind. Both are the same shape underneath — a pile of repeated strings
7
+ * and a pile of small integers — and both were going to grow the same four
8
+ * hundred lines of offset arithmetic independently.
9
+ *
10
+ * So the arithmetic is here and the schemas are not. This module knows about
11
+ * named sections, alignment, interned strings and the validation that a decode
12
+ * must perform before it believes a file. It knows nothing about what any
13
+ * section means, which is the property that lets one format change without
14
+ * touching the other.
15
+ *
16
+ * ## Why columns
17
+ *
18
+ * The reader that matters next is not this one. Every field is a run of
19
+ * fixed-width little-endian values at a computable offset, so a reader in
20
+ * another language takes a slice where this one takes a loop — the same bargain
21
+ * [`record-format.ts`](../../../sense/src/test-selection/record-format.ts)
22
+ * already made for the coverage segments.
23
+ *
24
+ * ## Why the header is JSON
25
+ *
26
+ * It is the one part that is read before anything is known, it is a few hundred
27
+ * bytes against a payload measured in megabytes, and being self-describing is
28
+ * what lets a decoder reject a file by name instead of by crash. The payload —
29
+ * the part that repeats — never is.
30
+ *
31
+ * ## The one rule
32
+ *
33
+ * A malformed reference rejects the whole file. There is no partial read and no
34
+ * salvage: a segment is a cache or a baseline, both of which may be rebuilt, and
35
+ * half of either is worse than neither.
36
+ */
37
+ /** The sentinel a `u32` column uses for "no value here". */
38
+ export declare const NONE = 4294967295;
39
+ /** A column, by the width its element type already declares. */
40
+ export type Column = Uint8Array | Uint32Array;
41
+ /**
42
+ * Write named columns as one aligned segment.
43
+ *
44
+ * Width is taken from the array's own type rather than from a convention about
45
+ * the section's name, so a column that is read back as `u8` is one that was
46
+ * written as `Uint8Array` and a mismatch is a compile error rather than a
47
+ * decode that succeeds against the wrong stride.
48
+ */
49
+ export declare function encodeSegment(format: string, version: number, columns: Readonly<Record<string, Column>>): Uint8Array;
50
+ /** Columns of one opened segment, by name. Every accessor may reject the file. */
51
+ export interface OpenSegment {
52
+ /** A `u8` column. Absent or of the wrong width rejects the file. */
53
+ u8(name: string): Uint8Array;
54
+ /** A `u32` column. Absent or of the wrong width rejects the file. */
55
+ u32(name: string): Uint32Array;
56
+ /** A `u32` column a writer of this version may legitimately not have written. */
57
+ maybeU32(name: string): Uint32Array;
58
+ /** Refuse the file, in the words the caller's format uses. */
59
+ reject(): Error;
60
+ }
61
+ /**
62
+ * Open a segment, refusing anything that is not one of the named format and
63
+ * version.
64
+ *
65
+ * `what` is the noun a rejection is phrased with — "source index", "suite index"
66
+ * — because the one thing a caller of this can usefully be told is which reader
67
+ * refused, and a shared message would name none of them.
68
+ */
69
+ export declare function openSegment(format: string, version: number, input: Uint8Array, what: string): OpenSegment;
70
+ /**
71
+ * Every distinct string, sorted, and the number each one is written as.
72
+ *
73
+ * Sorted rather than first-seen because two encodes of equal facts must produce
74
+ * equal bytes: a segment is content-addressed by whoever caches it, and an
75
+ * identical index that hashes differently defeats every transport that would
76
+ * otherwise have skipped the upload.
77
+ */
78
+ export declare function intern(values: Iterable<string>): {
79
+ readonly strings: readonly string[];
80
+ readonly id: (value: string) => number;
81
+ /** `NONE` for `undefined`, so an absent field costs a column and not a branch. */
82
+ readonly optionalId: (value: string | undefined) => number;
83
+ };
84
+ /** The two columns a dictionary is written as: the bytes, and where each one ends. */
85
+ export declare function stringColumns(strings: readonly string[]): {
86
+ readonly blob: Uint8Array;
87
+ readonly off: Uint32Array;
88
+ };
89
+ /** Read strings back out of a dictionary, rejecting any id the blob cannot hold. */
90
+ export declare function stringReader(blob: Uint8Array, off: Uint32Array, reject: () => Error): {
91
+ readonly text: (id: number) => string;
92
+ readonly optional: (id: number) => string | undefined;
93
+ };
94
+ /** A running-total column: `result[n]` is where row `n` starts, `result.at(-1)` the end. */
95
+ export declare function offsetsOf(lengths: readonly number[]): Uint32Array;
96
+ /**
97
+ * Check an offset column before any row is read through it.
98
+ *
99
+ * Checked here rather than per row because an offset column is the one thing in
100
+ * a segment that can turn a corrupt file into a *plausible* value: a pair that
101
+ * runs backwards reads a neighbouring row's data as this row's, and nothing
102
+ * downstream would ever notice.
103
+ */
104
+ export declare function validateOffsets(column: Uint32Array, end: number, rows: number, reject: () => Error): void;
105
+ /** Columns that describe the same rows must agree about how many there are. */
106
+ export declare function sameLength(length: number, columns: readonly {
107
+ readonly length: number;
108
+ }[], reject: () => Error): void;
109
+ /** The indices row `row` owns in the column an offset column points into. */
110
+ export declare function rangeOf(offsets: Uint32Array, row: number, reject: () => Error): number[];
111
+ /** A stored boolean, refusing the file for anything that is not one. */
112
+ export declare function flagOf(value: number | undefined, reject: () => Error): boolean;
113
+ /**
114
+ * Order two strings by UTF-16 code unit, which is the order a segment is sorted in.
115
+ *
116
+ * Exported because a schema that sorts its own values before interning them has
117
+ * to sort them the same way this does, and `localeCompare` is the trap: it is
118
+ * locale-dependent, so an index written on one machine would interleave
119
+ * differently on another and two encodes of equal facts would stop producing
120
+ * equal bytes.
121
+ */
122
+ export declare function codeUnitOrder(left: string, right: string): number;
123
+ //# sourceMappingURL=index.d.ts.map