@variance-authority/core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +79 -0
- package/LICENSE +21 -0
- package/README.md +169 -0
- package/dist/artifact.d.ts +47 -0
- package/dist/artifact.js +2 -0
- package/dist/artifact.js.map +1 -0
- package/dist/attribute/because.d.ts +43 -0
- package/dist/attribute/because.js +64 -0
- package/dist/attribute/because.js.map +1 -0
- package/dist/attribute/boundary.d.ts +191 -0
- package/dist/attribute/boundary.js +328 -0
- package/dist/attribute/boundary.js.map +1 -0
- package/dist/attribute/call-site.d.ts +96 -0
- package/dist/attribute/call-site.js +183 -0
- package/dist/attribute/call-site.js.map +1 -0
- package/dist/attribute/component-hash.d.ts +188 -0
- package/dist/attribute/component-hash.js +312 -0
- package/dist/attribute/component-hash.js.map +1 -0
- package/dist/attribute/composition-fixture.d.ts +18 -0
- package/dist/attribute/composition-fixture.js +53 -0
- package/dist/attribute/composition-fixture.js.map +1 -0
- package/dist/attribute/composition.d.ts +182 -0
- package/dist/attribute/composition.js +169 -0
- package/dist/attribute/composition.js.map +1 -0
- package/dist/attribute/control.d.ts +43 -0
- package/dist/attribute/control.js +54 -0
- package/dist/attribute/control.js.map +1 -0
- package/dist/attribute/coverage.d.ts +99 -0
- package/dist/attribute/coverage.js +86 -0
- package/dist/attribute/coverage.js.map +1 -0
- package/dist/attribute/divergence.d.ts +102 -0
- package/dist/attribute/divergence.js +152 -0
- package/dist/attribute/divergence.js.map +1 -0
- package/dist/attribute/index.d.ts +43 -0
- package/dist/attribute/index.js +29 -0
- package/dist/attribute/index.js.map +1 -0
- package/dist/attribute/instability.d.ts +89 -0
- package/dist/attribute/instability.js +183 -0
- package/dist/attribute/instability.js.map +1 -0
- package/dist/attribute/instances.d.ts +145 -0
- package/dist/attribute/instances.js +140 -0
- package/dist/attribute/instances.js.map +1 -0
- package/dist/attribute/locate.d.ts +22 -0
- package/dist/attribute/locate.js +100 -0
- package/dist/attribute/locate.js.map +1 -0
- package/dist/attribute/mask.d.ts +156 -0
- package/dist/attribute/mask.js +201 -0
- package/dist/attribute/mask.js.map +1 -0
- package/dist/attribute/movement.d.ts +211 -0
- package/dist/attribute/movement.js +181 -0
- package/dist/attribute/movement.js.map +1 -0
- package/dist/attribute/region.d.ts +188 -0
- package/dist/attribute/region.js +144 -0
- package/dist/attribute/region.js.map +1 -0
- package/dist/attribute/source-map.d.ts +98 -0
- package/dist/attribute/source-map.js +264 -0
- package/dist/attribute/source-map.js.map +1 -0
- package/dist/attribute/source.d.ts +77 -0
- package/dist/attribute/source.js +95 -0
- package/dist/attribute/source.js.map +1 -0
- package/dist/attribute/stack.d.ts +80 -0
- package/dist/attribute/stack.js +108 -0
- package/dist/attribute/stack.js.map +1 -0
- package/dist/compare/band.d.ts +85 -0
- package/dist/compare/band.js +70 -0
- package/dist/compare/band.js.map +1 -0
- package/dist/compare/cascade.d.ts +45 -0
- package/dist/compare/cascade.js +71 -0
- package/dist/compare/cascade.js.map +1 -0
- package/dist/compare/derive.d.ts +78 -0
- package/dist/compare/derive.js +39 -0
- package/dist/compare/derive.js.map +1 -0
- package/dist/compare/diff/attribution.d.ts +23 -0
- package/dist/compare/diff/attribution.js +244 -0
- package/dist/compare/diff/attribution.js.map +1 -0
- package/dist/compare/diff/compare-nodes.d.ts +15 -0
- package/dist/compare/diff/compare-nodes.js +227 -0
- package/dist/compare/diff/compare-nodes.js.map +1 -0
- package/dist/compare/diff/components.d.ts +20 -0
- package/dist/compare/diff/components.js +76 -0
- package/dist/compare/diff/components.js.map +1 -0
- package/dist/compare/diff/delta.d.ts +165 -0
- package/dist/compare/diff/delta.js +12 -0
- package/dist/compare/diff/delta.js.map +1 -0
- package/dist/compare/diff/index.d.ts +69 -0
- package/dist/compare/diff/index.js +101 -0
- package/dist/compare/diff/index.js.map +1 -0
- package/dist/compare/diff/match.d.ts +37 -0
- package/dist/compare/diff/match.js +167 -0
- package/dist/compare/diff/match.js.map +1 -0
- package/dist/compare/explain.d.ts +23 -0
- package/dist/compare/explain.js +190 -0
- package/dist/compare/explain.js.map +1 -0
- package/dist/compare/holding-diff.d.ts +25 -0
- package/dist/compare/holding-diff.js +74 -0
- package/dist/compare/holding-diff.js.map +1 -0
- package/dist/compare/impact.d.ts +56 -0
- package/dist/compare/impact.js +124 -0
- package/dist/compare/impact.js.map +1 -0
- package/dist/compare/index.d.ts +30 -0
- package/dist/compare/index.js +22 -0
- package/dist/compare/index.js.map +1 -0
- package/dist/compare/instance.d.ts +30 -0
- package/dist/compare/instance.js +145 -0
- package/dist/compare/instance.js.map +1 -0
- package/dist/compare/observability.d.ts +49 -0
- package/dist/compare/observability.js +57 -0
- package/dist/compare/observability.js.map +1 -0
- package/dist/compare/parting.d.ts +179 -0
- package/dist/compare/parting.js +244 -0
- package/dist/compare/parting.js.map +1 -0
- package/dist/compare/slice.d.ts +74 -0
- package/dist/compare/slice.js +79 -0
- package/dist/compare/slice.js.map +1 -0
- package/dist/compare/value.d.ts +60 -0
- package/dist/compare/value.js +111 -0
- package/dist/compare/value.js.map +1 -0
- package/dist/format/accessibility.d.ts +27 -0
- package/dist/format/accessibility.js +17 -0
- package/dist/format/accessibility.js.map +1 -0
- package/dist/format/canonical.d.ts +38 -0
- package/dist/format/canonical.js +65 -0
- package/dist/format/canonical.js.map +1 -0
- package/dist/format/capture.d.ts +212 -0
- package/dist/format/capture.js +2 -0
- package/dist/format/capture.js.map +1 -0
- package/dist/format/document.d.ts +279 -0
- package/dist/format/document.js +65 -0
- package/dist/format/document.js.map +1 -0
- package/dist/format/environment.d.ts +120 -0
- package/dist/format/environment.js +53 -0
- package/dist/format/environment.js.map +1 -0
- package/dist/format/hash.d.ts +45 -0
- package/dist/format/hash.js +41 -0
- package/dist/format/hash.js.map +1 -0
- package/dist/format/holding.d.ts +134 -0
- package/dist/format/holding.js +2 -0
- package/dist/format/holding.js.map +1 -0
- package/dist/format/index.d.ts +39 -0
- package/dist/format/index.js +25 -0
- package/dist/format/index.js.map +1 -0
- package/dist/format/intervention.d.ts +87 -0
- package/dist/format/intervention.js +11 -0
- package/dist/format/intervention.js.map +1 -0
- package/dist/format/profile.d.ts +59 -0
- package/dist/format/profile.js +66 -0
- package/dist/format/profile.js.map +1 -0
- package/dist/format/provenance.d.ts +179 -0
- package/dist/format/provenance.js +222 -0
- package/dist/format/provenance.js.map +1 -0
- package/dist/format/sha256.d.ts +34 -0
- package/dist/format/sha256.js +152 -0
- package/dist/format/sha256.js.map +1 -0
- package/dist/format/snapshot.d.ts +263 -0
- package/dist/format/snapshot.js +2 -0
- package/dist/format/snapshot.js.map +1 -0
- package/dist/format/stabilize.d.ts +190 -0
- package/dist/format/stabilize.js +333 -0
- package/dist/format/stabilize.js.map +1 -0
- package/dist/format/tier.d.ts +19 -0
- package/dist/format/tier.js +11 -0
- package/dist/format/tier.js.map +1 -0
- package/dist/format/value.d.ts +115 -0
- package/dist/format/value.js +182 -0
- package/dist/format/value.js.map +1 -0
- package/dist/format/wiring.d.ts +115 -0
- package/dist/format/wiring.js +60 -0
- package/dist/format/wiring.js.map +1 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.js +35 -0
- package/dist/index.js.map +1 -0
- package/dist/judge/docket.d.ts +85 -0
- package/dist/judge/docket.js +162 -0
- package/dist/judge/docket.js.map +1 -0
- package/dist/judge/fingerprint.d.ts +53 -0
- package/dist/judge/fingerprint.js +144 -0
- package/dist/judge/fingerprint.js.map +1 -0
- package/dist/judge/ignore.d.ts +206 -0
- package/dist/judge/ignore.js +237 -0
- package/dist/judge/ignore.js.map +1 -0
- package/dist/judge/index.d.ts +44 -0
- package/dist/judge/index.js +34 -0
- package/dist/judge/index.js.map +1 -0
- package/dist/judge/inspect.d.ts +148 -0
- package/dist/judge/inspect.js +208 -0
- package/dist/judge/inspect.js.map +1 -0
- package/dist/judge/intent.d.ts +146 -0
- package/dist/judge/intent.js +150 -0
- package/dist/judge/intent.js.map +1 -0
- package/dist/judge/locale.d.ts +111 -0
- package/dist/judge/locale.js +247 -0
- package/dist/judge/locale.js.map +1 -0
- package/dist/judge/scope.d.ts +41 -0
- package/dist/judge/scope.js +47 -0
- package/dist/judge/scope.js.map +1 -0
- package/dist/judge/sensitivity.d.ts +202 -0
- package/dist/judge/sensitivity.js +152 -0
- package/dist/judge/sensitivity.js.map +1 -0
- package/dist/judge/trail.d.ts +122 -0
- package/dist/judge/trail.js +96 -0
- package/dist/judge/trail.js.map +1 -0
- package/dist/judge/verdict.d.ts +39 -0
- package/dist/judge/verdict.js +32 -0
- package/dist/judge/verdict.js.map +1 -0
- package/dist/plan/index.d.ts +12 -0
- package/dist/plan/index.js +11 -0
- package/dist/plan/index.js.map +1 -0
- package/dist/plan/tool.d.ts +85 -0
- package/dist/plan/tool.js +87 -0
- package/dist/plan/tool.js.map +1 -0
- package/dist/relate/graph.d.ts +190 -0
- package/dist/relate/graph.js +234 -0
- package/dist/relate/graph.js.map +1 -0
- package/dist/relate/index.d.ts +16 -0
- package/dist/relate/index.js +16 -0
- package/dist/relate/index.js.map +1 -0
- package/dist/relate/merkle.d.ts +110 -0
- package/dist/relate/merkle.js +246 -0
- package/dist/relate/merkle.js.map +1 -0
- package/dist/relate/reach.d.ts +65 -0
- package/dist/relate/reach.js +111 -0
- package/dist/relate/reach.js.map +1 -0
- package/dist/relate/records.d.ts +129 -0
- package/dist/relate/records.js +112 -0
- package/dist/relate/records.js.map +1 -0
- package/dist/rules/index.d.ts +19 -0
- package/dist/rules/index.js +17 -0
- package/dist/rules/index.js.map +1 -0
- package/dist/rules/normalize/alias.d.ts +50 -0
- package/dist/rules/normalize/alias.js +111 -0
- package/dist/rules/normalize/alias.js.map +1 -0
- package/dist/rules/normalize/cascade.d.ts +85 -0
- package/dist/rules/normalize/cascade.js +324 -0
- package/dist/rules/normalize/cascade.js.map +1 -0
- package/dist/rules/normalize/color.d.ts +30 -0
- package/dist/rules/normalize/color.js +185 -0
- package/dist/rules/normalize/color.js.map +1 -0
- package/dist/rules/normalize/fixture.d.ts +83 -0
- package/dist/rules/normalize/fixture.js +74 -0
- package/dist/rules/normalize/fixture.js.map +1 -0
- package/dist/rules/normalize/index.d.ts +39 -0
- package/dist/rules/normalize/index.js +268 -0
- package/dist/rules/normalize/index.js.map +1 -0
- package/dist/rules/normalize/project.d.ts +42 -0
- package/dist/rules/normalize/project.js +83 -0
- package/dist/rules/normalize/project.js.map +1 -0
- package/dist/rules/normalize/shorthand.d.ts +38 -0
- package/dist/rules/normalize/shorthand.js +388 -0
- package/dist/rules/normalize/shorthand.js.map +1 -0
- package/dist/rules/normalize/value.d.ts +23 -0
- package/dist/rules/normalize/value.js +162 -0
- package/dist/rules/normalize/value.js.map +1 -0
- package/dist/rules/normalize/wrapper.d.ts +16 -0
- package/dist/rules/normalize/wrapper.js +137 -0
- package/dist/rules/normalize/wrapper.js.map +1 -0
- package/dist/rules/ruleset.d.ts +93 -0
- package/dist/rules/ruleset.js +175 -0
- package/dist/rules/ruleset.js.map +1 -0
- package/mark.svg +30 -0
- package/package.json +69 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"movement.js","sourceRoot":"","sources":["../../src/attribute/movement.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Band } from '../compare/band.js';\nimport type { Composition, ComponentEntry, Site } from './composition.js';\nimport { becauseUpstream, unexplainedBecause } from './because.js';\nimport { heldSites } from './control.js';\n\n/**\n * Why a component moved — and what it means when nothing here can say.\n *\n * A run already knows *that* a component's own content differs from its\n * baseline: that is `causesBetween`, and it is where every report starts. What it\n * has never known is whether anybody edited the thing. Those are different\n * questions, and the gap between them is where flakes live:\n *\n * > Detect a pixel change. Find the HTML area behind it. Find no related change.\n * > That is a flake — or the beginning of one.\n *\n * The last step is the one that needs evidence, and the evidence has to be\n * assembled rather than assumed. Four kinds are available to a run that has\n * already done its work, in descending order of how much they explain:\n *\n * 1. **An edit.** A file declaring this component is in the change set. Nothing\n * further is interesting; somebody changed it and the run noticed.\n * 2. **A token.** A custom property this component's own nodes resolve through\n * took a new value in this run. The component's code is untouched and its\n * output legitimately moved.\n * 3. **An ancestor.** A component that reaches it was edited, and it is reached\n * by climbing — the caller that wrote the element, or, on a build where React\n * kept no owner, the enclosure graph of the subject it moved in until an\n * edited name appears. Its props are not recoverable in detail, but *this\n * moved on what it was given* is the sentence, and it names a real file.\n * 4. **A contradiction.** The same component, with the same props, rendered\n * somewhere else in this same run and rendered *differently*. That is not an\n * explanation — it is proof the explanation is not in the component's own\n * code, because one commit produced two answers from one input.\n *\n * And then **unexplained**, which is the finding. A component whose content\n * moved, whose file nobody touched, whose tokens held, whose ancestors held, and\n * which rendered identically everywhere else in the suite at this commit. That\n * last clause is the part no previous version of this could say, and it is what\n * the cross-subject join bought: the *stable states to refer to* are the other\n * sites of the same rendering, and the run already has them.\n *\n * ## What this refuses to conclude\n *\n * Unexplained is not a flake. This system's position on what establishes a flake\n * has not moved: a subject is unstable when it fails to read the same way twice,\n * and one reading can never prove instability\n * ([`flakiness.md`](../../../../docs/flakiness.md)). What an unexplained movement\n * is, exactly, is **the shortlist of subjects worth reading twice** — which is\n * the scarce resource a sweep spends, and until now it was spent on `changed`\n * subjects in whatever order the plan produced them.\n */\n\nexport type Cause = 'edited' | 'token' | 'upstream' | 'contradicted' | 'unexplained';\n\n/** One component the run found to have moved in one subject. */\nexport interface Moved {\n readonly subject: string;\n readonly component: string;\n /**\n * Which bands moved, when the run could tell.\n *\n * Empty when the comparison produced names only — a baseline written before\n * the bands were split carries no per-band digests, and an empty list here\n * means *not known*, never *no band*.\n */\n readonly bands: readonly Band[];\n}\n\n/** What a run can put beside a movement to explain it. */\nexport interface Evidence {\n /**\n * Files in the change set, from whichever flag read a diff. Absent when none did.\n *\n * Absent and empty are different answers and are kept different: a run with no\n * `--since` has not established that nobody edited anything, and reading its\n * silence as \"no edits\" would attribute every movement in the suite to nothing\n * and call the result a flake list.\n */\n readonly changed?: readonly string[];\n\n /** Component name → the files that declare it, from the source index. */\n readonly declaredIn?: ReadonlyMap<string, readonly string[]>;\n\n /** Tokens whose resolved value moved in this run. Absent when nothing asked. */\n readonly tokens?: readonly string[];\n\n /** Subjects that failed to read the same way twice in this run. */\n readonly unstable?: ReadonlySet<string>;\n\n /**\n * Every component whose hashes moved, by subject — the whole reading, not the\n * causes.\n *\n * The control group is read from this and not from the movements, because the\n * movements are a *filtered* set: they come from regions the pixels named as\n * causes, so a component whose hashes moved and whose box no region named is\n * absent from them. Filtering the control group by the movements alone hands\n * back exactly those renders as *held still*, which is the one claim that\n * makes a control group worse than no control group.\n *\n * **Absent is not empty**, twice over. An absent map is a run that never\n * compared hashes, and the control group falls back to the movements with a\n * weaker guarantee. A subject *missing from* a present map had no hashes read\n * there — nobody measured it, so it cannot be a control for anything.\n */\n readonly hashesMoved?: ReadonlyMap<string, ReadonlySet<string>>;\n}\n\nexport interface Movement {\n readonly subject: string;\n readonly component: string;\n readonly bands: readonly Band[];\n readonly cause: Cause;\n /** One sentence, naming the evidence rather than the category. */\n readonly because: string;\n\n readonly file?: string;\n readonly tokens?: readonly string[];\n readonly upstream?: string;\n\n /**\n * Components between `upstream` and this one, outermost first.\n *\n * Empty when the edited component draws it directly, and present only on the\n * `upstream` rung. A reviewer sent to `ProductCard` for a `CardFooter` that\n * moved has one question — *how does that reach this* — and the answer is two\n * names the run already holds.\n */\n readonly through?: readonly string[];\n\n /**\n * Other subjects in which this same component moved in this run.\n *\n * The \"one cause, N subjects\" fold. A reviewer reading eleven changed subjects\n * is often reading one edit, and the count is the difference between a\n * frightening report and an accurate one.\n */\n readonly alsoIn: readonly string[];\n\n /**\n * Sites of this component, with the same props, whose hashes this run read and\n * found unmoved.\n *\n * The control group, and the reason an unexplained movement is worth\n * reporting rather than shrugging at. Empty means there was no control — the\n * component appears nowhere else with these inputs, or it moved in every one\n * of them — which weakens the finding and is why it is a list rather than a\n * flag.\n */\n readonly held: readonly Site[];\n\n /**\n * Sites of this component with the same props, other than this one, that the\n * run read — the pool `held` was drawn from.\n *\n * The denominator, and without it `held: []` is two opposite findings under one\n * shape. Zero is *nothing to compare against*; four with an empty `held` is a\n * comparison that was made, in four renders, and came back the same way in all\n * of them — the statement that nothing rendered under those props escaped\n * whatever moved here.\n */\n readonly compared: number;\n}\n\nexport interface Attribution {\n /** Every movement supplied, in the order supplied, each with a cause. */\n readonly movements: readonly Movement[];\n\n /**\n * Unexplained movements in subjects that also failed to read the same way\n * twice. A movement nothing explains, in a subject already proven unstable.\n */\n readonly flakes: readonly Movement[];\n\n /**\n * Unexplained movements in subjects nothing has read twice.\n *\n * Not a verdict. The shortlist a sweep should spend its second readings on,\n * ordered by how much control the suite has over each — a component that\n * rendered identically in six other places and moved here is a better use of a\n * second reading than one that appears nowhere else.\n */\n readonly suspects: readonly Movement[];\n}\n\n/**\n * Attribute every movement, given whatever evidence the run assembled.\n *\n * Degrades honestly. With no change set the `edited` rung is unreachable and\n * every movement falls to a lower one, so a run that did not ask cannot produce\n * a confident `unexplained` — which is checked here rather than trusted to the\n * caller, because the shortlist this feeds is exactly the thing somebody will\n * act on.\n */\nexport function attributeMovement(\n moved: readonly Moved[],\n composition: Composition,\n evidence: Evidence = {},\n): Attribution {\n const byComponent = new Map(composition.components.map((entry) => [entry.component, entry]));\n const movedIn = new Map<string, string[]>();\n for (const each of moved) {\n const subjects = movedIn.get(each.component) ?? [];\n subjects.push(each.subject);\n movedIn.set(each.component, subjects);\n }\n\n const edited = new Set(\n [...(evidence.declaredIn ?? new Map<string, readonly string[]>())]\n .filter(([, files]) => files.some((file) => (evidence.changed ?? []).includes(file)))\n .map(([component]) => component),\n );\n\n const graph = graphOf(composition);\n\n const movements = moved.map((each) =>\n attributeOne(each, byComponent.get(each.component), {\n movedIn,\n edited,\n evidence,\n composition,\n graph,\n }),\n );\n\n const unexplained = movements.filter((movement) => movement.cause === 'unexplained');\n\n return {\n movements,\n flakes: unexplained.filter((movement) => evidence.unstable?.has(movement.subject) === true),\n suspects: unexplained\n .filter((movement) => evidence.unstable?.has(movement.subject) !== true)\n .sort((a, b) => b.held.length - a.held.length),\n };\n}\n\n/** Everything the run assembled, gathered so one movement can be asked about. */\nexport interface Bench {\n readonly movedIn: ReadonlyMap<string, readonly string[]>;\n readonly edited: ReadonlySet<string>;\n readonly evidence: Evidence;\n readonly composition: Composition;\n readonly graph: Graph;\n}\n\nfunction attributeOne(moved: Moved, entry: ComponentEntry | undefined, bench: Bench): Movement {\n const { movedIn, edited, evidence, composition, graph } = bench;\n const alsoIn = (movedIn.get(moved.component) ?? []).filter(\n (subject) => subject !== moved.subject,\n );\n const { held, considered } = heldSites(moved, entry, bench);\n const base = { ...moved, alsoIn, held, compared: considered };\n\n const file = (evidence.declaredIn?.get(moved.component) ?? []).find((each) =>\n (evidence.changed ?? []).includes(each),\n );\n if (file !== undefined) {\n return { ...base, cause: 'edited', file, because: `\\`${file}\\` is in the change set` };\n }\n\n const tokens = movedTokens(moved.component, composition, evidence);\n if (tokens.length > 0) {\n return {\n ...base,\n cause: 'token',\n tokens,\n because: `it resolves through ${tokens.map((token) => `\\`${token}\\``).join(', ')}, which moved in this run`,\n };\n }\n\n const ancestor = editedAncestor(moved, edited, graph);\n if (ancestor !== undefined) {\n return {\n ...base,\n cause: 'upstream',\n upstream: ancestor.name,\n ...(ancestor.through.length === 0 ? {} : { through: ancestor.through }),\n because: becauseUpstream(ancestor),\n };\n }\n\n const contradiction = composition.divergences.find(\n (divergence) =>\n divergence.component === moved.component &&\n divergence.renderings.some((rendering) =>\n rendering.sites.some((site) => site.subject === moved.subject),\n ),\n );\n if (contradiction !== undefined) {\n return {\n ...base,\n cause: 'contradicted',\n because:\n `at this commit it renders ${contradiction.renderings.length} different ways from the same props ` +\n `(${contradiction.bands.join(', ')}), so the change is not in its own code`,\n };\n }\n\n return { ...base, cause: 'unexplained', because: unexplainedBecause(base, considered, evidence) };\n}\n\n/**\n * Who encloses what and who wrote what, in each subject separately.\n *\n * Keyed on the pair because the constraint is the whole point. `ComponentEntry.within`\n * and `ComponentEntry.createdBy` are both folded over the suite, so `Card` is enclosed by\n * `CartCard` and by `ProductCard` at once, and a walk that reads either hands a\n * reviewer the cart as the reason a product story moved. A site knows which\n * subject it was read in, so the graph can be built the way the question is\n * asked — and it has to be built that way for *both* edges, because the creator\n * is consulted first and an unconstrained answer there is never reached by the\n * constrained walk underneath it.\n */\ntype Enclosures = ReadonlyMap<string, readonly string[]>;\n\ninterface Graph {\n /** Who this component sits inside, per subject. */\n readonly within: Enclosures;\n /** Whose JSX wrote this component's element, per subject. */\n readonly wrote: Enclosures;\n}\n\nconst IN = '\\u0000';\n\nfunction graphOf(composition: Composition): Graph {\n const within = new Map<string, string[]>();\n const wrote = new Map<string, string[]>();\n\n const add = (graph: Map<string, string[]>, key: string, name: string): void => {\n const names = graph.get(key);\n if (names === undefined) graph.set(key, [name]);\n else if (!names.includes(name)) names.push(name);\n };\n\n for (const entry of composition.components) {\n for (const group of entry.classes) {\n for (const rendering of group.renderings) {\n for (const site of rendering.sites) {\n const key = `${site.subject}${IN}${entry.component}`;\n if (site.within !== undefined) add(within, key, site.within);\n if (site.createdBy !== undefined) add(wrote, key, site.createdBy);\n }\n }\n }\n }\n\n for (const names of within.values()) names.sort();\n for (const names of wrote.values()) names.sort();\n return { within, wrote };\n}\n\n/** An edited component above a moved one, and the components in between. */\nexport interface Ancestor {\n readonly name: string;\n /** Outermost first, and empty when the edited component draws it directly. */\n readonly through: readonly string[];\n}\n\n/**\n * The nearest edited component above this one, in the subject it moved in.\n *\n * The creator first, and it is not a tie-break. The component that *wrote the\n * element* is the one whose edit changed this component's inputs; the one it\n * happens to sit inside may be a presentational wrapper that knows nothing about\n * it. On `examples/todomvc` every `Chip` sits within a `Stack` and is created by\n * `TodoFooter`, so a run consulting only enclosure would fail to connect an edit\n * to `TodoFooter` with the chips it moved — and report five unexplained movements\n * instead of one caller. It is absent on a production build, which is why the\n * enclosure walk is not a fallback but the other half.\n *\n * Read off the per-subject graph and not off the entry. The entry's `createdBy`\n * is the suite's whole set, so an edit to whoever writes this component on one\n * page answers for every page it appears on — the same unconstrained answer the\n * walk below refuses, arriving one line earlier and winning.\n *\n * And that walk climbs rather than looking once. The rung has always said\n * *ancestor* and checked a parent, and a React tree is mostly components that\n * draw one wrapper each: `ProductCard` was edited, `ProductCard` draws `Card`,\n * `Card` draws `CardFooter`, and one look up arrives at `Card`, whose file nobody\n * touched. That is the shape of every unexplained movement this ladder used to\n * produce over an edit it was holding the graph for.\n */\nfunction editedAncestor(\n moved: Moved,\n edited: ReadonlySet<string>,\n graph: Graph,\n): Ancestor | undefined {\n const here = `${moved.subject}${IN}${moved.component}`;\n const wrote = (graph.wrote.get(here) ?? []).find((name) => edited.has(name));\n if (wrote !== undefined) return { name: wrote, through: [] };\n\n // Breadth-first, so the answer is the *nearest* edit and not whichever one the\n // recursion reached first. `seen` is written as a rung is built: an enclosure\n // graph folded over a suite can close a loop, and a component reached twice on\n // one rung would otherwise queue twice.\n const seen = new Set([moved.component]);\n const step = (from: Ancestor | null): Ancestor[] => {\n const child = from === null ? moved.component : from.name;\n const rung: Ancestor[] = [];\n for (const holder of graph.within.get(`${moved.subject}${IN}${child}`) ?? []) {\n if (seen.has(holder)) continue;\n seen.add(holder);\n rung.push({ name: holder, through: from === null ? [] : [from.name, ...from.through] });\n }\n return rung;\n };\n\n let rung = step(null);\n while (rung.length > 0) {\n const arrived = rung.find((each) => edited.has(each.name));\n if (arrived !== undefined) return arrived;\n rung = rung.flatMap(step);\n }\n\n return undefined;\n}\n\n/**\n * Tokens this component resolves through that moved in this run.\n *\n * Read off the instances rather than off the subject, which is the distinction\n * that makes the answer worth anything: every subject on a themed page resolves\n * through every token in the theme, so a subject-level intersection names them\n * all and explains nothing.\n */\nfunction movedTokens(\n component: string,\n composition: Composition,\n evidence: Evidence,\n): readonly string[] {\n if (evidence.tokens === undefined || evidence.tokens.length === 0) return [];\n\n const entry = composition.components.find((each) => each.component === component);\n if (entry === undefined) return [];\n\n const moved = new Set(evidence.tokens);\n return [...entry.tokens].filter((token) => moved.has(token)).sort();\n}\n"]}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import type { DiffRegion } from './mask.js';
|
|
2
|
+
import type { NodePath, SemanticSnapshot } from '../format/snapshot.js';
|
|
3
|
+
import type { SourceLocation } from '../format/provenance.js';
|
|
4
|
+
/**
|
|
5
|
+
* Isolating a pixel difference, and connecting it to the tree.
|
|
6
|
+
*
|
|
7
|
+
* A pixel differ answers one question — *how many pixels moved* — and that number
|
|
8
|
+
* is the reason "looks right, merge" exists. 5482 is not a finding. It cannot be
|
|
9
|
+
* read, it cannot be assigned, and the only available response to it is to open
|
|
10
|
+
* the image and look, which is the expensive act the tool was supposed to replace.
|
|
11
|
+
*
|
|
12
|
+
* Two phases turn it into something else, and they are separate because they fail
|
|
13
|
+
* differently. `mask.ts` is arithmetic on a bitmask — no DOM, no snapshot, no
|
|
14
|
+
* notion of what a component is — and answers *where on the canvas*.
|
|
15
|
+
* {@link attributeRegions} joins those coordinates to the box tree and answers
|
|
16
|
+
* *what is there*. The first can be tested with a hand-written mask; the second
|
|
17
|
+
* with a hand-written snapshot; neither needs a browser, which is what makes the
|
|
18
|
+
* pipeline testable at all.
|
|
19
|
+
*
|
|
20
|
+
* The join is geometric and therefore the weakest link in the chain, so it is
|
|
21
|
+
* explicit about failing: a region no box contains is reported `unattributed`.
|
|
22
|
+
* Painting escapes its box routinely — shadows, outlines, overflowing glyphs — and
|
|
23
|
+
* a system that resolved those by picking the nearest node would produce confident
|
|
24
|
+
* attributions of exactly the kind an agent then acts on.
|
|
25
|
+
*/
|
|
26
|
+
export interface AttributedRegion {
|
|
27
|
+
readonly region: DiffRegion;
|
|
28
|
+
/**
|
|
29
|
+
* Tightest node whose border box contains the region, when one does.
|
|
30
|
+
*
|
|
31
|
+
* Tightest rather than topmost: a change inside a button is also inside the
|
|
32
|
+
* card and inside `<main>`, and only the innermost answer is actionable.
|
|
33
|
+
*/
|
|
34
|
+
readonly path?: NodePath;
|
|
35
|
+
readonly component?: string;
|
|
36
|
+
/** Landmark phrase from {@link locate}, so a region has a name, not an address. */
|
|
37
|
+
readonly where?: string;
|
|
38
|
+
/**
|
|
39
|
+
* The line of JSX that produced this node, when the build recorded one.
|
|
40
|
+
*
|
|
41
|
+
* The last hop, and a different answer from the one a component name gets.
|
|
42
|
+
* Resolving `Button` against a source index names where `Button` is
|
|
43
|
+
* *declared*; this names where the element that changed is *written*, which
|
|
44
|
+
* for anything rendered more than once is the only one of the two that
|
|
45
|
+
* distinguishes the instances. Present only under
|
|
46
|
+
* `@variance-authority/jsx-source` or React ≤18 — the name scan is what a
|
|
47
|
+
* repository that has configured nothing still gets.
|
|
48
|
+
*/
|
|
49
|
+
readonly source?: SourceLocation;
|
|
50
|
+
/**
|
|
51
|
+
* `true` when no box contains the region at all.
|
|
52
|
+
*
|
|
53
|
+
* Rarer than it first looks, and that is worth being precise about. A screenshot
|
|
54
|
+
* clipped to the subject means the root's own box contains every pixel in it, so
|
|
55
|
+
* a region always lands *somewhere* — paint that escapes a button still falls
|
|
56
|
+
* inside the card, and attributing it to the card is correct rather than a
|
|
57
|
+
* consolation prize.
|
|
58
|
+
*
|
|
59
|
+
* What this flag actually catches is a region that fell outside the tree
|
|
60
|
+
* entirely, and the overwhelmingly likely cause is that `scale` or `origin` was
|
|
61
|
+
* wrong. That failure is worth a loud signal precisely because it does not look
|
|
62
|
+
* like one: at the wrong scale every region lands in the top-left quadrant and
|
|
63
|
+
* the report comes out complete, plausible, and about the wrong components.
|
|
64
|
+
*/
|
|
65
|
+
readonly unattributed: boolean;
|
|
66
|
+
/**
|
|
67
|
+
* Tightest node the region *overlaps*, present only when unattributed.
|
|
68
|
+
*
|
|
69
|
+
* Offered as orientation, never as attribution — hence the separate field. It
|
|
70
|
+
* answers "what is this near", which is a different claim from "this is what
|
|
71
|
+
* changed", and keeping them in one field is how the second gets asserted with
|
|
72
|
+
* the confidence of the first.
|
|
73
|
+
*/
|
|
74
|
+
readonly nearest?: {
|
|
75
|
+
readonly path: NodePath;
|
|
76
|
+
readonly component?: string;
|
|
77
|
+
readonly where?: string;
|
|
78
|
+
};
|
|
79
|
+
/**
|
|
80
|
+
* Nearest enclosing component, when it is not the one that authored the node.
|
|
81
|
+
*
|
|
82
|
+
* Two namespaces meet here and neither is wrong. `component` is `createdBy` —
|
|
83
|
+
* who wrote the JSX (ADR-0007) — and a component *hash* is named for the
|
|
84
|
+
* enclosure (ADR-0018). They diverge exactly where an element is passed as a
|
|
85
|
+
* prop: `<Card title={<h3>Invoice</h3>} />` gives the `<h3>` `createdBy: Page`
|
|
86
|
+
* and `owners[0]: Card`.
|
|
87
|
+
*
|
|
88
|
+
* Carried so that ranking can match a cause list without guessing which of the
|
|
89
|
+
* two it is written in. Matching one namespace against the other does not
|
|
90
|
+
* fail loudly — it silently finds nothing, and the ordering falls back to area,
|
|
91
|
+
* which is the thing the cause list exists to prevent.
|
|
92
|
+
*/
|
|
93
|
+
readonly owner?: string;
|
|
94
|
+
/**
|
|
95
|
+
* The shape of this difference, with its position and values removed.
|
|
96
|
+
*
|
|
97
|
+
* Filled in by whoever held the mask — `attributeRegions` never sets it,
|
|
98
|
+
* because a fingerprint is computed from pixels and this function is given
|
|
99
|
+
* boxes. It exists on this type rather than beside it so that the digest
|
|
100
|
+
* travels with the region a reader is looking at: writing an ignore means
|
|
101
|
+
* copying the fingerprint of the thing that annoyed you, and a digest printed
|
|
102
|
+
* in a different section is a digest nobody matches up.
|
|
103
|
+
*
|
|
104
|
+
* See `fingerprintOfMask` in `core/judge`.
|
|
105
|
+
*/
|
|
106
|
+
readonly fingerprint?: string;
|
|
107
|
+
}
|
|
108
|
+
export interface AttributionOptions {
|
|
109
|
+
/**
|
|
110
|
+
* Device pixels per CSS pixel in the raster.
|
|
111
|
+
*
|
|
112
|
+
* Required, with no default. A 2x screenshot silently attributed at 1x lands
|
|
113
|
+
* every region in the top-left quadrant of the page and attributes each of them
|
|
114
|
+
* to the wrong node — a failure that produces a full, plausible, entirely wrong
|
|
115
|
+
* report. Making the caller state it turns that into a decision someone made.
|
|
116
|
+
*/
|
|
117
|
+
readonly scale: number;
|
|
118
|
+
/**
|
|
119
|
+
* Page-space CSS origin of the raster's top-left corner.
|
|
120
|
+
*
|
|
121
|
+
* Defaults to the subject root's own rect, which is right when the screenshot
|
|
122
|
+
* was clipped to the subject — the normal case. A full-page shot passes `{x: 0,
|
|
123
|
+
* y: 0}`.
|
|
124
|
+
*/
|
|
125
|
+
readonly origin?: {
|
|
126
|
+
readonly x: number;
|
|
127
|
+
readonly y: number;
|
|
128
|
+
};
|
|
129
|
+
/**
|
|
130
|
+
* Fraction of a region's area that must fall inside a box to count as inside.
|
|
131
|
+
*
|
|
132
|
+
* Below 1 by default because a region's bounding box is inflated by
|
|
133
|
+
* antialiasing at its edges, so exact containment would reject the correct node
|
|
134
|
+
* on a text change. This is the one genuinely fuzzy number in the join and it
|
|
135
|
+
* is named rather than buried.
|
|
136
|
+
*/
|
|
137
|
+
readonly containment?: number;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Join isolated regions to the nodes that occupy them.
|
|
141
|
+
*
|
|
142
|
+
* Requires a snapshot carrying `rect`, i.e. a profile with layout (ADR-0002).
|
|
143
|
+
* Under a profile without one every region comes back unattributed, which is the
|
|
144
|
+
* correct answer — a rect that was never observed must never be inferred.
|
|
145
|
+
*/
|
|
146
|
+
export declare function attributeRegions(regions: readonly DiffRegion[], snapshot: SemanticSnapshot, options: AttributionOptions): readonly AttributedRegion[];
|
|
147
|
+
export interface RankedRegion extends AttributedRegion {
|
|
148
|
+
/**
|
|
149
|
+
* `true` when the semantic tier named this region's component as a *root* of
|
|
150
|
+
* the change, rather than as something the change happened to move.
|
|
151
|
+
*/
|
|
152
|
+
readonly cause: boolean;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Rank attributed regions by cause, falling back to area.
|
|
156
|
+
*
|
|
157
|
+
* This function exists because of a measurement, and the measurement is the most
|
|
158
|
+
* useful thing the raster tier has produced. Replacing a checkbox with a styled
|
|
159
|
+
* div on the todomvc corpus changes 1530 pixels in 5 regions, which attribute
|
|
160
|
+
* geometrically and rank by area as:
|
|
161
|
+
*
|
|
162
|
+
* ```
|
|
163
|
+
* 933px Text <- changed, and reflowed
|
|
164
|
+
* 511px Stack <- only reflowed
|
|
165
|
+
* 86px Toggle <- the edit
|
|
166
|
+
* ```
|
|
167
|
+
*
|
|
168
|
+
* Every one of those attributions is correct. The pixels really are inside those
|
|
169
|
+
* nodes. The *ordering* is still wrong, because **area measures displacement,
|
|
170
|
+
* not cause** — an edit that reflows its surroundings moves far more of them than
|
|
171
|
+
* of itself, so `Stack`, which nothing edited, outranks `Toggle`, which is the
|
|
172
|
+
* edit. That is the same defect the differ had when a list reorder blamed the
|
|
173
|
+
* element that moved rather than the code that moved it.
|
|
174
|
+
*
|
|
175
|
+
* Geometry cannot fix this; it has no access to why. The semantic tier does — it
|
|
176
|
+
* has provenance and props digests — so the raster tier stops claiming to rank
|
|
177
|
+
* and takes the ordering from the tier that can. On the case above that promotes
|
|
178
|
+
* `Toggle` above `Stack` while leaving both attributions untouched.
|
|
179
|
+
*
|
|
180
|
+
* Two limits, stated rather than smoothed over. The semantic tier may name more
|
|
181
|
+
* than one cause and here it names two, `Toggle` and `Text`, both real — this
|
|
182
|
+
* does not collapse them, because picking one would be inventing a fact. And
|
|
183
|
+
* with no causes supplied the order falls back to area, which is honest and is
|
|
184
|
+
* not good: pixels alone rank the displaced above the displacer, and nothing
|
|
185
|
+
* inside this file can change that.
|
|
186
|
+
*/
|
|
187
|
+
export declare function rankRegions(regions: readonly AttributedRegion[], causes?: readonly string[]): readonly RankedRegion[];
|
|
188
|
+
//# sourceMappingURL=region.d.ts.map
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { locate } from './locate.js';
|
|
2
|
+
const DEFAULT_CONTAINMENT = 0.9;
|
|
3
|
+
/**
|
|
4
|
+
* Join isolated regions to the nodes that occupy them.
|
|
5
|
+
*
|
|
6
|
+
* Requires a snapshot carrying `rect`, i.e. a profile with layout (ADR-0002).
|
|
7
|
+
* Under a profile without one every region comes back unattributed, which is the
|
|
8
|
+
* correct answer — a rect that was never observed must never be inferred.
|
|
9
|
+
*/
|
|
10
|
+
export function attributeRegions(regions, snapshot, options) {
|
|
11
|
+
const boxed = collectBoxes(snapshot.root);
|
|
12
|
+
const origin = options.origin ?? snapshot.root.rect ?? { x: 0, y: 0 };
|
|
13
|
+
const containment = options.containment ?? DEFAULT_CONTAINMENT;
|
|
14
|
+
return regions.map((region) => {
|
|
15
|
+
const css = {
|
|
16
|
+
x: region.x / options.scale + origin.x,
|
|
17
|
+
y: region.y / options.scale + origin.y,
|
|
18
|
+
width: region.width / options.scale,
|
|
19
|
+
height: region.height / options.scale,
|
|
20
|
+
};
|
|
21
|
+
const area = css.width * css.height;
|
|
22
|
+
let inside;
|
|
23
|
+
let overlapping;
|
|
24
|
+
for (const node of boxed) {
|
|
25
|
+
const rect = node.rect;
|
|
26
|
+
const shared = intersectionArea(css, rect);
|
|
27
|
+
if (shared <= 0)
|
|
28
|
+
continue;
|
|
29
|
+
const nodeArea = rect.width * rect.height;
|
|
30
|
+
// `<=`, not `<`, and the difference is one component name in every report
|
|
31
|
+
// over a design system. A wrapper that shrink-wraps its only child carries
|
|
32
|
+
// a *byte-identical* rect — measured on `cases/storybook-case`, where
|
|
33
|
+
// `Tokens` and `Button` are both `454.34,359 115.33×50` — so neither is
|
|
34
|
+
// tighter than the other and the walk decides. `collectBoxes` is pre-order,
|
|
35
|
+
// so a strict `<` keeps whichever came first, which is always the outer
|
|
36
|
+
// one: the run then names the wrapper nothing edited and sends a reviewer
|
|
37
|
+
// to its file. Ties go to the last box seen, which is the innermost, and is
|
|
38
|
+
// also the one the browser painted on top.
|
|
39
|
+
if (area > 0 && shared / area >= containment) {
|
|
40
|
+
if (inside === undefined || nodeArea <= inside.rect.width * inside.rect.height)
|
|
41
|
+
inside = node;
|
|
42
|
+
}
|
|
43
|
+
// The same rule for `nearest`. It is orientation rather than attribution,
|
|
44
|
+
// and a hint should point at the same node the join would have picked.
|
|
45
|
+
if (overlapping === undefined || nodeArea <= overlapping.rect.width * overlapping.rect.height) {
|
|
46
|
+
overlapping = node;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (inside !== undefined) {
|
|
50
|
+
return {
|
|
51
|
+
region,
|
|
52
|
+
path: inside.path,
|
|
53
|
+
...describe(snapshot, inside),
|
|
54
|
+
unattributed: false,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
region,
|
|
59
|
+
unattributed: true,
|
|
60
|
+
...(overlapping !== undefined
|
|
61
|
+
? { nearest: { path: overlapping.path, ...describe(snapshot, overlapping) } }
|
|
62
|
+
: {}),
|
|
63
|
+
};
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Rank attributed regions by cause, falling back to area.
|
|
68
|
+
*
|
|
69
|
+
* This function exists because of a measurement, and the measurement is the most
|
|
70
|
+
* useful thing the raster tier has produced. Replacing a checkbox with a styled
|
|
71
|
+
* div on the todomvc corpus changes 1530 pixels in 5 regions, which attribute
|
|
72
|
+
* geometrically and rank by area as:
|
|
73
|
+
*
|
|
74
|
+
* ```
|
|
75
|
+
* 933px Text <- changed, and reflowed
|
|
76
|
+
* 511px Stack <- only reflowed
|
|
77
|
+
* 86px Toggle <- the edit
|
|
78
|
+
* ```
|
|
79
|
+
*
|
|
80
|
+
* Every one of those attributions is correct. The pixels really are inside those
|
|
81
|
+
* nodes. The *ordering* is still wrong, because **area measures displacement,
|
|
82
|
+
* not cause** — an edit that reflows its surroundings moves far more of them than
|
|
83
|
+
* of itself, so `Stack`, which nothing edited, outranks `Toggle`, which is the
|
|
84
|
+
* edit. That is the same defect the differ had when a list reorder blamed the
|
|
85
|
+
* element that moved rather than the code that moved it.
|
|
86
|
+
*
|
|
87
|
+
* Geometry cannot fix this; it has no access to why. The semantic tier does — it
|
|
88
|
+
* has provenance and props digests — so the raster tier stops claiming to rank
|
|
89
|
+
* and takes the ordering from the tier that can. On the case above that promotes
|
|
90
|
+
* `Toggle` above `Stack` while leaving both attributions untouched.
|
|
91
|
+
*
|
|
92
|
+
* Two limits, stated rather than smoothed over. The semantic tier may name more
|
|
93
|
+
* than one cause and here it names two, `Toggle` and `Text`, both real — this
|
|
94
|
+
* does not collapse them, because picking one would be inventing a fact. And
|
|
95
|
+
* with no causes supplied the order falls back to area, which is honest and is
|
|
96
|
+
* not good: pixels alone rank the displaced above the displacer, and nothing
|
|
97
|
+
* inside this file can change that.
|
|
98
|
+
*/
|
|
99
|
+
export function rankRegions(regions, causes = []) {
|
|
100
|
+
const named = new Set(causes);
|
|
101
|
+
return regions
|
|
102
|
+
.map((region) => ({
|
|
103
|
+
...region,
|
|
104
|
+
// Either namespace. A cause list is named for enclosures and a region is
|
|
105
|
+
// named for its author, and where those differ a one-sided test finds
|
|
106
|
+
// nothing and silently reverts the ordering to area.
|
|
107
|
+
cause: (region.component !== undefined && named.has(region.component)) ||
|
|
108
|
+
(region.owner !== undefined && named.has(region.owner)),
|
|
109
|
+
}))
|
|
110
|
+
.sort((a, b) => Number(b.cause) - Number(a.cause) || b.region.pixels - a.region.pixels);
|
|
111
|
+
}
|
|
112
|
+
function describe(snapshot, node) {
|
|
113
|
+
// `createdBy` before `owners[0]`: the component whose JSX produced this element
|
|
114
|
+
// owns its appearance, while the nearest enclosing component merely contains it.
|
|
115
|
+
const owner = node.provenance?.owners[0]?.name;
|
|
116
|
+
const component = node.provenance?.createdBy ?? owner;
|
|
117
|
+
const where = locate(snapshot.root, node.path).where;
|
|
118
|
+
return {
|
|
119
|
+
...(component !== undefined ? { component } : {}),
|
|
120
|
+
// Only when it says something `component` does not. An `owner` that repeats
|
|
121
|
+
// the author is noise in every report that prints it.
|
|
122
|
+
...(owner !== undefined && owner !== component ? { owner } : {}),
|
|
123
|
+
...(where !== '' ? { where } : {}),
|
|
124
|
+
...(node.provenance?.source ? { source: node.provenance.source } : {}),
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
/** Every node carrying a rect, in document order. Zero-area boxes are skipped. */
|
|
128
|
+
function collectBoxes(root) {
|
|
129
|
+
const out = [];
|
|
130
|
+
const walk = (node) => {
|
|
131
|
+
if (node.rect !== undefined && node.rect.width > 0 && node.rect.height > 0)
|
|
132
|
+
out.push(node);
|
|
133
|
+
for (const child of node.children)
|
|
134
|
+
walk(child);
|
|
135
|
+
};
|
|
136
|
+
walk(root);
|
|
137
|
+
return out;
|
|
138
|
+
}
|
|
139
|
+
function intersectionArea(a, b) {
|
|
140
|
+
const width = Math.min(a.x + a.width, b.x + b.width) - Math.max(a.x, b.x);
|
|
141
|
+
const height = Math.min(a.y + a.height, b.y + b.height) - Math.max(a.y, b.y);
|
|
142
|
+
return width > 0 && height > 0 ? width * height : 0;
|
|
143
|
+
}
|
|
144
|
+
//# sourceMappingURL=region.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"region.js","sourceRoot":"","sources":["../../src/attribute/region.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Rect } from '../format/capture.js';\nimport { locate } from './locate.js';\nimport type { DiffRegion } from './mask.js';\nimport type { NodePath, SemanticNode, SemanticSnapshot } from '../format/snapshot.js';\nimport type { SourceLocation } from '../format/provenance.js';\n\n/**\n * Isolating a pixel difference, and connecting it to the tree.\n *\n * A pixel differ answers one question — *how many pixels moved* — and that number\n * is the reason \"looks right, merge\" exists. 5482 is not a finding. It cannot be\n * read, it cannot be assigned, and the only available response to it is to open\n * the image and look, which is the expensive act the tool was supposed to replace.\n *\n * Two phases turn it into something else, and they are separate because they fail\n * differently. `mask.ts` is arithmetic on a bitmask — no DOM, no snapshot, no\n * notion of what a component is — and answers *where on the canvas*.\n * {@link attributeRegions} joins those coordinates to the box tree and answers\n * *what is there*. The first can be tested with a hand-written mask; the second\n * with a hand-written snapshot; neither needs a browser, which is what makes the\n * pipeline testable at all.\n *\n * The join is geometric and therefore the weakest link in the chain, so it is\n * explicit about failing: a region no box contains is reported `unattributed`.\n * Painting escapes its box routinely — shadows, outlines, overflowing glyphs — and\n * a system that resolved those by picking the nearest node would produce confident\n * attributions of exactly the kind an agent then acts on.\n */\n\nexport interface AttributedRegion {\n readonly region: DiffRegion;\n\n /**\n * Tightest node whose border box contains the region, when one does.\n *\n * Tightest rather than topmost: a change inside a button is also inside the\n * card and inside `<main>`, and only the innermost answer is actionable.\n */\n readonly path?: NodePath;\n readonly component?: string;\n /** Landmark phrase from {@link locate}, so a region has a name, not an address. */\n readonly where?: string;\n\n /**\n * The line of JSX that produced this node, when the build recorded one.\n *\n * The last hop, and a different answer from the one a component name gets.\n * Resolving `Button` against a source index names where `Button` is\n * *declared*; this names where the element that changed is *written*, which\n * for anything rendered more than once is the only one of the two that\n * distinguishes the instances. Present only under\n * `@variance-authority/jsx-source` or React ≤18 — the name scan is what a\n * repository that has configured nothing still gets.\n */\n readonly source?: SourceLocation;\n\n /**\n * `true` when no box contains the region at all.\n *\n * Rarer than it first looks, and that is worth being precise about. A screenshot\n * clipped to the subject means the root's own box contains every pixel in it, so\n * a region always lands *somewhere* — paint that escapes a button still falls\n * inside the card, and attributing it to the card is correct rather than a\n * consolation prize.\n *\n * What this flag actually catches is a region that fell outside the tree\n * entirely, and the overwhelmingly likely cause is that `scale` or `origin` was\n * wrong. That failure is worth a loud signal precisely because it does not look\n * like one: at the wrong scale every region lands in the top-left quadrant and\n * the report comes out complete, plausible, and about the wrong components.\n */\n readonly unattributed: boolean;\n\n /**\n * Tightest node the region *overlaps*, present only when unattributed.\n *\n * Offered as orientation, never as attribution — hence the separate field. It\n * answers \"what is this near\", which is a different claim from \"this is what\n * changed\", and keeping them in one field is how the second gets asserted with\n * the confidence of the first.\n */\n readonly nearest?: { readonly path: NodePath; readonly component?: string; readonly where?: string };\n\n /**\n * Nearest enclosing component, when it is not the one that authored the node.\n *\n * Two namespaces meet here and neither is wrong. `component` is `createdBy` —\n * who wrote the JSX (ADR-0007) — and a component *hash* is named for the\n * enclosure (ADR-0018). They diverge exactly where an element is passed as a\n * prop: `<Card title={<h3>Invoice</h3>} />` gives the `<h3>` `createdBy: Page`\n * and `owners[0]: Card`.\n *\n * Carried so that ranking can match a cause list without guessing which of the\n * two it is written in. Matching one namespace against the other does not\n * fail loudly — it silently finds nothing, and the ordering falls back to area,\n * which is the thing the cause list exists to prevent.\n */\n readonly owner?: string;\n\n /**\n * The shape of this difference, with its position and values removed.\n *\n * Filled in by whoever held the mask — `attributeRegions` never sets it,\n * because a fingerprint is computed from pixels and this function is given\n * boxes. It exists on this type rather than beside it so that the digest\n * travels with the region a reader is looking at: writing an ignore means\n * copying the fingerprint of the thing that annoyed you, and a digest printed\n * in a different section is a digest nobody matches up.\n *\n * See `fingerprintOfMask` in `core/judge`.\n */\n readonly fingerprint?: string;\n}\n\nexport interface AttributionOptions {\n /**\n * Device pixels per CSS pixel in the raster.\n *\n * Required, with no default. A 2x screenshot silently attributed at 1x lands\n * every region in the top-left quadrant of the page and attributes each of them\n * to the wrong node — a failure that produces a full, plausible, entirely wrong\n * report. Making the caller state it turns that into a decision someone made.\n */\n readonly scale: number;\n\n /**\n * Page-space CSS origin of the raster's top-left corner.\n *\n * Defaults to the subject root's own rect, which is right when the screenshot\n * was clipped to the subject — the normal case. A full-page shot passes `{x: 0,\n * y: 0}`.\n */\n readonly origin?: { readonly x: number; readonly y: number };\n\n /**\n * Fraction of a region's area that must fall inside a box to count as inside.\n *\n * Below 1 by default because a region's bounding box is inflated by\n * antialiasing at its edges, so exact containment would reject the correct node\n * on a text change. This is the one genuinely fuzzy number in the join and it\n * is named rather than buried.\n */\n readonly containment?: number;\n}\n\nconst DEFAULT_CONTAINMENT = 0.9;\n\n/**\n * Join isolated regions to the nodes that occupy them.\n *\n * Requires a snapshot carrying `rect`, i.e. a profile with layout (ADR-0002).\n * Under a profile without one every region comes back unattributed, which is the\n * correct answer — a rect that was never observed must never be inferred.\n */\nexport function attributeRegions(\n regions: readonly DiffRegion[],\n snapshot: SemanticSnapshot,\n options: AttributionOptions,\n): readonly AttributedRegion[] {\n const boxed = collectBoxes(snapshot.root);\n const origin = options.origin ?? snapshot.root.rect ?? { x: 0, y: 0 };\n const containment = options.containment ?? DEFAULT_CONTAINMENT;\n\n return regions.map((region) => {\n const css: Rect = {\n x: region.x / options.scale + origin.x,\n y: region.y / options.scale + origin.y,\n width: region.width / options.scale,\n height: region.height / options.scale,\n };\n const area = css.width * css.height;\n\n let inside: SemanticNode | undefined;\n let overlapping: SemanticNode | undefined;\n\n for (const node of boxed) {\n const rect = node.rect!;\n const shared = intersectionArea(css, rect);\n if (shared <= 0) continue;\n\n const nodeArea = rect.width * rect.height;\n\n // `<=`, not `<`, and the difference is one component name in every report\n // over a design system. A wrapper that shrink-wraps its only child carries\n // a *byte-identical* rect — measured on `cases/storybook-case`, where\n // `Tokens` and `Button` are both `454.34,359 115.33×50` — so neither is\n // tighter than the other and the walk decides. `collectBoxes` is pre-order,\n // so a strict `<` keeps whichever came first, which is always the outer\n // one: the run then names the wrapper nothing edited and sends a reviewer\n // to its file. Ties go to the last box seen, which is the innermost, and is\n // also the one the browser painted on top.\n if (area > 0 && shared / area >= containment) {\n if (inside === undefined || nodeArea <= inside.rect!.width * inside.rect!.height) inside = node;\n }\n // The same rule for `nearest`. It is orientation rather than attribution,\n // and a hint should point at the same node the join would have picked.\n if (overlapping === undefined || nodeArea <= overlapping.rect!.width * overlapping.rect!.height) {\n overlapping = node;\n }\n }\n\n if (inside !== undefined) {\n return {\n region,\n path: inside.path,\n ...describe(snapshot, inside),\n unattributed: false,\n };\n }\n\n return {\n region,\n unattributed: true,\n ...(overlapping !== undefined\n ? { nearest: { path: overlapping.path, ...describe(snapshot, overlapping) } }\n : {}),\n };\n });\n}\n\nexport interface RankedRegion extends AttributedRegion {\n /**\n * `true` when the semantic tier named this region's component as a *root* of\n * the change, rather than as something the change happened to move.\n */\n readonly cause: boolean;\n}\n\n/**\n * Rank attributed regions by cause, falling back to area.\n *\n * This function exists because of a measurement, and the measurement is the most\n * useful thing the raster tier has produced. Replacing a checkbox with a styled\n * div on the todomvc corpus changes 1530 pixels in 5 regions, which attribute\n * geometrically and rank by area as:\n *\n * ```\n * 933px Text <- changed, and reflowed\n * 511px Stack <- only reflowed\n * 86px Toggle <- the edit\n * ```\n *\n * Every one of those attributions is correct. The pixels really are inside those\n * nodes. The *ordering* is still wrong, because **area measures displacement,\n * not cause** — an edit that reflows its surroundings moves far more of them than\n * of itself, so `Stack`, which nothing edited, outranks `Toggle`, which is the\n * edit. That is the same defect the differ had when a list reorder blamed the\n * element that moved rather than the code that moved it.\n *\n * Geometry cannot fix this; it has no access to why. The semantic tier does — it\n * has provenance and props digests — so the raster tier stops claiming to rank\n * and takes the ordering from the tier that can. On the case above that promotes\n * `Toggle` above `Stack` while leaving both attributions untouched.\n *\n * Two limits, stated rather than smoothed over. The semantic tier may name more\n * than one cause and here it names two, `Toggle` and `Text`, both real — this\n * does not collapse them, because picking one would be inventing a fact. And\n * with no causes supplied the order falls back to area, which is honest and is\n * not good: pixels alone rank the displaced above the displacer, and nothing\n * inside this file can change that.\n */\nexport function rankRegions(\n regions: readonly AttributedRegion[],\n causes: readonly string[] = [],\n): readonly RankedRegion[] {\n const named = new Set(causes);\n\n return regions\n .map((region) => ({\n ...region,\n // Either namespace. A cause list is named for enclosures and a region is\n // named for its author, and where those differ a one-sided test finds\n // nothing and silently reverts the ordering to area.\n cause:\n (region.component !== undefined && named.has(region.component)) ||\n (region.owner !== undefined && named.has(region.owner)),\n }))\n .sort((a, b) => Number(b.cause) - Number(a.cause) || b.region.pixels - a.region.pixels);\n}\n\nfunction describe(\n snapshot: SemanticSnapshot,\n node: SemanticNode,\n): { component?: string; owner?: string; where?: string; source?: SourceLocation } {\n // `createdBy` before `owners[0]`: the component whose JSX produced this element\n // owns its appearance, while the nearest enclosing component merely contains it.\n const owner = node.provenance?.owners[0]?.name;\n const component = node.provenance?.createdBy ?? owner;\n const where = locate(snapshot.root, node.path).where;\n\n return {\n ...(component !== undefined ? { component } : {}),\n // Only when it says something `component` does not. An `owner` that repeats\n // the author is noise in every report that prints it.\n ...(owner !== undefined && owner !== component ? { owner } : {}),\n ...(where !== '' ? { where } : {}),\n ...(node.provenance?.source ? { source: node.provenance.source } : {}),\n };\n}\n\n/** Every node carrying a rect, in document order. Zero-area boxes are skipped. */\nfunction collectBoxes(root: SemanticNode): readonly SemanticNode[] {\n const out: SemanticNode[] = [];\n\n const walk = (node: SemanticNode): void => {\n if (node.rect !== undefined && node.rect.width > 0 && node.rect.height > 0) out.push(node);\n for (const child of node.children) walk(child);\n };\n walk(root);\n\n return out;\n}\n\nfunction intersectionArea(a: Rect, b: Rect): number {\n const width = Math.min(a.x + a.width, b.x + b.width) - Math.max(a.x, b.x);\n const height = Math.min(a.y + a.height, b.y + b.height) - Math.max(a.y, b.y);\n return width > 0 && height > 0 ? width * height : 0;\n}\n"]}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A generated position, mapped back to the source somebody wrote.
|
|
3
|
+
*
|
|
4
|
+
* Every build in ordinary use already ships this. A dev server emits it inline
|
|
5
|
+
* on each module it transforms; a production bundler writes it beside the
|
|
6
|
+
* bundle. It is the same argument the rest of provenance keeps making — the
|
|
7
|
+
* information is not missing from the build, only from the hop that would carry
|
|
8
|
+
* it — and it is what makes a browser stack frame usable: a frame says
|
|
9
|
+
* `/src/probe.jsx:23:26` about a file the browser was served, and the file the
|
|
10
|
+
* reviewer has to open is line 21 of the file they wrote.
|
|
11
|
+
*
|
|
12
|
+
* **Written out rather than installed.** `core` has no third-party dependencies
|
|
13
|
+
* (ADR-0013) and a source map is a documented format with one interesting part,
|
|
14
|
+
* so decoding it here costs less than the rule it would break. What is
|
|
15
|
+
* deliberately *not* implemented is left as a refusal rather than a wrong
|
|
16
|
+
* answer: an index map's sections are resolved, and a map whose version this
|
|
17
|
+
* does not recognise returns nothing at all.
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* One decoded mapping: where a generated column came from.
|
|
21
|
+
*
|
|
22
|
+
* Column-only because the generated *line* is the index of the array this sits
|
|
23
|
+
* in — that is how the `;`-separated wire format is shaped, and keeping it that
|
|
24
|
+
* way is what makes the lookup a binary search rather than a scan.
|
|
25
|
+
*/
|
|
26
|
+
interface Segment {
|
|
27
|
+
readonly generatedColumn: number;
|
|
28
|
+
readonly sourceIndex: number;
|
|
29
|
+
readonly originalLine: number;
|
|
30
|
+
readonly originalColumn: number;
|
|
31
|
+
}
|
|
32
|
+
export interface SourceMap {
|
|
33
|
+
/** Source paths as the map states them, already resolved against `sourceRoot`. */
|
|
34
|
+
readonly sources: readonly string[];
|
|
35
|
+
/** Segments per generated line, `[0]` being generated line 1. */
|
|
36
|
+
readonly lines: readonly (readonly Segment[])[];
|
|
37
|
+
/** Non-zero only for a section of an index map. */
|
|
38
|
+
readonly offset: {
|
|
39
|
+
readonly line: number;
|
|
40
|
+
readonly column: number;
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
export interface OriginalPosition {
|
|
44
|
+
/** The source path, exactly as the map spells it. */
|
|
45
|
+
readonly source: string;
|
|
46
|
+
/** 1-based, matching how a stack frame and an editor both count. */
|
|
47
|
+
readonly line: number;
|
|
48
|
+
/** 1-based, for the same reason. The wire format is 0-based; this is not. */
|
|
49
|
+
readonly column: number;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Parse a source map, or decide this is not one.
|
|
53
|
+
*
|
|
54
|
+
* Returns a list because an index map is several maps at several offsets, and
|
|
55
|
+
* flattening them here means the lookup does not have to know which kind it was
|
|
56
|
+
* handed. An ordinary map is a list of one.
|
|
57
|
+
*
|
|
58
|
+
* Every refusal is silent and total. The caller is resolving a stack frame for a
|
|
59
|
+
* report, and a map that does not parse means one node reports no location —
|
|
60
|
+
* which is a state the whole provenance path already treats as normal.
|
|
61
|
+
*/
|
|
62
|
+
export declare function parseSourceMap(text: string): SourceMap[];
|
|
63
|
+
/**
|
|
64
|
+
* The source position a generated position came from, or nothing.
|
|
65
|
+
*
|
|
66
|
+
* `line` and `column` are 1-based on the way in and on the way out, because both
|
|
67
|
+
* ends of this are 1-based: a browser's stack frame counts from one, and so does
|
|
68
|
+
* every editor a reviewer opens the answer in. The wire format counts from zero
|
|
69
|
+
* and that stays inside this file.
|
|
70
|
+
*
|
|
71
|
+
* A position between two mappings resolves to the earlier one — which is the
|
|
72
|
+
* standard reading, and the right one: generated code that maps nowhere belongs
|
|
73
|
+
* to the last construct that did.
|
|
74
|
+
*/
|
|
75
|
+
export declare function originalPositionFor(maps: readonly SourceMap[], line: number, column: number): OriginalPosition | null;
|
|
76
|
+
/**
|
|
77
|
+
* The map a served module points at, as a URL or as the map itself.
|
|
78
|
+
*
|
|
79
|
+
* Two forms, and a build uses whichever suits it: a dev server inlines the map
|
|
80
|
+
* as a `data:` URI because it is transforming one module in memory, and a
|
|
81
|
+
* production bundler writes a sibling `.map` because inlining megabytes into
|
|
82
|
+
* every response is not free.
|
|
83
|
+
*
|
|
84
|
+
* Read from the end of the file. The comment is a trailing annotation by
|
|
85
|
+
* construction, and a bundle that merely *mentions* the token — this file does,
|
|
86
|
+
* a few lines up — would otherwise hand back its own prose.
|
|
87
|
+
*/
|
|
88
|
+
export declare function sourceMappingUrlOf(code: string): string | null;
|
|
89
|
+
/**
|
|
90
|
+
* The JSON of a `data:` source-map URL, when that is what it is.
|
|
91
|
+
*
|
|
92
|
+
* Base64 is what every bundler writes; the spec permits percent-encoding, and
|
|
93
|
+
* `decodeURIComponent` is what reads it. Both are handled because handling the
|
|
94
|
+
* second costs one branch.
|
|
95
|
+
*/
|
|
96
|
+
export declare function inlineSourceMapOf(url: string): string | null;
|
|
97
|
+
export {};
|
|
98
|
+
//# sourceMappingURL=source-map.d.ts.map
|