@variance-authority/core 0.14.0 → 0.17.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (147) hide show
  1. package/CHANGELOG.md +878 -0
  2. package/README.md +38 -17
  3. package/dist/attribute/component-hash.d.ts +14 -5
  4. package/dist/attribute/component-hash.js +14 -5
  5. package/dist/attribute/component-hash.js.map +1 -1
  6. package/dist/attribute/composition.d.ts +16 -0
  7. package/dist/attribute/composition.js +4 -6
  8. package/dist/attribute/composition.js.map +1 -1
  9. package/dist/attribute/index.d.ts +3 -1
  10. package/dist/attribute/index.js +2 -1
  11. package/dist/attribute/index.js.map +1 -1
  12. package/dist/attribute/movement.d.ts +1 -1
  13. package/dist/attribute/movement.js.map +1 -1
  14. package/dist/attribute/region.d.ts +47 -4
  15. package/dist/attribute/region.js +91 -22
  16. package/dist/attribute/region.js.map +1 -1
  17. package/dist/attribute/source.d.ts +10 -24
  18. package/dist/attribute/source.js +4 -38
  19. package/dist/attribute/source.js.map +1 -1
  20. package/dist/attribute/subject-pieces.d.ts +37 -0
  21. package/dist/attribute/subject-pieces.js +122 -0
  22. package/dist/attribute/subject-pieces.js.map +1 -0
  23. package/dist/compare/band.d.ts +8 -2
  24. package/dist/compare/band.js +7 -2
  25. package/dist/compare/band.js.map +1 -1
  26. package/dist/compare/diff/attribution.js +6 -4
  27. package/dist/compare/diff/attribution.js.map +1 -1
  28. package/dist/compare/diff/compare-nodes.js +9 -1
  29. package/dist/compare/diff/compare-nodes.js.map +1 -1
  30. package/dist/compare/diff/match.d.ts +2 -1
  31. package/dist/compare/diff/match.js +52 -0
  32. package/dist/compare/diff/match.js.map +1 -1
  33. package/dist/compare/parting.js +6 -4
  34. package/dist/compare/parting.js.map +1 -1
  35. package/dist/format/capture.d.ts +25 -0
  36. package/dist/format/capture.js +6 -0
  37. package/dist/format/capture.js.map +1 -1
  38. package/dist/format/document.d.ts +28 -5
  39. package/dist/format/document.js +8 -2
  40. package/dist/format/document.js.map +1 -1
  41. package/dist/format/drive.d.ts +191 -0
  42. package/dist/format/drive.js +343 -0
  43. package/dist/format/drive.js.map +1 -0
  44. package/dist/format/environment.d.ts +20 -2
  45. package/dist/format/environment.js +7 -0
  46. package/dist/format/environment.js.map +1 -1
  47. package/dist/format/index.d.ts +13 -3
  48. package/dist/format/index.js +8 -2
  49. package/dist/format/index.js.map +1 -1
  50. package/dist/format/intervention.d.ts +79 -0
  51. package/dist/format/intervention.js.map +1 -1
  52. package/dist/format/origin.d.ts +32 -0
  53. package/dist/format/origin.js +54 -0
  54. package/dist/format/origin.js.map +1 -0
  55. package/dist/format/packed.d.ts +32 -0
  56. package/dist/format/packed.js +23 -0
  57. package/dist/format/packed.js.map +1 -0
  58. package/dist/format/shape.d.ts +31 -0
  59. package/dist/format/shape.js +269 -0
  60. package/dist/format/shape.js.map +1 -0
  61. package/dist/format/snapshot.d.ts +2 -0
  62. package/dist/format/snapshot.js.map +1 -1
  63. package/dist/format/stabilize.d.ts +35 -6
  64. package/dist/format/stabilize.js +58 -103
  65. package/dist/format/stabilize.js.map +1 -1
  66. package/dist/format/steps.d.ts +44 -0
  67. package/dist/format/steps.js +67 -0
  68. package/dist/format/steps.js.map +1 -0
  69. package/dist/format/waits.d.ts +12 -0
  70. package/dist/format/waits.js +156 -0
  71. package/dist/format/waits.js.map +1 -0
  72. package/dist/format/width.d.ts +35 -0
  73. package/dist/format/width.js +38 -0
  74. package/dist/format/width.js.map +1 -0
  75. package/dist/index.d.ts +2 -1
  76. package/dist/index.js +2 -1
  77. package/dist/index.js.map +1 -1
  78. package/dist/judge/docket.js +1 -0
  79. package/dist/judge/docket.js.map +1 -1
  80. package/dist/judge/index.d.ts +1 -0
  81. package/dist/judge/index.js +1 -0
  82. package/dist/judge/index.js.map +1 -1
  83. package/dist/judge/restamp.d.ts +20 -0
  84. package/dist/judge/restamp.js +20 -0
  85. package/dist/judge/restamp.js.map +1 -0
  86. package/dist/nearest.d.ts +46 -0
  87. package/dist/nearest.js +102 -0
  88. package/dist/nearest.js.map +1 -0
  89. package/dist/relate/before.d.ts +19 -24
  90. package/dist/relate/before.js +37 -39
  91. package/dist/relate/before.js.map +1 -1
  92. package/dist/relate/beyond.d.ts +59 -0
  93. package/dist/relate/beyond.js +98 -0
  94. package/dist/relate/beyond.js.map +1 -0
  95. package/dist/relate/carried.d.ts +122 -0
  96. package/dist/relate/carried.js +279 -0
  97. package/dist/relate/carried.js.map +1 -0
  98. package/dist/relate/graph.d.ts +14 -12
  99. package/dist/relate/graph.js +12 -11
  100. package/dist/relate/graph.js.map +1 -1
  101. package/dist/relate/index.d.ts +3 -1
  102. package/dist/relate/index.js +3 -1
  103. package/dist/relate/index.js.map +1 -1
  104. package/dist/relate/merkle.js +2 -11
  105. package/dist/relate/merkle.js.map +1 -1
  106. package/dist/relate/narrow.d.ts +2 -2
  107. package/dist/relate/narrow.js +26 -73
  108. package/dist/relate/narrow.js.map +1 -1
  109. package/dist/relate/reach.d.ts +6 -6
  110. package/dist/relate/reach.js +10 -53
  111. package/dist/relate/reach.js.map +1 -1
  112. package/dist/relate/records.d.ts +6 -5
  113. package/dist/relate/records.js +3 -2
  114. package/dist/relate/records.js.map +1 -1
  115. package/dist/rules/index.d.ts +3 -3
  116. package/dist/rules/index.js +3 -3
  117. package/dist/rules/index.js.map +1 -1
  118. package/dist/rules/normalize/cascade.d.ts +12 -0
  119. package/dist/rules/normalize/cascade.js +34 -16
  120. package/dist/rules/normalize/cascade.js.map +1 -1
  121. package/dist/rules/normalize/fixture.d.ts +3 -0
  122. package/dist/rules/normalize/fixture.js +3 -1
  123. package/dist/rules/normalize/fixture.js.map +1 -1
  124. package/dist/rules/normalize/index.d.ts +2 -2
  125. package/dist/rules/normalize/index.js +9 -2
  126. package/dist/rules/normalize/index.js.map +1 -1
  127. package/dist/rules/normalize/value.d.ts +13 -2
  128. package/dist/rules/normalize/value.js +42 -8
  129. package/dist/rules/normalize/value.js.map +1 -1
  130. package/dist/rules/normalize/wrapper.js +5 -0
  131. package/dist/rules/normalize/wrapper.js.map +1 -1
  132. package/dist/rules/ruleset.d.ts +24 -4
  133. package/dist/rules/ruleset.js +24 -4
  134. package/dist/rules/ruleset.js.map +1 -1
  135. package/dist/segment/index.d.ts +20 -4
  136. package/dist/segment/index.js +28 -7
  137. package/dist/segment/index.js.map +1 -1
  138. package/dist/shard/index.d.ts +11 -0
  139. package/dist/shard/index.js +11 -0
  140. package/dist/shard/index.js.map +1 -0
  141. package/dist/shard/placement.d.ts +119 -0
  142. package/dist/shard/placement.js +156 -0
  143. package/dist/shard/placement.js.map +1 -0
  144. package/dist/share/index.d.ts +1 -1
  145. package/dist/share/index.js +1 -1
  146. package/dist/share/index.js.map +1 -1
  147. package/package.json +8 -1
