@variance-authority/core 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/README.md +1 -1
- package/dist/judge/sensitivity.js +1 -2
- package/dist/judge/sensitivity.js.map +1 -1
- package/dist/relate/graph.d.ts +10 -0
- package/dist/relate/graph.js +11 -1
- package/dist/relate/graph.js.map +1 -1
- package/dist/relate/index.d.ts +1 -0
- package/dist/relate/index.js +1 -0
- package/dist/relate/index.js.map +1 -1
- package/dist/relate/narrow.d.ts +53 -0
- package/dist/relate/narrow.js +133 -0
- package/dist/relate/narrow.js.map +1 -0
- package/dist/relate/reach.d.ts +7 -0
- package/dist/relate/reach.js +5 -0
- package/dist/relate/reach.js.map +1 -1
- package/dist/relate/records.d.ts +10 -1
- package/dist/relate/records.js +19 -2
- package/dist/relate/records.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# @variance-authority/core
|
|
2
2
|
|
|
3
|
+
## 0.9.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 2958674: `variance reach` and `variance run --since` walk from the exports a JavaScript or TypeScript edit changed, not from the whole file. A file that imports only exports the edit left as they were is not reached, and a barrel passes each changed export on under the name it republishes it as. Stderr names the changed exports of each file, and `variance reach --format json` lists them under `exports`. A namespace import, a `require`, a dynamic `import()` and an import that binds nothing are still walked whole. `affectedBy` in `@variance-authority/core/relate` takes `moved`, the exports each seed changed, and `relationsOfFiles` takes `uses`, the names each import binds; `readPublishedSources` in `@variance-authority/sense` returns that lookup, read from the parses the index already stores.
|
|
8
|
+
|
|
9
|
+
## 0.8.1
|
|
10
|
+
|
|
11
|
+
Lockstep release — nothing in this package changed. Every `@variance-authority/*` package shares one version.
|
|
12
|
+
|
|
3
13
|
## 0.8.0
|
|
4
14
|
|
|
5
15
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -254,7 +254,7 @@ observation profiles differ.
|
|
|
254
254
|
| `isolateRegions` / `attributeRegions` | `cell` and `limit` bound mask work; `origin` and `containment` describe the coordinate origin and how much a node must contain a region |
|
|
255
255
|
| `fingerprintOfMask` | `grid` controls the shape sample and `coverage` the minimum occupied share |
|
|
256
256
|
| `compareLocales` / ignore validation | `slack` permits a declared locale distance; `sites` supplies resolved ignore locations and `now` evaluates expiry |
|
|
257
|
-
| `buildDocket` / dependency reach | `sampleSize` limits review examples; `through` selects graph edge kinds, `avoid` names nodes a walk never covers, and `shadows` lists per file the modules its run never loads, so `affectedBy` leaves out a file every trail to which crosses one of its own shadows; `depends` adds the install — which package rests on which — so a dependency bump is a seed like any other and affects only the files that import it |
|
|
257
|
+
| `buildDocket` / dependency reach | `sampleSize` limits review examples; `through` selects graph edge kinds, `avoid` names nodes a walk never covers, and `shadows` lists per file the modules its run never loads, so `affectedBy` leaves out a file every trail to which crosses one of its own shadows; `depends` adds the install — which package rests on which — so a dependency bump is a seed like any other and affects only the files that import it; `uses` names what each import binds and each re-export republishes, and `moved` lists per seed the exports its change changed, so `affectedBy` covers only the files that import one of them |
|
|
258
258
|
| `beforeReach` | `sensed` names the directories the scan already answers for, and the descent from a declared entry point stops at the first file under one of them rather than dragging the repository's own source in behind the harness |
|
|
259
259
|
| `sharedClosures` | `floor` drops a shared subtree below a node count |
|
|
260
260
|
| `lexiconOf` | `examples`, `declaredIn` and `regions` supply what the instances cannot: which components a subject is the example of, the files declaring each component, and the regions its journey covered |
|
|
@@ -60,8 +60,7 @@ export function summarizeSensitivity(register) {
|
|
|
60
60
|
for (const entry of register.relaxations) {
|
|
61
61
|
if (entry.absorbed === 0) {
|
|
62
62
|
lines.push(` [dead] ${entry.rule} — asserts on ${entry.level} across ` +
|
|
63
|
-
`${entry.subjects.length} subject(s) and absorbed nothing (${entry.reason});
|
|
64
|
-
'nothing here needed relaxing');
|
|
63
|
+
`${entry.subjects.length} subject(s) and absorbed nothing (${entry.reason})`);
|
|
65
64
|
continue;
|
|
66
65
|
}
|
|
67
66
|
lines.push(` ${entry.rule} — asserts on ${entry.level}; absorbed ${entry.absorbed} ` +
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sensitivity.js","sourceRoot":"","sources":["../../src/judge/sensitivity.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAa,MAAM,oBAAoB,CAAC;AAEtD,OAAO,EAAE,YAAY,EAAuC,MAAM,aAAa,CAAC;AAgFhF,MAAM,MAAM,GAA6C;IACvD,MAAM,EAAE,KAAK;IACb,MAAM,EAAE,CAAC,MAAM,EAAE,UAAU,CAAC;IAC5B,OAAO,EAAE,CAAC,MAAM,EAAE,SAAS,CAAC;CAC7B,CAAC;AAEF,6EAA6E;AAC7E,MAAM,UAAU,OAAO,CAAC,KAAY;IAClC,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC;AACvB,CAAC;AAkED;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,QAAQ,CAAC,IAAqB;IAC5C,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC;IAC9C,MAAM,QAAQ,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC;IAE7D,8EAA8E;IAC9E,6EAA6E;IAC7E,8EAA8E;IAC9E,+CAA+C;IAC/C,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IAEvC,OAAO;QACL,EAAE,EAAE,IAAI,CAAC,EAAE;QACX,MAAM,EAAE,IAAI,CAAC,MAAM;QACnB,KAAK,EAAE,QAAQ;QACf,wEAAwE;QACxE,6CAA6C;QAC7C,KAAK,EAAE,IAAI;QACX,GAAG,CAAC,IAAI,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,IAAI,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KACpE,CAAC;AACJ,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,oBAAoB,CAAC,QAA6B;IAChE,IAAI,QAAQ,CAAC,WAAW,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IAEjD,MAAM,KAAK,GAAG;QACZ,iBAAiB,QAAQ,CAAC,aAAa,kCAAkC;YACvE,MAAM,QAAQ,CAAC,WAAW,CAAC,MAAM,UAAU;KAC9C,CAAC;IAEF,KAAK,MAAM,KAAK,IAAI,QAAQ,CAAC,WAAW,EAAE,CAAC;QACzC,IAAI,KAAK,CAAC,QAAQ,KAAK,CAAC,EAAE,CAAC;YACzB,KAAK,CAAC,IAAI,CACR,YAAY,KAAK,CAAC,IAAI,iBAAiB,KAAK,CAAC,KAAK,UAAU;gBAC1D,GAAG,KAAK,CAAC,QAAQ,CAAC,MAAM,qCAAqC,KAAK,CAAC,MAAM,KAAK;gBAC9E,8BAA8B,CACjC,CAAC;YACF,SAAS;QACX,CAAC;QACD,KAAK,CAAC,IAAI,CACR,KAAK,KAAK,CAAC,IAAI,iBAAiB,KAAK,CAAC,KAAK,cAAc,KAAK,CAAC,QAAQ,GAAG;YACxE,GAAG,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,qBAAqB,KAAK,CAAC,QAAQ,CAAC,MAAM,eAAe;YACjF,KAAK,CAAC,MAAM,CACf,CAAC;IACJ,CAAC;IAED,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,gBAAgB,CAC9B,KAA8B,EAC9B,KAAiC,EACjC,OAAO,GAAkB,EAAE;IAE3B,MAAM,UAAU,GAAG,IAAI,GAAG,EAA2B,CAAC;IACtD,MAAM,OAAO,GAAiB,EAAE,CAAC;IAEjC,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,MAAM,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC;QAC9B,IAAI,MAAM,KAAK,IAAI;YAAE,SAAS;QAC9B,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,EAAE,IAAI,CAAC,CAAC;QAC9B,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IACvB,CAAC;IAED,MAAM,OAAO,GAAG,YAAY,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC;IAEtD,MAAM,WAAW,GAAG,CAAC,GAAG,UAAU,CAAC,MAAM,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE;QACxD,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,KAAK,IAAI,CAAC,EAAE,CAAC,CAAC;QACnF,OAAO;YACL,IAAI,EAAE,IAAI,CAAC,EAAE;YACb,MAAM,EAAE,IAAI,CAAC,MAAM;YACnB,KAAK,EAAE,IAAI,CAAC,KAAK;YACjB,QAAQ,EAAE,QAAQ,EAAE,MAAM,IAAI,CAAC;YAC/B,QAAQ,EAAE,QAAQ,EAAE,QAAQ,IAAI,EAAE;YAClC,KAAK,EAAE,QAAQ,EAAE,KAAK,IAAI,EAAE;SAC7B,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH,OAAO;QACL,KAAK,EAAE,OAAO,CAAC,KAAK;QACpB,QAAQ,EAAE;YACR,WAAW;YACX,IAAI,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,QAAQ,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC;YACpF,aAAa,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,KAAK,EAAE,EAAE,CAAC,GAAG,GAAG,KAAK,CAAC,QAAQ,EAAE,CAAC,CAAC;SAC3E;KACF,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,UAAU,OAAO,CACrB,KAAY,EACZ,KAAsB;IAEtB,MAAM,SAAS,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC;IAE1C,OAAO;QACL,QAAQ,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACrD,QAAQ,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;KACvD,CAAC;AACJ,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,eAAe,CAAC,KAAY,EAAE,KAAsB;IAClE,OAAO,KAAK,CAAC,MAAM,GAAG,CAAC,IAAI,OAAO,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC,QAAQ,CAAC,MAAM,KAAK,CAAC,CAAC;AACzE,CAAC","sourcesContent":["import { BANDS, type Band } from '../compare/band.js';\nimport type { SemanticDiff } from '../compare/diff/index.js';\nimport { applyIgnores, type IgnoreOptions, type IgnoreRule } from './ignore.js';\n\n/**\n * How much of a subject is being asserted on, declared where it applies.\n *\n * A route-level test and a component-level test want opposite things from the\n * same machinery. A component's test asserts on everything: a colour token moved\n * and that is the change. A route's test asserts that the *page still assembles*\n * — the nav is where it was, the sidebar did not collapse, nothing overlaps — and\n * a design-system token landing in forty routes is noise it should never have\n * been shown. Run one policy over both and one of them is unusable: either the\n * routes are permanently red, or the components stop reporting the thing they\n * exist to catch.\n *\n * The usual answer is a second product, or a threshold, and both are worse than\n * the problem. What this offers instead is the same shape an\n * {@link IgnoreRule} has, because it is the same act: **naming what is not being\n * asserted on, where, by whom, and with the count in the report.**\n *\n * ## The unit is a band, and that is the whole design\n *\n * The frequency bands already say what *kind* of thing moved — `a11y`,\n * `geometry`, `token`, `content`, `texture` — and they are ordered by how rarely\n * they move and how much it matters when they do. \"Assert on layout and ignore\n * small token updates\" is not a tolerance, a percentage or a pixel count. It is\n * two band names.\n *\n * That is why this is not a threshold: a threshold absorbs *anything* small\n * enough, and a band absorbs exactly one kind of thing however large it is. A\n * route declared `layout` still reports a nav that moved by one pixel, and never\n * reports a rebrand that repainted every surface on the page.\n *\n * ## Why it is a declaration and not a mode\n *\n * The same reason an ignore is. A run-wide switch is a decision nobody attributes\n * and nobody revisits; a rule with an id, a required reason and a place is one a\n * reviewer can read six months later and disagree with. Everything an ignore owes\n * it owes: a reason, a scope, and a count of what it absorbed — including the\n * count of zero, which is how a declaration that has stopped being true is found.\n */\n\n/**\n * A named set of bands worth asserting on.\n *\n * A closed set. Three names cover the cases that have argued for themselves and\n * a fourth would need one; an open set of user-defined levels would put the\n * vocabulary in a config file, where the report cannot explain what a word meant\n * to whoever wrote it.\n */\nexport type Level =\n /**\n * Everything. What a component's own test wants, and the default everywhere.\n *\n * Named rather than left implicit so a subject can be declared strict *back*\n * inside a route that is not — the exception a broad declaration always grows.\n */\n | 'strict'\n /**\n * The page still assembles: `a11y` and `geometry`.\n *\n * `geometry` is the obvious half — boxes appeared, vanished, moved or resized.\n * `a11y` is the half that gets left out and should not be: a control that lost\n * its accessible name is a structural regression that happens to repaint\n * nothing, and a route test that ignored it would be asserting on the shape of\n * the page while blind to the shape a screen reader sees.\n *\n * What it absorbs is `token`, `content` and `texture` — a restyle, a copy edit,\n * anti-aliasing. Each is a real change, each is reported as absorbed, and none\n * of them is what a route-level test is for.\n */\n | 'layout'\n /**\n * What the page *says*: `a11y` and `content`.\n *\n * For a subject whose styling is somebody else's to change — a themed embed, a\n * page inside a design system being actively rebranded — where the assertion\n * worth keeping is that the words and the semantics survived.\n */\n | 'content';\n\nconst LEVELS: Readonly<Record<Level, readonly Band[]>> = {\n strict: BANDS,\n layout: ['a11y', 'geometry'],\n content: ['a11y', 'content'],\n};\n\n/** The bands a level asserts on. Everything else is absorbed and counted. */\nexport function bandsOf(level: Level): readonly Band[] {\n return LEVELS[level];\n}\n\nexport interface SensitivityRule {\n /** Stable name. Appears in the report and in every count this rule produces. */\n readonly id: string;\n\n /**\n * Why this subject is not asserted on in full. Required, as an ignore's is.\n *\n * The difference between a policy somebody chose and one that accreted. A\n * route declared `layout` two years ago by somebody who has left is a blind\n * spot with a plausible-looking config entry in front of it.\n */\n readonly reason: string;\n\n readonly level: Level;\n\n /** Subjects this applies to. `*` matches any run of characters. */\n readonly subjects?: readonly string[];\n\n /**\n * Tags the subject must carry, as its own artifact declared them.\n *\n * FIXME: read by nothing. `asIgnore` copies `subjects` and drops this, so a\n * rule scoped only by tags translates to an unscoped ignore and absorbs across\n * every subject in the run — the widest possible reading of the narrowest\n * scope an operator can write.\n */\n readonly tags?: readonly string[];\n}\n\n/** What one rule absorbed, per band, across a run. */\nexport interface Relaxation {\n readonly rule: string;\n readonly reason: string;\n readonly level: Level;\n\n /** Deltas absorbed because their band is not asserted on here. */\n readonly absorbed: number;\n readonly subjects: readonly string[];\n\n /** Which bands were absorbed, so a level can be tightened with evidence. */\n readonly bands: readonly Band[];\n}\n\nexport interface SensitivityRegister {\n readonly relaxations: readonly Relaxation[];\n\n /**\n * Rules that absorbed nothing in any subject they applied to.\n *\n * The same accounting an ignore's register keeps, and for the same reason: a\n * route declared `layout` that has reported no token change in six months is\n * either a route nothing styles, or a declaration nobody needed. Both are worth\n * a line, and neither is visible without the count.\n */\n readonly dead: readonly string[];\n\n readonly totalAbsorbed: number;\n}\n\nexport interface SensitivityOutcome {\n readonly diffs: readonly SemanticDiff[];\n readonly register: SensitivityRegister;\n}\n\n/**\n * Turn a sensitivity rule into the ignore it is.\n *\n * Not a shortcut — a statement about what these are. A sensitivity names a place\n * and narrows by band, which is exactly the shape `IgnoreRule` already refuses to\n * accept *without* a place, and exactly the shape it accepts with one. Sharing\n * the mechanism means sharing the safety property: the deltas are absorbed by\n * `applyIgnores`, counted per rule, and reported when a rule absorbs nothing.\n *\n * The bands are inverted on the way through, and that inversion is the whole\n * translation. A sensitivity says what it *asserts on*; an ignore says what it\n * *absorbs*. Stating the level positively is what makes a config readable — \"this\n * route asserts on layout\" rather than \"this route ignores token, content and\n * texture\" — and the negative form is what the machinery needs.\n */\nexport function asIgnore(rule: SensitivityRule): IgnoreRule | null {\n const asserted = new Set(bandsOf(rule.level));\n const absorbed = BANDS.filter((band) => !asserted.has(band));\n\n // `strict` asserts on everything, so it absorbs nothing and produces no rule.\n // Returning an ignore with an empty band list would produce a register entry\n // that is permanently dead, which is a line telling an operator to delete the\n // one declaration that is doing nothing wrong.\n if (absorbed.length === 0) return null;\n\n return {\n id: rule.id,\n reason: rule.reason,\n bands: absorbed,\n // The place is the subject, said rather than left to be inferred from a\n // missing selector — see `IgnoreRule.whole`.\n whole: true,\n ...(rule.subjects !== undefined ? { subjects: rule.subjects } : {}),\n };\n}\n\n/**\n * The register as the lines a run prints.\n *\n * Absorption is stated in the *positive* form the operator wrote, because that is\n * the sentence they can check: \"asserts on layout\" is a claim about intent, and\n * \"absorbed 412 token deltas\" is the evidence for or against it.\n */\nexport function summarizeSensitivity(register: SensitivityRegister): readonly string[] {\n if (register.relaxations.length === 0) return [];\n\n const lines = [\n `SENSITIVITY — ${register.totalAbsorbed} difference(s) not asserted on, ` +\n `by ${register.relaxations.length} rule(s)`,\n ];\n\n for (const entry of register.relaxations) {\n if (entry.absorbed === 0) {\n lines.push(\n ` [dead] ${entry.rule} — asserts on ${entry.level} across ` +\n `${entry.subjects.length} subject(s) and absorbed nothing (${entry.reason}); ` +\n 'nothing here needed relaxing',\n );\n continue;\n }\n lines.push(\n ` ${entry.rule} — asserts on ${entry.level}; absorbed ${entry.absorbed} ` +\n `${entry.bands.join('/')} difference(s) in ${entry.subjects.length} subject(s): ` +\n entry.reason,\n );\n }\n\n return lines;\n}\n\n/**\n * Apply declared sensitivities to a run's diffs.\n *\n * A thin fold over {@link applyIgnores}, and thin on purpose: the absorption, the\n * per-rule counting and the dead-rule reporting are one mechanism, and a second\n * implementation of them would be a second set of bugs in the half of the system\n * whose whole job is not to silently hide things.\n *\n * Rules that resolve to `strict` are dropped before the fold rather than passed\n * through, so a project that declares its default explicitly does not collect a\n * permanently-dead register line for saying so.\n */\nexport function applySensitivity(\n diffs: readonly SemanticDiff[],\n rules: readonly SensitivityRule[],\n options: IgnoreOptions = {},\n): SensitivityOutcome {\n const translated = new Map<string, SensitivityRule>();\n const ignores: IgnoreRule[] = [];\n\n for (const rule of rules) {\n const ignore = asIgnore(rule);\n if (ignore === null) continue;\n translated.set(rule.id, rule);\n ignores.push(ignore);\n }\n\n const outcome = applyIgnores(diffs, ignores, options);\n\n const relaxations = [...translated.values()].map((rule) => {\n const absorbed = outcome.register.absorbed.find((entry) => entry.rule === rule.id);\n return {\n rule: rule.id,\n reason: rule.reason,\n level: rule.level,\n absorbed: absorbed?.deltas ?? 0,\n subjects: absorbed?.subjects ?? [],\n bands: absorbed?.bands ?? [],\n };\n });\n\n return {\n diffs: outcome.diffs,\n register: {\n relaxations,\n dead: relaxations.filter((entry) => entry.absorbed === 0).map((entry) => entry.rule),\n totalAbsorbed: relaxations.reduce((sum, entry) => sum + entry.absorbed, 0),\n },\n };\n}\n\n/**\n * What a level does with a set of bands that moved.\n *\n * The arm for the path that holds no second document. `applySensitivity` folds\n * over deltas, which needs both revisions normalized; a `variance run` compares\n * an image against a stored baseline and has only what that baseline carried —\n * per-component hashes, one digest per band (ADR-0027, ADR-0029's split).\n * `bandsBetween` turns those into the same vocabulary, and this decides against\n * it, so the two paths agree by sharing {@link bandsOf} rather than by\n * inspection.\n *\n * **Empty in means nothing absorbed**, not everything. A run whose pixels moved\n * while every component hash held is raster residue — `texture`, which a\n * document cannot carry and which this must therefore never claim to have\n * decided. Absorbing on an empty set would turn the one band a hash comparison\n * is blind to into the one it silences.\n */\nexport function relaxes(\n level: Level,\n moved: readonly Band[],\n): { readonly asserted: readonly Band[]; readonly absorbed: readonly Band[] } {\n const asserting = new Set(bandsOf(level));\n\n return {\n asserted: moved.filter((band) => asserting.has(band)),\n absorbed: moved.filter((band) => !asserting.has(band)),\n };\n}\n\n/**\n * Whether a subject's every moved band falls outside what it is asserted on.\n *\n * The whole subject, because that is the unit a stored baseline can answer for:\n * one image, one verdict. A subject where *some* moved band is asserted on is\n * reported in full — including the bands that would have been absorbed — since\n * a reviewer looking at a nav that moved wants the restyle that came with it,\n * and hiding half a diff is worse than hiding none of it.\n */\nexport function absorbsEntirely(level: Level, moved: readonly Band[]): boolean {\n return moved.length > 0 && relaxes(level, moved).asserted.length === 0;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"sensitivity.js","sourceRoot":"","sources":["../../src/judge/sensitivity.ts"],"names":[],"mappings":"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","sourcesContent":["import { BANDS, type Band } from '../compare/band.js';\nimport type { SemanticDiff } from '../compare/diff/index.js';\nimport { applyIgnores, type IgnoreOptions, type IgnoreRule } from './ignore.js';\n\n/**\n * How much of a subject is being asserted on, declared where it applies.\n *\n * A route-level test and a component-level test want opposite things from the\n * same machinery. A component's test asserts on everything: a colour token moved\n * and that is the change. A route's test asserts that the *page still assembles*\n * — the nav is where it was, the sidebar did not collapse, nothing overlaps — and\n * a design-system token landing in forty routes is noise it should never have\n * been shown. Run one policy over both and one of them is unusable: either the\n * routes are permanently red, or the components stop reporting the thing they\n * exist to catch.\n *\n * The usual answer is a second product, or a threshold, and both are worse than\n * the problem. What this offers instead is the same shape an\n * {@link IgnoreRule} has, because it is the same act: **naming what is not being\n * asserted on, where, by whom, and with the count in the report.**\n *\n * ## The unit is a band, and that is the whole design\n *\n * The frequency bands already say what *kind* of thing moved — `a11y`,\n * `geometry`, `token`, `content`, `texture` — and they are ordered by how rarely\n * they move and how much it matters when they do. \"Assert on layout and ignore\n * small token updates\" is not a tolerance, a percentage or a pixel count. It is\n * two band names.\n *\n * That is why this is not a threshold: a threshold absorbs *anything* small\n * enough, and a band absorbs exactly one kind of thing however large it is. A\n * route declared `layout` still reports a nav that moved by one pixel, and never\n * reports a rebrand that repainted every surface on the page.\n *\n * ## Why it is a declaration and not a mode\n *\n * The same reason an ignore is. A run-wide switch is a decision nobody attributes\n * and nobody revisits; a rule with an id, a required reason and a place is one a\n * reviewer can read six months later and disagree with. Everything an ignore owes\n * it owes: a reason, a scope, and a count of what it absorbed — including the\n * count of zero, which is how a declaration that has stopped being true is found.\n */\n\n/**\n * A named set of bands worth asserting on.\n *\n * A closed set. Three names cover the cases that have argued for themselves and\n * a fourth would need one; an open set of user-defined levels would put the\n * vocabulary in a config file, where the report cannot explain what a word meant\n * to whoever wrote it.\n */\nexport type Level =\n /**\n * Everything. What a component's own test wants, and the default everywhere.\n *\n * Named rather than left implicit so a subject can be declared strict *back*\n * inside a route that is not — the exception a broad declaration always grows.\n */\n | 'strict'\n /**\n * The page still assembles: `a11y` and `geometry`.\n *\n * `geometry` is the obvious half — boxes appeared, vanished, moved or resized.\n * `a11y` is the half that gets left out and should not be: a control that lost\n * its accessible name is a structural regression that happens to repaint\n * nothing, and a route test that ignored it would be asserting on the shape of\n * the page while blind to the shape a screen reader sees.\n *\n * What it absorbs is `token`, `content` and `texture` — a restyle, a copy edit,\n * anti-aliasing. Each is a real change, each is reported as absorbed, and none\n * of them is what a route-level test is for.\n */\n | 'layout'\n /**\n * What the page *says*: `a11y` and `content`.\n *\n * For a subject whose styling is somebody else's to change — a themed embed, a\n * page inside a design system being actively rebranded — where the assertion\n * worth keeping is that the words and the semantics survived.\n */\n | 'content';\n\nconst LEVELS: Readonly<Record<Level, readonly Band[]>> = {\n strict: BANDS,\n layout: ['a11y', 'geometry'],\n content: ['a11y', 'content'],\n};\n\n/** The bands a level asserts on. Everything else is absorbed and counted. */\nexport function bandsOf(level: Level): readonly Band[] {\n return LEVELS[level];\n}\n\nexport interface SensitivityRule {\n /** Stable name. Appears in the report and in every count this rule produces. */\n readonly id: string;\n\n /**\n * Why this subject is not asserted on in full. Required, as an ignore's is.\n *\n * The difference between a policy somebody chose and one that accreted. A\n * route declared `layout` two years ago by somebody who has left is a blind\n * spot with a plausible-looking config entry in front of it.\n */\n readonly reason: string;\n\n readonly level: Level;\n\n /** Subjects this applies to. `*` matches any run of characters. */\n readonly subjects?: readonly string[];\n\n /**\n * Tags the subject must carry, as its own artifact declared them.\n *\n * FIXME: read by nothing. `asIgnore` copies `subjects` and drops this, so a\n * rule scoped only by tags translates to an unscoped ignore and absorbs across\n * every subject in the run — the widest possible reading of the narrowest\n * scope an operator can write.\n */\n readonly tags?: readonly string[];\n}\n\n/** What one rule absorbed, per band, across a run. */\nexport interface Relaxation {\n readonly rule: string;\n readonly reason: string;\n readonly level: Level;\n\n /** Deltas absorbed because their band is not asserted on here. */\n readonly absorbed: number;\n readonly subjects: readonly string[];\n\n /** Which bands were absorbed, so a level can be tightened with evidence. */\n readonly bands: readonly Band[];\n}\n\nexport interface SensitivityRegister {\n readonly relaxations: readonly Relaxation[];\n\n /**\n * Rules that absorbed nothing in any subject they applied to.\n *\n * The same accounting an ignore's register keeps, and for the same reason: a\n * route declared `layout` that has reported no token change in six months is\n * either a route nothing styles, or a declaration nobody needed. Both are worth\n * a line, and neither is visible without the count.\n */\n readonly dead: readonly string[];\n\n readonly totalAbsorbed: number;\n}\n\nexport interface SensitivityOutcome {\n readonly diffs: readonly SemanticDiff[];\n readonly register: SensitivityRegister;\n}\n\n/**\n * Turn a sensitivity rule into the ignore it is.\n *\n * Not a shortcut — a statement about what these are. A sensitivity names a place\n * and narrows by band, which is exactly the shape `IgnoreRule` already refuses to\n * accept *without* a place, and exactly the shape it accepts with one. Sharing\n * the mechanism means sharing the safety property: the deltas are absorbed by\n * `applyIgnores`, counted per rule, and reported when a rule absorbs nothing.\n *\n * The bands are inverted on the way through, and that inversion is the whole\n * translation. A sensitivity says what it *asserts on*; an ignore says what it\n * *absorbs*. Stating the level positively is what makes a config readable — \"this\n * route asserts on layout\" rather than \"this route ignores token, content and\n * texture\" — and the negative form is what the machinery needs.\n */\nexport function asIgnore(rule: SensitivityRule): IgnoreRule | null {\n const asserted = new Set(bandsOf(rule.level));\n const absorbed = BANDS.filter((band) => !asserted.has(band));\n\n // `strict` asserts on everything, so it absorbs nothing and produces no rule.\n // Returning an ignore with an empty band list would produce a register entry\n // that is permanently dead, which is a line telling an operator to delete the\n // one declaration that is doing nothing wrong.\n if (absorbed.length === 0) return null;\n\n return {\n id: rule.id,\n reason: rule.reason,\n bands: absorbed,\n // The place is the subject, said rather than left to be inferred from a\n // missing selector — see `IgnoreRule.whole`.\n whole: true,\n ...(rule.subjects !== undefined ? { subjects: rule.subjects } : {}),\n };\n}\n\n/**\n * The register as the lines a run prints.\n *\n * Absorption is stated in the *positive* form the operator wrote, because that is\n * the sentence they can check: \"asserts on layout\" is a claim about intent, and\n * \"absorbed 412 token deltas\" is the evidence for or against it.\n */\nexport function summarizeSensitivity(register: SensitivityRegister): readonly string[] {\n if (register.relaxations.length === 0) return [];\n\n const lines = [\n `SENSITIVITY — ${register.totalAbsorbed} difference(s) not asserted on, ` +\n `by ${register.relaxations.length} rule(s)`,\n ];\n\n for (const entry of register.relaxations) {\n if (entry.absorbed === 0) {\n lines.push(\n ` [dead] ${entry.rule} — asserts on ${entry.level} across ` +\n `${entry.subjects.length} subject(s) and absorbed nothing (${entry.reason})`,\n );\n continue;\n }\n lines.push(\n ` ${entry.rule} — asserts on ${entry.level}; absorbed ${entry.absorbed} ` +\n `${entry.bands.join('/')} difference(s) in ${entry.subjects.length} subject(s): ` +\n entry.reason,\n );\n }\n\n return lines;\n}\n\n/**\n * Apply declared sensitivities to a run's diffs.\n *\n * A thin fold over {@link applyIgnores}, and thin on purpose: the absorption, the\n * per-rule counting and the dead-rule reporting are one mechanism, and a second\n * implementation of them would be a second set of bugs in the half of the system\n * whose whole job is not to silently hide things.\n *\n * Rules that resolve to `strict` are dropped before the fold rather than passed\n * through, so a project that declares its default explicitly does not collect a\n * permanently-dead register line for saying so.\n */\nexport function applySensitivity(\n diffs: readonly SemanticDiff[],\n rules: readonly SensitivityRule[],\n options: IgnoreOptions = {},\n): SensitivityOutcome {\n const translated = new Map<string, SensitivityRule>();\n const ignores: IgnoreRule[] = [];\n\n for (const rule of rules) {\n const ignore = asIgnore(rule);\n if (ignore === null) continue;\n translated.set(rule.id, rule);\n ignores.push(ignore);\n }\n\n const outcome = applyIgnores(diffs, ignores, options);\n\n const relaxations = [...translated.values()].map((rule) => {\n const absorbed = outcome.register.absorbed.find((entry) => entry.rule === rule.id);\n return {\n rule: rule.id,\n reason: rule.reason,\n level: rule.level,\n absorbed: absorbed?.deltas ?? 0,\n subjects: absorbed?.subjects ?? [],\n bands: absorbed?.bands ?? [],\n };\n });\n\n return {\n diffs: outcome.diffs,\n register: {\n relaxations,\n dead: relaxations.filter((entry) => entry.absorbed === 0).map((entry) => entry.rule),\n totalAbsorbed: relaxations.reduce((sum, entry) => sum + entry.absorbed, 0),\n },\n };\n}\n\n/**\n * What a level does with a set of bands that moved.\n *\n * The arm for the path that holds no second document. `applySensitivity` folds\n * over deltas, which needs both revisions normalized; a `variance run` compares\n * an image against a stored baseline and has only what that baseline carried —\n * per-component hashes, one digest per band (ADR-0027, ADR-0029's split).\n * `bandsBetween` turns those into the same vocabulary, and this decides against\n * it, so the two paths agree by sharing {@link bandsOf} rather than by\n * inspection.\n *\n * **Empty in means nothing absorbed**, not everything. A run whose pixels moved\n * while every component hash held is raster residue — `texture`, which a\n * document cannot carry and which this must therefore never claim to have\n * decided. Absorbing on an empty set would turn the one band a hash comparison\n * is blind to into the one it silences.\n */\nexport function relaxes(\n level: Level,\n moved: readonly Band[],\n): { readonly asserted: readonly Band[]; readonly absorbed: readonly Band[] } {\n const asserting = new Set(bandsOf(level));\n\n return {\n asserted: moved.filter((band) => asserting.has(band)),\n absorbed: moved.filter((band) => !asserting.has(band)),\n };\n}\n\n/**\n * Whether a subject's every moved band falls outside what it is asserted on.\n *\n * The whole subject, because that is the unit a stored baseline can answer for:\n * one image, one verdict. A subject where *some* moved band is asserted on is\n * reported in full — including the bands that would have been absorbed — since\n * a reviewer looking at a nav that moved wants the restyle that came with it,\n * and hiding half a diff is worse than hiding none of it.\n */\nexport function absorbsEntirely(level: Level, moved: readonly Band[]): boolean {\n return moved.length > 0 && relaxes(level, moved).asserted.length === 0;\n}\n"]}
|
package/dist/relate/graph.d.ts
CHANGED
|
@@ -68,6 +68,7 @@
|
|
|
68
68
|
* `core` performs no I/O (ADR-0006); the records this folds are somebody else's
|
|
69
69
|
* disk.
|
|
70
70
|
*/
|
|
71
|
+
import type { Uses } from './narrow.js';
|
|
71
72
|
/** Node kinds, in id order. An id is an index into this, never the word. */
|
|
72
73
|
export declare const NODE_KINDS: readonly ['file', 'component', 'package'];
|
|
73
74
|
export type NodeKind = (typeof NODE_KINDS)[number];
|
|
@@ -176,6 +177,13 @@ export interface Relations {
|
|
|
176
177
|
* trail from the change arrives without crossing one of that file's shadows.
|
|
177
178
|
*/
|
|
178
179
|
readonly shadows: ReadonlyMap<string, readonly string[]>;
|
|
180
|
+
/**
|
|
181
|
+
* How each file uses each file it imports, asked per edge as a walk crosses
|
|
182
|
+
* it ([`narrow.ts`](./narrow.ts)). A lookup rather than a column: the parse
|
|
183
|
+
* that recorded the edge already holds the names, and a walk asks only the
|
|
184
|
+
* edges it reaches. Absent for a graph built without one, which walks whole.
|
|
185
|
+
*/
|
|
186
|
+
readonly uses?: Uses;
|
|
179
187
|
}
|
|
180
188
|
/**
|
|
181
189
|
* The lookup key for a node.
|
|
@@ -223,5 +231,7 @@ export declare function relationsOf(input: {
|
|
|
223
231
|
readonly isolated?: Iterable<Node>;
|
|
224
232
|
/** Per file, the files its run never reaches; see {@link Relations.shadows}. */
|
|
225
233
|
readonly shadows?: ReadonlyMap<string, readonly string[]>;
|
|
234
|
+
/** See {@link Relations.uses}. */
|
|
235
|
+
readonly uses?: Uses;
|
|
226
236
|
}): Relations;
|
|
227
237
|
//# sourceMappingURL=graph.d.ts.map
|
package/dist/relate/graph.js
CHANGED
|
@@ -196,7 +196,17 @@ export function relationsOf(input) {
|
|
|
196
196
|
if (because !== undefined)
|
|
197
197
|
reasons.set(id, because);
|
|
198
198
|
}
|
|
199
|
-
return {
|
|
199
|
+
return {
|
|
200
|
+
names,
|
|
201
|
+
kinds,
|
|
202
|
+
depends,
|
|
203
|
+
dependents,
|
|
204
|
+
unknown,
|
|
205
|
+
reasons,
|
|
206
|
+
index,
|
|
207
|
+
shadows: input.shadows ?? new Map(),
|
|
208
|
+
...(input.uses === undefined ? {} : { uses: input.uses }),
|
|
209
|
+
};
|
|
200
210
|
}
|
|
201
211
|
/**
|
|
202
212
|
* Edges as ids, sorted and deduplicated.
|
package/dist/relate/graph.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"graph.js","sourceRoot":"","sources":["../../src/relate/graph.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * What reaches what, stored so that asking is cheap.\n *\n * Every question this project asks about a change is a reachability question. A\n * diff names files; a subject is made of components; a component is declared in a\n * file that imports other files. *Which subjects could this diff have changed* is\n * one traversal of one graph, and the only reason it is not asked that way today\n * is that there has never been a graph to traverse.\n *\n * ## Why a structure rather than a `Map`\n *\n * The obvious shape is `Map<string, string[]>`, and it is wrong at the size this\n * runs at. A monorepo scan produces tens of thousands of files and several times\n * as many edges; the map holds one array object and one string copy per edge, and\n * a traversal chases a pointer for every step. What replaces it here is the\n * standard answer, and it is standard because nothing has beaten it:\n *\n * - **strings are interned once.** A node is an integer. Comparison is an integer\n * compare, membership is an index into a byte array, and a visited set is a\n * `Uint8Array` rather than a `Set<string>`.\n * - **edges are two typed arrays** in compressed-sparse-row form: `offset` says\n * where a node's row starts, `target` holds the rows end to end. One node's\n * edges are contiguous, so a traversal walks memory forwards instead of\n * following pointers into the heap.\n * - **both directions are materialized.** *What does this depend on* and *what\n * depends on this* are different questions, and a graph answering only the\n * first makes the second a whole-graph scan. The transpose is a counting sort —\n * one pass to count, one to place — so holding it costs `O(n + m)` once instead\n * of `O(n + m)` per query.\n *\n * ## Edges point at what a thing depends on\n *\n * One convention, held everywhere: **`A → B` means A depends on B**, so a change\n * in B may move A. Every affected-set question is therefore a walk against the\n * arrows, which is what `dependentsOf` does, and every *what would I have to read\n * to understand this* question is a walk along them.\n *\n * It decides the direction of the less obvious edges too. A component is\n * `declared-in` the file that declares it — the component depends on the file and\n * not the other way round — so editing that file reaches the component in the\n * same traversal that reaches every importer.\n *\n * ## Nodes are typed, because files are not the last kind\n *\n * Three kinds and the joins between them: a `file` graph built from imports, the\n * `component` nodes the source index already knows how to find, and a `package`\n * for each name an install resolved. That is deliberate rather than incidental. A\n * component relation — *`TodoFooter` renders `Chip`* — is another edge kind\n * between nodes of another kind in this same structure, and\n * [`composition.md`](../../../../docs/composition.md) already computes exactly\n * that relation from what a run rendered. One graph means the static answer and\n * the rendered answer are joinable rather than adjacent.\n *\n * A `package` earns its kind for the same reason and one more: it is the far end\n * of the same line. A run starts at the harness, arrives at our code through the\n * tests, and leaves it again at the packages — so a bumped dependency is a node\n * like any other, and *which of our files a new `jsdom` can affect* is the\n * one traversal this file already performs, seeded on the other side\n * ([`docs/selecting.md`](../../../../docs/selecting.md)).\n *\n * ## What it is not\n *\n * **Not a build graph.** It has no notion of a task, an output or a cache key,\n * and it does not know what an install produced. `nx` and `turbo` own that layer,\n * and what reads them reads their answer rather than competing with it.\n *\n * **Not a resolver.** Nothing here opens a file or knows what a specifier means.\n * `core` performs no I/O (ADR-0006); the records this folds are somebody else's\n * disk.\n */\n\n/** Node kinds, in id order. An id is an index into this, never the word. */\nexport const NODE_KINDS = ['file', 'component', 'package'] as const;\nexport type NodeKind = (typeof NODE_KINDS)[number];\n\n/**\n * Edge kinds, in id order.\n *\n * The distinctions are kept because they explain a finding. One of them also\n * decides a walk: `type` is erased by every compiler, so nothing behind it runs\n * and nothing behind it renders, and the default traversal leaves it out\n * (`RUNTIME_EDGES`). A caller whose question is about source rather than about a\n * runtime — a docgen that reads prop types, a type-check gate — passes\n * `EDGE_KINDS` and walks it.\n */\nexport const EDGE_KINDS = [\n /** `import x from './y'` — a value import. */\n 'imports',\n /** `export … from './y'` — an import that also republishes. */\n 'reexports',\n /** `import('./y')`, with a specifier that was a literal. */\n 'dynamic',\n /** `import type { T } from './y'` — erased before anything renders. */\n 'type',\n /** A stylesheet's `@import`, or a `url()` reaching a font or an image. */\n 'asset',\n /** A component to the file that declares it. */\n 'declared-in',\n /** A package to a package its install resolved beneath it. */\n 'depends-on',\n /** `/// <depends path=\"./y\" />` — a file the module reads without importing it. */\n 'depends',\n] as const;\nexport type EdgeKind = (typeof EDGE_KINDS)[number];\n\n/**\n * The edge kinds a runtime loads through: every kind but `type`. The\n * default for a traversal, and the list a caller widens from. A closure folds a\n * shorter list (`CLOSURE_EDGES`), because a digest is a claim about bytes and\n * one of these kinds leads nowhere any bytes are.\n */\nexport const RUNTIME_EDGES: readonly EdgeKind[] = EDGE_KINDS.filter((kind) => kind !== 'type');\n\nexport interface Node {\n readonly kind: NodeKind;\n /**\n * Repository-relative for a file, the identifier for a component, and for a\n * package the name source code imports it by — `@mui/material`, never a\n * version and never a resolution. Which copy a resolver handed any one\n * importer is unanswerable without running that resolver, and the question\n * this graph is asked never needs it ([`sense/lock`](../../../sense/src/lock/index.ts)).\n */\n readonly name: string;\n}\n\nexport interface Relation {\n readonly from: Node;\n readonly to: Node;\n readonly kind: EdgeKind;\n}\n\n/** An index into `Relations.names`. Valid only against the graph it came from. */\nexport type NodeId = number;\n\n/**\n * One direction of the edge set, compressed-sparse-row.\n *\n * Node `i`'s edges are `target[offset[i] … offset[i + 1])`, and `kind[j]`\n * describes `target[j]`. `offset` holds one more entry than there are nodes, so\n * the last row needs no special case.\n */\nexport interface Adjacency {\n readonly offset: Uint32Array;\n readonly target: Uint32Array;\n readonly kind: Uint8Array;\n}\n\nexport interface Relations {\n /** Node names by id, ordered by kind and then by code unit. */\n readonly names: readonly string[];\n /** The `NODE_KINDS` index of each node. */\n readonly kinds: Uint8Array;\n\n /** What each node depends on. */\n readonly depends: Adjacency;\n /** What depends on each node — the transpose, materialized. */\n readonly dependents: Adjacency;\n\n /**\n * `1` where this node's outgoing edges could not be enumerated.\n *\n * A parse failure, a specifier that was not a literal, a file in a language\n * nothing here reads. Kept for whoever reports on the scan. No walk reads it:\n * the edge a reader could not see is one a recorded run sees load.\n */\n readonly unknown: Uint8Array;\n\n /**\n * Why, for each of those nodes, in the words whoever read the file used.\n *\n * Sparse — a repository the scan understands carries none of these — and kept\n * apart from the mask because the mask is what the inner loop tests and this is\n * what an operator reads. Without it, a run that observed forty subjects instead\n * of twelve can say *because eight files could not be read* and can never say\n * **which**, which is the difference between a fact and a work item.\n */\n readonly reasons: ReadonlyMap<NodeId, string>;\n\n /** `keyOf` to id — the interning table, kept for lookup. */\n readonly index: ReadonlyMap<string, NodeId>;\n\n /**\n * Per file, the files its run never reaches at any depth: what a mock\n * replaces for the whole of a test's run.\n *\n * Carried with the graph rather than handed to each walk, because the graph\n * crosses every seam a walk does and a table left behind at one of them is a\n * selection that quietly widened. Empty for a graph nobody tainted. A walk\n * against the arrows consults it: a change affects a file only when some\n * trail from the change arrives without crossing one of that file's shadows.\n */\n readonly shadows: ReadonlyMap<string, readonly string[]>;\n}\n\n/**\n * The lookup key for a node.\n *\n * Runtime only; nothing serializes this, and no code parses it back — the\n * separator only has to be injective, and it is, because a kind is one of a few\n * fixed words and none contains a `:`. A name that contains one is therefore\n * fine.\n *\n * The obvious separator is a NUL, and it is the wrong one: a source file\n * carrying that byte stops being text to `grep`, `file` and `git diff`, which is\n * a search finding nothing looking exactly like a search that returned nothing.\n * `tools/boundaries.check.ts` fails a build over it.\n */\nexport function keyOf(kind: NodeKind, name: string): string {\n return `${kind}:${name}`;\n}\n\n/** The id a node is known by in these relations, or `undefined` for a name the scan never met. */\nexport function idOf(relations: Relations, kind: NodeKind, name: string): NodeId | undefined {\n return relations.index.get(keyOf(kind, name));\n}\n\n/** The node an id names: its kind and name, or `undefined` past the end. */\nexport function nodeAt(relations: Relations, id: NodeId): Node | undefined {\n const name = relations.names[id];\n if (name === undefined) return undefined;\n return { kind: NODE_KINDS[relations.kinds[id]!]!, name };\n}\n\n/** Every node of one kind, as ids. Contiguous, because the order is kind-major. */\nexport function nodesOfKind(relations: Relations, kind: NodeKind): readonly NodeId[] {\n const wanted = NODE_KINDS.indexOf(kind);\n const found: NodeId[] = [];\n for (let id = 0; id < relations.kinds.length; id += 1) {\n if (relations.kinds[id] === wanted) found.push(id);\n }\n return found;\n}\n\n/**\n * Fold relations into the queryable form.\n *\n * Deterministic to the byte: nodes are ordered by kind and then by **code unit** —\n * never `localeCompare`, which would make an id a promise about `LANG` — and edges\n * are sorted and deduplicated, so two runs over the same records produce identical\n * arrays and a serialized graph is stable.\n *\n * `O(n log n + m log m)` for the two sorts and `O(n + m)` for everything else. The\n * transpose is a counting sort rather than a second pass over a map, which is why\n * both directions cost one build instead of one per query.\n */\nexport function relationsOf(input: {\n readonly relations: Iterable<Relation>;\n /**\n * Nodes whose outgoing edges are not fully known, and must be treated as such.\n *\n * A bare node says *this one is blind*. A pair says why, in whatever words the\n * reader of the file used — and the sentence is the half an operator can act on,\n * so it travels with the fact rather than being reconstructed from it.\n */\n readonly unknown?: Iterable<Node | readonly [Node, string]>;\n /** Nodes with no edges at all, which would otherwise be absent from the graph. */\n readonly isolated?: Iterable<Node>;\n /** Per file, the files its run never reaches; see {@link Relations.shadows}. */\n readonly shadows?: ReadonlyMap<string, readonly string[]>;\n}): Relations {\n const relations = [...input.relations];\n const unknownNodes = [...(input.unknown ?? [])].map((entry) =>\n Array.isArray(entry) ? entry : ([entry, undefined] as const),\n ) as readonly (readonly [Node, string | undefined])[];\n\n const seen = new Map<string, Node>();\n const see = (node: Node): void => {\n const key = keyOf(node.kind, node.name);\n if (!seen.has(key)) seen.set(key, node);\n };\n\n for (const relation of relations) {\n see(relation.from);\n see(relation.to);\n }\n for (const [node] of unknownNodes) see(node);\n for (const node of input.isolated ?? []) see(node);\n\n // Kind-major, then code unit. Kind-major keeps one kind's nodes contiguous,\n // which is what lets a caller filter to files without touching a string.\n const ordered = [...seen.values()].sort(\n (a, b) =>\n NODE_KINDS.indexOf(a.kind) - NODE_KINDS.indexOf(b.kind) ||\n (a.name < b.name ? -1 : a.name > b.name ? 1 : 0),\n );\n\n const index = new Map<string, NodeId>();\n const names: string[] = Array.from<string>({ length: ordered.length });\n const kinds = new Uint8Array(ordered.length);\n\n for (const [id, node] of ordered.entries()) {\n index.set(keyOf(node.kind, node.name), id);\n names[id] = node.name;\n kinds[id] = NODE_KINDS.indexOf(node.kind);\n }\n\n const edges = sortedEdges(relations, index);\n const depends = adjacencyOf(edges, ordered.length, 'from');\n const dependents = adjacencyOf(edges, ordered.length, 'to');\n\n const unknown = new Uint8Array(ordered.length);\n const reasons = new Map<NodeId, string>();\n for (const [node, because] of unknownNodes) {\n const id = index.get(keyOf(node.kind, node.name));\n if (id === undefined) continue;\n\n unknown[id] = 1;\n if (because !== undefined) reasons.set(id, because);\n }\n\n return { names, kinds, depends, dependents, unknown, reasons, index, shadows: input.shadows ?? new Map() };\n}\n\ninterface Edge {\n readonly from: NodeId;\n readonly to: NodeId;\n readonly kind: number;\n}\n\n/**\n * Edges as ids, sorted and deduplicated.\n *\n * Sorting the whole list once is what makes every row of *both* adjacencies\n * sorted without sorting any row: each fill walks the list in order, so a row\n * receives its targets in order. Two files related twice — a value import and a\n * type import — stay two edges, because the kinds differ and one of them explains\n * a finding the other does not.\n */\nfunction sortedEdges(\n relations: readonly Relation[],\n index: ReadonlyMap<string, NodeId>,\n): readonly Edge[] {\n const edges: Edge[] = [];\n\n for (const relation of relations) {\n const from = index.get(keyOf(relation.from.kind, relation.from.name));\n const to = index.get(keyOf(relation.to.kind, relation.to.name));\n if (from === undefined || to === undefined) continue;\n edges.push({ from, to, kind: EDGE_KINDS.indexOf(relation.kind) });\n }\n\n edges.sort((a, b) => a.from - b.from || a.to - b.to || a.kind - b.kind);\n\n const unique: Edge[] = [];\n for (const edge of edges) {\n const last = unique[unique.length - 1];\n const same =\n last !== undefined && last.from === edge.from && last.to === edge.to && last.kind === edge.kind;\n if (!same) unique.push(edge);\n }\n\n return unique;\n}\n\n/**\n * One direction, by counting sort.\n *\n * `by` names the endpoint a row is keyed on: `from` produces *what this depends\n * on*, `to` produces the transpose. Two passes — count the degrees, then place —\n * and no per-node array is ever allocated.\n */\nfunction adjacencyOf(edges: readonly Edge[], nodes: number, by: 'from' | 'to'): Adjacency {\n const offset = new Uint32Array(nodes + 1);\n for (const edge of edges) offset[edge[by] + 1] = offset[edge[by] + 1]! + 1;\n for (let id = 0; id < nodes; id += 1) offset[id + 1] = offset[id + 1]! + offset[id]!;\n\n const target = new Uint32Array(edges.length);\n const kind = new Uint8Array(edges.length);\n const cursor = Uint32Array.from(offset.subarray(0, nodes));\n\n for (const edge of edges) {\n const row = edge[by];\n const at = cursor[row]!;\n target[at] = by === 'from' ? edge.to : edge.from;\n kind[at] = edge.kind;\n cursor[row] = at + 1;\n }\n\n return { offset, target, kind };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"graph.js","sourceRoot":"","sources":["../../src/relate/graph.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqEG;AAIH,4EAA4E;AAC5E,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,MAAM,EAAE,WAAW,EAAE,SAAS,CAAU,CAAC;AAGpE;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,UAAU,GAAG;IACxB,8CAA8C;IAC9C,SAAS;IACT,+DAA+D;IAC/D,WAAW;IACX,4DAA4D;IAC5D,SAAS;IACT,uEAAuE;IACvE,MAAM;IACN,0EAA0E;IAC1E,OAAO;IACP,gDAAgD;IAChD,aAAa;IACb,8DAA8D;IAC9D,YAAY;IACZ,mFAAmF;IACnF,SAAS;CACD,CAAC;AAGX;;;;;GAKG;AACH,MAAM,CAAC,MAAM,aAAa,GAAwB,UAAU,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,KAAK,MAAM,CAAC,CAAC;AA2F/F;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,KAAK,CAAC,IAAc,EAAE,IAAY;IAChD,OAAO,GAAG,IAAI,IAAI,IAAI,EAAE,CAAC;AAC3B,CAAC;AAED,kGAAkG;AAClG,MAAM,UAAU,IAAI,CAAC,SAAoB,EAAE,IAAc,EAAE,IAAY;IACrE,OAAO,SAAS,CAAC,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC;AAChD,CAAC;AAED,4EAA4E;AAC5E,MAAM,UAAU,MAAM,CAAC,SAAoB,EAAE,EAAU;IACrD,MAAM,IAAI,GAAG,SAAS,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;IACjC,IAAI,IAAI,KAAK,SAAS;QAAE,OAAO,SAAS,CAAC;IACzC,OAAO,EAAE,IAAI,EAAE,UAAU,CAAC,SAAS,CAAC,KAAK,CAAC,EAAE,CAAE,CAAE,EAAE,IAAI,EAAE,CAAC;AAC3D,CAAC;AAED,mFAAmF;AACnF,MAAM,UAAU,WAAW,CAAC,SAAoB,EAAE,IAAc;IAC9D,MAAM,MAAM,GAAG,UAAU,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IACxC,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,KAAK,IAAI,EAAE,GAAG,CAAC,EAAE,EAAE,GAAG,SAAS,CAAC,KAAK,CAAC,MAAM,EAAE,EAAE,IAAI,CAAC,EAAE,CAAC;QACtD,IAAI,SAAS,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,MAAM;YAAE,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACrD,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,WAAW,CAAC,KAgB3B;IACC,MAAM,SAAS,GAAG,CAAC,GAAG,KAAK,CAAC,SAAS,CAAC,CAAC;IACvC,MAAM,YAAY,GAAG,CAAC,GAAG,CAAC,KAAK,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAC5D,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAE,CAAC,KAAK,EAAE,SAAS,CAAW,CACT,CAAC;IAEtD,MAAM,IAAI,GAAG,IAAI,GAAG,EAAgB,CAAC;IACrC,MAAM,GAAG,GAAG,CAAC,IAAU,EAAQ,EAAE;QAC/B,MAAM,GAAG,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC;QACxC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC;YAAE,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;IAC1C,CAAC,CAAC;IAEF,KAAK,MAAM,QAAQ,IAAI,SAAS,EAAE,CAAC;QACjC,GAAG,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;QACnB,GAAG,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC;IACnB,CAAC;IACD,KAAK,MAAM,CAAC,IAAI,CAAC,IAAI,YAAY;QAAE,GAAG,CAAC,IAAI,CAAC,CAAC;IAC7C,KAAK,MAAM,IAAI,IAAI,KAAK,CAAC,QAAQ,IAAI,EAAE;QAAE,GAAG,CAAC,IAAI,CAAC,CAAC;IAEnD,4EAA4E;IAC5E,yEAAyE;IACzE,MAAM,OAAO,GAAG,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC,IAAI,CACrC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CACP,UAAU,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,UAAU,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC;QACvD,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CACnD,CAAC;IAEF,MAAM,KAAK,GAAG,IAAI,GAAG,EAAkB,CAAC;IACxC,MAAM,KAAK,GAAa,KAAK,CAAC,IAAI,CAAS,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IACvE,MAAM,KAAK,GAAG,IAAI,UAAU,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;IAE7C,KAAK,MAAM,CAAC,EAAE,EAAE,IAAI,CAAC,IAAI,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC;QAC3C,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC,CAAC;QAC3C,KAAK,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC;QACtB,KAAK,CAAC,EAAE,CAAC,GAAG,UAAU,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC5C,CAAC;IAED,MAAM,KAAK,GAAG,WAAW,CAAC,SAAS,EAAE,KAAK,CAAC,CAAC;IAC5C,MAAM,OAAO,GAAG,WAAW,CAAC,KAAK,EAAE,OAAO,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC3D,MAAM,UAAU,GAAG,WAAW,CAAC,KAAK,EAAE,OAAO,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;IAE5D,MAAM,OAAO,GAAG,IAAI,UAAU,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;IAC/C,MAAM,OAAO,GAAG,IAAI,GAAG,EAAkB,CAAC;IAC1C,KAAK,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,IAAI,YAAY,EAAE,CAAC;QAC3C,MAAM,EAAE,GAAG,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;QAClD,IAAI,EAAE,KAAK,SAAS;YAAE,SAAS;QAE/B,OAAO,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC;QAChB,IAAI,OAAO,KAAK,SAAS;YAAE,OAAO,CAAC,GAAG,CAAC,EAAE,EAAE,OAAO,CAAC,CAAC;IACtD,CAAC;IAED,OAAO;QACL,KAAK;QACL,KAAK;QACL,OAAO;QACP,UAAU;QACV,OAAO;QACP,OAAO;QACP,KAAK;QACL,OAAO,EAAE,KAAK,CAAC,OAAO,IAAI,IAAI,GAAG,EAAE;QACnC,GAAG,CAAC,KAAK,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,CAAC;KAC1D,CAAC;AACJ,CAAC;AAQD;;;;;;;;GAQG;AACH,SAAS,WAAW,CAClB,SAA8B,EAC9B,KAAkC;IAElC,MAAM,KAAK,GAAW,EAAE,CAAC;IAEzB,KAAK,MAAM,QAAQ,IAAI,SAAS,EAAE,CAAC;QACjC,MAAM,IAAI,GAAG,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,EAAE,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;QACtE,MAAM,EAAE,GAAG,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,QAAQ,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC;QAChE,IAAI,IAAI,KAAK,SAAS,IAAI,EAAE,KAAK,SAAS;YAAE,SAAS;QACrD,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,EAAE,EAAE,IAAI,EAAE,UAAU,CAAC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACpE,CAAC;IAED,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC;IAExE,MAAM,MAAM,GAAW,EAAE,CAAC;IAC1B,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,IAAI,GAAG,MAAM,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;QACvC,MAAM,IAAI,GACR,IAAI,KAAK,SAAS,IAAI,IAAI,CAAC,IAAI,KAAK,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,EAAE,KAAK,IAAI,CAAC,EAAE,IAAI,IAAI,CAAC,IAAI,KAAK,IAAI,CAAC,IAAI,CAAC;QAClG,IAAI,CAAC,IAAI;YAAE,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC/B,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;;;;;GAMG;AACH,SAAS,WAAW,CAAC,KAAsB,EAAE,KAAa,EAAE,EAAiB;IAC3E,MAAM,MAAM,GAAG,IAAI,WAAW,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC;IAC1C,KAAK,MAAM,IAAI,IAAI,KAAK;QAAE,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,GAAG,CAAC,CAAE,GAAG,CAAC,CAAC;IAC3E,KAAK,IAAI,EAAE,GAAG,CAAC,EAAE,EAAE,GAAG,KAAK,EAAE,EAAE,IAAI,CAAC;QAAE,MAAM,CAAC,EAAE,GAAG,CAAC,CAAC,GAAG,MAAM,CAAC,EAAE,GAAG,CAAC,CAAE,GAAG,MAAM,CAAC,EAAE,CAAE,CAAC;IAErF,MAAM,MAAM,GAAG,IAAI,WAAW,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;IAC7C,MAAM,IAAI,GAAG,IAAI,UAAU,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;IAC1C,MAAM,MAAM,GAAG,WAAW,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC;IAE3D,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,GAAG,GAAG,IAAI,CAAC,EAAE,CAAC,CAAC;QACrB,MAAM,EAAE,GAAG,MAAM,CAAC,GAAG,CAAE,CAAC;QACxB,MAAM,CAAC,EAAE,CAAC,GAAG,EAAE,KAAK,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC;QACjD,IAAI,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC;QACrB,MAAM,CAAC,GAAG,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;IACvB,CAAC;IAED,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC;AAClC,CAAC","sourcesContent":["/**\n * What reaches what, stored so that asking is cheap.\n *\n * Every question this project asks about a change is a reachability question. A\n * diff names files; a subject is made of components; a component is declared in a\n * file that imports other files. *Which subjects could this diff have changed* is\n * one traversal of one graph, and the only reason it is not asked that way today\n * is that there has never been a graph to traverse.\n *\n * ## Why a structure rather than a `Map`\n *\n * The obvious shape is `Map<string, string[]>`, and it is wrong at the size this\n * runs at. A monorepo scan produces tens of thousands of files and several times\n * as many edges; the map holds one array object and one string copy per edge, and\n * a traversal chases a pointer for every step. What replaces it here is the\n * standard answer, and it is standard because nothing has beaten it:\n *\n * - **strings are interned once.** A node is an integer. Comparison is an integer\n * compare, membership is an index into a byte array, and a visited set is a\n * `Uint8Array` rather than a `Set<string>`.\n * - **edges are two typed arrays** in compressed-sparse-row form: `offset` says\n * where a node's row starts, `target` holds the rows end to end. One node's\n * edges are contiguous, so a traversal walks memory forwards instead of\n * following pointers into the heap.\n * - **both directions are materialized.** *What does this depend on* and *what\n * depends on this* are different questions, and a graph answering only the\n * first makes the second a whole-graph scan. The transpose is a counting sort —\n * one pass to count, one to place — so holding it costs `O(n + m)` once instead\n * of `O(n + m)` per query.\n *\n * ## Edges point at what a thing depends on\n *\n * One convention, held everywhere: **`A → B` means A depends on B**, so a change\n * in B may move A. Every affected-set question is therefore a walk against the\n * arrows, which is what `dependentsOf` does, and every *what would I have to read\n * to understand this* question is a walk along them.\n *\n * It decides the direction of the less obvious edges too. A component is\n * `declared-in` the file that declares it — the component depends on the file and\n * not the other way round — so editing that file reaches the component in the\n * same traversal that reaches every importer.\n *\n * ## Nodes are typed, because files are not the last kind\n *\n * Three kinds and the joins between them: a `file` graph built from imports, the\n * `component` nodes the source index already knows how to find, and a `package`\n * for each name an install resolved. That is deliberate rather than incidental. A\n * component relation — *`TodoFooter` renders `Chip`* — is another edge kind\n * between nodes of another kind in this same structure, and\n * [`composition.md`](../../../../docs/composition.md) already computes exactly\n * that relation from what a run rendered. One graph means the static answer and\n * the rendered answer are joinable rather than adjacent.\n *\n * A `package` earns its kind for the same reason and one more: it is the far end\n * of the same line. A run starts at the harness, arrives at our code through the\n * tests, and leaves it again at the packages — so a bumped dependency is a node\n * like any other, and *which of our files a new `jsdom` can affect* is the\n * one traversal this file already performs, seeded on the other side\n * ([`docs/selecting.md`](../../../../docs/selecting.md)).\n *\n * ## What it is not\n *\n * **Not a build graph.** It has no notion of a task, an output or a cache key,\n * and it does not know what an install produced. `nx` and `turbo` own that layer,\n * and what reads them reads their answer rather than competing with it.\n *\n * **Not a resolver.** Nothing here opens a file or knows what a specifier means.\n * `core` performs no I/O (ADR-0006); the records this folds are somebody else's\n * disk.\n */\n\nimport type { Uses } from './narrow.js';\n\n/** Node kinds, in id order. An id is an index into this, never the word. */\nexport const NODE_KINDS = ['file', 'component', 'package'] as const;\nexport type NodeKind = (typeof NODE_KINDS)[number];\n\n/**\n * Edge kinds, in id order.\n *\n * The distinctions are kept because they explain a finding. One of them also\n * decides a walk: `type` is erased by every compiler, so nothing behind it runs\n * and nothing behind it renders, and the default traversal leaves it out\n * (`RUNTIME_EDGES`). A caller whose question is about source rather than about a\n * runtime — a docgen that reads prop types, a type-check gate — passes\n * `EDGE_KINDS` and walks it.\n */\nexport const EDGE_KINDS = [\n /** `import x from './y'` — a value import. */\n 'imports',\n /** `export … from './y'` — an import that also republishes. */\n 'reexports',\n /** `import('./y')`, with a specifier that was a literal. */\n 'dynamic',\n /** `import type { T } from './y'` — erased before anything renders. */\n 'type',\n /** A stylesheet's `@import`, or a `url()` reaching a font or an image. */\n 'asset',\n /** A component to the file that declares it. */\n 'declared-in',\n /** A package to a package its install resolved beneath it. */\n 'depends-on',\n /** `/// <depends path=\"./y\" />` — a file the module reads without importing it. */\n 'depends',\n] as const;\nexport type EdgeKind = (typeof EDGE_KINDS)[number];\n\n/**\n * The edge kinds a runtime loads through: every kind but `type`. The\n * default for a traversal, and the list a caller widens from. A closure folds a\n * shorter list (`CLOSURE_EDGES`), because a digest is a claim about bytes and\n * one of these kinds leads nowhere any bytes are.\n */\nexport const RUNTIME_EDGES: readonly EdgeKind[] = EDGE_KINDS.filter((kind) => kind !== 'type');\n\nexport interface Node {\n readonly kind: NodeKind;\n /**\n * Repository-relative for a file, the identifier for a component, and for a\n * package the name source code imports it by — `@mui/material`, never a\n * version and never a resolution. Which copy a resolver handed any one\n * importer is unanswerable without running that resolver, and the question\n * this graph is asked never needs it ([`sense/lock`](../../../sense/src/lock/index.ts)).\n */\n readonly name: string;\n}\n\nexport interface Relation {\n readonly from: Node;\n readonly to: Node;\n readonly kind: EdgeKind;\n}\n\n/** An index into `Relations.names`. Valid only against the graph it came from. */\nexport type NodeId = number;\n\n/**\n * One direction of the edge set, compressed-sparse-row.\n *\n * Node `i`'s edges are `target[offset[i] … offset[i + 1])`, and `kind[j]`\n * describes `target[j]`. `offset` holds one more entry than there are nodes, so\n * the last row needs no special case.\n */\nexport interface Adjacency {\n readonly offset: Uint32Array;\n readonly target: Uint32Array;\n readonly kind: Uint8Array;\n}\n\nexport interface Relations {\n /** Node names by id, ordered by kind and then by code unit. */\n readonly names: readonly string[];\n /** The `NODE_KINDS` index of each node. */\n readonly kinds: Uint8Array;\n\n /** What each node depends on. */\n readonly depends: Adjacency;\n /** What depends on each node — the transpose, materialized. */\n readonly dependents: Adjacency;\n\n /**\n * `1` where this node's outgoing edges could not be enumerated.\n *\n * A parse failure, a specifier that was not a literal, a file in a language\n * nothing here reads. Kept for whoever reports on the scan. No walk reads it:\n * the edge a reader could not see is one a recorded run sees load.\n */\n readonly unknown: Uint8Array;\n\n /**\n * Why, for each of those nodes, in the words whoever read the file used.\n *\n * Sparse — a repository the scan understands carries none of these — and kept\n * apart from the mask because the mask is what the inner loop tests and this is\n * what an operator reads. Without it, a run that observed forty subjects instead\n * of twelve can say *because eight files could not be read* and can never say\n * **which**, which is the difference between a fact and a work item.\n */\n readonly reasons: ReadonlyMap<NodeId, string>;\n\n /** `keyOf` to id — the interning table, kept for lookup. */\n readonly index: ReadonlyMap<string, NodeId>;\n\n /**\n * Per file, the files its run never reaches at any depth: what a mock\n * replaces for the whole of a test's run.\n *\n * Carried with the graph rather than handed to each walk, because the graph\n * crosses every seam a walk does and a table left behind at one of them is a\n * selection that quietly widened. Empty for a graph nobody tainted. A walk\n * against the arrows consults it: a change affects a file only when some\n * trail from the change arrives without crossing one of that file's shadows.\n */\n readonly shadows: ReadonlyMap<string, readonly string[]>;\n\n /**\n * How each file uses each file it imports, asked per edge as a walk crosses\n * it ([`narrow.ts`](./narrow.ts)). A lookup rather than a column: the parse\n * that recorded the edge already holds the names, and a walk asks only the\n * edges it reaches. Absent for a graph built without one, which walks whole.\n */\n readonly uses?: Uses;\n}\n\n/**\n * The lookup key for a node.\n *\n * Runtime only; nothing serializes this, and no code parses it back — the\n * separator only has to be injective, and it is, because a kind is one of a few\n * fixed words and none contains a `:`. A name that contains one is therefore\n * fine.\n *\n * The obvious separator is a NUL, and it is the wrong one: a source file\n * carrying that byte stops being text to `grep`, `file` and `git diff`, which is\n * a search finding nothing looking exactly like a search that returned nothing.\n * `tools/boundaries.check.ts` fails a build over it.\n */\nexport function keyOf(kind: NodeKind, name: string): string {\n return `${kind}:${name}`;\n}\n\n/** The id a node is known by in these relations, or `undefined` for a name the scan never met. */\nexport function idOf(relations: Relations, kind: NodeKind, name: string): NodeId | undefined {\n return relations.index.get(keyOf(kind, name));\n}\n\n/** The node an id names: its kind and name, or `undefined` past the end. */\nexport function nodeAt(relations: Relations, id: NodeId): Node | undefined {\n const name = relations.names[id];\n if (name === undefined) return undefined;\n return { kind: NODE_KINDS[relations.kinds[id]!]!, name };\n}\n\n/** Every node of one kind, as ids. Contiguous, because the order is kind-major. */\nexport function nodesOfKind(relations: Relations, kind: NodeKind): readonly NodeId[] {\n const wanted = NODE_KINDS.indexOf(kind);\n const found: NodeId[] = [];\n for (let id = 0; id < relations.kinds.length; id += 1) {\n if (relations.kinds[id] === wanted) found.push(id);\n }\n return found;\n}\n\n/**\n * Fold relations into the queryable form.\n *\n * Deterministic to the byte: nodes are ordered by kind and then by **code unit** —\n * never `localeCompare`, which would make an id a promise about `LANG` — and edges\n * are sorted and deduplicated, so two runs over the same records produce identical\n * arrays and a serialized graph is stable.\n *\n * `O(n log n + m log m)` for the two sorts and `O(n + m)` for everything else. The\n * transpose is a counting sort rather than a second pass over a map, which is why\n * both directions cost one build instead of one per query.\n */\nexport function relationsOf(input: {\n readonly relations: Iterable<Relation>;\n /**\n * Nodes whose outgoing edges are not fully known, and must be treated as such.\n *\n * A bare node says *this one is blind*. A pair says why, in whatever words the\n * reader of the file used — and the sentence is the half an operator can act on,\n * so it travels with the fact rather than being reconstructed from it.\n */\n readonly unknown?: Iterable<Node | readonly [Node, string]>;\n /** Nodes with no edges at all, which would otherwise be absent from the graph. */\n readonly isolated?: Iterable<Node>;\n /** Per file, the files its run never reaches; see {@link Relations.shadows}. */\n readonly shadows?: ReadonlyMap<string, readonly string[]>;\n /** See {@link Relations.uses}. */\n readonly uses?: Uses;\n}): Relations {\n const relations = [...input.relations];\n const unknownNodes = [...(input.unknown ?? [])].map((entry) =>\n Array.isArray(entry) ? entry : ([entry, undefined] as const),\n ) as readonly (readonly [Node, string | undefined])[];\n\n const seen = new Map<string, Node>();\n const see = (node: Node): void => {\n const key = keyOf(node.kind, node.name);\n if (!seen.has(key)) seen.set(key, node);\n };\n\n for (const relation of relations) {\n see(relation.from);\n see(relation.to);\n }\n for (const [node] of unknownNodes) see(node);\n for (const node of input.isolated ?? []) see(node);\n\n // Kind-major, then code unit. Kind-major keeps one kind's nodes contiguous,\n // which is what lets a caller filter to files without touching a string.\n const ordered = [...seen.values()].sort(\n (a, b) =>\n NODE_KINDS.indexOf(a.kind) - NODE_KINDS.indexOf(b.kind) ||\n (a.name < b.name ? -1 : a.name > b.name ? 1 : 0),\n );\n\n const index = new Map<string, NodeId>();\n const names: string[] = Array.from<string>({ length: ordered.length });\n const kinds = new Uint8Array(ordered.length);\n\n for (const [id, node] of ordered.entries()) {\n index.set(keyOf(node.kind, node.name), id);\n names[id] = node.name;\n kinds[id] = NODE_KINDS.indexOf(node.kind);\n }\n\n const edges = sortedEdges(relations, index);\n const depends = adjacencyOf(edges, ordered.length, 'from');\n const dependents = adjacencyOf(edges, ordered.length, 'to');\n\n const unknown = new Uint8Array(ordered.length);\n const reasons = new Map<NodeId, string>();\n for (const [node, because] of unknownNodes) {\n const id = index.get(keyOf(node.kind, node.name));\n if (id === undefined) continue;\n\n unknown[id] = 1;\n if (because !== undefined) reasons.set(id, because);\n }\n\n return {\n names,\n kinds,\n depends,\n dependents,\n unknown,\n reasons,\n index,\n shadows: input.shadows ?? new Map(),\n ...(input.uses === undefined ? {} : { uses: input.uses }),\n };\n}\n\ninterface Edge {\n readonly from: NodeId;\n readonly to: NodeId;\n readonly kind: number;\n}\n\n/**\n * Edges as ids, sorted and deduplicated.\n *\n * Sorting the whole list once is what makes every row of *both* adjacencies\n * sorted without sorting any row: each fill walks the list in order, so a row\n * receives its targets in order. Two files related twice — a value import and a\n * type import — stay two edges, because the kinds differ and one of them explains\n * a finding the other does not.\n */\nfunction sortedEdges(\n relations: readonly Relation[],\n index: ReadonlyMap<string, NodeId>,\n): readonly Edge[] {\n const edges: Edge[] = [];\n\n for (const relation of relations) {\n const from = index.get(keyOf(relation.from.kind, relation.from.name));\n const to = index.get(keyOf(relation.to.kind, relation.to.name));\n if (from === undefined || to === undefined) continue;\n edges.push({ from, to, kind: EDGE_KINDS.indexOf(relation.kind) });\n }\n\n edges.sort((a, b) => a.from - b.from || a.to - b.to || a.kind - b.kind);\n\n const unique: Edge[] = [];\n for (const edge of edges) {\n const last = unique[unique.length - 1];\n const same =\n last !== undefined && last.from === edge.from && last.to === edge.to && last.kind === edge.kind;\n if (!same) unique.push(edge);\n }\n\n return unique;\n}\n\n/**\n * One direction, by counting sort.\n *\n * `by` names the endpoint a row is keyed on: `from` produces *what this depends\n * on*, `to` produces the transpose. Two passes — count the degrees, then place —\n * and no per-node array is ever allocated.\n */\nfunction adjacencyOf(edges: readonly Edge[], nodes: number, by: 'from' | 'to'): Adjacency {\n const offset = new Uint32Array(nodes + 1);\n for (const edge of edges) offset[edge[by] + 1] = offset[edge[by] + 1]! + 1;\n for (let id = 0; id < nodes; id += 1) offset[id + 1] = offset[id + 1]! + offset[id]!;\n\n const target = new Uint32Array(edges.length);\n const kind = new Uint8Array(edges.length);\n const cursor = Uint32Array.from(offset.subarray(0, nodes));\n\n for (const edge of edges) {\n const row = edge[by];\n const at = cursor[row]!;\n target[at] = by === 'from' ? edge.to : edge.from;\n kind[at] = edge.kind;\n cursor[row] = at + 1;\n }\n\n return { offset, target, kind };\n}\n"]}
|
package/dist/relate/index.d.ts
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
export { EDGE_KINDS, NODE_KINDS, RUNTIME_EDGES, idOf, keyOf, nodeAt, nodesOfKind, relationsOf, type Adjacency, type EdgeKind, type Node, type NodeId, type NodeKind, type Relation, type Relations, } from './graph.js';
|
|
13
13
|
export { beforeReach, changedBefore, within, type BeforeReach, type BeforeReachOptions, } from './before.js';
|
|
14
|
+
export { type EdgeUse, type Uses } from './narrow.js';
|
|
14
15
|
export { dependenciesOf, dependentsOf, trailOf, type Traversal, type TraversalOptions, } from './reach.js';
|
|
15
16
|
export { CLOSURE_EDGES, closureOf, driftedBetween, type Closure, type ClosureInput, type Drift, } from './merkle.js';
|
|
16
17
|
export { explain, affectedBy, relationsOfFiles, type FileEdge, type FileRecord, type AffectedOptions, type PackageEdge, type RelationsOptions, type Affected, } from './records.js';
|
package/dist/relate/index.js
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
export { EDGE_KINDS, NODE_KINDS, RUNTIME_EDGES, idOf, keyOf, nodeAt, nodesOfKind, relationsOf, } from './graph.js';
|
|
13
13
|
export { beforeReach, changedBefore, within, } from './before.js';
|
|
14
|
+
export {} from './narrow.js';
|
|
14
15
|
export { dependenciesOf, dependentsOf, trailOf, } from './reach.js';
|
|
15
16
|
export { CLOSURE_EDGES, closureOf, driftedBetween, } from './merkle.js';
|
|
16
17
|
export { explain, affectedBy, relationsOfFiles, } from './records.js';
|
package/dist/relate/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/relate/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EACL,UAAU,EACV,UAAU,EACV,aAAa,EACb,IAAI,EACJ,KAAK,EACL,MAAM,EACN,WAAW,EACX,WAAW,GAQZ,MAAM,YAAY,CAAC;AAEpB,OAAO,EACL,WAAW,EACX,aAAa,EACb,MAAM,GAGP,MAAM,aAAa,CAAC;AAErB,OAAO,EACL,cAAc,EACd,YAAY,EACZ,OAAO,GAGR,MAAM,YAAY,CAAC;AAEpB,OAAO,EACL,aAAa,EACb,SAAS,EACT,cAAc,GAIf,MAAM,aAAa,CAAC;AAErB,OAAO,EACL,OAAO,EACP,UAAU,EACV,gBAAgB,GAOjB,MAAM,cAAc,CAAC","sourcesContent":["/**\n * `core/relate` — what reaches what.\n *\n * A typed, interned, bidirectional graph and the two traversals over it, plus the\n * file-shaped record the scanner produces and the fold that turns a pile of them\n * into the structure. No I/O, no parser, no resolver: the expensive half belongs\n * to whoever owns the disk, and this is the half that has to be fast and has to\n * be provable without one.\n *\n * See [`docs/selecting.md`](../../../../docs/selecting.md) for what it is for.\n */\n\nexport {\n EDGE_KINDS,\n NODE_KINDS,\n RUNTIME_EDGES,\n idOf,\n keyOf,\n nodeAt,\n nodesOfKind,\n relationsOf,\n type Adjacency,\n type EdgeKind,\n type Node,\n type NodeId,\n type NodeKind,\n type Relation,\n type Relations,\n} from './graph.js';\n\nexport {\n beforeReach,\n changedBefore,\n within,\n type BeforeReach,\n type BeforeReachOptions,\n} from './before.js';\n\nexport {\n dependenciesOf,\n dependentsOf,\n trailOf,\n type Traversal,\n type TraversalOptions,\n} from './reach.js';\n\nexport {\n CLOSURE_EDGES,\n closureOf,\n driftedBetween,\n type Closure,\n type ClosureInput,\n type Drift,\n} from './merkle.js';\n\nexport {\n explain,\n affectedBy,\n relationsOfFiles,\n type FileEdge,\n type FileRecord,\n type AffectedOptions,\n type PackageEdge,\n type RelationsOptions,\n type Affected,\n} from './records.js';\n"]}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/relate/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EACL,UAAU,EACV,UAAU,EACV,aAAa,EACb,IAAI,EACJ,KAAK,EACL,MAAM,EACN,WAAW,EACX,WAAW,GAQZ,MAAM,YAAY,CAAC;AAEpB,OAAO,EACL,WAAW,EACX,aAAa,EACb,MAAM,GAGP,MAAM,aAAa,CAAC;AAErB,OAAO,EAA2B,MAAM,aAAa,CAAC;AAEtD,OAAO,EACL,cAAc,EACd,YAAY,EACZ,OAAO,GAGR,MAAM,YAAY,CAAC;AAEpB,OAAO,EACL,aAAa,EACb,SAAS,EACT,cAAc,GAIf,MAAM,aAAa,CAAC;AAErB,OAAO,EACL,OAAO,EACP,UAAU,EACV,gBAAgB,GAOjB,MAAM,cAAc,CAAC","sourcesContent":["/**\n * `core/relate` — what reaches what.\n *\n * A typed, interned, bidirectional graph and the two traversals over it, plus the\n * file-shaped record the scanner produces and the fold that turns a pile of them\n * into the structure. No I/O, no parser, no resolver: the expensive half belongs\n * to whoever owns the disk, and this is the half that has to be fast and has to\n * be provable without one.\n *\n * See [`docs/selecting.md`](../../../../docs/selecting.md) for what it is for.\n */\n\nexport {\n EDGE_KINDS,\n NODE_KINDS,\n RUNTIME_EDGES,\n idOf,\n keyOf,\n nodeAt,\n nodesOfKind,\n relationsOf,\n type Adjacency,\n type EdgeKind,\n type Node,\n type NodeId,\n type NodeKind,\n type Relation,\n type Relations,\n} from './graph.js';\n\nexport {\n beforeReach,\n changedBefore,\n within,\n type BeforeReach,\n type BeforeReachOptions,\n} from './before.js';\n\nexport { type EdgeUse, type Uses } from './narrow.js';\n\nexport {\n dependenciesOf,\n dependentsOf,\n trailOf,\n type Traversal,\n type TraversalOptions,\n} from './reach.js';\n\nexport {\n CLOSURE_EDGES,\n closureOf,\n driftedBetween,\n type Closure,\n type ClosureInput,\n type Drift,\n} from './merkle.js';\n\nexport {\n explain,\n affectedBy,\n relationsOfFiles,\n type FileEdge,\n type FileRecord,\n type AffectedOptions,\n type PackageEdge,\n type RelationsOptions,\n type Affected,\n} from './records.js';\n"]}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The walk against the arrows, narrowed by the names each importer uses.
|
|
3
|
+
*
|
|
4
|
+
* A file-graph edge says *this file imports that one*, and a plain walk charges
|
|
5
|
+
* every importer for every change. An importer reaches a module only through
|
|
6
|
+
* the names it binds, though, so a change that moved `total` and left `label`
|
|
7
|
+
* alone reaches the files that import `total` and not the ones that import
|
|
8
|
+
* `label`. That is tree shaking, asked of reach instead of a bundle.
|
|
9
|
+
*
|
|
10
|
+
* Each visited file carries one of two states:
|
|
11
|
+
*
|
|
12
|
+
* - **whole** — what it does as it loads moved, or nobody said which of its
|
|
13
|
+
* exports did. Every dependent is entered, as the plain walk does.
|
|
14
|
+
* - **the exports that moved** — read from the file's two texts for a seed,
|
|
15
|
+
* or carried through a re-export for a barrel. A dependent that imports one
|
|
16
|
+
* of them is entered whole: which of *its* exports read the name is a
|
|
17
|
+
* question about its body, and nothing here reads one. A dependent that
|
|
18
|
+
* re-exports one is entered carrying the names it republishes them under.
|
|
19
|
+
*
|
|
20
|
+
* Every answer the use cannot give is whole: an edge the lookup does not
|
|
21
|
+
* know, a namespace import, a `require`, an `import()`, a stylesheet, a
|
|
22
|
+
* component declared in the file. A narrower walk has to be earned by a name
|
|
23
|
+
* the parse recorded, so an unknown can only cost a wider one.
|
|
24
|
+
*
|
|
25
|
+
* A file's names only grow, and a file that grew is expanded again, so the walk
|
|
26
|
+
* reaches the same fixpoint whatever order the seeds arrive in.
|
|
27
|
+
*/
|
|
28
|
+
import { type Adjacency, type NodeId } from './graph.js';
|
|
29
|
+
import type { Traversal } from './reach.js';
|
|
30
|
+
/** How one file uses one file it imports, read from the parse that recorded the edge. */
|
|
31
|
+
export interface EdgeUse {
|
|
32
|
+
/**
|
|
33
|
+
* The names an `import` binds from the target, `default` for a default
|
|
34
|
+
* import. Absent when the use is the whole module: a namespace, a `require`,
|
|
35
|
+
* an `import` that binds nothing, which a `require` is stored as.
|
|
36
|
+
*/
|
|
37
|
+
readonly imports?: readonly string[];
|
|
38
|
+
/**
|
|
39
|
+
* What a re-export republishes, as `[the target's name, this file's name]`.
|
|
40
|
+
* `['*', '*']` is `export * from`, which republishes every name but
|
|
41
|
+
* `default`; `['*', 'ns']` is `export * as ns from`. Absent when the parse
|
|
42
|
+
* did not record what the file publishes.
|
|
43
|
+
*/
|
|
44
|
+
readonly reexports?: readonly (readonly [string, string])[];
|
|
45
|
+
}
|
|
46
|
+
/** How `importer` uses `target`, both by file path; `undefined` is whole. */
|
|
47
|
+
export type Uses = (importer: string, target: string) => EdgeUse | undefined;
|
|
48
|
+
/**
|
|
49
|
+
* Breadth-first against the arrows from `seeds`, each seed in `moved` carrying
|
|
50
|
+
* the exports its change moved and every other one whole.
|
|
51
|
+
*/
|
|
52
|
+
export declare function narrowed(dependents: Adjacency, names: readonly string[], seeds: Iterable<NodeId>, moved: ReadonlyMap<NodeId, readonly string[]>, uses: Uses, allowed: Uint8Array, avoid?: Iterable<NodeId>): Traversal;
|
|
53
|
+
//# sourceMappingURL=narrow.d.ts.map
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The walk against the arrows, narrowed by the names each importer uses.
|
|
3
|
+
*
|
|
4
|
+
* A file-graph edge says *this file imports that one*, and a plain walk charges
|
|
5
|
+
* every importer for every change. An importer reaches a module only through
|
|
6
|
+
* the names it binds, though, so a change that moved `total` and left `label`
|
|
7
|
+
* alone reaches the files that import `total` and not the ones that import
|
|
8
|
+
* `label`. That is tree shaking, asked of reach instead of a bundle.
|
|
9
|
+
*
|
|
10
|
+
* Each visited file carries one of two states:
|
|
11
|
+
*
|
|
12
|
+
* - **whole** — what it does as it loads moved, or nobody said which of its
|
|
13
|
+
* exports did. Every dependent is entered, as the plain walk does.
|
|
14
|
+
* - **the exports that moved** — read from the file's two texts for a seed,
|
|
15
|
+
* or carried through a re-export for a barrel. A dependent that imports one
|
|
16
|
+
* of them is entered whole: which of *its* exports read the name is a
|
|
17
|
+
* question about its body, and nothing here reads one. A dependent that
|
|
18
|
+
* re-exports one is entered carrying the names it republishes them under.
|
|
19
|
+
*
|
|
20
|
+
* Every answer the use cannot give is whole: an edge the lookup does not
|
|
21
|
+
* know, a namespace import, a `require`, an `import()`, a stylesheet, a
|
|
22
|
+
* component declared in the file. A narrower walk has to be earned by a name
|
|
23
|
+
* the parse recorded, so an unknown can only cost a wider one.
|
|
24
|
+
*
|
|
25
|
+
* A file's names only grow, and a file that grew is expanded again, so the walk
|
|
26
|
+
* reaches the same fixpoint whatever order the seeds arrive in.
|
|
27
|
+
*/
|
|
28
|
+
import { EDGE_KINDS } from './graph.js';
|
|
29
|
+
const IMPORTS = EDGE_KINDS.indexOf('imports');
|
|
30
|
+
const REEXPORTS = EDGE_KINDS.indexOf('reexports');
|
|
31
|
+
/** What a re-export hands on from `moved`, under the names the importer publishes. */
|
|
32
|
+
function republished(passes, moved) {
|
|
33
|
+
const out = [];
|
|
34
|
+
for (const [imported, exported] of passes) {
|
|
35
|
+
if (imported === '*' && exported === '*') {
|
|
36
|
+
for (const name of moved)
|
|
37
|
+
if (name !== 'default')
|
|
38
|
+
out.push(name);
|
|
39
|
+
}
|
|
40
|
+
else if (imported === '*' || moved.has(imported)) {
|
|
41
|
+
out.push(exported);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Breadth-first against the arrows from `seeds`, each seed in `moved` carrying
|
|
48
|
+
* the exports its change moved and every other one whole.
|
|
49
|
+
*/
|
|
50
|
+
export function narrowed(dependents, names, seeds, moved, uses, allowed, avoid = []) {
|
|
51
|
+
const nodes = names.length;
|
|
52
|
+
const mask = new Uint8Array(nodes);
|
|
53
|
+
const via = new Int32Array(nodes).fill(-1);
|
|
54
|
+
const avoided = new Uint8Array(nodes);
|
|
55
|
+
for (const id of avoid)
|
|
56
|
+
if (id >= 0 && id < nodes)
|
|
57
|
+
avoided[id] = 1;
|
|
58
|
+
// A visited node absent here is whole.
|
|
59
|
+
const partial = new Map();
|
|
60
|
+
const queue = [];
|
|
61
|
+
const enter = (next, from, exports) => {
|
|
62
|
+
if (avoided[next] === 1)
|
|
63
|
+
return;
|
|
64
|
+
if (mask[next] !== 1) {
|
|
65
|
+
mask[next] = 1;
|
|
66
|
+
via[next] = from;
|
|
67
|
+
if (exports !== undefined)
|
|
68
|
+
partial.set(next, new Set(exports));
|
|
69
|
+
queue.push(next);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const held = partial.get(next);
|
|
73
|
+
if (held === undefined)
|
|
74
|
+
return;
|
|
75
|
+
if (exports === undefined) {
|
|
76
|
+
partial.delete(next);
|
|
77
|
+
queue.push(next);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const size = held.size;
|
|
81
|
+
for (const name of exports)
|
|
82
|
+
held.add(name);
|
|
83
|
+
if (held.size > size)
|
|
84
|
+
queue.push(next);
|
|
85
|
+
};
|
|
86
|
+
for (const seed of seeds) {
|
|
87
|
+
if (seed < 0 || seed >= nodes || mask[seed] === 1 || avoided[seed] === 1)
|
|
88
|
+
continue;
|
|
89
|
+
enter(seed, -1, moved.get(seed));
|
|
90
|
+
}
|
|
91
|
+
for (let head = 0; head < queue.length; head += 1) {
|
|
92
|
+
const node = queue[head];
|
|
93
|
+
const held = partial.get(node);
|
|
94
|
+
if (held?.size === 0)
|
|
95
|
+
continue;
|
|
96
|
+
const end = dependents.offset[node + 1];
|
|
97
|
+
for (let at = dependents.offset[node]; at < end; at += 1) {
|
|
98
|
+
const kind = dependents.kind[at];
|
|
99
|
+
if (allowed[kind] !== 1)
|
|
100
|
+
continue;
|
|
101
|
+
const next = dependents.target[at];
|
|
102
|
+
if (held === undefined || (kind !== IMPORTS && kind !== REEXPORTS)) {
|
|
103
|
+
enter(next, node);
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
const use = uses(names[next], names[node]);
|
|
107
|
+
if (kind === IMPORTS) {
|
|
108
|
+
const imported = use?.imports;
|
|
109
|
+
// TODO: enter the importer carrying the exports whose bodies read the
|
|
110
|
+
// name, which needs the addon's per-export reach within the importer;
|
|
111
|
+
// until then an import hop two out is charged whole.
|
|
112
|
+
if (imported === undefined || imported.some((name) => held.has(name)))
|
|
113
|
+
enter(next, node);
|
|
114
|
+
}
|
|
115
|
+
else if (use?.reexports === undefined) {
|
|
116
|
+
enter(next, node);
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
const out = republished(use.reexports, held);
|
|
120
|
+
if (out.length > 0)
|
|
121
|
+
enter(next, node, out);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
// In id order, so a report built from it is byte-stable whatever order the
|
|
126
|
+
// caller listed its changed files in.
|
|
127
|
+
const visited = [];
|
|
128
|
+
for (let id = 0; id < nodes; id += 1)
|
|
129
|
+
if (mask[id] === 1)
|
|
130
|
+
visited.push(id);
|
|
131
|
+
return { mask, via, nodes: visited };
|
|
132
|
+
}
|
|
133
|
+
//# sourceMappingURL=narrow.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"narrow.js","sourceRoot":"","sources":["../../src/relate/narrow.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The walk against the arrows, narrowed by the names each importer uses.\n *\n * A file-graph edge says *this file imports that one*, and a plain walk charges\n * every importer for every change. An importer reaches a module only through\n * the names it binds, though, so a change that moved `total` and left `label`\n * alone reaches the files that import `total` and not the ones that import\n * `label`. That is tree shaking, asked of reach instead of a bundle.\n *\n * Each visited file carries one of two states:\n *\n * - **whole** — what it does as it loads moved, or nobody said which of its\n * exports did. Every dependent is entered, as the plain walk does.\n * - **the exports that moved** — read from the file's two texts for a seed,\n * or carried through a re-export for a barrel. A dependent that imports one\n * of them is entered whole: which of *its* exports read the name is a\n * question about its body, and nothing here reads one. A dependent that\n * re-exports one is entered carrying the names it republishes them under.\n *\n * Every answer the use cannot give is whole: an edge the lookup does not\n * know, a namespace import, a `require`, an `import()`, a stylesheet, a\n * component declared in the file. A narrower walk has to be earned by a name\n * the parse recorded, so an unknown can only cost a wider one.\n *\n * A file's names only grow, and a file that grew is expanded again, so the walk\n * reaches the same fixpoint whatever order the seeds arrive in.\n */\n\nimport { EDGE_KINDS, type Adjacency, type NodeId } from './graph.js';\nimport type { Traversal } from './reach.js';\n\n/** How one file uses one file it imports, read from the parse that recorded the edge. */\nexport interface EdgeUse {\n /**\n * The names an `import` binds from the target, `default` for a default\n * import. Absent when the use is the whole module: a namespace, a `require`,\n * an `import` that binds nothing, which a `require` is stored as.\n */\n readonly imports?: readonly string[];\n /**\n * What a re-export republishes, as `[the target's name, this file's name]`.\n * `['*', '*']` is `export * from`, which republishes every name but\n * `default`; `['*', 'ns']` is `export * as ns from`. Absent when the parse\n * did not record what the file publishes.\n */\n readonly reexports?: readonly (readonly [string, string])[];\n}\n\n/** How `importer` uses `target`, both by file path; `undefined` is whole. */\nexport type Uses = (importer: string, target: string) => EdgeUse | undefined;\n\nconst IMPORTS = EDGE_KINDS.indexOf('imports');\nconst REEXPORTS = EDGE_KINDS.indexOf('reexports');\n\n/** What a re-export hands on from `moved`, under the names the importer publishes. */\nfunction republished(passes: readonly (readonly [string, string])[], moved: ReadonlySet<string>): string[] {\n const out: string[] = [];\n for (const [imported, exported] of passes) {\n if (imported === '*' && exported === '*') {\n for (const name of moved) if (name !== 'default') out.push(name);\n } else if (imported === '*' || moved.has(imported)) {\n out.push(exported);\n }\n }\n return out;\n}\n\n/**\n * Breadth-first against the arrows from `seeds`, each seed in `moved` carrying\n * the exports its change moved and every other one whole.\n */\nexport function narrowed(\n dependents: Adjacency,\n names: readonly string[],\n seeds: Iterable<NodeId>,\n moved: ReadonlyMap<NodeId, readonly string[]>,\n uses: Uses,\n allowed: Uint8Array,\n avoid: Iterable<NodeId> = [],\n): Traversal {\n const nodes = names.length;\n const mask = new Uint8Array(nodes);\n const via = new Int32Array(nodes).fill(-1);\n const avoided = new Uint8Array(nodes);\n for (const id of avoid) if (id >= 0 && id < nodes) avoided[id] = 1;\n\n // A visited node absent here is whole.\n const partial = new Map<NodeId, Set<string>>();\n const queue: NodeId[] = [];\n\n const enter = (next: NodeId, from: NodeId, exports?: readonly string[]): void => {\n if (avoided[next] === 1) return;\n if (mask[next] !== 1) {\n mask[next] = 1;\n via[next] = from;\n if (exports !== undefined) partial.set(next, new Set(exports));\n queue.push(next);\n return;\n }\n const held = partial.get(next);\n if (held === undefined) return;\n if (exports === undefined) {\n partial.delete(next);\n queue.push(next);\n return;\n }\n const size = held.size;\n for (const name of exports) held.add(name);\n if (held.size > size) queue.push(next);\n };\n\n for (const seed of seeds) {\n if (seed < 0 || seed >= nodes || mask[seed] === 1 || avoided[seed] === 1) continue;\n enter(seed, -1, moved.get(seed));\n }\n\n for (let head = 0; head < queue.length; head += 1) {\n const node = queue[head]!;\n const held = partial.get(node);\n if (held?.size === 0) continue;\n const end = dependents.offset[node + 1]!;\n\n for (let at = dependents.offset[node]!; at < end; at += 1) {\n const kind = dependents.kind[at]!;\n if (allowed[kind] !== 1) continue;\n const next = dependents.target[at]!;\n if (held === undefined || (kind !== IMPORTS && kind !== REEXPORTS)) {\n enter(next, node);\n continue;\n }\n\n const use = uses(names[next]!, names[node]!);\n if (kind === IMPORTS) {\n const imported = use?.imports;\n // TODO: enter the importer carrying the exports whose bodies read the\n // name, which needs the addon's per-export reach within the importer;\n // until then an import hop two out is charged whole.\n if (imported === undefined || imported.some((name) => held.has(name))) enter(next, node);\n } else if (use?.reexports === undefined) {\n enter(next, node);\n } else {\n const out = republished(use.reexports, held);\n if (out.length > 0) enter(next, node, out);\n }\n }\n }\n\n // In id order, so a report built from it is byte-stable whatever order the\n // caller listed its changed files in.\n const visited: NodeId[] = [];\n for (let id = 0; id < nodes; id += 1) if (mask[id] === 1) visited.push(id);\n\n return { mask, via, nodes: visited };\n}\n"]}
|
package/dist/relate/reach.d.ts
CHANGED
|
@@ -63,6 +63,13 @@ export interface TraversalOptions {
|
|
|
63
63
|
* walk starts is exactly that removal, at no cost in the inner loop.
|
|
64
64
|
*/
|
|
65
65
|
readonly avoid?: Iterable<NodeId>;
|
|
66
|
+
/**
|
|
67
|
+
* Per seed, the exports its change moved; a seed absent is charged whole.
|
|
68
|
+
* Read against the arrows only, and only over a graph that carries
|
|
69
|
+
* {@link Relations.uses}: an importer is then entered only through a name it
|
|
70
|
+
* uses ([`narrow.ts`](./narrow.ts)).
|
|
71
|
+
*/
|
|
72
|
+
readonly moved?: ReadonlyMap<NodeId, readonly string[]>;
|
|
66
73
|
}
|
|
67
74
|
/** What depends on these nodes, transitively. Against the arrows. */
|
|
68
75
|
export declare function dependentsOf(relations: Relations, seeds: Iterable<NodeId>, options?: TraversalOptions): Traversal;
|
package/dist/relate/reach.js
CHANGED
|
@@ -28,8 +28,13 @@
|
|
|
28
28
|
* cycle terminates the same way any revisit does.
|
|
29
29
|
*/
|
|
30
30
|
import { EDGE_KINDS, RUNTIME_EDGES } from './graph.js';
|
|
31
|
+
import { narrowed } from './narrow.js';
|
|
31
32
|
/** What depends on these nodes, transitively. Against the arrows. */
|
|
32
33
|
export function dependentsOf(relations, seeds, options = {}) {
|
|
34
|
+
if (options.moved !== undefined && relations.uses !== undefined) {
|
|
35
|
+
const allowed = allowedKinds(options.through);
|
|
36
|
+
return narrowed(relations.dependents, relations.names, seeds, options.moved, relations.uses, allowed, options.avoid);
|
|
37
|
+
}
|
|
33
38
|
return search(relations.dependents, relations.names.length, seeds, options);
|
|
34
39
|
}
|
|
35
40
|
/** What these nodes depend on, transitively. Along the arrows. */
|
package/dist/relate/reach.js.map
CHANGED
|
@@ -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 affect?*\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 to each node they visit, 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 visited: a mark over every node, the node\n * each one was entered from, and the visited ids in id order. `trailOf` reads\n * the chain from any visited node back to its seed.\n */\nexport interface Traversal {\n /** `1` where the node was visited, including the seeds themselves. */\n readonly mask: Uint8Array;\n /**\n * The node each one was entered from, or `-1` for a seed and for anything\n * not visited. Walking it back lands on a seed, which is what `trailOf` does.\n */\n readonly via: Int32Array;\n /** Visited nodes in id order, which is kind-major and then code unit. */\n readonly nodes: readonly NodeId[];\n}\n\nexport interface TraversalOptions {\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 * fails no test and changes 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: TraversalOptions = {},\n): Traversal {\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: TraversalOptions = {},\n): Traversal {\n return search(relations.depends, relations.names.length, seeds, options);\n}\n\n/**\n * The chain from a visited node back to the seed the walk started it from.\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 visited.\n */\nexport function trailOf(traversal: Traversal, id: NodeId): readonly NodeId[] {\n if (traversal.mask[id] !== 1) return [];\n\n const trail: NodeId[] = [];\n for (let at: number = id; at !== -1; at = traversal.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: TraversalOptions,\n): Traversal {\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 visited: NodeId[] = [];\n for (let id = 0; id < nodes; id += 1) if (mask[id] === 1) visited.push(id);\n\n return { mask, via, nodes: visited };\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"]}
|
|
1
|
+
{"version":3,"file":"reach.js","sourceRoot":"","sources":["../../src/relate/reach.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,OAAO,EAAE,UAAU,EAAE,aAAa,EAA8D,MAAM,YAAY,CAAC;AACnH,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AA+CvC,qEAAqE;AACrE,MAAM,UAAU,YAAY,CAC1B,SAAoB,EACpB,KAAuB,EACvB,OAAO,GAAqB,EAAE;IAE9B,IAAI,OAAO,CAAC,KAAK,KAAK,SAAS,IAAI,SAAS,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;QAChE,MAAM,OAAO,GAAG,YAAY,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;QAC9C,OAAO,QAAQ,CAAC,SAAS,CAAC,UAAU,EAAE,SAAS,CAAC,KAAK,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE,SAAS,CAAC,IAAI,EAAE,OAAO,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC;IACvH,CAAC;IACD,OAAO,MAAM,CAAC,SAAS,CAAC,UAAU,EAAE,SAAS,CAAC,KAAK,CAAC,MAAM,EAAE,KAAK,EAAE,OAAO,CAAC,CAAC;AAC9E,CAAC;AAED,kEAAkE;AAClE,MAAM,UAAU,cAAc,CAC5B,SAAoB,EACpB,KAAuB,EACvB,OAAO,GAAqB,EAAE;IAE9B,OAAO,MAAM,CAAC,SAAS,CAAC,OAAO,EAAE,SAAS,CAAC,KAAK,CAAC,MAAM,EAAE,KAAK,EAAE,OAAO,CAAC,CAAC;AAC3E,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,OAAO,CAAC,SAAoB,EAAE,EAAU;IACtD,IAAI,SAAS,CAAC,IAAI,CAAC,EAAE,CAAC,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IAExC,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,KAAK,IAAI,EAAE,GAAW,EAAE,EAAE,EAAE,KAAK,CAAC,CAAC,EAAE,EAAE,GAAG,SAAS,CAAC,GAAG,CAAC,EAAE,CAAE;QAAE,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAC7E,OAAO,KAAK,CAAC,OAAO,EAAE,CAAC;AACzB,CAAC;AAED;;;;;;;;;;GAUG;AACH,SAAS,MAAM,CACb,SAAoB,EACpB,KAAa,EACb,KAAuB,EACvB,OAAyB;IAEzB,MAAM,IAAI,GAAG,IAAI,UAAU,CAAC,KAAK,CAAC,CAAC;IACnC,MAAM,GAAG,GAAG,IAAI,UAAU,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;IAC3C,MAAM,KAAK,GAAa,EAAE,CAAC;IAE3B,MAAM,OAAO,GAAG,IAAI,UAAU,CAAC,KAAK,CAAC,CAAC;IACtC,KAAK,MAAM,EAAE,IAAI,OAAO,CAAC,KAAK,IAAI,EAAE;QAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,GAAG,KAAK;YAAE,OAAO,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC;IAEjF,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,IAAI,IAAI,GAAG,CAAC,IAAI,IAAI,IAAI,KAAK,IAAI,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC;YAAE,SAAS;QACnF,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACf,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACnB,CAAC;IAED,MAAM,OAAO,GAAG,YAAY,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;IAE9C,KAAK,IAAI,IAAI,GAAG,CAAC,EAAE,IAAI,GAAG,KAAK,CAAC,MAAM,EAAE,IAAI,IAAI,CAAC,EAAE,CAAC;QAClD,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAE,CAAC;QAC1B,MAAM,GAAG,GAAG,SAAS,CAAC,MAAM,CAAC,IAAI,GAAG,CAAC,CAAE,CAAC;QAExC,KAAK,IAAI,EAAE,GAAG,SAAS,CAAC,MAAM,CAAC,IAAI,CAAE,EAAE,EAAE,GAAG,GAAG,EAAE,EAAE,IAAI,CAAC,EAAE,CAAC;YACzD,IAAI,OAAO,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,CAAE,CAAC,KAAK,CAAC;gBAAE,SAAS;YAEjD,MAAM,IAAI,GAAG,SAAS,CAAC,MAAM,CAAC,EAAE,CAAE,CAAC;YACnC,IAAI,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC;gBAAE,SAAS;YAEtD,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;YACf,GAAG,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;YACjB,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACnB,CAAC;IACH,CAAC;IAED,8EAA8E;IAC9E,6EAA6E;IAC7E,sCAAsC;IACtC,MAAM,OAAO,GAAa,EAAE,CAAC;IAC7B,KAAK,IAAI,EAAE,GAAG,CAAC,EAAE,EAAE,GAAG,KAAK,EAAE,EAAE,IAAI,CAAC;QAAE,IAAI,IAAI,CAAC,EAAE,CAAC,KAAK,CAAC;YAAE,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAE3E,OAAO,EAAE,IAAI,EAAE,GAAG,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC;AACvC,CAAC;AAED,sCAAsC;AACtC,SAAS,YAAY,CAAC,OAAuC;IAC3D,MAAM,OAAO,GAAG,IAAI,UAAU,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC;IAClD,KAAK,MAAM,IAAI,IAAI,OAAO,IAAI,aAAa,EAAE,CAAC;QAC5C,MAAM,EAAE,GAAG,UAAU,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;QACpC,IAAI,EAAE,KAAK,CAAC,CAAC;YAAE,OAAO,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC;IACjC,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC","sourcesContent":["/**\n * Walking the graph, and being able to say why the walk arrived.\n *\n * Two questions, one traversal each way:\n *\n * - **`dependentsOf`** — against the arrows. *What could this change affect?*\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 to each node they visit, 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';\nimport { narrowed } from './narrow.js';\n\n/**\n * What one walk from a set of seeds visited: a mark over every node, the node\n * each one was entered from, and the visited ids in id order. `trailOf` reads\n * the chain from any visited node back to its seed.\n */\nexport interface Traversal {\n /** `1` where the node was visited, including the seeds themselves. */\n readonly mask: Uint8Array;\n /**\n * The node each one was entered from, or `-1` for a seed and for anything\n * not visited. Walking it back lands on a seed, which is what `trailOf` does.\n */\n readonly via: Int32Array;\n /** Visited nodes in id order, which is kind-major and then code unit. */\n readonly nodes: readonly NodeId[];\n}\n\nexport interface TraversalOptions {\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 * fails no test and changes 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 * Per seed, the exports its change moved; a seed absent is charged whole.\n * Read against the arrows only, and only over a graph that carries\n * {@link Relations.uses}: an importer is then entered only through a name it\n * uses ([`narrow.ts`](./narrow.ts)).\n */\n readonly moved?: ReadonlyMap<NodeId, readonly string[]>;\n}\n\n/** What depends on these nodes, transitively. Against the arrows. */\nexport function dependentsOf(\n relations: Relations,\n seeds: Iterable<NodeId>,\n options: TraversalOptions = {},\n): Traversal {\n if (options.moved !== undefined && relations.uses !== undefined) {\n const allowed = allowedKinds(options.through);\n return narrowed(relations.dependents, relations.names, seeds, options.moved, relations.uses, allowed, options.avoid);\n }\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: TraversalOptions = {},\n): Traversal {\n return search(relations.depends, relations.names.length, seeds, options);\n}\n\n/**\n * The chain from a visited node back to the seed the walk started it from.\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 visited.\n */\nexport function trailOf(traversal: Traversal, id: NodeId): readonly NodeId[] {\n if (traversal.mask[id] !== 1) return [];\n\n const trail: NodeId[] = [];\n for (let at: number = id; at !== -1; at = traversal.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: TraversalOptions,\n): Traversal {\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 visited: NodeId[] = [];\n for (let id = 0; id < nodes; id += 1) if (mask[id] === 1) visited.push(id);\n\n return { mask, via, nodes: visited };\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"]}
|
package/dist/relate/records.d.ts
CHANGED
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
*/
|
|
21
21
|
import type { Digest } from '../format/hash.js';
|
|
22
22
|
import { type EdgeKind, type Node, type Relations } from './graph.js';
|
|
23
|
+
import type { Uses } from './narrow.js';
|
|
23
24
|
import { type Traversal, type TraversalOptions } from './reach.js';
|
|
24
25
|
export interface FileEdge {
|
|
25
26
|
/** Repository-relative, already resolved. A specifier is not an edge. */
|
|
@@ -107,6 +108,8 @@ export declare function relationsOfFiles(records: Iterable<FileRecord>, options?
|
|
|
107
108
|
export interface RelationsOptions {
|
|
108
109
|
/** Per file, the files its run never reaches; see {@link Relations.shadows}. */
|
|
109
110
|
readonly shadows?: ReadonlyMap<string, readonly string[]>;
|
|
111
|
+
/** How each file uses what it imports; see {@link Relations.uses}. */
|
|
112
|
+
readonly uses?: Uses;
|
|
110
113
|
/**
|
|
111
114
|
* `[dependent, dependency]` pairs between packages, from a lockfile
|
|
112
115
|
* ([`packageRelations`](../../../sense/src/lock/lockfile.ts)).
|
|
@@ -160,7 +163,13 @@ export interface Affected {
|
|
|
160
163
|
/** The walk itself, kept so a caller can ask why any one file is in the answer. */
|
|
161
164
|
readonly traversal: Traversal;
|
|
162
165
|
}
|
|
163
|
-
export interface AffectedOptions extends TraversalOptions {
|
|
166
|
+
export interface AffectedOptions extends Omit<TraversalOptions, 'moved'> {
|
|
167
|
+
/**
|
|
168
|
+
* Per changed file, the exports its change moved, read from both of its
|
|
169
|
+
* texts; a file absent is charged whole. Narrows the walk only over a graph
|
|
170
|
+
* that carries {@link Relations.uses}.
|
|
171
|
+
*/
|
|
172
|
+
readonly moved?: ReadonlyMap<string, readonly string[]>;
|
|
164
173
|
/**
|
|
165
174
|
* Per file, the files it takes out of its own graph: what a test mocks.
|
|
166
175
|
*
|
package/dist/relate/records.js
CHANGED
|
@@ -64,7 +64,13 @@ export function relationsOfFiles(records, options = {}) {
|
|
|
64
64
|
for (const [from, to] of options.depends ?? []) {
|
|
65
65
|
relations.push({ from: pkg(from), to: pkg(to), kind: 'depends-on' });
|
|
66
66
|
}
|
|
67
|
-
return relationsOf({
|
|
67
|
+
return relationsOf({
|
|
68
|
+
relations,
|
|
69
|
+
isolated,
|
|
70
|
+
unknown,
|
|
71
|
+
...(options.shadows === undefined ? {} : { shadows: options.shadows }),
|
|
72
|
+
...(options.uses === undefined ? {} : { uses: options.uses }),
|
|
73
|
+
});
|
|
68
74
|
}
|
|
69
75
|
/**
|
|
70
76
|
* What a set of changed files could affect.
|
|
@@ -95,7 +101,8 @@ export function affectedBy(relations, changed, options = {}) {
|
|
|
95
101
|
else
|
|
96
102
|
seeds.push(id);
|
|
97
103
|
}
|
|
98
|
-
const
|
|
104
|
+
const { moved, ...rest } = options;
|
|
105
|
+
const traversal = unshadowed(relations, seeds, moved === undefined ? rest : { ...rest, moved: movedIds(relations, moved) });
|
|
99
106
|
const found = { file: [], component: [], package: [] };
|
|
100
107
|
for (const id of traversal.nodes) {
|
|
101
108
|
const node = nodeAt(relations, id);
|
|
@@ -125,6 +132,16 @@ export function affectedBy(relations, changed, options = {}) {
|
|
|
125
132
|
* file visited only through it is not visited either; the repeat ends when a
|
|
126
133
|
* walk finds no new shadowed file, and a row once decided is not asked again.
|
|
127
134
|
*/
|
|
135
|
+
/** The moved exports by node id, for the walk; files the graph never met drop out. */
|
|
136
|
+
function movedIds(relations, moved) {
|
|
137
|
+
const ids = new Map();
|
|
138
|
+
for (const [name, exports] of moved) {
|
|
139
|
+
const id = idOf(relations, 'file', name);
|
|
140
|
+
if (id !== undefined)
|
|
141
|
+
ids.set(id, exports);
|
|
142
|
+
}
|
|
143
|
+
return ids;
|
|
144
|
+
}
|
|
128
145
|
function unshadowed(relations, seeds, options) {
|
|
129
146
|
const shadowed = [];
|
|
130
147
|
const avoid = new Set(options.avoid ?? []);
|
|
@@ -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 unreadable file contributes what was read of it\n *\n * A `require(name)` with a variable for a name, an `import('./' + page)`, a\n * parse that did not finish: the edges a reader could see are recorded, and the\n * one it could not is not guessed at. `unknown` carries the sentence saying why,\n * for whoever reports on the scan. It widens no walk. Seeding every such file as\n * though it had changed turns one computed `require` into a whole run on every\n * diff, forever, and the edge it hides is exactly what a recorded run sees: the\n * module loaded, whatever expression named it.\n */\n\nimport type { Digest } from '../format/hash.js';\nimport {\n idOf,\n nodeAt,\n relationsOf,\n type EdgeKind,\n type Node,\n type NodeId,\n type NodeKind,\n type Relation,\n type Relations,\n} from './graph.js';\nimport { dependenciesOf, dependentsOf, trailOf, type Traversal, type TraversalOptions } from './reach.js';\n\nexport interface FileEdge {\n /** Repository-relative, already resolved. A specifier is not an edge. */\n readonly to: string;\n readonly kind: EdgeKind;\n}\n\n/**\n * A file's import of something the install provides.\n *\n * The name a specifier asked for, never a version and never a resolution:\n * which copy a resolver handed this importer is unanswerable without\n * reproducing that resolver, and a selector that guessed would skip on the\n * guess. Names over-include, which is the safe direction.\n */\nexport interface PackageEdge {\n /** The package name the specifier asked for — `@mui/material`, not a path. */\n readonly to: string;\n /** The import's own kind, so a `type` import of a package is erased like any other. */\n readonly kind: EdgeKind;\n}\n\nexport interface FileRecord {\n /** Repository-relative, so a graph is portable between machines and CI. */\n readonly file: string;\n\n /**\n * The content digest this record was read from.\n *\n * Two jobs. It lets a second scan skip the parse for a file that has not changed,\n * which is the difference between a scan that costs a second and one nobody\n * leaves enabled. And it is the leaf of the Merkle fold in\n * [`merkle.ts`](./merkle.ts) — a file with no digest is one nothing can prove\n * unchanged, and is marked as such rather than assumed still.\n */\n readonly digest?: Digest;\n\n /** Resolved outgoing edges. Absent and empty mean the same thing here. */\n readonly edges?: readonly FileEdge[];\n\n /** Component names this file declares, from `indexSource` or better. */\n readonly declares?: readonly string[];\n\n /**\n * Packages this file imports, by the name it imports them by.\n *\n * The far end of the line. Nothing resolves these to a path — under pnpm's\n * store or Yarn PnP there may not be one, and under a custom resolver the path\n * would be a lie about a different machine — so the name stands for the\n * dependency, and what the install currently has under that name is a question\n * for the lockfile ([`sense/lock`](../../../sense/src/lock/index.ts)).\n *\n * A file that imports `@mui/material` is affected when `@mui/material`\n * changes; a file that does not, is not. That is the whole rule, and it is why this is\n * read from the imports rather than from a manifest: a `package.json` says what\n * a workspace may use, and the imports say what a file does use.\n */\n readonly packages?: readonly PackageEdge[];\n\n /**\n * Specifiers that were read but did not resolve.\n *\n * Kept for the report rather than for the graph. A bare specifier that failed\n * to resolve is usually a package that is not installed; a **relative** one\n * that failed is a hole, and whoever produced the record says so by also\n * setting `unknown`.\n */\n readonly unresolved?: readonly string[];\n\n /**\n * Why this file's outgoing edges could not all be enumerated, when they could not.\n *\n * The edges that were read are in `edges`; this names what was not. A walk\n * treats the file like any other, and the edge nobody could read is left to a\n * recorded run, which sees the module load whatever named it.\n */\n readonly unknown?: string;\n}\n\nconst file = (name: string): Node => ({ kind: 'file', name });\nconst component = (name: string): Node => ({ kind: 'component', name });\nconst pkg = (name: string): Node => ({ kind: 'package', name });\n\n/**\n * Fold file records into the graph.\n *\n * Every record contributes its own node even when it has no edges, so a changed\n * file that imports nothing is still findable — otherwise the one file in the\n * diff would be missing from the structure that exists to answer questions about\n * it.\n *\n * A declaration becomes an edge from the **component to the file**, which is the\n * direction the convention requires: the component depends on the file that\n * declares it, so one walk against the arrows from a changed file reaches every\n * importer and every component in the same pass.\n */\nexport function relationsOfFiles(records: Iterable<FileRecord>, options: RelationsOptions = {}): Relations {\n const relations: Relation[] = [];\n const isolated: Node[] = [];\n const unknown: (readonly [Node, string])[] = [];\n\n for (const record of records) {\n const from = file(record.file);\n isolated.push(from);\n\n for (const edge of record.edges ?? []) {\n relations.push({ from, to: file(edge.to), kind: edge.kind });\n }\n for (const edge of record.packages ?? []) {\n relations.push({ from, to: pkg(edge.to), kind: edge.kind });\n }\n for (const name of record.declares ?? []) {\n relations.push({ from: component(name), to: from, kind: 'declared-in' });\n }\n // The sentence travels with the node. Reduced to a flag here, the paragraph\n // this record exists to produce could name a count and never a cause.\n if (record.unknown !== undefined) unknown.push([from, record.unknown]);\n }\n\n // The lock's own edges arrive here rather than as records, because they are\n // not a file's reading of anything: one lockfile answers for the whole\n // install, and folding them in beside the imports is what lets a bump three\n // packages deep walk up to the file that imports the one at the top.\n for (const [from, to] of options.depends ?? []) {\n relations.push({ from: pkg(from), to: pkg(to), kind: 'depends-on' });\n }\n\n return relationsOf({ relations, isolated, unknown, ...(options.shadows === undefined ? {} : { shadows: options.shadows }) });\n}\n\nexport interface RelationsOptions {\n /** Per file, the files its run never reaches; see {@link Relations.shadows}. */\n readonly shadows?: ReadonlyMap<string, readonly string[]>;\n\n /**\n * `[dependent, dependency]` pairs between packages, from a lockfile\n * ([`packageRelations`](../../../sense/src/lock/lockfile.ts)).\n *\n * Optional, and a graph without them is not wrong, only shorter-sighted: it\n * answers a change to a package some file imports directly, and says nothing\n * about a change three levels down. Supplying them is what turns *`jsdom`\n * changed* into *every test whose environment is built on it*.\n */\n readonly depends?: Iterable<readonly [string, string]>;\n}\n\n/**\n * What one backwards walk over the file graph found.\n *\n * Every field is part of one answer and none of them stands alone: `files` is\n * the answer, `missing` is what it could not say anything about, and\n * `traversal` is what an explanation is read back out of. A caller that took\n * `files` and dropped the rest would be a caller that cannot say whether the\n * list is narrow because the diff was small or because the scan never ran.\n */\nexport interface Affected {\n /** Files the change could affect, including the changed files themselves. */\n readonly files: readonly string[];\n /** Components declared in any of them. */\n readonly components: readonly string[];\n\n /**\n * Packages the change could affect, including the changed packages\n * themselves and everything the install resolved beneath them.\n *\n * Reported rather than used: the answer a caller acts on is in `files`, and\n * this is the trail of package names that got there. Empty for a walk seeded\n * only with files, because a package is downstream of every file that imports\n * it and a walk against the arrows never descends.\n */\n readonly packages: readonly string[];\n\n /**\n * Changed paths the graph does not hold.\n *\n * Never silently ignored. A `README.md` belongs here and means nothing; a\n * source file belongs here only because the scan never read it, and the\n * caller — which knows where it told the scan to look — is the one that can\n * tell those apart.\n */\n readonly missing: readonly string[];\n\n /**\n * Files the walk visited and their own shadows cut: every trail from a\n * changed file to each one crossed a module the file replaces for its run.\n * Listed so the report can say a test was left out and by whose word.\n */\n readonly shadowed: readonly string[];\n\n /** The walk itself, kept so a caller can ask why any one file is in the answer. */\n readonly traversal: Traversal;\n}\n\nexport interface AffectedOptions extends TraversalOptions {\n /**\n * Per file, the files it takes out of its own graph: what a test mocks.\n *\n * A row is a fact about one file's run and nothing else's, so it is kept\n * beside the records rather than in them — the records say what the text\n * imports, this says what the run never loads. A file with a row is affected\n * only if some trail from a changed file arrives at it without crossing a\n * file in its row, and what depends on the change only *through* it goes with it: the\n * component a story declares, the test a setup file's mocks hold for. The\n * graph's own table when absent ({@link Relations.shadows}).\n */\n readonly shadows?: ReadonlyMap<string, readonly string[]>;\n}\n\n/**\n * What a set of changed files could affect.\n *\n * The seed set is the changed files and nothing else. A file whose edges could\n * not all be read is walked through the edges that were; the one nobody saw is\n * a recorded run's to answer, not a reason to seed the file on every diff.\n *\n * A seed is a path, or a node naming its own kind. The second form is how a\n * changed *package* enters: a bare string keeps meaning a file, because that is\n * what every caller written before packages existed meant by one, and a caller\n * with a lockfile in hand passes `{ kind: 'package', name: 'jsdom' }` into the\n * same walk. There is no second selector, and there is no order between the two\n * — a commit that bumps a dependency and edits a file is one seed set and one\n * traversal.\n *\n * One breadth-first search, whatever the number of changed files, over the\n * runtime edges unless the caller names others.\n */\nexport function affectedBy(\n relations: Relations,\n changed: Iterable<string | Node>,\n options: AffectedOptions = {},\n): Affected {\n const seeds: NodeId[] = [];\n const missing: string[] = [];\n\n for (const entry of changed) {\n const node = typeof entry === 'string' ? file(entry) : entry;\n const id = idOf(relations, node.kind, node.name);\n if (id === undefined) missing.push(node.name);\n else seeds.push(id);\n }\n\n const traversal = unshadowed(relations, seeds, options);\n const found: Record<NodeKind, string[]> = { file: [], component: [], package: [] };\n\n for (const id of traversal.nodes) {\n const node = nodeAt(relations, id);\n if (node === undefined) continue;\n found[node.kind].push(node.name);\n }\n\n return {\n files: found.file,\n components: found.component,\n packages: found.package,\n missing: missing.sort(byCodeUnit),\n shadowed: traversal.shadowed,\n traversal,\n };\n}\n\n/**\n * The walk against the arrows with every file left out whose own shadows cut\n * every trail to it, and with nothing visited through such a file.\n *\n * The walk records one parent per node, so it cannot say whether *another*\n * trail avoided the shadows. Asking the other way can: a walk along the arrows\n * from the file, entering none of its shadows, either finds a seed or does not.\n * It runs only for a visited file whose row names a visited node — a mock of\n * something the change never touched cuts nothing, and costs nothing. A file\n * found shadowed is then avoided and the walk repeated, so a component or a\n * file visited only through it is not visited either; the repeat ends when a\n * walk finds no new shadowed file, and a row once decided is not asked again.\n */\nfunction unshadowed(\n relations: Relations,\n seeds: readonly NodeId[],\n options: AffectedOptions,\n): Traversal & { readonly shadowed: readonly string[] } {\n const shadowed: string[] = [];\n const avoid = new Set<NodeId>(options.avoid ?? []);\n const shadows = options.shadows ?? relations.shadows;\n let traversal = dependentsOf(relations, seeds, options);\n if (shadows.size === 0) return { ...traversal, shadowed };\n\n const seeded = new Uint8Array(relations.names.length);\n for (const seed of seeds) seeded[seed] = 1;\n const decided = new Set<NodeId>();\n\n for (;;) {\n let found = 0;\n for (const [name, row] of shadows) {\n const id = idOf(relations, 'file', name);\n if (id === undefined || traversal.mask[id] !== 1 || seeded[id] === 1 || decided.has(id)) continue;\n\n const cut: NodeId[] = [];\n for (const shadow of row) {\n const target = idOf(relations, 'file', shadow);\n if (target !== undefined && traversal.mask[target] === 1) cut.push(target);\n }\n if (cut.length === 0) continue;\n decided.add(id);\n\n const forward = dependenciesOf(relations, [id], { ...options, avoid: cut });\n if (forward.nodes.some((node) => seeded[node] === 1)) continue;\n\n avoid.add(id);\n shadowed.push(name);\n found += 1;\n }\n if (found === 0) break;\n traversal = dependentsOf(relations, seeds, { ...options, avoid });\n }\n\n return { ...traversal, shadowed: shadowed.sort(byCodeUnit) };\n}\n\n/**\n * Why one file or component is affected: the chain of imports from a changed file to it.\n *\n * The sentence a run prints when somebody asks why a subject was observed. Empty\n * when the node is not affected at all.\n */\nexport function explain(relations: Relations, affected: Affected, node: Node): readonly string[] {\n const id = idOf(relations, node.kind, node.name);\n if (id === undefined) return [];\n return trailOf(affected.traversal, id).map((step) => relations.names[step]!);\n}\n\nfunction byCodeUnit(a: string, b: string): number {\n return a < b ? -1 : a > b ? 1 : 0;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"records.js","sourceRoot":"","sources":["../../src/relate/records.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * One file's outgoing edges, and the fold that turns a pile of them into a graph.\n *\n * A record is what reading **one** file produced, and that is the unit on\n * purpose. It makes the expensive half — open, parse, resolve — cacheable by\n * content digest and parallelizable per file, and it leaves the graph a pure\n * function of the records, so the structure can be rebuilt, sharded or merged\n * without touching a disk. `core` performs no I/O (ADR-0006); whoever owns the\n * disk produces these and hands them over.\n *\n * ## An unreadable file contributes what was read of it\n *\n * A `require(name)` with a variable for a name, an `import('./' + page)`, a\n * parse that did not finish: the edges a reader could see are recorded, and the\n * one it could not is not guessed at. `unknown` carries the sentence saying why,\n * for whoever reports on the scan. It widens no walk. Seeding every such file as\n * though it had changed turns one computed `require` into a whole run on every\n * diff, forever, and the edge it hides is exactly what a recorded run sees: the\n * module loaded, whatever expression named it.\n */\n\nimport type { Digest } from '../format/hash.js';\nimport {\n idOf,\n nodeAt,\n relationsOf,\n type EdgeKind,\n type Node,\n type NodeId,\n type NodeKind,\n type Relation,\n type Relations,\n} from './graph.js';\nimport type { Uses } from './narrow.js';\nimport { dependenciesOf, dependentsOf, trailOf, type Traversal, type TraversalOptions } from './reach.js';\n\nexport interface FileEdge {\n /** Repository-relative, already resolved. A specifier is not an edge. */\n readonly to: string;\n readonly kind: EdgeKind;\n}\n\n/**\n * A file's import of something the install provides.\n *\n * The name a specifier asked for, never a version and never a resolution:\n * which copy a resolver handed this importer is unanswerable without\n * reproducing that resolver, and a selector that guessed would skip on the\n * guess. Names over-include, which is the safe direction.\n */\nexport interface PackageEdge {\n /** The package name the specifier asked for — `@mui/material`, not a path. */\n readonly to: string;\n /** The import's own kind, so a `type` import of a package is erased like any other. */\n readonly kind: EdgeKind;\n}\n\nexport interface FileRecord {\n /** Repository-relative, so a graph is portable between machines and CI. */\n readonly file: string;\n\n /**\n * The content digest this record was read from.\n *\n * Two jobs. It lets a second scan skip the parse for a file that has not changed,\n * which is the difference between a scan that costs a second and one nobody\n * leaves enabled. And it is the leaf of the Merkle fold in\n * [`merkle.ts`](./merkle.ts) — a file with no digest is one nothing can prove\n * unchanged, and is marked as such rather than assumed still.\n */\n readonly digest?: Digest;\n\n /** Resolved outgoing edges. Absent and empty mean the same thing here. */\n readonly edges?: readonly FileEdge[];\n\n /** Component names this file declares, from `indexSource` or better. */\n readonly declares?: readonly string[];\n\n /**\n * Packages this file imports, by the name it imports them by.\n *\n * The far end of the line. Nothing resolves these to a path — under pnpm's\n * store or Yarn PnP there may not be one, and under a custom resolver the path\n * would be a lie about a different machine — so the name stands for the\n * dependency, and what the install currently has under that name is a question\n * for the lockfile ([`sense/lock`](../../../sense/src/lock/index.ts)).\n *\n * A file that imports `@mui/material` is affected when `@mui/material`\n * changes; a file that does not, is not. That is the whole rule, and it is why this is\n * read from the imports rather than from a manifest: a `package.json` says what\n * a workspace may use, and the imports say what a file does use.\n */\n readonly packages?: readonly PackageEdge[];\n\n /**\n * Specifiers that were read but did not resolve.\n *\n * Kept for the report rather than for the graph. A bare specifier that failed\n * to resolve is usually a package that is not installed; a **relative** one\n * that failed is a hole, and whoever produced the record says so by also\n * setting `unknown`.\n */\n readonly unresolved?: readonly string[];\n\n /**\n * Why this file's outgoing edges could not all be enumerated, when they could not.\n *\n * The edges that were read are in `edges`; this names what was not. A walk\n * treats the file like any other, and the edge nobody could read is left to a\n * recorded run, which sees the module load whatever named it.\n */\n readonly unknown?: string;\n}\n\nconst file = (name: string): Node => ({ kind: 'file', name });\nconst component = (name: string): Node => ({ kind: 'component', name });\nconst pkg = (name: string): Node => ({ kind: 'package', name });\n\n/**\n * Fold file records into the graph.\n *\n * Every record contributes its own node even when it has no edges, so a changed\n * file that imports nothing is still findable — otherwise the one file in the\n * diff would be missing from the structure that exists to answer questions about\n * it.\n *\n * A declaration becomes an edge from the **component to the file**, which is the\n * direction the convention requires: the component depends on the file that\n * declares it, so one walk against the arrows from a changed file reaches every\n * importer and every component in the same pass.\n */\nexport function relationsOfFiles(records: Iterable<FileRecord>, options: RelationsOptions = {}): Relations {\n const relations: Relation[] = [];\n const isolated: Node[] = [];\n const unknown: (readonly [Node, string])[] = [];\n\n for (const record of records) {\n const from = file(record.file);\n isolated.push(from);\n\n for (const edge of record.edges ?? []) {\n relations.push({ from, to: file(edge.to), kind: edge.kind });\n }\n for (const edge of record.packages ?? []) {\n relations.push({ from, to: pkg(edge.to), kind: edge.kind });\n }\n for (const name of record.declares ?? []) {\n relations.push({ from: component(name), to: from, kind: 'declared-in' });\n }\n // The sentence travels with the node. Reduced to a flag here, the paragraph\n // this record exists to produce could name a count and never a cause.\n if (record.unknown !== undefined) unknown.push([from, record.unknown]);\n }\n\n // The lock's own edges arrive here rather than as records, because they are\n // not a file's reading of anything: one lockfile answers for the whole\n // install, and folding them in beside the imports is what lets a bump three\n // packages deep walk up to the file that imports the one at the top.\n for (const [from, to] of options.depends ?? []) {\n relations.push({ from: pkg(from), to: pkg(to), kind: 'depends-on' });\n }\n\n return relationsOf({\n relations,\n isolated,\n unknown,\n ...(options.shadows === undefined ? {} : { shadows: options.shadows }),\n ...(options.uses === undefined ? {} : { uses: options.uses }),\n });\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 /** How each file uses what it imports; see {@link Relations.uses}. */\n readonly uses?: Uses;\n\n /**\n * `[dependent, dependency]` pairs between packages, from a lockfile\n * ([`packageRelations`](../../../sense/src/lock/lockfile.ts)).\n *\n * Optional, and a graph without them is not wrong, only shorter-sighted: it\n * answers a change to a package some file imports directly, and says nothing\n * about a change three levels down. Supplying them is what turns *`jsdom`\n * changed* into *every test whose environment is built on it*.\n */\n readonly depends?: Iterable<readonly [string, string]>;\n}\n\n/**\n * What one backwards walk over the file graph found.\n *\n * Every field is part of one answer and none of them stands alone: `files` is\n * the answer, `missing` is what it could not say anything about, and\n * `traversal` is what an explanation is read back out of. A caller that took\n * `files` and dropped the rest would be a caller that cannot say whether the\n * list is narrow because the diff was small or because the scan never ran.\n */\nexport interface Affected {\n /** Files the change could affect, including the changed files themselves. */\n readonly files: readonly string[];\n /** Components declared in any of them. */\n readonly components: readonly string[];\n\n /**\n * Packages the change could affect, including the changed packages\n * themselves and everything the install resolved beneath them.\n *\n * Reported rather than used: the answer a caller acts on is in `files`, and\n * this is the trail of package names that got there. Empty for a walk seeded\n * only with files, because a package is downstream of every file that imports\n * it and a walk against the arrows never descends.\n */\n readonly packages: readonly string[];\n\n /**\n * Changed paths the graph does not hold.\n *\n * Never silently ignored. A `README.md` belongs here and means nothing; a\n * source file belongs here only because the scan never read it, and the\n * caller — which knows where it told the scan to look — is the one that can\n * tell those apart.\n */\n readonly missing: readonly string[];\n\n /**\n * Files the walk visited and their own shadows cut: every trail from a\n * changed file to each one crossed a module the file replaces for its run.\n * Listed so the report can say a test was left out and by whose word.\n */\n readonly shadowed: readonly string[];\n\n /** The walk itself, kept so a caller can ask why any one file is in the answer. */\n readonly traversal: Traversal;\n}\n\nexport interface AffectedOptions extends Omit<TraversalOptions, 'moved'> {\n /**\n * Per changed file, the exports its change moved, read from both of its\n * texts; a file absent is charged whole. Narrows the walk only over a graph\n * that carries {@link Relations.uses}.\n */\n readonly moved?: ReadonlyMap<string, readonly string[]>;\n\n /**\n * Per file, the files it takes out of its own graph: what a test mocks.\n *\n * A row is a fact about one file's run and nothing else's, so it is kept\n * beside the records rather than in them — the records say what the text\n * imports, this says what the run never loads. A file with a row is affected\n * only if some trail from a changed file arrives at it without crossing a\n * file in its row, and what depends on the change only *through* it goes with it: the\n * component a story declares, the test a setup file's mocks hold for. The\n * graph's own table when absent ({@link Relations.shadows}).\n */\n readonly shadows?: ReadonlyMap<string, readonly string[]>;\n}\n\n/**\n * What a set of changed files could affect.\n *\n * The seed set is the changed files and nothing else. A file whose edges could\n * not all be read is walked through the edges that were; the one nobody saw is\n * a recorded run's to answer, not a reason to seed the file on every diff.\n *\n * A seed is a path, or a node naming its own kind. The second form is how a\n * changed *package* enters: a bare string keeps meaning a file, because that is\n * what every caller written before packages existed meant by one, and a caller\n * with a lockfile in hand passes `{ kind: 'package', name: 'jsdom' }` into the\n * same walk. There is no second selector, and there is no order between the two\n * — a commit that bumps a dependency and edits a file is one seed set and one\n * traversal.\n *\n * One breadth-first search, whatever the number of changed files, over the\n * runtime edges unless the caller names others.\n */\nexport function affectedBy(\n relations: Relations,\n changed: Iterable<string | Node>,\n options: AffectedOptions = {},\n): Affected {\n const seeds: NodeId[] = [];\n const missing: string[] = [];\n\n for (const entry of changed) {\n const node = typeof entry === 'string' ? file(entry) : entry;\n const id = idOf(relations, node.kind, node.name);\n if (id === undefined) missing.push(node.name);\n else seeds.push(id);\n }\n\n const { moved, ...rest } = options;\n const traversal = unshadowed(relations, seeds, moved === undefined ? rest : { ...rest, moved: movedIds(relations, moved) });\n const found: Record<NodeKind, string[]> = { file: [], component: [], package: [] };\n\n for (const id of traversal.nodes) {\n const node = nodeAt(relations, id);\n if (node === undefined) continue;\n found[node.kind].push(node.name);\n }\n\n return {\n files: found.file,\n components: found.component,\n packages: found.package,\n missing: missing.sort(byCodeUnit),\n shadowed: traversal.shadowed,\n traversal,\n };\n}\n\n/**\n * The walk against the arrows with every file left out whose own shadows cut\n * every trail to it, and with nothing visited through such a file.\n *\n * The walk records one parent per node, so it cannot say whether *another*\n * trail avoided the shadows. Asking the other way can: a walk along the arrows\n * from the file, entering none of its shadows, either finds a seed or does not.\n * It runs only for a visited file whose row names a visited node — a mock of\n * something the change never touched cuts nothing, and costs nothing. A file\n * found shadowed is then avoided and the walk repeated, so a component or a\n * file visited only through it is not visited either; the repeat ends when a\n * walk finds no new shadowed file, and a row once decided is not asked again.\n */\n/** The moved exports by node id, for the walk; files the graph never met drop out. */\nfunction movedIds(\n relations: Relations,\n moved: ReadonlyMap<string, readonly string[]>,\n): ReadonlyMap<NodeId, readonly string[]> {\n const ids = new Map<NodeId, readonly string[]>();\n for (const [name, exports] of moved) {\n const id = idOf(relations, 'file', name);\n if (id !== undefined) ids.set(id, exports);\n }\n return ids;\n}\n\nfunction unshadowed(\n relations: Relations,\n seeds: readonly NodeId[],\n options: Omit<AffectedOptions, 'moved'> & TraversalOptions,\n): Traversal & { readonly shadowed: readonly string[] } {\n const shadowed: string[] = [];\n const avoid = new Set<NodeId>(options.avoid ?? []);\n const shadows = options.shadows ?? relations.shadows;\n let traversal = dependentsOf(relations, seeds, options);\n if (shadows.size === 0) return { ...traversal, shadowed };\n\n const seeded = new Uint8Array(relations.names.length);\n for (const seed of seeds) seeded[seed] = 1;\n const decided = new Set<NodeId>();\n\n for (;;) {\n let found = 0;\n for (const [name, row] of shadows) {\n const id = idOf(relations, 'file', name);\n if (id === undefined || traversal.mask[id] !== 1 || seeded[id] === 1 || decided.has(id)) continue;\n\n const cut: NodeId[] = [];\n for (const shadow of row) {\n const target = idOf(relations, 'file', shadow);\n if (target !== undefined && traversal.mask[target] === 1) cut.push(target);\n }\n if (cut.length === 0) continue;\n decided.add(id);\n\n const forward = dependenciesOf(relations, [id], { ...options, avoid: cut });\n if (forward.nodes.some((node) => seeded[node] === 1)) continue;\n\n avoid.add(id);\n shadowed.push(name);\n found += 1;\n }\n if (found === 0) break;\n traversal = dependentsOf(relations, seeds, { ...options, avoid });\n }\n\n return { ...traversal, shadowed: shadowed.sort(byCodeUnit) };\n}\n\n/**\n * Why one file or component is affected: the chain of imports from a changed file to it.\n *\n * The sentence a run prints when somebody asks why a subject was observed. Empty\n * when the node is not affected at all.\n */\nexport function explain(relations: Relations, affected: Affected, node: Node): readonly string[] {\n const id = idOf(relations, node.kind, node.name);\n if (id === undefined) return [];\n return trailOf(affected.traversal, id).map((step) => relations.names[step]!);\n}\n\nfunction byCodeUnit(a: string, b: string): number {\n return a < b ? -1 : a > b ? 1 : 0;\n}\n"]}
|
package/package.json
CHANGED