@variance-authority/core 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/CHANGELOG.md +192 -0
  2. package/README.md +51 -9
  3. package/dist/attribute/closure.d.ts +105 -0
  4. package/dist/attribute/closure.js +180 -0
  5. package/dist/attribute/closure.js.map +1 -0
  6. package/dist/attribute/composition.d.ts +5 -3
  7. package/dist/attribute/composition.js.map +1 -1
  8. package/dist/attribute/divergence.js +7 -1
  9. package/dist/attribute/divergence.js.map +1 -1
  10. package/dist/attribute/index.d.ts +5 -1
  11. package/dist/attribute/index.js +3 -1
  12. package/dist/attribute/index.js.map +1 -1
  13. package/dist/attribute/lexicon.d.ts +88 -0
  14. package/dist/attribute/lexicon.js +150 -0
  15. package/dist/attribute/lexicon.js.map +1 -0
  16. package/dist/attribute/source.d.ts +19 -2
  17. package/dist/attribute/source.js +12 -0
  18. package/dist/attribute/source.js.map +1 -1
  19. package/dist/compare/derive.d.ts +17 -0
  20. package/dist/compare/derive.js.map +1 -1
  21. package/dist/compare/diff/index.d.ts +25 -1
  22. package/dist/compare/diff/index.js +8 -5
  23. package/dist/compare/diff/index.js.map +1 -1
  24. package/dist/compare/explain.d.ts +1 -1
  25. package/dist/compare/explain.js +15 -8
  26. package/dist/compare/explain.js.map +1 -1
  27. package/dist/compare/index.d.ts +2 -2
  28. package/dist/compare/index.js +1 -1
  29. package/dist/compare/index.js.map +1 -1
  30. package/dist/compare/observability.d.ts +25 -1
  31. package/dist/compare/observability.js +54 -0
  32. package/dist/compare/observability.js.map +1 -1
  33. package/dist/compare/parting.d.ts +24 -2
  34. package/dist/compare/parting.js +14 -9
  35. package/dist/compare/parting.js.map +1 -1
  36. package/dist/compare/slice.d.ts +15 -2
  37. package/dist/compare/slice.js +6 -2
  38. package/dist/compare/slice.js.map +1 -1
  39. package/dist/format/capture.d.ts +15 -0
  40. package/dist/format/capture.js +30 -1
  41. package/dist/format/capture.js.map +1 -1
  42. package/dist/format/document.d.ts +65 -5
  43. package/dist/format/document.js +34 -0
  44. package/dist/format/document.js.map +1 -1
  45. package/dist/format/hash.d.ts +26 -0
  46. package/dist/format/hash.js +41 -0
  47. package/dist/format/hash.js.map +1 -1
  48. package/dist/format/index.d.ts +4 -3
  49. package/dist/format/index.js +3 -2
  50. package/dist/format/index.js.map +1 -1
  51. package/dist/format/sha256.d.ts +10 -2
  52. package/dist/format/sha256.js +42 -9
  53. package/dist/format/sha256.js.map +1 -1
  54. package/dist/format/stabilize.d.ts +15 -0
  55. package/dist/format/stabilize.js +22 -1
  56. package/dist/format/stabilize.js.map +1 -1
  57. package/dist/index.d.ts +3 -11
  58. package/dist/index.js +3 -11
  59. package/dist/index.js.map +1 -1
  60. package/dist/relate/graph.d.ts +26 -6
  61. package/dist/relate/graph.js +14 -7
  62. package/dist/relate/graph.js.map +1 -1
  63. package/dist/relate/index.d.ts +2 -2
  64. package/dist/relate/index.js +1 -1
  65. package/dist/relate/index.js.map +1 -1
  66. package/dist/relate/merkle.d.ts +8 -5
  67. package/dist/relate/merkle.js +8 -6
  68. package/dist/relate/merkle.js.map +1 -1
  69. package/dist/relate/reach.d.ts +19 -5
  70. package/dist/relate/reach.js +10 -8
  71. package/dist/relate/reach.js.map +1 -1
  72. package/dist/relate/records.d.ts +29 -4
  73. package/dist/relate/records.js +60 -7
  74. package/dist/relate/records.js.map +1 -1
  75. package/dist/segment/index.d.ts +123 -0
  76. package/dist/segment/index.js +265 -0
  77. package/dist/segment/index.js.map +1 -0
  78. package/dist/share/index.d.ts +148 -0
  79. package/dist/share/index.js +190 -0
  80. package/dist/share/index.js.map +1 -0
  81. package/package.json +10 -2
@@ -11,7 +11,7 @@ import { sameTree, sliceOf } from './slice.js';
11
11
  * mismatch, for `compareTrees`' stated reason: this produces an *explanation*,
12
12
  * not a verdict, and two arms of an experiment are two subjects on purpose.
13
13
  */