@@ -0,0 +1,102 @@
1
+ /**
2
+ * The name somebody meant, when exactly one name is close enough.
3
+ *
4
+ * A refusal names what was typed and then the command's synopsis, which lists
5
+ * what it accepts. The reader is usually an agent, and an agent that mistyped
6
+ * `--quer` should correct it from the same message instead of spending a second
7
+ * turn on it. So one sentence goes in front of the synopsis. Naming a candidate
8
+ * is cheap and reversible: nothing is run on the guess, and a wrong guess costs
9
+ * one line.
10
+ *
11
+ * ## Why one, and why not always
12
+ *
13
+ * Two names equally close is not a suggestion, it is a second decision handed
14
+ * back — so a tie suggests nothing and the set alone answers. And the distance
15
+ * is bounded by the length of what was typed: without that, `--x` is one edit
16
+ * from nothing and three from half the table, and a suggestion that fires on
17
+ * anything is one nobody can read as a signal.
18
+ *
19
+ * Damerau-Levenshtein, iterative, over three rows. Transposed neighbours count
20
+ * as one edit rather than two, which is not a refinement: `aks` for `ask` and
21
+ * `--fromat` for `--format` are the typo, and under plain Levenshtein both sit
22
+ * at distance two and are missed by the bound below. No dependency, for the
23
+ * reason `parse.ts` gives — a tool whose claim is that its output can be trusted
24
+ * does not acquire a transitive graph to spell-check a flag.
25
+ */
26
+ /** How far a name may be from what was typed, as a share of the typed length. */
27
+ function within(typed) {
28
+ // A third of it, at least one and at most three. One edit on a short name and
29
+ // three on `--exit-zero-on-changes`, which is the range real typos fall in:
30
+ // a transposition, a dropped character, a repeated one.
31
+ return Math.min(3, Math.max(1, Math.floor(typed.length / 3)));
32
+ }
33
+ /**
34
+ * The single closest candidate, or nothing.
35
+ *
36
+ * Case-insensitive, because a name typed in the wrong case is the same mistake
37
+ * and the answer is the same name. Comparison only — what comes back is the
38
+ * candidate as it is actually spelled, since that is what has to be typed next.
39
+ */
40
+ export function nearest(typed, candidates) {
41
+ const limit = within(typed);
42
+ let best;
43
+ let bestDistance = Number.POSITIVE_INFINITY;
44
+ let tied = false;
45
+ for (const candidate of candidates) {
46
+ const distance = editDistance(typed.toLowerCase(), candidate.toLowerCase());
47
+ if (distance > limit)
48
+ continue;
49
+ if (distance < bestDistance) {
50
+ best = candidate;
51
+ bestDistance = distance;
52
+ tied = false;
53
+ }
54
+ else if (distance === bestDistance) {
55
+ tied = true;
56
+ }
57
+ }
58
+ return tied ? undefined : best;
59
+ }
60
+ /**
61
+ * The line a refusal adds beside its list, or nothing.
62
+ *
63
+ * Its own function so every refusal says it the same way. An agent reading three
64
+ * different phrasings for one idea has three things to recognise.
65
+ *
66
+ * A line of its own, and it carries the newline itself so a refusal with nothing
67
+ * to suggest does not print a blank one. Beside the accepted set rather than
68
+ * inside that sentence: the set is the part a reader parses — a name split out
69
+ * of a list should not have half a second sentence attached to it.
70
+ */
71
+ export function didYouMean(typed, candidates) {
72
+ const found = nearest(typed, candidates);
73
+ return found === undefined ? '' : `\nDid you mean \`${found}\`?`;
74
+ }
75
+ /**
76
+ * Edits between two strings, counting a swap of neighbours as one.
77
+ *
78
+ * Three rows rather than a matrix: a transposition needs the row before last,
79
+ * and nothing needs the one before that.
80
+ */
81
+ function editDistance(from, to) {
82
+ let twoBack = [];
83
+ let previous = Array.from({ length: to.length + 1 }, (_, index) => index);
84
+ for (let row = 1; row <= from.length; row += 1) {
85
+ const current = [row];
86
+ for (let column = 1; column <= to.length; column += 1) {
87
+ const same = from[row - 1] === to[column - 1];
88
+ let best = Math.min(previous[column - 1] + (same ? 0 : 1), previous[column] + 1, current[column - 1] + 1);
89
+ if (row > 1 &&
90
+ column > 1 &&
91
+ from[row - 1] === to[column - 2] &&
92
+ from[row - 2] === to[column - 1]) {
93
+ best = Math.min(best, twoBack[column - 2] + 1);
94
+ }
95
+ current[column] = best;
96
+ }
97
+ twoBack = previous;
98
+ previous = current;
99
+ }
100
+ return previous[to.length];
101
+ }
102
+ //# sourceMappingURL=nearest.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"nearest.js","sourceRoot":"","sources":["../src/nearest.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The name somebody meant, when exactly one name is close enough.\n *\n * A refusal names what was typed and then the command's synopsis, which lists\n * what it accepts. The reader is usually an agent, and an agent that mistyped\n * `--quer` should correct it from the same message instead of spending a second\n * turn on it. So one sentence goes in front of the synopsis. Naming a candidate\n * is cheap and reversible: nothing is run on the guess, and a wrong guess costs\n * one line.\n *\n * ## Why one, and why not always\n *\n * Two names equally close is not a suggestion, it is a second decision handed\n * back — so a tie suggests nothing and the set alone answers. And the distance\n * is bounded by the length of what was typed: without that, `--x` is one edit\n * from nothing and three from half the table, and a suggestion that fires on\n * anything is one nobody can read as a signal.\n *\n * Damerau-Levenshtein, iterative, over three rows. Transposed neighbours count\n * as one edit rather than two, which is not a refinement: `aks` for `ask` and\n * `--fromat` for `--format` are the typo, and under plain Levenshtein both sit\n * at distance two and are missed by the bound below. No dependency, for the\n * reason `parse.ts` gives — a tool whose claim is that its output can be trusted\n * does not acquire a transitive graph to spell-check a flag.\n */\n\n/** How far a name may be from what was typed, as a share of the typed length. */\nfunction within(typed: string): number {\n // A third of it, at least one and at most three. One edit on a short name and\n // three on `--exit-zero-on-changes`, which is the range real typos fall in:\n // a transposition, a dropped character, a repeated one.\n return Math.min(3, Math.max(1, Math.floor(typed.length / 3)));\n}\n\n/**\n * The single closest candidate, or nothing.\n *\n * Case-insensitive, because a name typed in the wrong case is the same mistake\n * and the answer is the same name. Comparison only — what comes back is the\n * candidate as it is actually spelled, since that is what has to be typed next.\n */\nexport function nearest(typed: string, candidates: readonly string[]): string | undefined {\n const limit = within(typed);\n let best: string | undefined;\n let bestDistance = Number.POSITIVE_INFINITY;\n let tied = false;\n\n for (const candidate of candidates) {\n const distance = editDistance(typed.toLowerCase(), candidate.toLowerCase());\n if (distance > limit) continue;\n if (distance < bestDistance) {\n best = candidate;\n bestDistance = distance;\n tied = false;\n } else if (distance === bestDistance) {\n tied = true;\n }\n }\n\n return tied ? undefined : best;\n}\n\n/**\n * The line a refusal adds beside its list, or nothing.\n *\n * Its own function so every refusal says it the same way. An agent reading three\n * different phrasings for one idea has three things to recognise.\n *\n * A line of its own, and it carries the newline itself so a refusal with nothing\n * to suggest does not print a blank one. Beside the accepted set rather than\n * inside that sentence: the set is the part a reader parses — a name split out\n * of a list should not have half a second sentence attached to it.\n */\nexport function didYouMean(typed: string, candidates: readonly string[]): string {\n const found = nearest(typed, candidates);\n return found === undefined ? '' : `\\nDid you mean \\`${found}\\`?`;\n}\n\n/**\n * Edits between two strings, counting a swap of neighbours as one.\n *\n * Three rows rather than a matrix: a transposition needs the row before last,\n * and nothing needs the one before that.\n */\nfunction editDistance(from: string, to: string): number {\n let twoBack: number[] = [];\n let previous = Array.from({ length: to.length + 1 }, (_, index) => index);\n\n for (let row = 1; row <= from.length; row += 1) {\n const current = [row];\n for (let column = 1; column <= to.length; column += 1) {\n const same = from[row - 1] === to[column - 1];\n let best = Math.min(\n previous[column - 1]! + (same ? 0 : 1),\n previous[column]! + 1,\n current[column - 1]! + 1,\n );\n if (\n row > 1 &&\n column > 1 &&\n from[row - 1] === to[column - 2] &&\n from[row - 2] === to[column - 1]\n ) {\n best = Math.min(best, twoBack[column - 2]! + 1);\n }\n current[column] = best;\n }\n twoBack = previous;\n previous = current;\n }\n\n return previous[to.length]!;\n}\n"]}
@@ -34,11 +34,16 @@
34
34
  *
35
35
  * ## What declaring one buys beyond its own name
36
36
  *
37
- * Everything below it. A `setup.ts` the config loads, a fixture module only
38
- * that setup imports, a polyfill, the environment package it names: each is an
39
- * ordinary file the scan already holds, that nothing imports, whose change
40
- * reaches no component and narrows the run to nothing. Declared, the entry
41
- * point is walked *along* the arrows once and that whole set comes with it.
37
+ * Everything below it. A fixture module only the setup imports, a polyfill,
38
+ * the environment package it registers: each is an ordinary file the scan
39
+ * already holds, that nothing in a test imports, whose change reaches no
40
+ * component and narrows the run to nothing. Declared, the entry point is walked
41
+ * *along* the arrows once and that whole set comes with it.
42
+ *
43
+ * Only along imports. A runner config names its setup files as strings, and a
44
+ * string is no edge, so the setup is declared beside the config rather than
45
+ * found below it. Reading what a runner would make of its own config is the
46
+ * runner's answer, and guessing at it here would be wrong the day it changed.
42
47
  *
43
48
  * ## Where the descent stops
44
49
  *
@@ -52,17 +57,12 @@
52
57
  *
53
58
  * ## Where the two ends meet
54
59
  *
55
- * The same walk carries into the package layer, and that is the whole of the
56
- * mixed case. A config imports `jest-environment-jsdom`, which rests on
57
- * `jsdom`; `jsdom` is bumped; the install comparison names it. It is a package
58
- * the harness reaches, so the run does not narrow — and no file in the
59
- * repository ever wrote the word. A change beyond reach, arriving before it.
60
- *
61
- * That cost is real and is the operator's to spend: a config that imports a
62
- * bundler reaches everything the bundler rests on, so a bump inside that set
63
- * widens. It is the correct answer — the harness did change — and a repository
64
- * that finds it too wide narrows what it declares rather than what this
65
- * concludes.
60
+ * Not here. A setup imports `jest-environment-jsdom`, which rests on `jsdom`;
61
+ * `jsdom` is bumped. [`beyond.ts`](./beyond.ts) reads that bump back into the
62
+ * file whose import rests on it, the setup, before this stage is asked, so the
63
+ * closure holds files alone and a bump the harness rests on arrives as the
64
+ * harness file it changed. No file in the repository ever wrote the word, and
65
+ * the run still does not narrow.
66
66
  */
67
67
  import { type Relations } from './graph.js';
68
68
  /** The part of a run that sits before the tests, as names. */