14
- export function partingOf(baseline, candidate) {
14
+ export function partingOf(baseline, candidate, place = 'same') {
15
15
  const comparison = compareTrees(baseline, candidate);
16
16
  const matching = matchTrees(baseline.root, candidate.root);
17
17
  const partner = new Map();
@@ -25,7 +25,7 @@ export function partingOf(baseline, candidate) {
25
25
  });
26
26
  if (found === undefined) {
27
27
  return {
28
- slice: sliceOf(tree, undefined, moved),
28
+ slice: sliceOf(tree, undefined, moved, place),
29
29
  identical: comparison.identical,
30
30
  deltas: comparison.deltas,
31
31
  };
@@ -33,10 +33,13 @@ export function partingOf(baseline, candidate) {
33
33
  attribute(found, comparison.deltas);
34
34
  const boundaries = found
35
35
  .filter((entry) => entry.inputs.length > 0 || entry.owned.length > 0)
36
- .map(settle);
37
- const origins = boundaries.filter((entry) => entry.rung === 'stateful' || entry.rung === 'external' || entry.rung === 'undetermined');
36
+ .map((draft) => settle(draft, place));
37
+ const origins = boundaries.filter((entry) => entry.rung === 'stateful' ||
38
+ entry.rung === 'external' ||
39
+ entry.rung === 'undetermined' ||
40
+ entry.rung === 'placed');
38
41
  return {
39
- slice: sliceOf(tree, boundaries, moved),
42
+ slice: sliceOf(tree, boundaries, moved, place),
40
43
  identical: comparison.identical,
41
44
  deltas: comparison.deltas,
42
45
  boundaries,
@@ -201,7 +204,7 @@ function attribute(drafts, deltas) {
201
204
  }
202
205
  }
203
206
  /** Apply the precedence in {@link PartingRung} and freeze the row. */
204
- function settle(draft) {
207
+ function settle(draft, place) {
205
208
  const bands = BANDS.filter((band) => draft.owned.some((delta) => bandOf(delta.kind) === band));
206
209
  const moved = [
207
210
  ...new Set(draft.owned.map((delta) => delta.property).filter((name) => name !== undefined)),
@@ -210,14 +213,14 @@ function settle(draft) {
210
213
  component: draft.component,
211
214
  path: draft.path,
212
215
  depth: draft.depth,
213
- rung: rungOf(draft),
216
+ rung: rungOf(draft, place),
214
217
  inputs: draft.inputs,
215
218
  deltas: draft.owned.length,
216
219
  bands,
217
220
  ...(moved.length > 0 ? { moved } : {}),
218
221
  };
219
222
  }
220
- function rungOf(draft) {
223
+ function rungOf(draft, place) {
221
224
  if (!draft.paired)
222
225
  return 'unpaired';
223
226
  if (draft.inputs.some((input) => input.kind === 'prop'))
@@ -230,7 +233,9 @@ function rungOf(draft) {
230
233
  return 'external';
231
234
  if (draft.inputs.length > 0)
232
235
  return 'stateful';
233
- return draft.unread ? 'unread' : 'undetermined';
236
+ if (draft.unread)
237
+ return 'unread';
238
+ return place === 'same' ? 'undetermined' : 'placed';
234
239
  }
235
240
  function depthOf(path) {
236
241
  return path === '' ? 0 : path.split('/').length;
@@ -1 +1 @@
1
- {"version":3,"file":"parting.js","sourceRoot":"","sources":["../../src/compare/parting.ts"],"names":[],"mappings":"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","sourcesContent":["import { boundaries as componentBoundaries, UNATTRIBUTED } from '../attribute/boundary.js';\nimport type { Digest } from '../format/hash.js';\nimport type { NodePath, SemanticNode, SemanticSnapshot } from '../format/snapshot.js';\nimport { BANDS, bandOf, type Band } from './band.js';\nimport { compareTrees, matchTrees, type Delta } from './diff/index.js';\nimport { cascadeInputs, declaredIn } from './cascade.js';\nimport { compareHoldings } from './holding-diff.js';\nimport { sameTree, sliceOf, type PartingSlice } from './slice.js';\n\n/**\n * Where two readings of one page parted.\n *\n * A diff says a `<div>` rendered a `<p>` on one side and a `<span>` on the\n * other. That is a symptom, and acting on it means reading the component,\n * guessing which branch ran, and guessing why. This says: `Cart`'s `useState` at\n * call 2 held different values, so `Summary` was handed a different `total`, so\n * the tag moved. Same evidence, three rungs up.\n *\n * The rung it can reach is decided by how much of the boundary was readable, and\n * the difference between the top two rungs is the whole point:\n *\n * - **collateral** — a prop or a context this boundary *received* moved, so the\n * difference arrived from outside and something above it is the cause.\n * - **origin** — every input this boundary received held, and its own retained\n * state moved. Nothing above it explains this; it is the answer.\n *\n * The mechanism is one rule applied at every boundary: *a component whose\n * inputs agreed and whose output moved decided differently.* Walking the tree\n * for the shallowest boundary where that holds is what turns a page of deltas\n * into one sentence — and the same rule, run over two readings of one page\n * rather than two arms of an experiment, is what separates a flake from a\n * change. A flake is the case where nothing readable moved at all\n * ({@link PartingRung} `undetermined`), and it is only nameable because\n * `unread` is a rung of its own: an accusation of nondeterminism may only be\n * made about inputs somebody actually read (ADR-0002).\n *\n * Not `attribute/composition.ts`'s `Divergence`, which is the same suspicion\n * arrived at from the other side: one component rendering two ways from one\n * props digest *within* a run, found by counting sites. That one can say a\n * component's inputs do not determine its output; this one can say which input.\n *\n * Nothing here reaches a hash. {@link Holding} is evidence carried beside the\n * snapshot, exactly as `styleProvenance` is, and for the reason `wiring.ts`\n * gives: a hook's value is the thing that legitimately differs between two\n * readings of one page, so a band carrying it would be a flake generator wearing\n * a band's name. This module is the reader that value was kept for.\n */\nexport interface Parting {\n /**\n * What kind of parting this is, decided before which input moved.\n *\n * The triage line. `refactor` and `settled` are pages nobody needs to open,\n * `flake` is a page whose baseline is the problem, and `variation` is the\n * only one where the rungs below are worth reading.\n */\n readonly slice: PartingSlice;\n\n /** `true` when both render hashes agree. Inputs may still have moved. */\n readonly identical: boolean;\n\n /** Every delta, unchanged, so a caller need not run the comparison twice. */\n readonly deltas: readonly Delta[];\n\n /**\n * Boundaries where something moved — an input, an output, or both — in\n * document order.\n *\n * Absent, never `[]`, when no node on either side started a component: a page\n * read with no framework adapter has not been found to agree, it has not been\n * asked (ADR-0002). Present with every boundary at rung `unread` is the next\n * reading up — the components were found and their inputs were not.\n */\n readonly boundaries?: readonly PartedBoundary[];\n\n /**\n * The boundaries the difference started at, shallowest first.\n *\n * A subset of {@link boundaries}: those whose output moved without an\n * incoming input to explain it. Usually one. More than one means the two\n * renders differ in more than one place, which is a real answer rather than a\n * failure to narrow.\n */\n readonly origins?: readonly PartedBoundary[];\n}\n\n/**\n * One component boundary, and what moved at it.\n *\n * A boundary is a component's root host node — `componentFiberOf`'s definition,\n * shared with the wiring band so the two can never disagree about where a\n * component starts. A component that vanished entirely has no boundary here: its\n * nodes leave `node-removed` deltas that attribute to the *parent* boundary,\n * which is the correct answer, because deciding not to render a child is\n * something the parent did.\n */\nexport interface PartedBoundary {\n /** `displayName`, or `(anonymous)` when provenance did not name it. */\n readonly component: string;\n\n /** Path in the candidate tree. */\n readonly path: NodePath;\n\n /** Depth of {@link path}, so callers can order without re-parsing it. */\n readonly depth: number;\n\n readonly rung: PartingRung;\n\n /**\n * Inputs that differ: props, then contexts, then hook calls in order, then\n * the inherited properties no declaration at this boundary accounts for.\n */\n readonly inputs: readonly MovedInput[];\n\n /**\n * Deltas at or under this boundary that no nested boundary owns.\n *\n * Zero is meaningful and is not a filtered-out row: a boundary with moved\n * inputs and no deltas of its own is the negative result an experiment\n * usually wants — this arm was assigned differently and rendered the same.\n */\n readonly deltas: number;\n\n /** Bands those deltas fall in, in band order. */\n readonly bands: readonly Band[];\n\n /**\n * The properties the owned deltas named, deduplicated and in code-unit order.\n *\n * The last joint of the chain the rungs above climb: a hook cell moved, a prop\n * carried it down, and *this* is what the prop turned into on the page —\n * `color`, `padding-top`, `width`. A reader chasing a visual regression is\n * looking for this list, and `deltas: 7` alone sends them back to the delta\n * array to assemble it.\n *\n * Absent when no owned delta named a property, which is a reading rather than\n * a gap: a removed node and a changed accessible name are whole-node facts and\n * have no property to name. `deltas` and `bands` still say what happened\n * there.\n */\n readonly moved?: readonly string[];\n}\n\n/**\n * How far up the difference could be traced at one boundary.\n *\n * Ordered by precedence, and the ordering is the reasoning. Any input that\n * arrived from outside settles the question — this boundary is downstream of\n * the cause, whatever else it may also be holding — so `handed` and `provided`\n * outrank the state rungs. `unread` outranks `undetermined` for the same reason\n * pointed the other way: silence is not agreement.\n */\nexport type PartingRung =\n /** A named prop differs. The parent decided this; look up. */\n | 'handed'\n /** A context value differs. A provider above decided this; look up. */\n | 'provided'\n /**\n * An inherited style value differs, and this boundary declares none of it.\n * An ancestor's cascade decided this; look up.\n */\n | 'inherited'\n /** A `useSyncExternalStore` snapshot differs — the store moved, outside React. */\n | 'external'\n /** An own hook cell differs. **This is the cause.** */\n | 'stateful'\n /** The output moved and something this boundary depends on could not be read. */\n | 'unread'\n /** Every input was read, every input agreed, and the output moved anyway. */\n | 'undetermined'\n /** The boundary exists on one side only. */\n | 'unpaired';\n\nexport interface MovedInput {\n readonly kind: 'prop' | 'context' | 'hook' | 'inherited';\n\n /**\n * Prop name, context display name, hook name (`useState`), or — for\n * `inherited` — the CSS property an ancestor decided.\n */\n readonly name: string;\n\n /** Hook call position — the index a reader gets counting down the component. */\n readonly index?: number;\n\n /**\n * Explicitly `| undefined`, on {@link Delta}'s rule: an input present on one\n * side only is the reading, not a field somebody forgot.\n */\n readonly from?: Digest | undefined;\n readonly to?: Digest | undefined;\n}\n\n/**\n * Two snapshots of the same page, read for where they parted.\n *\n * Takes snapshots rather than trees, and takes them without refusing a subject\n * mismatch, for `compareTrees`' stated reason: this produces an *explanation*,\n * not a verdict, and two arms of an experiment are two subjects on purpose.\n */\nexport function partingOf(baseline: SemanticSnapshot, candidate: SemanticSnapshot): Parting {\n const comparison = compareTrees(baseline, candidate);\n const matching = matchTrees(baseline.root, candidate.root);\n\n const partner = new Map<SemanticNode, SemanticNode>();\n for (const [left, right] of matching.pairs) partner.set(right, left);\n\n const tree = sameTree(baseline.root, candidate.root);\n const moved = comparison.deltas.length > 0;\n\n const found = boundariesOf(baseline.root, candidate.root, partner, {\n there: declaredIn(baseline),\n here: declaredIn(candidate),\n });\n if (found === undefined) {\n return {\n slice: sliceOf(tree, undefined, moved),\n identical: comparison.identical,\n deltas: comparison.deltas,\n };\n }\n\n attribute(found, comparison.deltas);\n\n const boundaries = found\n .filter((entry) => entry.inputs.length > 0 || entry.owned.length > 0)\n .map(settle);\n\n const origins = boundaries.filter(\n (entry) => entry.rung === 'stateful' || entry.rung === 'external' || entry.rung === 'undetermined',\n );\n\n return {\n slice: sliceOf(tree, boundaries, moved),\n identical: comparison.identical,\n deltas: comparison.deltas,\n boundaries,\n ...(origins.length > 0 ? { origins: [...origins].sort((a, b) => a.depth - b.depth) } : {}),\n };\n}\n\n/** A boundary mid-construction, before its deltas are counted. */\ninterface Draft {\n readonly component: string;\n readonly path: NodePath;\n readonly basePath?: NodePath;\n readonly depth: number;\n readonly inputs: readonly MovedInput[];\n readonly unread: boolean;\n readonly paired: boolean;\n readonly owned: Delta[];\n}\n\n/**\n * Every boundary in the candidate tree, in document order, paired with its\n * baseline counterpart.\n *\n * Undefined when neither tree carried a holding at all, which is the ADR-0002\n * distinction this whole module rests on: nothing to read is not nothing to\n * report.\n */\nfunction boundariesOf(\n baselineRoot: SemanticNode,\n candidateRoot: SemanticNode,\n partner: ReadonlyMap<SemanticNode, SemanticNode>,\n declared: { readonly there: ReadonlySet<string>; readonly here: ReadonlySet<string> },\n): Draft[] | undefined {\n const mine = boundaryNodes(candidateRoot);\n const theirs = boundaryNodes(baselineRoot);\n if (mine.length === 0 && theirs.length === 0) return undefined;\n\n const paired = pairBoundaries(mine, theirs, partner);\n\n return mine.map((node) => {\n const other = paired.get(node);\n const moved = compareHoldings(other?.holding, node.holding);\n const inputs = [\n ...moved.inputs,\n ...cascadeInputs(other, node, declared.there, declared.here),\n ];\n return {\n component: node.provenance?.owners[0]?.name ?? '(anonymous)',\n path: node.path,\n ...(other === undefined ? {} : { basePath: other.path }),\n depth: depthOf(node.path),\n inputs,\n unread: moved.unread,\n paired: other !== undefined,\n owned: [],\n };\n });\n}\n\n/**\n * Every node where a component starts, in document order.\n *\n * Read from provenance and not from `holding`, which is the difference between\n * this module working on one collector and working on all of them. A holding is\n * what a *framework adapter* managed to read at a boundary — props values, hook\n * cells — and only `@variance-authority/unit-test` supplies one today. Keying the\n * boundary set on it made every rung unreachable from a browser run, including\n * the two that need no adapter at all: the ancestor cascade, which is read from\n * `styleProvenance`, and the tree comparison, which is read from the owner\n * chains. Those runs got `unread` — \"nothing can be said about why\" — while\n * holding the evidence to say it.\n *\n * {@link componentBoundaries} is the shared definition, so a boundary here is\n * the same node the component hashes and the wiring band call one. It reports a\n * node once per component that opens there; the nodes are what this needs, and\n * `boundariesOf` names each by its innermost owner exactly as before.\n *\n * A node with a holding and no provenance is still kept. That is an adapter that\n * read the fiber's values but not its owner chain, and dropping its evidence\n * because of a second failure would lose the one rung it can still reach.\n */\nfunction boundaryNodes(root: SemanticNode): readonly SemanticNode[] {\n const found = new Set<SemanticNode>();\n for (const boundary of componentBoundaries(root)) {\n // `(unattributed)` is the bucket for nodes whose owner chain broke, not a\n // component, and `causesBetween` refuses it for the same reason: a boundary\n // reported there would name a component nobody wrote and pool unrelated\n // parts of the page under one name.\n if (boundary.component === UNATTRIBUTED) continue;\n found.add(boundary.node);\n }\n\n const holders = (node: SemanticNode): void => {\n if (node.holding !== undefined) found.add(node);\n for (const child of node.children) holders(child);\n };\n holders(root);\n\n return [...found].sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));\n}\n\n/**\n * Pair boundaries, preferring the tree matcher and falling back to the owner\n * chain.\n *\n * The fallback is not a nicety, it is the case the whole module exists for. A\n * component that took the other branch returns a `<span>` where it returned a\n * `<p>`, and no host-level matcher pairs those: the tag is the thing that\n * changed, and every identity key ends in it. Reporting the boundary as present\n * on one side only would throw away the props and the hook cells at exactly the\n * moment they are the answer — `Summary` did not appear, it *chose differently*,\n * and the evidence for that is sitting in a holding nobody joined.\n *\n * The chain — the boundary's owner names, outermost component included — is\n * stable under precisely that change, since neither the branch nor the tag\n * appears in it. Ties inside one chain are broken by document order, which is\n * positional and therefore shifts when a sibling is inserted; that is the same\n * cost `match.ts` pays for the same reason, and it is bounded here to\n * components sharing one chain that the matcher already failed to pair.\n */\nfunction pairBoundaries(\n mine: readonly SemanticNode[],\n theirs: readonly SemanticNode[],\n partner: ReadonlyMap<SemanticNode, SemanticNode>,\n): ReadonlyMap<SemanticNode, SemanticNode> {\n const paired = new Map<SemanticNode, SemanticNode>();\n const taken = new Set<SemanticNode>();\n\n for (const node of mine) {\n const other = partner.get(node);\n if (other?.holding === undefined || taken.has(other)) continue;\n paired.set(node, other);\n taken.add(other);\n }\n\n const spare = new Map<string, SemanticNode[]>();\n for (const node of theirs) {\n if (taken.has(node)) continue;\n const key = chainOf(node);\n const bucket = spare.get(key);\n if (bucket === undefined) spare.set(key, [node]);\n else bucket.push(node);\n }\n\n for (const node of mine) {\n if (paired.has(node)) continue;\n const bucket = spare.get(chainOf(node));\n const other = bucket?.shift();\n if (other !== undefined) paired.set(node, other);\n }\n\n return paired;\n}\n\nfunction chainOf(node: SemanticNode): string {\n return (node.provenance?.owners ?? []).map((owner) => owner.name).join('>');\n}\n\n/**\n * Give each delta to the deepest boundary that encloses it.\n *\n * By prefix rather than by scanning every boundary: a node's path *is* its\n * ancestry, so walking it up one segment at a time reaches the owning boundary\n * in as many steps as the node is deep. A `node-removed` delta carries a\n * baseline path, so it is resolved in baseline space — mixing the two spaces\n * would silently hand a removal to whichever candidate node now sits at that\n * address, which is the exact misattribution positional diffing is famous for.\n */\nfunction attribute(drafts: readonly Draft[], deltas: readonly Delta[]): void {\n const here = new Map<NodePath, Draft>();\n const there = new Map<NodePath, Draft>();\n for (const draft of drafts) {\n here.set(draft.path, draft);\n if (draft.basePath !== undefined) there.set(draft.basePath, draft);\n }\n\n for (const delta of deltas) {\n const index = delta.kind === 'node-removed' ? there : here;\n for (let path: NodePath | undefined = delta.path; path !== undefined; path = parentOf(path)) {\n const owner = index.get(path);\n if (owner !== undefined) {\n owner.owned.push(delta);\n break;\n }\n }\n }\n}\n\n/** Apply the precedence in {@link PartingRung} and freeze the row. */\nfunction settle(draft: Draft): PartedBoundary {\n const bands = BANDS.filter((band) => draft.owned.some((delta) => bandOf(delta.kind) === band));\n\n const moved = [\n ...new Set(draft.owned.map((delta) => delta.property).filter((name) => name !== undefined)),\n ].sort();\n\n return {\n component: draft.component,\n path: draft.path,\n depth: draft.depth,\n rung: rungOf(draft),\n inputs: draft.inputs,\n deltas: draft.owned.length,\n bands,\n ...(moved.length > 0 ? { moved } : {}),\n };\n}\n\nfunction rungOf(draft: Draft): PartingRung {\n if (!draft.paired) return 'unpaired';\n if (draft.inputs.some((input) => input.kind === 'prop')) return 'handed';\n if (draft.inputs.some((input) => input.kind === 'context')) return 'provided';\n if (draft.inputs.some((input) => input.kind === 'inherited')) return 'inherited';\n if (draft.inputs.some((input) => input.name === 'useSyncExternalStore')) return 'external';\n if (draft.inputs.length > 0) return 'stateful';\n return draft.unread ? 'unread' : 'undetermined';\n}\n\n\nfunction depthOf(path: NodePath): number {\n return path === '' ? 0 : path.split('/').length;\n}\n\nfunction parentOf(path: NodePath): NodePath | undefined {\n if (path === '') return undefined;\n const cut = path.lastIndexOf('/');\n return cut === -1 ? '' : path.slice(0, cut);\n}\n"]}
1
+ {"version":3,"file":"parting.js","sourceRoot":"","sources":["../../src/compare/parting.ts"],"names":[],"mappings":"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","sourcesContent":["import { boundaries as componentBoundaries, UNATTRIBUTED } from '../attribute/boundary.js';\nimport type { Digest } from '../format/hash.js';\nimport type { NodePath, SemanticNode, SemanticSnapshot } from '../format/snapshot.js';\nimport { BANDS, bandOf, type Band } from './band.js';\nimport { compareTrees, matchTrees, type Delta } from './diff/index.js';\nimport { cascadeInputs, declaredIn } from './cascade.js';\nimport { compareHoldings } from './holding-diff.js';\nimport { sameTree, sliceOf, type PartingSlice } from './slice.js';\n\n/**\n * Where two readings of one page parted.\n *\n * A diff says a `<div>` rendered a `<p>` on one side and a `<span>` on the\n * other. That is a symptom, and acting on it means reading the component,\n * guessing which branch ran, and guessing why. This says: `Cart`'s `useState` at\n * call 2 held different values, so `Summary` was handed a different `total`, so\n * the tag moved. Same evidence, three rungs up.\n *\n * The rung it can reach is decided by how much of the boundary was readable, and\n * the difference between the top two rungs is the whole point:\n *\n * - **collateral** — a prop or a context this boundary *received* moved, so the\n * difference arrived from outside and something above it is the cause.\n * - **origin** — every input this boundary received held, and its own retained\n * state moved. Nothing above it explains this; it is the answer.\n *\n * The mechanism is one rule applied at every boundary: *a component whose\n * inputs agreed and whose output moved decided differently.* Walking the tree\n * for the shallowest boundary where that holds is what turns a page of deltas\n * into one sentence — and the same rule, run over two readings of one page\n * rather than two arms of an experiment, is what separates a flake from a\n * change. A flake is the case where nothing readable moved at all\n * ({@link PartingRung} `undetermined`), and it is only nameable because\n * `unread` is a rung of its own: an accusation of nondeterminism may only be\n * made about inputs somebody actually read (ADR-0002). It is also only nameable\n * when the two readings were taken at one address — see {@link PartingPlace}.\n *\n * Not `attribute/composition.ts`'s `Divergence`, which is the same suspicion\n * arrived at from the other side: one component rendering two ways from one\n * props digest *within* a run, found by counting sites. That one can say a\n * component's inputs do not determine its output; this one can say which input.\n *\n * Nothing here reaches a hash. {@link Holding} is evidence carried beside the\n * snapshot, exactly as `styleProvenance` is, and for the reason `wiring.ts`\n * gives: a hook's value is the thing that legitimately differs between two\n * readings of one page, so a band carrying it would be a flake generator wearing\n * a band's name. This module is the reader that value was kept for.\n */\nexport interface Parting {\n /**\n * What kind of parting this is, decided before which input moved.\n *\n * The triage line. `refactor` and `settled` are pages nobody needs to open,\n * `flake` is a page whose baseline is the problem, and `variation` is the\n * only one where the rungs below are worth reading.\n */\n readonly slice: PartingSlice;\n\n /** `true` when both render hashes agree. Inputs may still have moved. */\n readonly identical: boolean;\n\n /** Every delta, unchanged, so a caller need not run the comparison twice. */\n readonly deltas: readonly Delta[];\n\n /**\n * Boundaries where something moved — an input, an output, or both — in\n * document order.\n *\n * Absent, never `[]`, when no node on either side started a component: a page\n * read with no framework adapter has not been found to agree, it has not been\n * asked (ADR-0002). Present with every boundary at rung `unread` is the next\n * reading up — the components were found and their inputs were not.\n */\n readonly boundaries?: readonly PartedBoundary[];\n\n /**\n * The boundaries the difference started at, shallowest first.\n *\n * A subset of {@link boundaries}: those whose output moved without an\n * incoming input to explain it. Usually one. More than one means the two\n * renders differ in more than one place, which is a real answer rather than a\n * failure to narrow.\n */\n readonly origins?: readonly PartedBoundary[];\n}\n\n/**\n * One component boundary, and what moved at it.\n *\n * A boundary is a component's root host node — `componentFiberOf`'s definition,\n * shared with the wiring band so the two can never disagree about where a\n * component starts. A component that vanished entirely has no boundary here: its\n * nodes leave `node-removed` deltas that attribute to the *parent* boundary,\n * which is the correct answer, because deciding not to render a child is\n * something the parent did.\n */\nexport interface PartedBoundary {\n /** `displayName`, or `(anonymous)` when provenance did not name it. */\n readonly component: string;\n\n /** Path in the candidate tree. */\n readonly path: NodePath;\n\n /** Depth of {@link path}, so callers can order without re-parsing it. */\n readonly depth: number;\n\n readonly rung: PartingRung;\n\n /**\n * Inputs that differ: props, then contexts, then hook calls in order, then\n * the inherited properties no declaration at this boundary accounts for.\n */\n readonly inputs: readonly MovedInput[];\n\n /**\n * Deltas at or under this boundary that no nested boundary owns.\n *\n * Zero is meaningful and is not a filtered-out row: a boundary with moved\n * inputs and no deltas of its own is the negative result an experiment\n * usually wants — this arm was assigned differently and rendered the same.\n */\n readonly deltas: number;\n\n /** Bands those deltas fall in, in band order. */\n readonly bands: readonly Band[];\n\n /**\n * The properties the owned deltas named, deduplicated and in code-unit order.\n *\n * The last joint of the chain the rungs above climb: a hook cell moved, a prop\n * carried it down, and *this* is what the prop turned into on the page —\n * `color`, `padding-top`, `width`. A reader chasing a visual regression is\n * looking for this list, and `deltas: 7` alone sends them back to the delta\n * array to assemble it.\n *\n * Absent when no owned delta named a property, which is a reading rather than\n * a gap: a removed node and a changed accessible name are whole-node facts and\n * have no property to name. `deltas` and `bands` still say what happened\n * there.\n */\n readonly moved?: readonly string[];\n}\n\n/**\n * How far up the difference could be traced at one boundary.\n *\n * Ordered by precedence, and the ordering is the reasoning. Any input that\n * arrived from outside settles the question — this boundary is downstream of\n * the cause, whatever else it may also be holding — so `handed` and `provided`\n * outrank the state rungs. `unread` outranks `undetermined` for the same reason\n * pointed the other way: silence is not agreement.\n */\nexport type PartingRung =\n /** A named prop differs. The parent decided this; look up. */\n | 'handed'\n /** A context value differs. A provider above decided this; look up. */\n | 'provided'\n /**\n * An inherited style value differs, and this boundary declares none of it.\n * An ancestor's cascade decided this; look up.\n */\n | 'inherited'\n /** A `useSyncExternalStore` snapshot differs — the store moved, outside React. */\n | 'external'\n /** An own hook cell differs. **This is the cause.** */\n | 'stateful'\n /** The output moved and something this boundary depends on could not be read. */\n | 'unread'\n /** Every input was read, every input agreed, and the output moved anyway. */\n | 'undetermined'\n /**\n * The same, between two readings taken in different places. Where a component\n * sits is decided by the boxes around it and is handed to it as nothing, so\n * two instances that agreed on every input and landed apart have contradicted\n * nothing. {@link PartingPlace} is how a caller says which it is asking.\n */\n | 'placed'\n /** The boundary exists on one side only. */\n | 'unpaired';\n\n/**\n * Whether two readings were taken at one address.\n *\n * `same` is a subject read twice — across two revisions, or across two moments\n * of one scenario. There, an output that moved with every input holding is the\n * accusation this system is careful about: the reading is not repeatable.\n *\n * `elsewhere` is two instances lifted out of two subjects at one commit, which\n * is `attribute/divergence.ts`' whole shape. The same evidence means something\n * different there: position, and everything a measured box decides downstream of\n * it, is a function of the context an instance was mounted in, not of the props\n * it received. Calling that nondeterminism blames a component for its page.\n */\nexport type PartingPlace = 'same' | 'elsewhere';\n\nexport interface MovedInput {\n readonly kind: 'prop' | 'context' | 'hook' | 'inherited';\n\n /**\n * Prop name, context display name, hook name (`useState`), or — for\n * `inherited` — the CSS property an ancestor decided.\n */\n readonly name: string;\n\n /** Hook call position — the index a reader gets counting down the component. */\n readonly index?: number;\n\n /**\n * Explicitly `| undefined`, on {@link Delta}'s rule: an input present on one\n * side only is the reading, not a field somebody forgot.\n */\n readonly from?: Digest | undefined;\n readonly to?: Digest | undefined;\n}\n\n/**\n * Two snapshots of the same page, read for where they parted.\n *\n * Takes snapshots rather than trees, and takes them without refusing a subject\n * mismatch, for `compareTrees`' stated reason: this produces an *explanation*,\n * not a verdict, and two arms of an experiment are two subjects on purpose.\n */\nexport function partingOf(\n baseline: SemanticSnapshot,\n candidate: SemanticSnapshot,\n place: PartingPlace = 'same',\n): Parting {\n const comparison = compareTrees(baseline, candidate);\n const matching = matchTrees(baseline.root, candidate.root);\n\n const partner = new Map<SemanticNode, SemanticNode>();\n for (const [left, right] of matching.pairs) partner.set(right, left);\n\n const tree = sameTree(baseline.root, candidate.root);\n const moved = comparison.deltas.length > 0;\n\n const found = boundariesOf(baseline.root, candidate.root, partner, {\n there: declaredIn(baseline),\n here: declaredIn(candidate),\n });\n if (found === undefined) {\n return {\n slice: sliceOf(tree, undefined, moved, place),\n identical: comparison.identical,\n deltas: comparison.deltas,\n };\n }\n\n attribute(found, comparison.deltas);\n\n const boundaries = found\n .filter((entry) => entry.inputs.length > 0 || entry.owned.length > 0)\n .map((draft) => settle(draft, place));\n\n const origins = boundaries.filter(\n (entry) =>\n entry.rung === 'stateful' ||\n entry.rung === 'external' ||\n entry.rung === 'undetermined' ||\n entry.rung === 'placed',\n );\n\n return {\n slice: sliceOf(tree, boundaries, moved, place),\n identical: comparison.identical,\n deltas: comparison.deltas,\n boundaries,\n ...(origins.length > 0 ? { origins: [...origins].sort((a, b) => a.depth - b.depth) } : {}),\n };\n}\n\n/** A boundary mid-construction, before its deltas are counted. */\ninterface Draft {\n readonly component: string;\n readonly path: NodePath;\n readonly basePath?: NodePath;\n readonly depth: number;\n readonly inputs: readonly MovedInput[];\n readonly unread: boolean;\n readonly paired: boolean;\n readonly owned: Delta[];\n}\n\n/**\n * Every boundary in the candidate tree, in document order, paired with its\n * baseline counterpart.\n *\n * Undefined when neither tree carried a holding at all, which is the ADR-0002\n * distinction this whole module rests on: nothing to read is not nothing to\n * report.\n */\nfunction boundariesOf(\n baselineRoot: SemanticNode,\n candidateRoot: SemanticNode,\n partner: ReadonlyMap<SemanticNode, SemanticNode>,\n declared: { readonly there: ReadonlySet<string>; readonly here: ReadonlySet<string> },\n): Draft[] | undefined {\n const mine = boundaryNodes(candidateRoot);\n const theirs = boundaryNodes(baselineRoot);\n if (mine.length === 0 && theirs.length === 0) return undefined;\n\n const paired = pairBoundaries(mine, theirs, partner);\n\n return mine.map((node) => {\n const other = paired.get(node);\n const moved = compareHoldings(other?.holding, node.holding);\n const inputs = [\n ...moved.inputs,\n ...cascadeInputs(other, node, declared.there, declared.here),\n ];\n return {\n component: node.provenance?.owners[0]?.name ?? '(anonymous)',\n path: node.path,\n ...(other === undefined ? {} : { basePath: other.path }),\n depth: depthOf(node.path),\n inputs,\n unread: moved.unread,\n paired: other !== undefined,\n owned: [],\n };\n });\n}\n\n/**\n * Every node where a component starts, in document order.\n *\n * Read from provenance and not from `holding`, which is the difference between\n * this module working on one collector and working on all of them. A holding is\n * what a *framework adapter* managed to read at a boundary — props values, hook\n * cells — and only `@variance-authority/unit-test` supplies one today. Keying the\n * boundary set on it made every rung unreachable from a browser run, including\n * the two that need no adapter at all: the ancestor cascade, which is read from\n * `styleProvenance`, and the tree comparison, which is read from the owner\n * chains. Those runs got `unread` — \"nothing can be said about why\" — while\n * holding the evidence to say it.\n *\n * {@link componentBoundaries} is the shared definition, so a boundary here is\n * the same node the component hashes and the wiring band call one. It reports a\n * node once per component that opens there; the nodes are what this needs, and\n * `boundariesOf` names each by its innermost owner exactly as before.\n *\n * A node with a holding and no provenance is still kept. That is an adapter that\n * read the fiber's values but not its owner chain, and dropping its evidence\n * because of a second failure would lose the one rung it can still reach.\n */\nfunction boundaryNodes(root: SemanticNode): readonly SemanticNode[] {\n const found = new Set<SemanticNode>();\n for (const boundary of componentBoundaries(root)) {\n // `(unattributed)` is the bucket for nodes whose owner chain broke, not a\n // component, and `causesBetween` refuses it for the same reason: a boundary\n // reported there would name a component nobody wrote and pool unrelated\n // parts of the page under one name.\n if (boundary.component === UNATTRIBUTED) continue;\n found.add(boundary.node);\n }\n\n const holders = (node: SemanticNode): void => {\n if (node.holding !== undefined) found.add(node);\n for (const child of node.children) holders(child);\n };\n holders(root);\n\n return [...found].sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));\n}\n\n/**\n * Pair boundaries, preferring the tree matcher and falling back to the owner\n * chain.\n *\n * The fallback is not a nicety, it is the case the whole module exists for. A\n * component that took the other branch returns a `<span>` where it returned a\n * `<p>`, and no host-level matcher pairs those: the tag is the thing that\n * changed, and every identity key ends in it. Reporting the boundary as present\n * on one side only would throw away the props and the hook cells at exactly the\n * moment they are the answer — `Summary` did not appear, it *chose differently*,\n * and the evidence for that is sitting in a holding nobody joined.\n *\n * The chain — the boundary's owner names, outermost component included — is\n * stable under precisely that change, since neither the branch nor the tag\n * appears in it. Ties inside one chain are broken by document order, which is\n * positional and therefore shifts when a sibling is inserted; that is the same\n * cost `match.ts` pays for the same reason, and it is bounded here to\n * components sharing one chain that the matcher already failed to pair.\n */\nfunction pairBoundaries(\n mine: readonly SemanticNode[],\n theirs: readonly SemanticNode[],\n partner: ReadonlyMap<SemanticNode, SemanticNode>,\n): ReadonlyMap<SemanticNode, SemanticNode> {\n const paired = new Map<SemanticNode, SemanticNode>();\n const taken = new Set<SemanticNode>();\n\n for (const node of mine) {\n const other = partner.get(node);\n if (other?.holding === undefined || taken.has(other)) continue;\n paired.set(node, other);\n taken.add(other);\n }\n\n const spare = new Map<string, SemanticNode[]>();\n for (const node of theirs) {\n if (taken.has(node)) continue;\n const key = chainOf(node);\n const bucket = spare.get(key);\n if (bucket === undefined) spare.set(key, [node]);\n else bucket.push(node);\n }\n\n for (const node of mine) {\n if (paired.has(node)) continue;\n const bucket = spare.get(chainOf(node));\n const other = bucket?.shift();\n if (other !== undefined) paired.set(node, other);\n }\n\n return paired;\n}\n\nfunction chainOf(node: SemanticNode): string {\n return (node.provenance?.owners ?? []).map((owner) => owner.name).join('>');\n}\n\n/**\n * Give each delta to the deepest boundary that encloses it.\n *\n * By prefix rather than by scanning every boundary: a node's path *is* its\n * ancestry, so walking it up one segment at a time reaches the owning boundary\n * in as many steps as the node is deep. A `node-removed` delta carries a\n * baseline path, so it is resolved in baseline space — mixing the two spaces\n * would silently hand a removal to whichever candidate node now sits at that\n * address, which is the exact misattribution positional diffing is famous for.\n */\nfunction attribute(drafts: readonly Draft[], deltas: readonly Delta[]): void {\n const here = new Map<NodePath, Draft>();\n const there = new Map<NodePath, Draft>();\n for (const draft of drafts) {\n here.set(draft.path, draft);\n if (draft.basePath !== undefined) there.set(draft.basePath, draft);\n }\n\n for (const delta of deltas) {\n const index = delta.kind === 'node-removed' ? there : here;\n for (let path: NodePath | undefined = delta.path; path !== undefined; path = parentOf(path)) {\n const owner = index.get(path);\n if (owner !== undefined) {\n owner.owned.push(delta);\n break;\n }\n }\n }\n}\n\n/** Apply the precedence in {@link PartingRung} and freeze the row. */\nfunction settle(draft: Draft, place: PartingPlace): PartedBoundary {\n const bands = BANDS.filter((band) => draft.owned.some((delta) => bandOf(delta.kind) === band));\n\n const moved = [\n ...new Set(draft.owned.map((delta) => delta.property).filter((name) => name !== undefined)),\n ].sort();\n\n return {\n component: draft.component,\n path: draft.path,\n depth: draft.depth,\n rung: rungOf(draft, place),\n inputs: draft.inputs,\n deltas: draft.owned.length,\n bands,\n ...(moved.length > 0 ? { moved } : {}),\n };\n}\n\nfunction rungOf(draft: Draft, place: PartingPlace): PartingRung {\n if (!draft.paired) return 'unpaired';\n if (draft.inputs.some((input) => input.kind === 'prop')) return 'handed';\n if (draft.inputs.some((input) => input.kind === 'context')) return 'provided';\n if (draft.inputs.some((input) => input.kind === 'inherited')) return 'inherited';\n if (draft.inputs.some((input) => input.name === 'useSyncExternalStore')) return 'external';\n if (draft.inputs.length > 0) return 'stateful';\n if (draft.unread) return 'unread';\n return place === 'same' ? 'undetermined' : 'placed';\n}\n\n\nfunction depthOf(path: NodePath): number {\n return path === '' ? 0 : path.split('/').length;\n}\n\nfunction parentOf(path: NodePath): NodePath | undefined {\n if (path === '') return undefined;\n const cut = path.lastIndexOf('/');\n return cut === -1 ? '' : path.slice(0, cut);\n}\n"]}
@@ -1,5 +1,5 @@
1
1
  import type { SemanticNode } from '../format/snapshot.js';
2
- import type { PartedBoundary } from './parting.js';
2
+ import type { PartedBoundary, PartingPlace } from './parting.js';
3
3
  /**
4
4
  * What kind of parting this is, before anything about which one.
5
5
  *
@@ -19,6 +19,13 @@ import type { PartedBoundary } from './parting.js';
19
19
  * | same | same | same | `settled` |
20
20
  * | unread | — | **moved** | `unread` |
21
21
  *
22
+ * A fourth fact splits the first row, and it is not about the page: *were the
23
+ * two readings taken in the same place*. Where a thing sits is decided by the
24
+ * boxes around it, and no component receives its own position as a prop — so
25
+ * two instances that agreed on every input and landed at different coordinates
26
+ * have not contradicted anything. That row is `placed`. `flake` keeps the case
27
+ * it was named for: one subject, read twice, answering differently.
28
+ *
22
29
  * `reshaped` is the row that used to be missing. It was answered `variation`,
23
30
  * whose sentence says an input moved — and the whole point of this table is
24
31
  * that in that row no input did. Two readings whose component trees are not the
@@ -40,6 +47,8 @@ export type PartingSlice =
40
47
  | 'variation'
41
48
  /** Every input agreed, the tree held, and the output moved anyway. */
42
49
  | 'flake'
50
+ /** The same, in two places. Context decided it, and context is not a prop. */
51
+ | 'placed'
43
52
  /** The component tree is a different tree, no input moved, and the output followed. */
44
53
  | 'reshaped'
45
54
  /** The component tree moved and the output did not. */
@@ -51,12 +60,16 @@ export type PartingSlice =
51
60
  /**
52
61
  * Decide the slice from three independent readings.
53
62
  *
63
+ * `place` says whether the two readings were taken at one address. It only ever
64
+ * decides between `flake` and `placed`, and it defaults to `same` because that
65
+ * is what every comparison across revisions is.
66
+ *
54
67
  * `tree` is `undefined` when neither side carried provenance — not `false`.
55
68
  * A run that never asked what components were there has not found them equal,
56
69
  * and both `refactor` and `flake` are claims about the component tree that such
57
70
  * a run is not entitled to make (ADR-0002).
58
71
  */
59
- export declare function sliceOf(tree: boolean | undefined, boundaries: readonly PartedBoundary[] | undefined, moved: boolean): PartingSlice;
72
+ export declare function sliceOf(tree: boolean | undefined, boundaries: readonly PartedBoundary[] | undefined, moved: boolean, place?: PartingPlace): PartingSlice;
60
73
  /**
61
74
  * Whether the two component trees are the same tree.
62
75
  *
@@ -1,12 +1,16 @@
1
1
  /**
2
2
  * Decide the slice from three independent readings.
3
3
  *
4
+ * `place` says whether the two readings were taken at one address. It only ever
5
+ * decides between `flake` and `placed`, and it defaults to `same` because that
6
+ * is what every comparison across revisions is.
7
+ *
4
8
  * `tree` is `undefined` when neither side carried provenance — not `false`.
5
9
  * A run that never asked what components were there has not found them equal,
6
10
  * and both `refactor` and `flake` are claims about the component tree that such
7
11
  * a run is not entitled to make (ADR-0002).
8
12
  */
9
- export function sliceOf(tree, boundaries, moved) {
13
+ export function sliceOf(tree, boundaries, moved, place = 'same') {
10
14
  // Nothing was read, so nothing may be alleged. `flake` in particular is an
11
15
  // accusation, and it is the one this project must never make on silence.
12
16
  if (boundaries === undefined)
@@ -26,7 +30,7 @@ export function sliceOf(tree, boundaries, moved) {
26
30
  // gets the rung that says so rather than the one that blames the page.
27
31
  if (tree === undefined)
28
32
  return 'unread';
29
- return 'flake';
33
+ return place === 'same' ? 'flake' : 'placed';
30
34
  }
31
35
  /**
32
36
  * Whether the two component trees are the same tree.
@@ -1 +1 @@
1
- {"version":3,"file":"slice.js","sourceRoot":"","sources":["../../src/compare/slice.ts"],"names":[],"mappings":"AAqDA;;;;;;;GAOG;AACH,MAAM,UAAU,OAAO,CACrB,IAAyB,EACzB,UAAiD,EACjD,KAAc;IAEd,2EAA2E;IAC3E,yEAAyE;IACzE,IAAI,UAAU,KAAK,SAAS;QAAE,OAAO,KAAK,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC;IAElE,MAAM,MAAM,GAAG,UAAU,CAAC,IAAI,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,QAAQ,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IAEzE,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,IAAI,IAAI,KAAK,KAAK;YAAE,OAAO,UAAU,CAAC;QACtC,OAAO,MAAM,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,SAAS,CAAC;IACzC,CAAC;IAED,IAAI,MAAM;QAAE,OAAO,WAAW,CAAC;IAC/B,IAAI,IAAI,KAAK,KAAK;QAAE,OAAO,UAAU,CAAC;IACtC,6EAA6E;IAC7E,2EAA2E;IAC3E,uEAAuE;IACvE,IAAI,IAAI,KAAK,SAAS;QAAE,OAAO,QAAQ,CAAC;IACxC,OAAO,OAAO,CAAC;AACjB,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,QAAQ,CAAC,QAAsB,EAAE,SAAuB;IACtE,MAAM,IAAI,GAAG,SAAS,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;IACrC,MAAM,KAAK,GAAG,SAAS,CAAC,SAAS,EAAE,EAAE,CAAC,CAAC;IACvC,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,SAAS,CAAC;IAC9D,IAAI,IAAI,CAAC,MAAM,KAAK,KAAK,CAAC,MAAM;QAAE,OAAO,KAAK,CAAC;IAC/C,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC,KAAK,EAAE,KAAK,EAAE,EAAE,CAAC,KAAK,KAAK,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC;AAC9D,CAAC;AAED,SAAS,SAAS,CAAC,IAAkB,EAAE,GAAa;IAClD,MAAM,MAAM,GAAG,IAAI,CAAC,UAAU,EAAE,MAAM,CAAC;IACvC,IAAI,MAAM,KAAK,SAAS,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC9C,GAAG,CAAC,IAAI,CAAC,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,SAAS,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;IACvF,CAAC;IACD,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,QAAQ;QAAE,SAAS,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;IACzD,OAAO,GAAG,CAAC;AACb,CAAC;AAED;;;;;;GAMG;AACH,SAAS,SAAS,CAAC,MAA0B;IAC3C,IAAI,MAAM,KAAK,SAAS;QAAE,OAAO,EAAE,CAAC;IACpC,OAAO;QACL,MAAM,CAAC,KAAK,EAAE,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE;QAC7B,MAAM,CAAC,QAAQ,EAAE,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE;QAChC,MAAM,CAAC,QAAQ,EAAE,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE;QAChC,MAAM,CAAC,GAAG,IAAI,EAAE;KACjB,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AACd,CAAC","sourcesContent":["import type { SemanticNode } from '../format/snapshot.js';\nimport type { Wiring } from '../format/wiring.js';\nimport type { PartedBoundary } from './parting.js';\n\n/**\n * What kind of parting this is, before anything about which one.\n *\n * The rungs in `parting.ts` answer *which input moved*. This answers the\n * question asked first and answered least: **is this worth looking at at all.**\n * Three facts are read independently — did the component tree move, did any\n * input move, did the output move — and the eight combinations collapse to\n * seven sentences, five of which are a triage decision on their own:\n *\n * | component tree | inputs | output | slice |\n * |---|---|---|---|\n * | same | same | **moved** | `flake` |\n * | any | **moved** | **moved** | `variation` |\n * | **moved** | same | **moved** | `reshaped` |\n * | **moved** | any | same | `refactor` |\n * | same | **moved** | same | `absorbed` |\n * | same | same | same | `settled` |\n * | unread | — | **moved** | `unread` |\n *\n * `reshaped` is the row that used to be missing. It was answered `variation`,\n * whose sentence says an input moved — and the whole point of this table is\n * that in that row no input did. Two readings whose component trees are not the\n * same tree, reached from inputs that all agreed, is a component that chose a\n * different shape: a branch taken differently between two arms, or a rewrite\n * between two revisions. Either way the tree is the finding and the rungs below\n * have nothing to add, because there is no moved input for them to name.\n *\n * `refactor` is the one that pays for the other six. A component tree that\n * moved while the page did not is the receipt a refactor never gets: the\n * screenshots match, which is what a pixel differ says and all it says, and\n * *the components underneath were rewritten*, which is the fact somebody wanted\n * confirmed before merging.\n */\nexport type PartingSlice =\n /** Nothing moved: not the tree, not an input, not the output. */\n | 'settled'\n /** An input moved and the output followed. The ordinary parting. */\n | 'variation'\n /** Every input agreed, the tree held, and the output moved anyway. */\n | 'flake'\n /** The component tree is a different tree, no input moved, and the output followed. */\n | 'reshaped'\n /** The component tree moved and the output did not. */\n | 'refactor'\n /** An input moved and the output did not: the component ignored it. */\n | 'absorbed'\n /** The output moved and what would explain it was not read. */\n | 'unread';\n\n/**\n * Decide the slice from three independent readings.\n *\n * `tree` is `undefined` when neither side carried provenance — not `false`.\n * A run that never asked what components were there has not found them equal,\n * and both `refactor` and `flake` are claims about the component tree that such\n * a run is not entitled to make (ADR-0002).\n */\nexport function sliceOf(\n tree: boolean | undefined,\n boundaries: readonly PartedBoundary[] | undefined,\n moved: boolean,\n): PartingSlice {\n // Nothing was read, so nothing may be alleged. `flake` in particular is an\n // accusation, and it is the one this project must never make on silence.\n if (boundaries === undefined) return moved ? 'unread' : 'settled';\n\n const inputs = boundaries.some((boundary) => boundary.inputs.length > 0);\n\n if (!moved) {\n if (tree === false) return 'refactor';\n return inputs ? 'absorbed' : 'settled';\n }\n\n if (inputs) return 'variation';\n if (tree === false) return 'reshaped';\n // `flake` is the accusation, and it rests on the component tree having held.\n // A run that read holdings but no provenance never established that, so it\n // gets the rung that says so rather than the one that blames the page.\n if (tree === undefined) return 'unread';\n return 'flake';\n}\n\n/**\n * Whether the two component trees are the same tree.\n *\n * The signature is the owner chain plus the wiring at each node that carries\n * one, in document order — the shape `wiringOf` already records, which is what\n * makes this a reading of the fiber rather than of the markup it produced. A\n * renamed component, an added `memo`, a hook inserted, a `key` changed and a\n * subtree moved all land here; a repainted pixel does not.\n *\n * `undefined` when neither side carried provenance at any node. A page read\n * without a framework adapter has no component tree to compare, which is a\n * different answer from two trees that matched.\n */\nexport function sameTree(baseline: SemanticNode, candidate: SemanticNode): boolean | undefined {\n const left = signature(baseline, []);\n const right = signature(candidate, []);\n if (left.length === 0 && right.length === 0) return undefined;\n if (left.length !== right.length) return false;\n return left.every((entry, index) => entry === right[index]);\n}\n\nfunction signature(node: SemanticNode, out: string[]): string[] {\n const owners = node.provenance?.owners;\n if (owners !== undefined && owners.length > 0) {\n out.push(`${owners.map((owner) => owner.name).join('>')}|${wiringKey(node.wiring)}`);\n }\n for (const child of node.children) signature(child, out);\n return out;\n}\n\n/**\n * Wiring as one comparable string.\n *\n * Field order is fixed here rather than taken from `Object.keys`, so that a\n * wiring gaining a field it did not have changes the key for a reason a reader\n * can name, and never because the two objects were built in a different order.\n */\nfunction wiringKey(wiring: Wiring | undefined): string {\n if (wiring === undefined) return '';\n return [\n wiring.hooks?.join(',') ?? '',\n wiring.wrappers?.join(',') ?? '',\n wiring.contexts?.join(',') ?? '',\n wiring.key ?? '',\n ].join(';');\n}\n"]}
1
+ {"version":3,"file":"slice.js","sourceRoot":"","sources":["../../src/compare/slice.ts"],"names":[],"mappings":"AA8DA;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,OAAO,CACrB,IAAyB,EACzB,UAAiD,EACjD,KAAc,EACd,KAAK,GAAiB,MAAM;IAE5B,2EAA2E;IAC3E,yEAAyE;IACzE,IAAI,UAAU,KAAK,SAAS;QAAE,OAAO,KAAK,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC;IAElE,MAAM,MAAM,GAAG,UAAU,CAAC,IAAI,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,QAAQ,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IAEzE,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,IAAI,IAAI,KAAK,KAAK;YAAE,OAAO,UAAU,CAAC;QACtC,OAAO,MAAM,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,SAAS,CAAC;IACzC,CAAC;IAED,IAAI,MAAM;QAAE,OAAO,WAAW,CAAC;IAC/B,IAAI,IAAI,KAAK,KAAK;QAAE,OAAO,UAAU,CAAC;IACtC,6EAA6E;IAC7E,2EAA2E;IAC3E,uEAAuE;IACvE,IAAI,IAAI,KAAK,SAAS;QAAE,OAAO,QAAQ,CAAC;IACxC,OAAO,KAAK,KAAK,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,QAAQ,CAAC;AAC/C,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,QAAQ,CAAC,QAAsB,EAAE,SAAuB;IACtE,MAAM,IAAI,GAAG,SAAS,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;IACrC,MAAM,KAAK,GAAG,SAAS,CAAC,SAAS,EAAE,EAAE,CAAC,CAAC;IACvC,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,SAAS,CAAC;IAC9D,IAAI,IAAI,CAAC,MAAM,KAAK,KAAK,CAAC,MAAM;QAAE,OAAO,KAAK,CAAC;IAC/C,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC,KAAK,EAAE,KAAK,EAAE,EAAE,CAAC,KAAK,KAAK,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC;AAC9D,CAAC;AAED,SAAS,SAAS,CAAC,IAAkB,EAAE,GAAa;IAClD,MAAM,MAAM,GAAG,IAAI,CAAC,UAAU,EAAE,MAAM,CAAC;IACvC,IAAI,MAAM,KAAK,SAAS,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC9C,GAAG,CAAC,IAAI,CAAC,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,SAAS,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;IACvF,CAAC;IACD,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,QAAQ;QAAE,SAAS,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;IACzD,OAAO,GAAG,CAAC;AACb,CAAC;AAED;;;;;;GAMG;AACH,SAAS,SAAS,CAAC,MAA0B;IAC3C,IAAI,MAAM,KAAK,SAAS;QAAE,OAAO,EAAE,CAAC;IACpC,OAAO;QACL,MAAM,CAAC,KAAK,EAAE,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE;QAC7B,MAAM,CAAC,QAAQ,EAAE,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE;QAChC,MAAM,CAAC,QAAQ,EAAE,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE;QAChC,MAAM,CAAC,GAAG,IAAI,EAAE;KACjB,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AACd,CAAC","sourcesContent":["import type { SemanticNode } from '../format/snapshot.js';\nimport type { Wiring } from '../format/wiring.js';\nimport type { PartedBoundary, PartingPlace } from './parting.js';\n\n/**\n * What kind of parting this is, before anything about which one.\n *\n * The rungs in `parting.ts` answer *which input moved*. This answers the\n * question asked first and answered least: **is this worth looking at at all.**\n * Three facts are read independently — did the component tree move, did any\n * input move, did the output move — and the eight combinations collapse to\n * seven sentences, five of which are a triage decision on their own:\n *\n * | component tree | inputs | output | slice |\n * |---|---|---|---|\n * | same | same | **moved** | `flake` |\n * | any | **moved** | **moved** | `variation` |\n * | **moved** | same | **moved** | `reshaped` |\n * | **moved** | any | same | `refactor` |\n * | same | **moved** | same | `absorbed` |\n * | same | same | same | `settled` |\n * | unread | — | **moved** | `unread` |\n *\n * A fourth fact splits the first row, and it is not about the page: *were the\n * two readings taken in the same place*. Where a thing sits is decided by the\n * boxes around it, and no component receives its own position as a prop — so\n * two instances that agreed on every input and landed at different coordinates\n * have not contradicted anything. That row is `placed`. `flake` keeps the case\n * it was named for: one subject, read twice, answering differently.\n *\n * `reshaped` is the row that used to be missing. It was answered `variation`,\n * whose sentence says an input moved — and the whole point of this table is\n * that in that row no input did. Two readings whose component trees are not the\n * same tree, reached from inputs that all agreed, is a component that chose a\n * different shape: a branch taken differently between two arms, or a rewrite\n * between two revisions. Either way the tree is the finding and the rungs below\n * have nothing to add, because there is no moved input for them to name.\n *\n * `refactor` is the one that pays for the other six. A component tree that\n * moved while the page did not is the receipt a refactor never gets: the\n * screenshots match, which is what a pixel differ says and all it says, and\n * *the components underneath were rewritten*, which is the fact somebody wanted\n * confirmed before merging.\n */\nexport type PartingSlice =\n /** Nothing moved: not the tree, not an input, not the output. */\n | 'settled'\n /** An input moved and the output followed. The ordinary parting. */\n | 'variation'\n /** Every input agreed, the tree held, and the output moved anyway. */\n | 'flake'\n /** The same, in two places. Context decided it, and context is not a prop. */\n | 'placed'\n /** The component tree is a different tree, no input moved, and the output followed. */\n | 'reshaped'\n /** The component tree moved and the output did not. */\n | 'refactor'\n /** An input moved and the output did not: the component ignored it. */\n | 'absorbed'\n /** The output moved and what would explain it was not read. */\n | 'unread';\n\n/**\n * Decide the slice from three independent readings.\n *\n * `place` says whether the two readings were taken at one address. It only ever\n * decides between `flake` and `placed`, and it defaults to `same` because that\n * is what every comparison across revisions is.\n *\n * `tree` is `undefined` when neither side carried provenance — not `false`.\n * A run that never asked what components were there has not found them equal,\n * and both `refactor` and `flake` are claims about the component tree that such\n * a run is not entitled to make (ADR-0002).\n */\nexport function sliceOf(\n tree: boolean | undefined,\n boundaries: readonly PartedBoundary[] | undefined,\n moved: boolean,\n place: PartingPlace = 'same',\n): PartingSlice {\n // Nothing was read, so nothing may be alleged. `flake` in particular is an\n // accusation, and it is the one this project must never make on silence.\n if (boundaries === undefined) return moved ? 'unread' : 'settled';\n\n const inputs = boundaries.some((boundary) => boundary.inputs.length > 0);\n\n if (!moved) {\n if (tree === false) return 'refactor';\n return inputs ? 'absorbed' : 'settled';\n }\n\n if (inputs) return 'variation';\n if (tree === false) return 'reshaped';\n // `flake` is the accusation, and it rests on the component tree having held.\n // A run that read holdings but no provenance never established that, so it\n // gets the rung that says so rather than the one that blames the page.\n if (tree === undefined) return 'unread';\n return place === 'same' ? 'flake' : 'placed';\n}\n\n/**\n * Whether the two component trees are the same tree.\n *\n * The signature is the owner chain plus the wiring at each node that carries\n * one, in document order — the shape `wiringOf` already records, which is what\n * makes this a reading of the fiber rather than of the markup it produced. A\n * renamed component, an added `memo`, a hook inserted, a `key` changed and a\n * subtree moved all land here; a repainted pixel does not.\n *\n * `undefined` when neither side carried provenance at any node. A page read\n * without a framework adapter has no component tree to compare, which is a\n * different answer from two trees that matched.\n */\nexport function sameTree(baseline: SemanticNode, candidate: SemanticNode): boolean | undefined {\n const left = signature(baseline, []);\n const right = signature(candidate, []);\n if (left.length === 0 && right.length === 0) return undefined;\n if (left.length !== right.length) return false;\n return left.every((entry, index) => entry === right[index]);\n}\n\nfunction signature(node: SemanticNode, out: string[]): string[] {\n const owners = node.provenance?.owners;\n if (owners !== undefined && owners.length > 0) {\n out.push(`${owners.map((owner) => owner.name).join('>')}|${wiringKey(node.wiring)}`);\n }\n for (const child of node.children) signature(child, out);\n return out;\n}\n\n/**\n * Wiring as one comparable string.\n *\n * Field order is fixed here rather than taken from `Object.keys`, so that a\n * wiring gaining a field it did not have changes the key for a reason a reader\n * can name, and never because the two objects were built in a different order.\n */\nfunction wiringKey(wiring: Wiring | undefined): string {\n if (wiring === undefined) return '';\n return [\n wiring.hooks?.join(',') ?? '',\n wiring.wrappers?.join(',') ?? '',\n wiring.contexts?.join(',') ?? '',\n wiring.key ?? '',\n ].join(';');\n}\n"]}
@@ -209,4 +209,19 @@ export interface Diagnostic {
209
209
  readonly message: string;
210
210
  readonly nodePath?: string;
211
211
  }
212
+ /**
213
+ * One list from several, with a diagnostic said once however many times it was
214
+ * found.
215
+ *
216
+ * A subject is described by a document, a snapshot and a comparison, and the
217
+ * same condition is visible to more than one of them -- a missing font is in the
218
+ * document that declared it and in the snapshot that measured it. A reader asks
219
+ * "what is wrong with this subject" once, and a list that answers twice reads as
220
+ * two findings.
221
+ *
222
+ * Identity is every field, `nodePath` included: the same code on two nodes is
223
+ * two facts, and collapsing them would report one element where the page has a
224
+ * dozen.
225
+ */
226
+ export declare function mergeDiagnostics(...lists: readonly (readonly Diagnostic[] | undefined)[]): readonly Diagnostic[];
212
227
  //# sourceMappingURL=capture.d.ts.map
@@ -1,2 +1,31 @@
1
- export {};
1
+ /**
2
+ * One list from several, with a diagnostic said once however many times it was
3
+ * found.
4
+ *
5
+ * A subject is described by a document, a snapshot and a comparison, and the
6
+ * same condition is visible to more than one of them -- a missing font is in the
7
+ * document that declared it and in the snapshot that measured it. A reader asks
8
+ * "what is wrong with this subject" once, and a list that answers twice reads as
9
+ * two findings.
10
+ *
11
+ * Identity is every field, `nodePath` included: the same code on two nodes is
12
+ * two facts, and collapsing them would report one element where the page has a
13
+ * dozen.
14
+ */
15
+ export function mergeDiagnostics(...lists) {
16
+ const byIdentity = new Map();
17
+ for (const list of lists) {
18
+ for (const diagnostic of list ?? []) {
19
+ const key = JSON.stringify([
20
+ diagnostic.severity,
21
+ diagnostic.code,
22
+ diagnostic.message,
23
+ diagnostic.nodePath ?? null,
24
+ ]);
25
+ if (!byIdentity.has(key))
26
+ byIdentity.set(key, diagnostic);
27
+ }
28
+ }
29
+ return [...byIdentity.values()];
30
+ }
2
31
  //# sourceMappingURL=capture.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"capture.js","sourceRoot":"","sources":["../../src/format/capture.ts"],"names":[],"mappings":"","sourcesContent":["import type { EnvironmentInputs } from './environment.js';\nimport type { ObservationProfile } from './profile.js';\nimport type { Provenance } from './provenance.js';\nimport type { Wiring } from './wiring.js';\nimport type { Holding } from './holding.js';\n\n/**\n * The collector/core boundary.\n *\n * Collectors **extract**; `core` **normalizes**. Nothing in this file is a live\n * object — a `RawCapture` is plain data, fully serializable, produced by touching\n * a DOM and consumed by code that has never seen one.\n *\n * Two properties fall out of that split, and both are the reason for it:\n *\n * 1. **One ruleset, by construction.** Normalization is the moat (ADR-0003) and\n * the thing most likely to drift between two implementations. If the JSDOM and\n * Chromium collectors each normalized their own captures, \"the same rules ran\"\n * would be a claim maintained by discipline. Here it is a fact of the call\n * graph: there is one normalizer and both profiles enter it.\n * 2. **The sub-renderer can be anywhere.** A capture crossing a worker boundary,\n * a pipe, or a network hop to a device farm is the same value it was in\n * process. No code may assume the collector is local.\n */\nexport interface RawCapture {\n readonly captureVersion: 1;\n readonly subject: SubjectRef;\n readonly profile: ObservationProfile;\n\n /**\n * Environment inputs the *collector* is positioned to know — engine version,\n * fonts actually loaded, resolved conditions, asset hashes. The ruleset and\n * allowlist versions are `core`'s to supply, so they are absent here and the\n * key is completed during normalization.\n */\n readonly environment: Omit<EnvironmentInputs, 'ruleset' | 'allowlist'>;\n\n readonly root: RawNode;\n\n /**\n * Inherited values in force at the subject root.\n *\n * Mandatory, not an optimization. CSS applicability pruning (ADR-0003) drops\n * every rule that matches nothing inside the subtree — including rules on\n * ancestors *outside* it whose inheritable properties still reach in. Without\n * this seed the cheap tier is unsound and will report false `unchanged`.\n */\n readonly inheritedSeed: Readonly<Record<string, string>>;\n\n /**\n * Subtrees this subject renders through portals, in fiber traversal order.\n *\n * A subject's boundary is a component-tree question, not a DOM-containment\n * one: `createPortal` renders elsewhere in the document while remaining part\n * of the tree rooted here. Omitting these makes an opening modal report\n * `unchanged`, because its own container is byte-identical (ADR-0007).\n */\n readonly portals?: readonly RawNode[];\n\n /**\n * Shared root state this subject is *latently* coupled to.\n *\n * Rules like `html.dark .card` contribute nothing today and everything the\n * moment `dark` lands on `<html>`. In a session that reuses one document across\n * subjects, that moment may arrive because a *different* subject put it there —\n * turning this subject's baseline order-dependent with no visible cause.\n * Recorded so the coupling can be reported before it bites.\n */\n readonly couplings?: readonly string[];\n\n /** Anything the collector could not do. Empty is the expected case. */\n readonly diagnostics: readonly Diagnostic[];\n}\n\nexport interface SubjectRef {\n /** Stable across renames of the file, e.g. `story:components-button--primary`. */\n readonly id: string;\n\n /**\n * What kind of thing this is, which is not the same question as what produced\n * it.\n *\n * `value` is the one that is not a rendering: a JSON body, a schema, a route\n * table. It is a fourth member rather than a reuse of `route`, which already\n * means *a page rendered at a URL* — a route table is a value about the same\n * paths, and a subject list where one word meant both is one nobody can filter.\n */\n readonly kind: 'story' | 'route' | 'fixture' | 'value';\n readonly title?: string;\n}\n\n/**\n * A node exactly as observed, before any rule is applied.\n *\n * Raw means raw: `attributes` still holds generated ids and hashed class names,\n * `matchedRules` still holds cascade losers. Normalization needs the unedited\n * input — a collector that helpfully pre-cleaned would be a second, invisible\n * ruleset, versioned by nothing.\n */\nexport interface RawNode {\n readonly tag: string;\n readonly attributes: Readonly<Record<string, string>>;\n\n /** Resolved accessibility properties, when the profile provides them. */\n readonly aria?: RawAria;\n\n /**\n * Rules matching this node, as authored.\n *\n * Losing declarations are retained: `core` resolves the cascade for profiles\n * whose engine did not (ADR-0003 step 5), and cannot do that from winners\n * alone. Shorthands are retained *unexpanded* — expansion is `core`'s job, so\n * that it is versioned by the ruleset rather than by each collector.\n *\n * A collector MAY drop a declaration whose property is neither allowlisted nor\n * a shorthand of an allowlisted property, and SHOULD when the capture crosses\n * a network hop. It MUST NOT normalize values; a helpfully pre-cleaned capture\n * is a second, invisible ruleset versioned by nothing.\n */\n readonly matchedRules: readonly MatchedRule[];\n\n readonly inlineStyle?: Readonly<Record<string, string>>;\n\n /** Engine-resolved computed style. Present only when `profile.computedStyle`. */\n readonly computedStyle?: Readonly<Record<string, string>>;\n\n /** Present only when `profile.layout`. */\n readonly rect?: Rect;\n\n /** Literal text of a text node. Digested during normalization, per policy. */\n readonly text?: string;\n\n readonly provenance?: Provenance;\n\n /**\n * How the framework holds this node's component, when an adapter supplied it.\n *\n * Beside `provenance` because it arrives from the same seam and under the same\n * rule: the collector carries no framework dependency and both are injected.\n * See {@link Wiring} for why it is a dimension rather than another attribution\n * field.\n */\n readonly wiring?: Wiring;\n\n /**\n * What the component that authored this node was holding, when an adapter\n * supplied it.\n *\n * A third injection at the same seam, and deliberately not folded into\n * `wiring`: wiring is hashed into a band and this may never be, so a caller\n * that wants the band must be able to have it without also carrying values,\n * and a caller that wants explanations must be able to ask for them without\n * changing a single digest. See {@link Holding}.\n */\n readonly holding?: Holding;\n\n readonly children: readonly RawNode[];\n\n /** Nodes inside a shadow root, kept distinct from light-DOM children. */\n readonly shadowChildren?: readonly RawNode[];\n\n /**\n * Ignore rules the collector found this element under (spec 0024).\n *\n * A mark, not a removal. The collector is the only thing holding a live DOM, so\n * it is the only thing that can run a selector or read a marker attribute — but\n * it must not act on what it finds. An element deleted from the capture would\n * be invisible to every count downstream, and the difference between \"absorbed\n * by the `carousel` rule\" and \"was never there\" is the difference between an\n * ignore and a blind spot.\n *\n * Never hashed. Normalization turns these into\n * {@link import('./snapshot.js').SemanticSnapshot.ignoreSites}, which sits\n * beside `styleProvenance` outside the render hash for the same reason: masking\n * a clock changes what a run *says*, not what it *renders*, and must not\n * re-baseline the repository.\n */\n readonly ignoredBy?: readonly string[];\n}\n\nexport interface RawAria {\n readonly role: string | null;\n readonly name: string | null;\n\n /**\n * Accessible description — what `aria-describedby` and `title` resolve to.\n *\n * Captured because nothing else captures it, and its absence was a hole. The\n * attribute allowlist drops every `aria-*` attribute on the stated grounds\n * that they \"are resolved into role/name/state\" — true of `aria-label` and\n * `aria-selected`, and false of `aria-describedby`, which resolves into a\n * description and had nowhere to land. A field whose `aria-describedby`\n * pointed at a deleted error message compared **equal**: the reference was\n * dropped by the allowlist, the name was unaffected, and the regression was\n * silent on every tier including raster.\n */\n readonly description?: string | null;\n\n /** `checked`, `disabled`, `expanded`, `selected`, … */\n readonly state: Readonly<Record<string, string | boolean | number>>;\n}\n\nexport interface MatchedRule {\n /** Identifies the origin sheet for attribution: href, or a synthetic id. */\n readonly sheet: string;\n readonly selector: string;\n /** `[idCount, classCount, typeCount]` — CSS specificity, most significant first. */\n readonly specificity: readonly [number, number, number];\n /** Document order of the rule. Breaks specificity ties, per the cascade. */\n readonly order: number;\n readonly declarations: readonly Declaration[];\n /** Source position, when the sheet exposes one. Turns a diff into a file:line. */\n readonly source?: { readonly file: string; readonly line: number };\n}\n\nexport interface Declaration {\n readonly property: string;\n readonly value: string;\n readonly important: boolean;\n /** Custom properties referenced by the value, for token-keyed attribution. */\n readonly references?: readonly string[];\n}\n\n/** Browser-layout coordinates retained as evidence, without a preferred layout policy. */\nexport interface Rect {\n readonly x: number;\n readonly y: number;\n readonly width: number;\n readonly height: number;\n}\n\nexport interface Diagnostic {\n readonly severity: 'warn' | 'error';\n readonly code: string;\n readonly message: string;\n readonly nodePath?: string;\n}\n"]}
1
+ {"version":3,"file":"capture.js","sourceRoot":"","sources":["../../src/format/capture.ts"],"names":[],"mappings":"AA8OA;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,gBAAgB,CAC9B,GAAG,KAAqD;IAExD,MAAM,UAAU,GAAG,IAAI,GAAG,EAAsB,CAAC;IACjD,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,KAAK,MAAM,UAAU,IAAI,IAAI,IAAI,EAAE,EAAE,CAAC;YACpC,MAAM,GAAG,GAAG,IAAI,CAAC,SAAS,CAAC;gBACzB,UAAU,CAAC,QAAQ;gBACnB,UAAU,CAAC,IAAI;gBACf,UAAU,CAAC,OAAO;gBAClB,UAAU,CAAC,QAAQ,IAAI,IAAI;aAC5B,CAAC,CAAC;YACH,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,GAAG,CAAC;gBAAE,UAAU,CAAC,GAAG,CAAC,GAAG,EAAE,UAAU,CAAC,CAAC;QAC5D,CAAC;IACH,CAAC;IACD,OAAO,CAAC,GAAG,UAAU,CAAC,MAAM,EAAE,CAAC,CAAC;AAClC,CAAC","sourcesContent":["import type { EnvironmentInputs } from './environment.js';\nimport type { ObservationProfile } from './profile.js';\nimport type { Provenance } from './provenance.js';\nimport type { Wiring } from './wiring.js';\nimport type { Holding } from './holding.js';\n\n/**\n * The collector/core boundary.\n *\n * Collectors **extract**; `core` **normalizes**. Nothing in this file is a live\n * object — a `RawCapture` is plain data, fully serializable, produced by touching\n * a DOM and consumed by code that has never seen one.\n *\n * Two properties fall out of that split, and both are the reason for it:\n *\n * 1. **One ruleset, by construction.** Normalization is the moat (ADR-0003) and\n * the thing most likely to drift between two implementations. If the JSDOM and\n * Chromium collectors each normalized their own captures, \"the same rules ran\"\n * would be a claim maintained by discipline. Here it is a fact of the call\n * graph: there is one normalizer and both profiles enter it.\n * 2. **The sub-renderer can be anywhere.** A capture crossing a worker boundary,\n * a pipe, or a network hop to a device farm is the same value it was in\n * process. No code may assume the collector is local.\n */\nexport interface RawCapture {\n readonly captureVersion: 1;\n readonly subject: SubjectRef;\n readonly profile: ObservationProfile;\n\n /**\n * Environment inputs the *collector* is positioned to know — engine version,\n * fonts actually loaded, resolved conditions, asset hashes. The ruleset and\n * allowlist versions are `core`'s to supply, so they are absent here and the\n * key is completed during normalization.\n */\n readonly environment: Omit<EnvironmentInputs, 'ruleset' | 'allowlist'>;\n\n readonly root: RawNode;\n\n /**\n * Inherited values in force at the subject root.\n *\n * Mandatory, not an optimization. CSS applicability pruning (ADR-0003) drops\n * every rule that matches nothing inside the subtree — including rules on\n * ancestors *outside* it whose inheritable properties still reach in. Without\n * this seed the cheap tier is unsound and will report false `unchanged`.\n */\n readonly inheritedSeed: Readonly<Record<string, string>>;\n\n /**\n * Subtrees this subject renders through portals, in fiber traversal order.\n *\n * A subject's boundary is a component-tree question, not a DOM-containment\n * one: `createPortal` renders elsewhere in the document while remaining part\n * of the tree rooted here. Omitting these makes an opening modal report\n * `unchanged`, because its own container is byte-identical (ADR-0007).\n */\n readonly portals?: readonly RawNode[];\n\n /**\n * Shared root state this subject is *latently* coupled to.\n *\n * Rules like `html.dark .card` contribute nothing today and everything the\n * moment `dark` lands on `<html>`. In a session that reuses one document across\n * subjects, that moment may arrive because a *different* subject put it there —\n * turning this subject's baseline order-dependent with no visible cause.\n * Recorded so the coupling can be reported before it bites.\n */\n readonly couplings?: readonly string[];\n\n /** Anything the collector could not do. Empty is the expected case. */\n readonly diagnostics: readonly Diagnostic[];\n}\n\nexport interface SubjectRef {\n /** Stable across renames of the file, e.g. `story:components-button--primary`. */\n readonly id: string;\n\n /**\n * What kind of thing this is, which is not the same question as what produced\n * it.\n *\n * `value` is the one that is not a rendering: a JSON body, a schema, a route\n * table. It is a fourth member rather than a reuse of `route`, which already\n * means *a page rendered at a URL* — a route table is a value about the same\n * paths, and a subject list where one word meant both is one nobody can filter.\n */\n readonly kind: 'story' | 'route' | 'fixture' | 'value';\n readonly title?: string;\n}\n\n/**\n * A node exactly as observed, before any rule is applied.\n *\n * Raw means raw: `attributes` still holds generated ids and hashed class names,\n * `matchedRules` still holds cascade losers. Normalization needs the unedited\n * input — a collector that helpfully pre-cleaned would be a second, invisible\n * ruleset, versioned by nothing.\n */\nexport interface RawNode {\n readonly tag: string;\n readonly attributes: Readonly<Record<string, string>>;\n\n /** Resolved accessibility properties, when the profile provides them. */\n readonly aria?: RawAria;\n\n /**\n * Rules matching this node, as authored.\n *\n * Losing declarations are retained: `core` resolves the cascade for profiles\n * whose engine did not (ADR-0003 step 5), and cannot do that from winners\n * alone. Shorthands are retained *unexpanded* — expansion is `core`'s job, so\n * that it is versioned by the ruleset rather than by each collector.\n *\n * A collector MAY drop a declaration whose property is neither allowlisted nor\n * a shorthand of an allowlisted property, and SHOULD when the capture crosses\n * a network hop. It MUST NOT normalize values; a helpfully pre-cleaned capture\n * is a second, invisible ruleset versioned by nothing.\n */\n readonly matchedRules: readonly MatchedRule[];\n\n readonly inlineStyle?: Readonly<Record<string, string>>;\n\n /** Engine-resolved computed style. Present only when `profile.computedStyle`. */\n readonly computedStyle?: Readonly<Record<string, string>>;\n\n /** Present only when `profile.layout`. */\n readonly rect?: Rect;\n\n /** Literal text of a text node. Digested during normalization, per policy. */\n readonly text?: string;\n\n readonly provenance?: Provenance;\n\n /**\n * How the framework holds this node's component, when an adapter supplied it.\n *\n * Beside `provenance` because it arrives from the same seam and under the same\n * rule: the collector carries no framework dependency and both are injected.\n * See {@link Wiring} for why it is a dimension rather than another attribution\n * field.\n */\n readonly wiring?: Wiring;\n\n /**\n * What the component that authored this node was holding, when an adapter\n * supplied it.\n *\n * A third injection at the same seam, and deliberately not folded into\n * `wiring`: wiring is hashed into a band and this may never be, so a caller\n * that wants the band must be able to have it without also carrying values,\n * and a caller that wants explanations must be able to ask for them without\n * changing a single digest. See {@link Holding}.\n */\n readonly holding?: Holding;\n\n readonly children: readonly RawNode[];\n\n /** Nodes inside a shadow root, kept distinct from light-DOM children. */\n readonly shadowChildren?: readonly RawNode[];\n\n /**\n * Ignore rules the collector found this element under (spec 0024).\n *\n * A mark, not a removal. The collector is the only thing holding a live DOM, so\n * it is the only thing that can run a selector or read a marker attribute — but\n * it must not act on what it finds. An element deleted from the capture would\n * be invisible to every count downstream, and the difference between \"absorbed\n * by the `carousel` rule\" and \"was never there\" is the difference between an\n * ignore and a blind spot.\n *\n * Never hashed. Normalization turns these into\n * {@link import('./snapshot.js').SemanticSnapshot.ignoreSites}, which sits\n * beside `styleProvenance` outside the render hash for the same reason: masking\n * a clock changes what a run *says*, not what it *renders*, and must not\n * re-baseline the repository.\n */\n readonly ignoredBy?: readonly string[];\n}\n\nexport interface RawAria {\n readonly role: string | null;\n readonly name: string | null;\n\n /**\n * Accessible description — what `aria-describedby` and `title` resolve to.\n *\n * Captured because nothing else captures it, and its absence was a hole. The\n * attribute allowlist drops every `aria-*` attribute on the stated grounds\n * that they \"are resolved into role/name/state\" — true of `aria-label` and\n * `aria-selected`, and false of `aria-describedby`, which resolves into a\n * description and had nowhere to land. A field whose `aria-describedby`\n * pointed at a deleted error message compared **equal**: the reference was\n * dropped by the allowlist, the name was unaffected, and the regression was\n * silent on every tier including raster.\n */\n readonly description?: string | null;\n\n /** `checked`, `disabled`, `expanded`, `selected`, … */\n readonly state: Readonly<Record<string, string | boolean | number>>;\n}\n\nexport interface MatchedRule {\n /** Identifies the origin sheet for attribution: href, or a synthetic id. */\n readonly sheet: string;\n readonly selector: string;\n /** `[idCount, classCount, typeCount]` — CSS specificity, most significant first. */\n readonly specificity: readonly [number, number, number];\n /** Document order of the rule. Breaks specificity ties, per the cascade. */\n readonly order: number;\n readonly declarations: readonly Declaration[];\n /** Source position, when the sheet exposes one. Turns a diff into a file:line. */\n readonly source?: { readonly file: string; readonly line: number };\n}\n\nexport interface Declaration {\n readonly property: string;\n readonly value: string;\n readonly important: boolean;\n /** Custom properties referenced by the value, for token-keyed attribution. */\n readonly references?: readonly string[];\n}\n\n/** Browser-layout coordinates retained as evidence, without a preferred layout policy. */\nexport interface Rect {\n readonly x: number;\n readonly y: number;\n readonly width: number;\n readonly height: number;\n}\n\nexport interface Diagnostic {\n readonly severity: 'warn' | 'error';\n readonly code: string;\n readonly message: string;\n readonly nodePath?: string;\n}\n\n/**\n * One list from several, with a diagnostic said once however many times it was\n * found.\n *\n * A subject is described by a document, a snapshot and a comparison, and the\n * same condition is visible to more than one of them -- a missing font is in the\n * document that declared it and in the snapshot that measured it. A reader asks\n * \"what is wrong with this subject\" once, and a list that answers twice reads as\n * two findings.\n *\n * Identity is every field, `nodePath` included: the same code on two nodes is\n * two facts, and collapsing them would report one element where the page has a\n * dozen.\n */\nexport function mergeDiagnostics(\n ...lists: readonly (readonly Diagnostic[] | undefined)[]\n): readonly Diagnostic[] {\n const byIdentity = new Map<string, Diagnostic>();\n for (const list of lists) {\n for (const diagnostic of list ?? []) {\n const key = JSON.stringify([\n diagnostic.severity,\n diagnostic.code,\n diagnostic.message,\n diagnostic.nodePath ?? null,\n ]);\n if (!byIdentity.has(key)) byIdentity.set(key, diagnostic);\n }\n }\n return [...byIdentity.values()];\n}\n"]}
@@ -110,6 +110,15 @@ export interface RenderResource {
110
110
  /** Response bytes, base64. */
111
111
  readonly bytes: string;
112
112
  readonly digest: Digest;
113
+ /**
114
+ * The status a renderer answers this URL with. Absent means 200.
115
+ *
116
+ * A subject is allowed to reference something that is not there: a fallback
117
+ * test does it on purpose, and the browser it was acquired in painted the
118
+ * broken state rather than the image. Recording the failure as a failure is
119
+ * what lets a renderer reproduce that state without asking the network.
120
+ */
121
+ readonly status?: number;
113
122
  }
114
123
  export interface RenderFrame {
115
124
  /** Attributes on `<html>`. `class="dark"` here decides half a design system. */
@@ -213,15 +222,29 @@ export declare function identityDigest(identity: RenderIdentity): Digest;
213
222
  * `bytes` is base64 rather than a `Buffer` so the whole value survives the same
214
223
  * hops the document does. A raster that could only exist in Node would make the
215
224
  * remote route a special case in every function that touches one.
225
+ *
226
+ * The image is optional, and the three fields that describe it move together.
227
+ * A subject that occupies no pixels — a wrapper whose only child went to a
228
+ * portal, a `describeConformance` mount with no children — still has a
229
+ * document, rules, components and an accessibility tree, all of which compare.
230
+ * Refusing to photograph it is right; refusing the subject reported a quarter
231
+ * of Material UI's unit tier as unobserved over an axis nothing was asking
232
+ * about. Absent here means *this subject has no pixels to observe*, which is a
233
+ * measurement; it never means *the image was lost*.
216
234
  */
217
235
  export interface Raster {
218
236
  readonly documentDigest: Digest;
219
237
  readonly identity: RenderIdentity;
220
- /** Device pixels. `width / viewport.width` is the scale, and is asserted. */
221
- readonly width: number;
222
- readonly height: number;
223
- /** PNG, base64. */
224
- readonly bytes: string;
238
+ /**
239
+ * Device pixels. `width / viewport.width` is the scale, and is asserted.
240
+ *
241
+ * Absent exactly when {@link bytes} is, and {@link pictured} is the one place
242
+ * that reads the three together.
243
+ */
244
+ readonly width?: number;
245
+ readonly height?: number;
246
+ /** PNG, base64. Absent when the subject occupies no pixels. */
247
+ readonly bytes?: string;
225
248
  /** Fonts the document declared that the renderer did not have. */
226
249
  readonly missingFonts: readonly string[];
227
250
  /**
@@ -276,4 +299,41 @@ export interface Raster {
276
299
  */
277
300
  readonly findingMarks?: readonly string[];
278
301
  }
302
+ /**
303
+ * A raster whose image is in hand.
304
+ *
305
+ * Every pixel comparison in the codebase needs all three of `bytes`, `width`
306
+ * and `height`, and they are absent together or present together. Narrowing
307
+ * them one at a time is how a caller ends up reading the bytes of a subject
308
+ * that was never photographed, so the narrowing lives here and is done once.
309
+ */
310
+ export type Pictured = Raster & {
311
+ readonly width: number;
312
+ readonly height: number;
313
+ readonly bytes: string;
314
+ };
315
+ /** Whether this record carries an image, narrowing all three fields together. */
316
+ export declare function pictured(raster: Raster): raster is Pictured;
317
+ /**
318
+ * Whether the subject occupied any pixels, answered without the image.
319
+ *
320
+ * The question {@link pictured} cannot be asked of a sidecar. Bytes live in a
321
+ * separate file, row or object, so a record read without them carries the width
322
+ * and the height and nothing else — and those *are* the store's record of
323
+ * whether there was ever anything to photograph, which is why `sidecarFrom`
324
+ * refuses a sidecar carrying one of them.
325
+ *
326
+ * So: this for a record, {@link pictured} for an image in hand. A caller that
327
+ * reached for `pictured` here would read every sidecar in the store as a subject
328
+ * with no pixels, because none of them have bytes.
329
+ */
330
+ export declare function occupiesPixels(raster: Omit<Raster, 'bytes'>): boolean;
331
+ /**
332
+ * The image, or a refusal naming the subject that has none.
333
+ *
334
+ * For the paths where a missing image is a programming error rather than a
335
+ * measurement — a diff, a PNG encode — so they read as assertions instead of
336
+ * as a non-null assertion nobody can audit.
337
+ */
338
+ export declare function picture(raster: Raster, subject: string): Pictured;
279
339
  //# sourceMappingURL=document.d.ts.map
@@ -62,4 +62,38 @@ export function identityDigest(identity) {
62
62
  fonts: [...identity.fonts],
63
63
  });
64
64
  }
65
+ /** Whether this record carries an image, narrowing all three fields together. */
66
+ export function pictured(raster) {
67
+ return raster.bytes !== undefined && raster.width !== undefined && raster.height !== undefined;
68
+ }
69
+ /**
70
+ * Whether the subject occupied any pixels, answered without the image.
71
+ *
72
+ * The question {@link pictured} cannot be asked of a sidecar. Bytes live in a
73
+ * separate file, row or object, so a record read without them carries the width
74
+ * and the height and nothing else — and those *are* the store's record of
75
+ * whether there was ever anything to photograph, which is why `sidecarFrom`
76
+ * refuses a sidecar carrying one of them.
77
+ *
78
+ * So: this for a record, {@link pictured} for an image in hand. A caller that
79
+ * reached for `pictured` here would read every sidecar in the store as a subject
80
+ * with no pixels, because none of them have bytes.
81
+ */
82
+ export function occupiesPixels(raster) {
83
+ return raster.width !== undefined && raster.height !== undefined;
84
+ }
85
+ /**
86
+ * The image, or a refusal naming the subject that has none.
87
+ *
88
+ * For the paths where a missing image is a programming error rather than a
89
+ * measurement — a diff, a PNG encode — so they read as assertions instead of
90
+ * as a non-null assertion nobody can audit.
91
+ */
92
+ export function picture(raster, subject) {
93
+ if (!pictured(raster)) {
94
+ throw new Error(`\`${subject}\` occupies no pixels, and this path needs an image; ` +
95
+ 'a subject with no raster compares on its document alone');
96
+ }
97
+ return raster;
98
+ }
65
99
  //# sourceMappingURL=document.js.map