@@ -77,8 +77,6 @@ export interface BeforeReach {
77
77
  readonly entries: readonly string[];
78
78
  /** Files below the entry points that the scan does not otherwise sense. */
79
79
  readonly files: ReadonlySet<string>;
80
- /** Packages they rest on, direct and transitive. */
81
- readonly packages: ReadonlySet<string>;
82
80
  /**
83
81
  * Declared entry points the graph does not hold, and so has no closure for.
84
82
  *
@@ -111,11 +109,8 @@ export interface BeforeReachOptions {
111
109
  */
112
110
  export declare function beforeReach(relations: Relations, entries: readonly string[], options?: BeforeReachOptions): BeforeReach;
113
111
  /**
114
- * Which of a diff's own inputs the harness rests on, as they were named.
115
- *
116
- * Files and packages together and in that order, because the caller's next act
117
- * is to print them in one sentence and a reader does not care which list a name
118
- * came out of. Empty is a real answer: the diff changed nothing the run rests on.
112
+ * Which of a diff's changed files the harness rests on, as they were named.
113
+ * Empty is a real answer: the diff changed nothing the run rests on.
119
114
  *
120
115
  * A declared entry claims every path under it, so naming a directory of
121
116
  * workflows is one line of configuration rather than one per file. A file the
@@ -123,7 +118,7 @@ export declare function beforeReach(relations: Relations, entries: readonly stri
123
118
  * widening it to its directory would put its neighbours before reach without
124
119
  * anybody saying they were.
125
120
  */
126
- export declare function changedBefore(before: BeforeReach, changed: readonly string[], packages?: readonly string[]): readonly string[];
121
+ export declare function changedBefore(before: BeforeReach, changed: readonly string[]): readonly string[];
127
122
  /**
128
123
  * Whether a path lies under one of these directories.
129
124
  *
@@ -34,11 +34,16 @@
34
34
  *
35
35
  * ## What declaring one buys beyond its own name
36
36
  *
37
- * Everything below it. A `setup.ts` the config loads, a fixture module only
38
- * that setup imports, a polyfill, the environment package it names: each is an
39
- * ordinary file the scan already holds, that nothing imports, whose change
40
- * reaches no component and narrows the run to nothing. Declared, the entry
41
- * point is walked *along* the arrows once and that whole set comes with it.
37
+ * Everything below it. A fixture module only the setup imports, a polyfill,
38
+ * the environment package it registers: each is an ordinary file the scan
39
+ * already holds, that nothing in a test imports, whose change reaches no
40
+ * component and narrows the run to nothing. Declared, the entry point is walked
41
+ * *along* the arrows once and that whole set comes with it.
42
+ *
43
+ * Only along imports. A runner config names its setup files as strings, and a
44
+ * string is no edge, so the setup is declared beside the config rather than
45
+ * found below it. Reading what a runner would make of its own config is the
46
+ * runner's answer, and guessing at it here would be wrong the day it changed.
42
47
  *
43
48
  * ## Where the descent stops
44
49
  *
@@ -52,19 +57,14 @@
52
57
  *
53
58
  * ## Where the two ends meet
54
59
  *
55
- * The same walk carries into the package layer, and that is the whole of the
56
- * mixed case. A config imports `jest-environment-jsdom`, which rests on
57
- * `jsdom`; `jsdom` is bumped; the install comparison names it. It is a package
58
- * the harness reaches, so the run does not narrow — and no file in the
59
- * repository ever wrote the word. A change beyond reach, arriving before it.
60
- *
61
- * That cost is real and is the operator's to spend: a config that imports a
62
- * bundler reaches everything the bundler rests on, so a bump inside that set
63
- * widens. It is the correct answer — the harness did change — and a repository
64
- * that finds it too wide narrows what it declares rather than what this
65
- * concludes.
60
+ * Not here. A setup imports `jest-environment-jsdom`, which rests on `jsdom`;
61
+ * `jsdom` is bumped. [`beyond.ts`](./beyond.ts) reads that bump back into the
62
+ * file whose import rests on it, the setup, before this stage is asked, so the
63
+ * closure holds files alone and a bump the harness rests on arrives as the
64
+ * harness file it changed. No file in the repository ever wrote the word, and
65
+ * the run still does not narrow.
66
66
  */
67
- import { nodeAt } from './graph.js';
67
+ import { RUNTIME_EDGES, nodeAt } from './graph.js';
68
68
  import { dependenciesOf } from './reach.js';
69
69
  /**
70
70
  * Walk down from each declared entry point, stopping where the scan takes over.
@@ -76,10 +76,9 @@ import { dependenciesOf } from './reach.js';
76
76
  */
77
77
  export function beforeReach(relations, entries, options = {}) {
78
78
  const files = new Set();
79
- const packages = new Set();
80
79
  const unread = [];
81
80
  if (entries.length === 0)
82
- return { entries, files, packages, unread };
81
+ return { entries, files, unread };
83
82
  const sensed = options.sensed ?? [];
84
83
  const held = new Map();
85
84
  for (let id = 0; id < relations.names.length; id += 1) {
@@ -87,16 +86,25 @@ export function beforeReach(relations, entries, options = {}) {
87
86
  if (node?.kind === 'file')
88
87
  held.set(node.name, id);
89
88
  }
89
+ // A declared directory is every file the graph holds under it, each walked
90
+ // as an entry point: a setup under `test/` loads what it loads whether the
91
+ // operator named the file or the directory it sits in.
90
92
  const seeds = [];
91
93
  for (const entry of entries) {
92
94
  const id = held.get(entry);
93
- if (id === undefined)
95
+ if (id !== undefined) {
96
+ seeds.push(id);
97
+ continue;
98
+ }
99
+ const under = [...held].filter(([name]) => within(name, [entry]));
100
+ if (under.length === 0)
94
101
  unread.push(entry);
95
102
  else
96
- seeds.push(id);
103
+ for (const [, below] of under)
104
+ seeds.push(below);
97
105
  }
98
106
  if (seeds.length === 0)
99
- return { entries, files, packages, unread };
107
+ return { entries, files, unread };
100
108
  // Avoided before the walk rather than filtered after it. A sensed file is not
101
109
  // a step on the way to something else here: whatever it imports, it imports
102
110
  // as a file the scan holds, and the dependents of *that* file are the exact
@@ -107,23 +115,16 @@ export function beforeReach(relations, entries, options = {}) {
107
115
  if (!entered.has(id) && within(name, sensed))
108
116
  avoid.push(id);
109
117
  }
110
- for (const id of dependenciesOf(relations, seeds, { avoid }).nodes) {
118
+ for (const id of dependenciesOf(relations, seeds, { through: RUNTIME_EDGES, avoid }).nodes) {
111
119
  const node = nodeAt(relations, id);
112
- if (node === undefined)
113
- continue;
114
- if (node.kind === 'file')
120
+ if (node?.kind === 'file')
115
121
  files.add(node.name);
116
- else if (node.kind === 'package')
117
- packages.add(node.name);
118
122
  }
119
- return { entries, files, packages, unread };
123
+ return { entries, files, unread };
120
124
  }
121
125
  /**
122
- * Which of a diff's own inputs the harness rests on, as they were named.
123
- *
124
- * Files and packages together and in that order, because the caller's next act
125
- * is to print them in one sentence and a reader does not care which list a name
126
- * came out of. Empty is a real answer: the diff changed nothing the run rests on.
126
+ * Which of a diff's changed files the harness rests on, as they were named.
127
+ * Empty is a real answer: the diff changed nothing the run rests on.
127
128
  *
128
129
  * A declared entry claims every path under it, so naming a directory of
129
130
  * workflows is one line of configuration rather than one per file. A file the
@@ -131,11 +132,8 @@ export function beforeReach(relations, entries, options = {}) {
131
132
  * widening it to its directory would put its neighbours before reach without
132
133
  * anybody saying they were.
133
134
  */
134
- export function changedBefore(before, changed, packages = []) {
135
- return [
136
- ...changed.filter((file) => before.files.has(file) || within(file, before.entries)),
137
- ...packages.filter((name) => before.packages.has(name)),
138
- ];
135
+ export function changedBefore(before, changed) {
136
+ return changed.filter((file) => before.files.has(file) || within(file, before.entries));
139
137
  }
140
138
  /**
141
139
  * Whether a path lies under one of these directories.
@@ -1 +1 @@
1
- {"version":3,"file":"before.js","sourceRoot":"","sources":["../../src/relate/before.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * What a run rests on that nothing in it imports.\n *\n * Selection walks one line. It starts at the test harness, moves right through\n * the tests into the repository's own files, and goes out past them into the\n * dependencies ([`records.ts`](./records.ts) holds all three as nodes). Every\n * question it asks is asked *against* the arrows: what depends on the thing\n * that changed.\n *\n * The far-left end of that line answers no such question. A `vitest.config.ts`,\n * a `jest.config.js`, a Next.js setup, a CI workflow, the Node version in a\n * `.nvmrc`: nothing imports them, so nothing has an edge to them, so a walk\n * from one reaches nothing and a walk to one arrives never. They are *before\n * reach* — able to repaint the whole suite and invisible to the only mechanism\n * that could say so.\n *\n * ## Why the accident is not enough\n *\n * A diff that names only such a file already runs everything, because a diff no\n * part of which is in the graph is refused. That holds only while it is the\n * *whole* diff. A CI workflow edited beside one component file is a diff with a\n * seed in it, and the walk from that seed answers confidently for a change it\n * never looked at. The widening was a side effect of having nothing to say, and\n * a side effect stops the moment somebody else has something to say.\n *\n * ## Why the operator names them\n *\n * Because no rule can. *Every changed path the graph does not hold* is the\n * README, the changelog, the fixture JSON, the editor settings: a whole run\n * each, forever, and an operator who turns that off has turned off the config\n * files too. Which paths govern a run is a fact about the repository that only\n * the repository knows, so it is declared — and once declared it is exact,\n * where a heuristic would be both too wide and too narrow on the same diff.\n *\n * ## What declaring one buys beyond its own name\n *\n * Everything below it. A `setup.ts` the config loads, a fixture module only\n * that setup imports, a polyfill, the environment package it names: each is an\n * ordinary file the scan already holds, that nothing imports, whose change\n * reaches no component and narrows the run to nothing. Declared, the entry\n * point is walked *along* the arrows once and that whole set comes with it.\n *\n * ## Where the descent stops\n *\n * At the first file the scan can already reason about. A setup file that\n * imports `src/theme.ts` does not drag `src/theme.ts` in with it: that file has\n * dependents, a change to it is answered exactly by walking them, and pulling\n * it into the harness would trade an exact answer for a whole run — the one\n * direction this is not allowed to be wrong in. The walk avoids sensed files,\n * and avoiding a node stops the walk there rather than stepping over it: what\n * lies below a sensed file is imported *through* it, and it already answers.\n *\n * ## Where the two ends meet\n *\n * The same walk carries into the package layer, and that is the whole of the\n * mixed case. A config imports `jest-environment-jsdom`, which rests on\n * `jsdom`; `jsdom` is bumped; the install comparison names it. It is a package\n * the harness reaches, so the run does not narrow — and no file in the\n * repository ever wrote the word. A change beyond reach, arriving before it.\n *\n * That cost is real and is the operator's to spend: a config that imports a\n * bundler reaches everything the bundler rests on, so a bump inside that set\n * widens. It is the correct answer — the harness did change — and a repository\n * that finds it too wide narrows what it declares rather than what this\n * concludes.\n */\n\nimport { nodeAt, type NodeId, type Relations } from './graph.js';\nimport { dependenciesOf } from './reach.js';\n\n/** The part of a run that sits before the tests, as names. */\nexport interface BeforeReach {\n /**\n * The entry points as they were declared, matched against a diff by path.\n *\n * Kept apart from what they reach because they are matched differently: a\n * declared path claims everything under it, so a directory of workflows is\n * one entry, while a file the walk found is one file.\n */\n readonly entries: readonly string[];\n /** Files below the entry points that the scan does not otherwise sense. */\n readonly files: ReadonlySet<string>;\n /** Packages they rest on, direct and transitive. */\n readonly packages: ReadonlySet<string>;\n /**\n * Declared entry points the graph does not hold, and so has no closure for.\n *\n * Ordinary for most of them: a `.nvmrc`, a CI workflow and a `tsconfig` have\n * nothing below them a scan could read, and they contribute their own name,\n * which is all they have. Not ordinary for a config the scan should have\n * read, where it means the closure that would have come with it — the setup\n * module, the environment — is missing and those files still narrow to\n * nothing. The two are indistinguishable from here, so both are reported and\n * neither is guessed at.\n */\n readonly unread: readonly string[];\n}\n\nexport interface BeforeReachOptions {\n /**\n * Files the scan already answers for, as the directories they live under.\n *\n * The descent stops at these. Absent, nothing stops it, and one setup import\n * into `src/` would put the repository's own source before reach.\n */\n readonly sensed?: readonly string[];\n}\n\n/**\n * Walk down from each declared entry point, stopping where the scan takes over.\n *\n * Along the arrows, unlike every other walk here, because this is the one\n * question whose subject has no dependents: *what does the harness rest on*.\n * Type-only edges are not walked — a config's `import type` is erased before\n * the suite runs, exactly as a component's is.\n */\nexport function beforeReach(\n relations: Relations,\n entries: readonly string[],\n options: BeforeReachOptions = {},\n): BeforeReach {\n const files = new Set<string>();\n const packages = new Set<string>();\n const unread: string[] = [];\n if (entries.length === 0) return { entries, files, packages, unread };\n\n const sensed = options.sensed ?? [];\n const held = new Map<string, NodeId>();\n for (let id = 0; id < relations.names.length; id += 1) {\n const node = nodeAt(relations, id);\n if (node?.kind === 'file') held.set(node.name, id);\n }\n\n const seeds: NodeId[] = [];\n for (const entry of entries) {\n const id = held.get(entry);\n if (id === undefined) unread.push(entry);\n else seeds.push(id);\n }\n if (seeds.length === 0) return { entries, files, packages, unread };\n\n // Avoided before the walk rather than filtered after it. A sensed file is not\n // a step on the way to something else here: whatever it imports, it imports\n // as a file the scan holds, and the dependents of *that* file are the exact\n // answer the harness would have replaced with a whole run.\n const entered = new Set(seeds);\n const avoid: NodeId[] = [];\n for (const [name, id] of held) {\n if (!entered.has(id) && within(name, sensed)) avoid.push(id);\n }\n\n for (const id of dependenciesOf(relations, seeds, { avoid }).nodes) {\n const node = nodeAt(relations, id);\n if (node === undefined) continue;\n if (node.kind === 'file') files.add(node.name);\n else if (node.kind === 'package') packages.add(node.name);\n }\n\n return { entries, files, packages, unread };\n}\n\n/**\n * Which of a diff's own inputs the harness rests on, as they were named.\n *\n * Files and packages together and in that order, because the caller's next act\n * is to print them in one sentence and a reader does not care which list a name\n * came out of. Empty is a real answer: the diff changed nothing the run rests on.\n *\n * A declared entry claims every path under it, so naming a directory of\n * workflows is one line of configuration rather than one per file. A file the\n * walk found claims only itself: the walk visited it, so it is known exactly, and\n * widening it to its directory would put its neighbours before reach without\n * anybody saying they were.\n */\nexport function changedBefore(\n before: BeforeReach,\n changed: readonly string[],\n packages: readonly string[] = [],\n): readonly string[] {\n return [\n ...changed.filter((file) => before.files.has(file) || within(file, before.entries)),\n ...packages.filter((name) => before.packages.has(name)),\n ];\n}\n\n/**\n * Whether a path lies under one of these directories.\n *\n * On directory boundaries rather than characters: `src` must not claim\n * `srcery/`, or a repository would put half of itself before reach and find out\n * from the runs that never narrowed. Exported because every caller that draws\n * this boundary over paths draws the same one, and two spellings of it would\n * disagree on the day one of them was fixed.\n */\nexport function within(file: string, dirs: readonly string[]): boolean {\n return dirs.some((dir) => {\n const normalized = dir.replace(/\\/+$/, '');\n return normalized === '.' || file === normalized || file.startsWith(`${normalized}/`);\n });\n}\n"]}
1
+ {"version":3,"file":"before.js","sourceRoot":"","sources":["../../src/relate/before.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * What a run rests on that nothing in it imports.\n *\n * Selection walks one line. It starts at the test harness, moves right through\n * the tests into the repository's own files, and goes out past them into the\n * dependencies ([`records.ts`](./records.ts) holds all three as nodes). Every\n * question it asks is asked *against* the arrows: what depends on the thing\n * that changed.\n *\n * The far-left end of that line answers no such question. A `vitest.config.ts`,\n * a `jest.config.js`, a Next.js setup, a CI workflow, the Node version in a\n * `.nvmrc`: nothing imports them, so nothing has an edge to them, so a walk\n * from one reaches nothing and a walk to one arrives never. They are *before\n * reach* — able to repaint the whole suite and invisible to the only mechanism\n * that could say so.\n *\n * ## Why the accident is not enough\n *\n * A diff that names only such a file already runs everything, because a diff no\n * part of which is in the graph is refused. That holds only while it is the\n * *whole* diff. A CI workflow edited beside one component file is a diff with a\n * seed in it, and the walk from that seed answers confidently for a change it\n * never looked at. The widening was a side effect of having nothing to say, and\n * a side effect stops the moment somebody else has something to say.\n *\n * ## Why the operator names them\n *\n * Because no rule can. *Every changed path the graph does not hold* is the\n * README, the changelog, the fixture JSON, the editor settings: a whole run\n * each, forever, and an operator who turns that off has turned off the config\n * files too. Which paths govern a run is a fact about the repository that only\n * the repository knows, so it is declared — and once declared it is exact,\n * where a heuristic would be both too wide and too narrow on the same diff.\n *\n * ## What declaring one buys beyond its own name\n *\n * Everything below it. A fixture module only the setup imports, a polyfill,\n * the environment package it registers: each is an ordinary file the scan\n * already holds, that nothing in a test imports, whose change reaches no\n * component and narrows the run to nothing. Declared, the entry point is walked\n * *along* the arrows once and that whole set comes with it.\n *\n * Only along imports. A runner config names its setup files as strings, and a\n * string is no edge, so the setup is declared beside the config rather than\n * found below it. Reading what a runner would make of its own config is the\n * runner's answer, and guessing at it here would be wrong the day it changed.\n *\n * ## Where the descent stops\n *\n * At the first file the scan can already reason about. A setup file that\n * imports `src/theme.ts` does not drag `src/theme.ts` in with it: that file has\n * dependents, a change to it is answered exactly by walking them, and pulling\n * it into the harness would trade an exact answer for a whole run — the one\n * direction this is not allowed to be wrong in. The walk avoids sensed files,\n * and avoiding a node stops the walk there rather than stepping over it: what\n * lies below a sensed file is imported *through* it, and it already answers.\n *\n * ## Where the two ends meet\n *\n * Not here. A setup imports `jest-environment-jsdom`, which rests on `jsdom`;\n * `jsdom` is bumped. [`beyond.ts`](./beyond.ts) reads that bump back into the\n * file whose import rests on it, the setup, before this stage is asked, so the\n * closure holds files alone and a bump the harness rests on arrives as the\n * harness file it changed. No file in the repository ever wrote the word, and\n * the run still does not narrow.\n */\n\nimport { RUNTIME_EDGES, nodeAt, type NodeId, type Relations } from './graph.js';\nimport { dependenciesOf } from './reach.js';\n\n/** The part of a run that sits before the tests, as names. */\nexport interface BeforeReach {\n /**\n * The entry points as they were declared, matched against a diff by path.\n *\n * Kept apart from what they reach because they are matched differently: a\n * declared path claims everything under it, so a directory of workflows is\n * one entry, while a file the walk found is one file.\n */\n readonly entries: readonly string[];\n /** Files below the entry points that the scan does not otherwise sense. */\n readonly files: ReadonlySet<string>;\n /**\n * Declared entry points the graph does not hold, and so has no closure for.\n *\n * Ordinary for most of them: a `.nvmrc`, a CI workflow and a `tsconfig` have\n * nothing below them a scan could read, and they contribute their own name,\n * which is all they have. Not ordinary for a config the scan should have\n * read, where it means the closure that would have come with it — the setup\n * module, the environment — is missing and those files still narrow to\n * nothing. The two are indistinguishable from here, so both are reported and\n * neither is guessed at.\n */\n readonly unread: readonly string[];\n}\n\nexport interface BeforeReachOptions {\n /**\n * Files the scan already answers for, as the directories they live under.\n *\n * The descent stops at these. Absent, nothing stops it, and one setup import\n * into `src/` would put the repository's own source before reach.\n */\n readonly sensed?: readonly string[];\n}\n\n/**\n * Walk down from each declared entry point, stopping where the scan takes over.\n *\n * Along the arrows, unlike every other walk here, because this is the one\n * question whose subject has no dependents: *what does the harness rest on*.\n * Type-only edges are not walked — a config's `import type` is erased before\n * the suite runs, exactly as a component's is.\n */\nexport function beforeReach(\n relations: Relations,\n entries: readonly string[],\n options: BeforeReachOptions = {},\n): BeforeReach {\n const files = new Set<string>();\n const unread: string[] = [];\n if (entries.length === 0) return { entries, files, unread };\n\n const sensed = options.sensed ?? [];\n const held = new Map<string, NodeId>();\n for (let id = 0; id < relations.names.length; id += 1) {\n const node = nodeAt(relations, id);\n if (node?.kind === 'file') held.set(node.name, id);\n }\n\n // A declared directory is every file the graph holds under it, each walked\n // as an entry point: a setup under `test/` loads what it loads whether the\n // operator named the file or the directory it sits in.\n const seeds: NodeId[] = [];\n for (const entry of entries) {\n const id = held.get(entry);\n if (id !== undefined) {\n seeds.push(id);\n continue;\n }\n const under = [...held].filter(([name]) => within(name, [entry]));\n if (under.length === 0) unread.push(entry);\n else for (const [, below] of under) seeds.push(below);\n }\n if (seeds.length === 0) return { entries, files, unread };\n\n // Avoided before the walk rather than filtered after it. A sensed file is not\n // a step on the way to something else here: whatever it imports, it imports\n // as a file the scan holds, and the dependents of *that* file are the exact\n // answer the harness would have replaced with a whole run.\n const entered = new Set(seeds);\n const avoid: NodeId[] = [];\n for (const [name, id] of held) {\n if (!entered.has(id) && within(name, sensed)) avoid.push(id);\n }\n\n for (const id of dependenciesOf(relations, seeds, { through: RUNTIME_EDGES, avoid }).nodes) {\n const node = nodeAt(relations, id);\n if (node?.kind === 'file') files.add(node.name);\n }\n\n return { entries, files, unread };\n}\n\n/**\n * Which of a diff's changed files the harness rests on, as they were named.\n * Empty is a real answer: the diff changed nothing the run rests on.\n *\n * A declared entry claims every path under it, so naming a directory of\n * workflows is one line of configuration rather than one per file. A file the\n * walk found claims only itself: the walk visited it, so it is known exactly, and\n * widening it to its directory would put its neighbours before reach without\n * anybody saying they were.\n */\nexport function changedBefore(before: BeforeReach, changed: readonly string[]): readonly string[] {\n return changed.filter((file) => before.files.has(file) || within(file, before.entries));\n}\n\n/**\n * Whether a path lies under one of these directories.\n *\n * On directory boundaries rather than characters: `src` must not claim\n * `srcery/`, or a repository would put half of itself before reach and find out\n * from the runs that never narrowed. Exported because every caller that draws\n * this boundary over paths draws the same one, and two spellings of it would\n * disagree on the day one of them was fixed.\n */\nexport function within(file: string, dirs: readonly string[]): boolean {\n return dirs.some((dir) => {\n const normalized = dir.replace(/\\/+$/, '');\n return normalized === '.' || file === normalized || file.startsWith(`${normalized}/`);\n });\n}\n"]}
@@ -0,0 +1,59 @@
1
+ /**
2
+ * What a change to the install did to the repository's own files.
3
+ *
4
+ * The far-right end of the line selection walks. A bumped package and a moved
5
+ * manifest change how files load without changing a line of any of them, and
6
+ * every later stage of a selection asks about files: whether one sits before
7
+ * reach, whether the record measured it, which tests entered it. So the install
8
+ * is read back into files once, here, and nothing after this stage hears of a
9
+ * package.
10
+ *
11
+ * ## Where a bump stops
12
+ *
13
+ * Against the arrows through the install, then one step into the repository. A
14
+ * bump of `jsdom` reaches `jest-environment-jsdom`, which rests on it, and from
15
+ * there the setup file that imports that: the first file whose import loads the
16
+ * changed code. It stops there because everything further along loads the
17
+ * package through that file, so a test that entered anything further along
18
+ * evaluated it on the way, and the stages after this one answer for its
19
+ * importers as they do for any changed file's.
20
+ *
21
+ * Runtime edges only. `import type { Theme } from '@mui/material'` is erased
22
+ * before anything runs, so a bump of `@mui/material` reaches nothing through it.
23
+ *
24
+ * ## Absence
25
+ *
26
+ * A package the graph holds no node for is imported by nothing here, and it
27
+ * reaches nothing. That is an answer rather than a gap: every file that names a
28
+ * package has an edge to it.
29
+ */
30
+ import { type Relations } from './graph.js';
31
+ /** What the install comparison says moved, by name. */
32
+ export interface InstallMoved {
33
+ /** Packages whose installed version moved. */
34
+ readonly packages: readonly string[];
35
+ /** Manifests whose `exports`, `main` or `type` moved, as repository paths. */
36
+ readonly moved: readonly string[];
37
+ }
38
+ /** The install, read as files. */
39
+ export interface BeyondReach {
40
+ /** Every repository file a bump or a moved manifest changed, whole, sorted. */
41
+ readonly files: readonly string[];
42
+ /**
43
+ * Moved manifests the graph holds no file beside: a gap under the scanned
44
+ * roots, or a package outside them. Each is read as the ordinary changed path
45
+ * it would have been, rather than as a package that reached nothing.
46
+ */
47
+ readonly unplaced: readonly string[];
48
+ /** The bumped packages that changed a file, for the sentence that says what the answer was built from. */
49
+ readonly traced: readonly string[];
50
+ /**
51
+ * For each file a bump reached, the packages it was reached through, from the
52
+ * bumped one to the one the file imports: `['jsdom', 'jest-environment-jsdom']`.
53
+ * The first bump to reach a file names it.
54
+ */
55
+ readonly chains: ReadonlyMap<string, readonly string[]>;
56
+ }
57
+ /** Trace the install's moves to the repository files they change. */
58
+ export declare function beyondReach(relations: Relations, install: InstallMoved): BeyondReach;
59
+ //# sourceMappingURL=beyond.d.ts.map
@@ -0,0 +1,98 @@
1
+ // compass: variance-authority.reach
2
+ /**
3
+ * What a change to the install did to the repository's own files.
4
+ *
5
+ * The far-right end of the line selection walks. A bumped package and a moved
6
+ * manifest change how files load without changing a line of any of them, and
7
+ * every later stage of a selection asks about files: whether one sits before
8
+ * reach, whether the record measured it, which tests entered it. So the install
9
+ * is read back into files once, here, and nothing after this stage hears of a
10
+ * package.
11
+ *
12
+ * ## Where a bump stops
13
+ *
14
+ * Against the arrows through the install, then one step into the repository. A
15
+ * bump of `jsdom` reaches `jest-environment-jsdom`, which rests on it, and from
16
+ * there the setup file that imports that: the first file whose import loads the
17
+ * changed code. It stops there because everything further along loads the
18
+ * package through that file, so a test that entered anything further along
19
+ * evaluated it on the way, and the stages after this one answer for its
20
+ * importers as they do for any changed file's.
21
+ *
22
+ * Runtime edges only. `import type { Theme } from '@mui/material'` is erased
23
+ * before anything runs, so a bump of `@mui/material` reaches nothing through it.
24
+ *
25
+ * ## Absence
26
+ *
27
+ * A package the graph holds no node for is imported by nothing here, and it
28
+ * reaches nothing. That is an answer rather than a gap: every file that names a
29
+ * package has an edge to it.
30
+ */
31
+ import { EDGE_KINDS, RUNTIME_EDGES, idOf, nodeAt, nodesOfKind } from './graph.js';
32
+ import { within } from './before.js';
33
+ import { dependentsOf, trailOf } from './reach.js';
34
+ /** The edge kinds a file loads a package through, as a byte lookup over `EDGE_KINDS`. */
35
+ const IMPORTS = new Uint8Array(EDGE_KINDS.length);
36
+ for (const kind of RUNTIME_EDGES)
37
+ if (kind !== 'depends-on')
38
+ IMPORTS[EDGE_KINDS.indexOf(kind)] = 1;
39
+ /** Trace the install's moves to the repository files they change. */
40
+ export function beyondReach(relations, install) {
41
+ const files = new Set();
42
+ const traced = [];
43
+ const chains = new Map();
44
+ for (const name of install.packages) {
45
+ const id = idOf(relations, 'package', name);
46
+ if (id === undefined)
47
+ continue;
48
+ // Every package whose install rests on this one, itself included.
49
+ const traversal = dependentsOf(relations, [id], { through: ['depends-on'] });
50
+ let reached = false;
51
+ for (const at of traversal.nodes) {
52
+ for (const importer of importersOf(relations, at)) {
53
+ const file = relations.names[importer];
54
+ files.add(file);
55
+ if (!chains.has(file))
56
+ chains.set(file, trailOf(traversal, at).map((step) => relations.names[step]));
57
+ reached = true;
58
+ }
59
+ }
60
+ if (reached)
61
+ traced.push(name);
62
+ }
63
+ // A moved manifest's directory is expanded the way a monorepo tool's changed
64
+ // directory is: every file the graph holds under it.
65
+ const unplaced = [];
66
+ if (install.moved.length > 0) {
67
+ const names = nodesOfKind(relations, 'file').map((id) => relations.names[id]);
68
+ for (const manifest of install.moved) {
69
+ const beside = names.filter((file) => within(file, [directoryOf(manifest)]));
70
+ if (beside.length === 0)
71
+ unplaced.push(manifest);
72
+ for (const file of beside)
73
+ files.add(file);
74
+ }
75
+ }
76
+ return { files: [...files].sort(byCodeUnit), unplaced, traced, chains };
77
+ }
78
+ /** The files with a runtime import of `id`, one step against the arrows. */
79
+ function importersOf(relations, id) {
80
+ const { offset, kind, target } = relations.dependents;
81
+ const importers = [];
82
+ for (let at = offset[id]; at < offset[id + 1]; at += 1) {
83
+ if (IMPORTS[kind[at]] !== 1)
84
+ continue;
85
+ const other = target[at];
86
+ if (nodeAt(relations, other)?.kind === 'file')
87
+ importers.push(other);
88
+ }
89
+ return importers;
90
+ }
91
+ function directoryOf(file) {
92
+ const at = file.lastIndexOf('/');
93
+ return at === -1 ? '.' : file.slice(0, at);
94
+ }
95
+ function byCodeUnit(a, b) {
96
+ return a < b ? -1 : a > b ? 1 : 0;
97
+ }
98
+ //# sourceMappingURL=beyond.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"beyond.js","sourceRoot":"","sources":["../../src/relate/beyond.ts"],"names":[],"mappings":"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","sourcesContent":["// compass: variance-authority.reach\n/**\n * What a change to the install did to the repository's own files.\n *\n * The far-right end of the line selection walks. A bumped package and a moved\n * manifest change how files load without changing a line of any of them, and\n * every later stage of a selection asks about files: whether one sits before\n * reach, whether the record measured it, which tests entered it. So the install\n * is read back into files once, here, and nothing after this stage hears of a\n * package.\n *\n * ## Where a bump stops\n *\n * Against the arrows through the install, then one step into the repository. A\n * bump of `jsdom` reaches `jest-environment-jsdom`, which rests on it, and from\n * there the setup file that imports that: the first file whose import loads the\n * changed code. It stops there because everything further along loads the\n * package through that file, so a test that entered anything further along\n * evaluated it on the way, and the stages after this one answer for its\n * importers as they do for any changed file's.\n *\n * Runtime edges only. `import type { Theme } from '@mui/material'` is erased\n * before anything runs, so a bump of `@mui/material` reaches nothing through it.\n *\n * ## Absence\n *\n * A package the graph holds no node for is imported by nothing here, and it\n * reaches nothing. That is an answer rather than a gap: every file that names a\n * package has an edge to it.\n */\n\nimport { EDGE_KINDS, RUNTIME_EDGES, idOf, nodeAt, nodesOfKind, type NodeId, type Relations } from './graph.js';\nimport { within } from './before.js';\nimport { dependentsOf, trailOf } from './reach.js';\n\n/** What the install comparison says moved, by name. */\nexport interface InstallMoved {\n /** Packages whose installed version moved. */\n readonly packages: readonly string[];\n /** Manifests whose `exports`, `main` or `type` moved, as repository paths. */\n readonly moved: readonly string[];\n}\n\n/** The install, read as files. */\nexport interface BeyondReach {\n /** Every repository file a bump or a moved manifest changed, whole, sorted. */\n readonly files: readonly string[];\n /**\n * Moved manifests the graph holds no file beside: a gap under the scanned\n * roots, or a package outside them. Each is read as the ordinary changed path\n * it would have been, rather than as a package that reached nothing.\n */\n readonly unplaced: readonly string[];\n /** The bumped packages that changed a file, for the sentence that says what the answer was built from. */\n readonly traced: readonly string[];\n /**\n * For each file a bump reached, the packages it was reached through, from the\n * bumped one to the one the file imports: `['jsdom', 'jest-environment-jsdom']`.\n * The first bump to reach a file names it.\n */\n readonly chains: ReadonlyMap<string, readonly string[]>;\n}\n\n/** The edge kinds a file loads a package through, as a byte lookup over `EDGE_KINDS`. */\nconst IMPORTS = new Uint8Array(EDGE_KINDS.length);\nfor (const kind of RUNTIME_EDGES) if (kind !== 'depends-on') IMPORTS[EDGE_KINDS.indexOf(kind)] = 1;\n\n/** Trace the install's moves to the repository files they change. */\nexport function beyondReach(relations: Relations, install: InstallMoved): BeyondReach {\n const files = new Set<string>();\n\n const traced: string[] = [];\n const chains = new Map<string, readonly string[]>();\n for (const name of install.packages) {\n const id = idOf(relations, 'package', name);\n if (id === undefined) continue;\n // Every package whose install rests on this one, itself included.\n const traversal = dependentsOf(relations, [id], { through: ['depends-on'] });\n let reached = false;\n for (const at of traversal.nodes) {\n for (const importer of importersOf(relations, at)) {\n const file = relations.names[importer]!;\n files.add(file);\n if (!chains.has(file)) chains.set(file, trailOf(traversal, at).map((step) => relations.names[step]!));\n reached = true;\n }\n }\n if (reached) traced.push(name);\n }\n\n // A moved manifest's directory is expanded the way a monorepo tool's changed\n // directory is: every file the graph holds under it.\n const unplaced: string[] = [];\n if (install.moved.length > 0) {\n const names = nodesOfKind(relations, 'file').map((id) => relations.names[id]!);\n for (const manifest of install.moved) {\n const beside = names.filter((file) => within(file, [directoryOf(manifest)]));\n if (beside.length === 0) unplaced.push(manifest);\n for (const file of beside) files.add(file);\n }\n }\n\n return { files: [...files].sort(byCodeUnit), unplaced, traced, chains };\n}\n\n/** The files with a runtime import of `id`, one step against the arrows. */\nfunction importersOf(relations: Relations, id: NodeId): readonly NodeId[] {\n const { offset, kind, target } = relations.dependents;\n const importers: NodeId[] = [];\n for (let at = offset[id]!; at < offset[id + 1]!; at += 1) {\n if (IMPORTS[kind[at]!] !== 1) continue;\n const other = target[at]!;\n if (nodeAt(relations, other)?.kind === 'file') importers.push(other);\n }\n return importers;\n}\n\nfunction directoryOf(file: string): string {\n const at = file.lastIndexOf('/');\n return at === -1 ? '.' : file.slice(0, at);\n}\n\nfunction byCodeUnit(a: string, b: string): number {\n return a < b ? -1 : a > b ? 1 : 0;\n}\n"]}