@variance-authority/core 0.8.1 → 0.10.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 +23 -0
- package/README.md +47 -21
- package/dist/attribute/boundary.d.ts +21 -14
- package/dist/attribute/boundary.js +23 -29
- package/dist/attribute/boundary.js.map +1 -1
- package/dist/attribute/component-hash.d.ts +21 -0
- package/dist/attribute/component-hash.js +48 -3
- package/dist/attribute/component-hash.js.map +1 -1
- package/dist/attribute/index.d.ts +3 -3
- package/dist/attribute/index.js +1 -1
- package/dist/attribute/index.js.map +1 -1
- package/dist/attribute/lexicon.d.ts +37 -0
- package/dist/attribute/lexicon.js +40 -12
- package/dist/attribute/lexicon.js.map +1 -1
- package/dist/attribute/rename.d.ts +18 -0
- package/dist/attribute/rename.js +24 -0
- package/dist/attribute/rename.js.map +1 -0
- package/dist/compare/cascade.d.ts +2 -0
- package/dist/compare/cascade.js +5 -1
- package/dist/compare/cascade.js.map +1 -1
- package/dist/format/hash.d.ts +20 -0
- package/dist/format/hash.js +27 -1
- package/dist/format/hash.js.map +1 -1
- package/dist/format/index.d.ts +1 -1
- package/dist/format/index.js +1 -1
- package/dist/format/index.js.map +1 -1
- package/dist/format/snapshot.d.ts +15 -0
- package/dist/format/snapshot.js.map +1 -1
- package/dist/judge/sensitivity.js +1 -2
- package/dist/judge/sensitivity.js.map +1 -1
- package/dist/relate/graph.d.ts +10 -0
- package/dist/relate/graph.js +11 -1
- package/dist/relate/graph.js.map +1 -1
- package/dist/relate/index.d.ts +1 -0
- package/dist/relate/index.js +1 -0
- package/dist/relate/index.js.map +1 -1
- package/dist/relate/narrow.d.ts +53 -0
- package/dist/relate/narrow.js +133 -0
- package/dist/relate/narrow.js.map +1 -0
- package/dist/relate/reach.d.ts +7 -0
- package/dist/relate/reach.js +5 -0
- package/dist/relate/reach.js.map +1 -1
- package/dist/relate/records.d.ts +10 -1
- package/dist/relate/records.js +19 -2
- package/dist/relate/records.js.map +1 -1
- package/dist/share/http-line.d.ts +30 -0
- package/dist/share/http-line.js +164 -0
- package/dist/share/http-line.js.map +1 -0
- package/dist/share/index.d.ts +9 -126
- package/dist/share/index.js +9 -168
- package/dist/share/index.js.map +1 -1
- package/dist/share/line.d.ts +153 -0
- package/dist/share/line.js +184 -0
- package/dist/share/line.js.map +1 -0
- package/dist/share/publish.d.ts +79 -0
- package/dist/share/publish.js +231 -0
- package/dist/share/publish.js.map +1 -0
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"component-hash.js","sourceRoot":"","sources":["../../src/attribute/component-hash.ts"],"names":[],"mappings":"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","sourcesContent":["import { BANDS, type Band } from '../compare/band.js';\nimport type { CanonicalValue } from '../format/canonical.js';\nimport type { Rect } from '../format/capture.js';\nimport type { Digest } from '../format/hash.js';\nimport { digestValue } from '../format/hash.js';\nimport type { ComponentHash, SemanticSnapshot } from '../format/snapshot.js';\nimport { boundaries, shapeOf, UNATTRIBUTED } from './boundary.js';\n\nexport type { ComponentHash } from '../format/snapshot.js';\nexport { UNATTRIBUTED } from './boundary.js';\n\n/**\n * The digests a comparison of two component states actually reads.\n *\n * `movedBands` used to take two `ComponentHash`es, which carry a name and an\n * instance count neither side of the comparison consults. Narrowing it to the\n * digests is what lets a per-instance record (`ComponentInstance`, which has no\n * `instances` count because it *is* one) be compared by the same function —\n * rather than by a second copy of the band mapping, which is the one thing in\n * this file that must not exist twice: a subject relaxed to `layout` has to\n * absorb the same bands whichever shape the caller happened to be holding.\n */\nexport interface BandDigests {\n readonly structure: Digest;\n readonly semantics: Digest;\n readonly text: Digest;\n readonly style: Digest;\n readonly geometry?: Digest;\n}\n\n/**\n * Per-component content hashes, one per band.\n *\n * `SemanticSnapshot` hashes a whole subject. That answers \"did anything change\"\n * and nothing else: it moves whenever anything inside it moves, so it cannot say\n * *which area*, and a record built on it says \"the page changed\" on every commit.\n *\n * These hashes are the unit a history is kept in. Text, and small — a component\n * that did not change contributes the same digest it did last time, so the only\n * thing worth recording is the difference.\n */\n\n/**\n * Hash every component boundary in a subject.\n *\n * Ordered by component name so an unchanged subject produces byte-identical\n * output across runs.\n *\n * The walk and the per-boundary shape live in {@link ./boundary.js}, which\n * {@link ./instances.js} shares — one definition of what a component's own\n * content is, read by the aggregate and by the per-instance form.\n */\nexport function hashComponents(snapshot: SemanticSnapshot): readonly ComponentHash[] {\n const layout = snapshot.profile.layout;\n\n const accumulated = new Map<\n string,\n {\n structure: CanonicalValue[];\n semantics: CanonicalValue[];\n text: CanonicalValue[];\n style: CanonicalValue[];\n geometry: CanonicalValue[];\n boxes: (Rect | null)[];\n }\n >();\n\n for (const boundary of boundaries(snapshot.root)) {\n const shape = shapeOf(boundary, layout);\n\n const entry = accumulated.get(boundary.component) ?? {\n structure: [],\n semantics: [],\n text: [],\n style: [],\n geometry: [],\n boxes: [],\n };\n entry.structure.push(shape.structure);\n entry.semantics.push(shape.semantics);\n entry.text.push(shape.text);\n entry.style.push(shape.style);\n entry.geometry.push(shape.geometry);\n entry.boxes.push(shape.box);\n accumulated.set(boundary.component, entry);\n }\n\n return [...accumulated.entries()]\n .map(([component, entry]) => ({\n component,\n instances: entry.structure.length,\n structure: digestValue(entry.structure),\n semantics: digestValue(entry.semantics),\n text: digestValue(entry.text),\n style: digestValue(entry.style),\n ...(layout ? { geometry: digestValue(entry.geometry) } : {}),\n // Under a profile with no layout every entry is `null`, and a list of\n // nulls is a measurement nobody took written as one they did. Absent\n // instead, which is the same rule `geometry` above obeys.\n ...(layout ? { boxes: entry.boxes } : {}),\n }))\n // Code-unit order, not `localeCompare`. The doc above promises byte-identical\n // output for an unchanged subject, and a locale-aware comparison makes that a\n // promise about the machine's `LANG` — which was tolerable while these were\n // internal and is not now that they are written into a sidecar, committed\n // beside a baseline, and read back on someone else's runner.\n .sort((a, b) => (a.component < b.component ? -1 : a.component > b.component ? 1 : 0));\n}\n\n/**\n * Which components *caused* a change, given both revisions' hashes.\n *\n * The arithmetic behind cause-first ranking, and the reason a baseline carries\n * its component hashes at all. `rankRegions` takes a list of cause names and,\n * given none, falls back to area — which\n * [journal 0013](../../../../docs/context/journal/0013-observability.md)\n * measured as backwards by 6×, because area measures displacement rather than\n * cause. This is where that list comes from on a path with no second document.\n *\n * The distinction is the split ADR-0018 built the bands for:\n *\n * - **`structure` or `style` moved** — the component's own content is different.\n * It is a cause.\n * - **only `geometry` moved** — the component is byte-identical and its box is\n * somewhere else. Something *else* moved it, so it is collateral, and naming\n * it would send a reviewer to a file nobody edited.\n * - **appeared or disappeared** — a component present on one side only is a\n * cause. Something decided to render it or to stop.\n *\n * `instances` is deliberately not consulted. A component rendered five times\n * instead of four has a different `structure` digest for the subject, and the\n * count moving on its own — the same component, the same content, one more of\n * them — is a change in whatever decided how many, not in this component.\n *\n * A profile with no layout supplies no `geometry`, so on that tier every\n * difference is `structure` or `style` and every changed component is a cause.\n * That is correct rather than degraded: with no boxes, nothing was displaced.\n */\nexport function causesBetween(\n before: readonly ComponentHash[],\n after: readonly ComponentHash[],\n): readonly string[] {\n const previous = new Map(before.map((entry) => [entry.component, entry]));\n const causes: string[] = [];\n\n for (const entry of after) {\n // `(unattributed)` is not a component and can never be a cause. It is the\n // bucket for nodes whose provenance chain broke, so it collects unrelated\n // parts of a page under one name — and nothing downstream could act on it\n // anyway: a region with no owner reports no component, so it would never\n // match. A broken chain is a defect in this tool and is reported as\n // `unattributed` where that means something, not smuggled in here as a\n // culprit.\n if (entry.component === UNATTRIBUTED) continue;\n\n const was = previous.get(entry.component);\n if (was === undefined) {\n causes.push(entry.component);\n continue;\n }\n if (ownContentMoved(was, entry)) causes.push(entry.component);\n }\n\n // Removals too, and they are the case a candidate-only walk cannot see: a\n // component that stopped rendering leaves regions behind it, and the component\n // that used to be there is exactly the name a reviewer needs.\n const present = new Set(after.map((entry) => entry.component));\n for (const entry of before) {\n if (entry.component === UNATTRIBUTED) continue;\n if (!present.has(entry.component)) causes.push(entry.component);\n }\n\n return causes.sort();\n}\n\n/**\n * Whether a component's *own content* differs, ignoring where its box ended up.\n *\n * Deliberately not phrased in bands, because it is not a band question. Both\n * `structure` and `geometry` map to the `geometry` band, and this has to keep\n * them apart: a component whose tree changed edited itself, and a component\n * whose rect moved was pushed. That distinction is the entire cause/collateral\n * result, and asking it through the band mapping would need the digests back\n * again to answer it.\n *\n * The list is every digest a component owns except `geometry`. It grew by two on\n * 2026-08-06 without changing meaning: `semantics` and `text` used to be inside\n * `structure`.\n */\nfunction ownContentMoved(before: BandDigests, after: BandDigests): boolean {\n return (\n before.structure !== after.structure ||\n before.semantics !== after.semantics ||\n before.text !== after.text ||\n before.style !== after.style\n );\n}\n\n/**\n * Which frequency bands moved between one component's two hashes.\n *\n * The reason the digests were split. A baseline carries hashes and not\n * documents, so \"what changed here\" used to be answerable only as a boolean —\n * and a boolean cannot serve a route-level test, whose entire request is *tell\n * me when the page stops assembling and never when it is repainted*.\n *\n * The mapping is exact and it is the same one `bandOf` applies to a delta, which\n * is the property that matters: a subject relaxed to `layout` must absorb the\n * same things whether the run held two documents or two sidecars. Two mappings\n * would be one drift away from a config key meaning different things on the two\n * paths, discovered as a regression somebody let through.\n *\n * | digest | band | what it covers |\n * |---|---|---|\n * | `semantics` | `a11y` | role, accessible name, ARIA state |\n * | `text` | `content` | text runs |\n * | `structure` | `geometry` | tags, aliases, attributes, child boundaries |\n * | `geometry` | `geometry` | rects and computed layout output |\n * | `style` | `token` | declared values and custom properties |\n *\n * `texture` never appears. It is raster residue by definition, and a component\n * hash is built from a document — so the band a comparison of hashes cannot\n * decide is *absent* from the answer rather than reported as unmoved, which is\n * ADR-0002's rule applied to a narrower question.\n *\n * A missing `geometry` on either side is the profile saying it has no layout\n * engine, and is not a difference. Treating absent as a change would report\n * every component as having moved the moment a jsdom baseline met a Chromium\n * run — which the environment key already refuses as `incomparable`, so this\n * would be a second, wronger answer to a question already settled.\n */\nexport function movedBands(before: BandDigests, after: BandDigests): readonly Band[] {\n const moved = new Set<Band>();\n\n if (before.semantics !== after.semantics) moved.add('a11y');\n if (before.text !== after.text) moved.add('content');\n if (before.structure !== after.structure) moved.add('geometry');\n if (before.style !== after.style) moved.add('token');\n if (\n before.geometry !== undefined &&\n after.geometry !== undefined &&\n before.geometry !== after.geometry\n ) {\n moved.add('geometry');\n }\n\n return BANDS.filter((band) => moved.has(band));\n}\n\n/**\n * Every band that moved anywhere in the subject, given both revisions' hashes.\n *\n * A component present on one side only contributes `geometry`: something was\n * added or removed, which is the structural half of that band however the rest\n * of it compares. It deliberately does not contribute `a11y` or `content` as\n * well — a component that is simply not there did not *rename* anything, and\n * inflating the answer would make a level that absorbs nothing look like the\n * only safe choice.\n */\nexport function bandsBetween(\n before: readonly ComponentHash[],\n after: readonly ComponentHash[],\n): readonly Band[] {\n const previous = new Map(before.map((entry) => [entry.component, entry]));\n const present = new Set(after.map((entry) => entry.component));\n const moved = new Set<Band>();\n\n for (const entry of after) {\n const was = previous.get(entry.component);\n if (was === undefined) moved.add('geometry');\n else for (const band of movedBands(was, entry)) moved.add(band);\n }\n\n for (const entry of before) {\n if (!present.has(entry.component)) moved.add('geometry');\n }\n\n return BANDS.filter((band) => moved.has(band));\n}\n\n/**\n * Which bands moved, kept per component instead of folded into one list.\n *\n * {@link bandsBetween} answers *what kind of change is in this subject*, which is\n * the question a sensitivity level asks. It cannot answer the one a reviewer\n * asks — *what changed, and where* — because the fold is lossy in exactly the\n * place attribution lives: a subject reporting `content, geometry, token` has\n * told you a colour and a string and a size all moved somewhere in it, and left\n * you to guess which of the forty components on the page owns which.\n *\n * Unfolded, the same two sidecars say `Button — geometry, token` and\n * `CardFooter — a11y, content`, and that is a sentence a page can print beside a\n * picture. It is also the record that survives when the raster tier loses the\n * name: a difference that reflowed its neighbours merges into one blob whose box\n * fits no component, so the region resolves to the document root and the edit\n * arrives unattributed — while the hashes, which never looked at a pixel, still\n * hold the component that moved and the sense in which it moved.\n *\n * `cause` repeats the {@link causesBetween} predicate rather than being derived\n * from `bands`, and the repetition is the point: `structure` and `geometry` both\n * map to the `geometry` band, so a component that edited its own tree and one\n * that was merely pushed by a neighbour are indistinguishable *after* the band\n * mapping. Losing that here would make every reflowed container a culprit.\n */\nexport interface ComponentBands {\n readonly component: string;\n /** Non-empty: a component whose digests all matched has no entry at all. */\n readonly bands: readonly Band[];\n /** Its own content moved, as opposed to only its rect. `causesBetween`'s test. */\n readonly cause: boolean;\n /** Set only when the component is on one side alone, which `bands` cannot say. */\n readonly presence?: 'added' | 'removed';\n\n /**\n * How much bigger its own box got, when every instance agrees on the answer.\n *\n * The band a reviewer actually wanted. `geometry` says a rect under here is\n * not the rect it was; this says the control is eight pixels taller and\n * thirty-six wider, which is the padding somebody edited, arriving as a\n * measurement rather than as a guess about which property produced it.\n *\n * Absent for four different reasons and they are one reason: nobody can say.\n * No boxes on one side, a different number of instances, instances that\n * disagree about the delta, or a box that did not change size. Present is\n * always non-zero on at least one axis.\n */\n readonly grew?: { readonly width: number; readonly height: number };\n}\n\n/**\n * Every component whose hashes differ, with the bands it differs in.\n *\n * One-sided components contribute `geometry` and nothing else, which is\n * {@link bandsBetween}'s rule and must stay identical to it: a component that is\n * simply not there did not rename anything, and inflating the answer would put\n * an `a11y` claim on a page for a component nobody can look at. `presence` is\n * what carries the rest of that meaning, so no reader has to infer *appeared*\n * from a lone `geometry`.\n *\n * `(unattributed)` is excluded for the reason {@link causesBetween} excludes it:\n * it is a bucket for nodes whose provenance chain broke, so it collects\n * unrelated parts of a page under one name and nothing downstream could act on\n * it. The broken chain is reported where it means something, not here as a\n * component that moved.\n */\nexport function movedBandsBetween(\n before: readonly ComponentHash[],\n after: readonly ComponentHash[],\n): readonly ComponentBands[] {\n const previous = new Map(before.map((entry) => [entry.component, entry]));\n const present = new Set(after.map((entry) => entry.component));\n const moved: ComponentBands[] = [];\n\n for (const entry of after) {\n if (entry.component === UNATTRIBUTED) continue;\n const was = previous.get(entry.component);\n if (was === undefined) {\n moved.push({ component: entry.component, bands: ['geometry'], cause: true, presence: 'added' });\n continue;\n }\n const bands = movedBands(was, entry);\n if (bands.length === 0) continue;\n const grew = grewBetween(was.boxes, entry.boxes);\n moved.push({\n component: entry.component,\n bands,\n cause: ownContentMoved(was, entry),\n ...(grew === undefined ? {} : { grew }),\n });\n }\n\n for (const entry of before) {\n if (entry.component === UNATTRIBUTED) continue;\n if (present.has(entry.component)) continue;\n moved.push({ component: entry.component, bands: ['geometry'], cause: true, presence: 'removed' });\n }\n\n return moved.sort((left, right) => left.component.localeCompare(right.component));\n}\n\n/**\n * One size delta both sides agree on, or nothing.\n *\n * Instances are paired by document order, which is the only order either side\n * has. That pairing is sound exactly while the counts match: a component that\n * gained an instance shifted every index after the insertion, and the deltas\n * that fell out would be measurements of one instance against a different one.\n * So a changed count answers nothing rather than answering wrongly — the count\n * itself already moved `structure`, and the reviewer is told that instead.\n *\n * Instances that disagree also answer nothing. Three buttons where one grew and\n * two did not is a real finding and it is not *this* one, and printing the first\n * or the largest would be the page picking a representative and not saying so.\n *\n * Position is deliberately not read. Everything below a control that got taller\n * moved down, and a delta drawn from `x`/`y` would name every one of them.\n */\nfunction grewBetween(\n before: readonly (Rect | null)[] | undefined,\n after: readonly (Rect | null)[] | undefined,\n): { readonly width: number; readonly height: number } | undefined {\n if (before === undefined || after === undefined) return undefined;\n if (before.length === 0 || before.length !== after.length) return undefined;\n\n let agreed: { width: number; height: number } | undefined;\n for (const [index, was] of before.entries()) {\n const now = after[index];\n if (was === null || was === undefined || now === null || now === undefined) return undefined;\n\n const width = now.width - was.width;\n const height = now.height - was.height;\n if (agreed === undefined) agreed = { width, height };\n else if (agreed.width !== width || agreed.height !== height) return undefined;\n }\n\n return agreed === undefined || (agreed.width === 0 && agreed.height === 0) ? undefined : agreed;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"component-hash.js","sourceRoot":"","sources":["../../src/attribute/component-hash.ts"],"names":[],"mappings":"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","sourcesContent":["import { BANDS, type Band } from '../compare/band.js';\nimport { declaredIn } from '../compare/cascade.js';\nimport type { CanonicalValue } from '../format/canonical.js';\nimport type { Rect } from '../format/capture.js';\nimport type { Digest } from '../format/hash.js';\nimport { digestValue } from '../format/hash.js';\nimport type { ComponentHash, SemanticSnapshot } from '../format/snapshot.js';\nimport { boundaries, shapeOf, UNATTRIBUTED } from './boundary.js';\n\nexport type { ComponentHash } from '../format/snapshot.js';\nexport { UNATTRIBUTED } from './boundary.js';\n\n/**\n * The digests a comparison of two component states actually reads.\n *\n * `movedBands` used to take two `ComponentHash`es, which carry a name and an\n * instance count neither side of the comparison consults. Narrowing it to the\n * digests is what lets a per-instance record (`ComponentInstance`, which has no\n * `instances` count because it *is* one) be compared by the same function —\n * rather than by a second copy of the band mapping, which is the one thing in\n * this file that must not exist twice: a subject relaxed to `layout` has to\n * absorb the same bands whichever shape the caller happened to be holding.\n */\nexport interface BandDigests {\n readonly structure: Digest;\n readonly semantics: Digest;\n readonly text: Digest;\n readonly style: Digest;\n readonly geometry?: Digest;\n}\n\n/**\n * Per-component content hashes, one per band.\n *\n * `SemanticSnapshot` hashes a whole subject. That answers \"did anything change\"\n * and nothing else: it moves whenever anything inside it moves, so it cannot say\n * *which area*, and a record built on it says \"the page changed\" on every commit.\n *\n * These hashes are the unit a history is kept in. Text, and small — a component\n * that did not change contributes the same digest it did last time, so the only\n * thing worth recording is the difference.\n */\n\n/**\n * Hash every component boundary in a subject.\n *\n * Ordered by component name so an unchanged subject produces byte-identical\n * output across runs.\n *\n * The walk and the per-boundary shape live in {@link ./boundary.js}, which\n * {@link ./instances.js} shares — one definition of what a component's own\n * content is, read by the aggregate and by the per-instance form.\n */\nexport function hashComponents(snapshot: SemanticSnapshot): readonly ComponentHash[] {\n const layout = snapshot.profile.layout;\n const declared = declaredIn(snapshot);\n\n const accumulated = new Map<\n string,\n {\n structure: CanonicalValue[];\n semantics: CanonicalValue[];\n text: CanonicalValue[];\n style: CanonicalValue[];\n geometry: CanonicalValue[];\n boxes: (Rect | null)[];\n values: Map<string, Set<string>>;\n }\n >();\n\n for (const boundary of boundaries(snapshot.root)) {\n const shape = shapeOf(boundary, layout, undefined, declared);\n\n const entry = accumulated.get(boundary.component) ?? {\n structure: [],\n semantics: [],\n text: [],\n style: [],\n geometry: [],\n boxes: [],\n values: new Map<string, Set<string>>(),\n };\n entry.structure.push(shape.structure);\n entry.semantics.push(shape.semantics);\n entry.text.push(shape.text);\n entry.style.push(shape.style);\n entry.geometry.push(shape.geometry);\n entry.boxes.push(shape.box);\n for (const [property, values] of Object.entries(shape.values ?? {})) {\n const seen = entry.values.get(property) ?? new Set<string>();\n for (const value of values) seen.add(value);\n entry.values.set(property, seen);\n }\n accumulated.set(boundary.component, entry);\n }\n\n return [...accumulated.entries()]\n .map(([component, entry]) => ({\n component,\n instances: entry.structure.length,\n structure: digestValue(entry.structure),\n semantics: digestValue(entry.semantics),\n text: digestValue(entry.text),\n style: digestValue(entry.style),\n ...(layout ? { geometry: digestValue(entry.geometry) } : {}),\n // Under a profile with no layout every entry is `null`, and a list of\n // nulls is a measurement nobody took written as one they did. Absent\n // instead, which is the same rule `geometry` above obeys.\n ...(layout ? { boxes: entry.boxes } : {}),\n values: sortedValues(entry.values),\n }))\n // Code-unit order, not `localeCompare`. The doc above promises byte-identical\n // output for an unchanged subject, and a locale-aware comparison makes that a\n // promise about the machine's `LANG` — which was tolerable while these were\n // internal and is not now that they are written into a sidecar, committed\n // beside a baseline, and read back on someone else's runner.\n .sort((a, b) => byCodeUnit(a.component, b.component));\n}\n\n/** Properties and their values in code-unit order, so the sidecar is byte-stable. */\nfunction sortedValues(values: ReadonlyMap<string, ReadonlySet<string>>): Record<string, string[]> {\n const sorted: Record<string, string[]> = {};\n for (const property of [...values.keys()].sort(byCodeUnit)) {\n sorted[property] = [...values.get(property)!].sort(byCodeUnit);\n }\n return sorted;\n}\n\nfunction byCodeUnit(a: string, b: string): number {\n return a < b ? -1 : a > b ? 1 : 0;\n}\n\n/**\n * Which components *caused* a change, given both revisions' hashes.\n *\n * The arithmetic behind cause-first ranking, and the reason a baseline carries\n * its component hashes at all. `rankRegions` takes a list of cause names and,\n * given none, falls back to area — which\n * [journal 0013](../../../../docs/context/journal/0013-observability.md)\n * measured as backwards by 6×, because area measures displacement rather than\n * cause. This is where that list comes from on a path with no second document.\n *\n * The distinction is the split ADR-0018 built the bands for:\n *\n * - **`structure` or `style` moved** — the component's own content is different.\n * It is a cause.\n * - **only `geometry` moved** — the component is byte-identical and its box is\n * somewhere else. Something *else* moved it, so it is collateral, and naming\n * it would send a reviewer to a file nobody edited.\n * - **appeared or disappeared** — a component present on one side only is a\n * cause. Something decided to render it or to stop.\n *\n * `instances` is deliberately not consulted. A component rendered five times\n * instead of four has a different `structure` digest for the subject, and the\n * count moving on its own — the same component, the same content, one more of\n * them — is a change in whatever decided how many, not in this component.\n *\n * A profile with no layout supplies no `geometry`, so on that tier every\n * difference is `structure` or `style` and every changed component is a cause.\n * That is correct rather than degraded: with no boxes, nothing was displaced.\n */\nexport function causesBetween(\n before: readonly ComponentHash[],\n after: readonly ComponentHash[],\n): readonly string[] {\n const previous = new Map(before.map((entry) => [entry.component, entry]));\n const causes: string[] = [];\n\n for (const entry of after) {\n // `(unattributed)` is not a component and can never be a cause. It is the\n // bucket for nodes whose provenance chain broke, so it collects unrelated\n // parts of a page under one name — and nothing downstream could act on it\n // anyway: a region with no owner reports no component, so it would never\n // match. A broken chain is a defect in this tool and is reported as\n // `unattributed` where that means something, not smuggled in here as a\n // culprit.\n if (entry.component === UNATTRIBUTED) continue;\n\n const was = previous.get(entry.component);\n if (was === undefined) {\n causes.push(entry.component);\n continue;\n }\n if (ownContentMoved(was, entry)) causes.push(entry.component);\n }\n\n // Removals too, and they are the case a candidate-only walk cannot see: a\n // component that stopped rendering leaves regions behind it, and the component\n // that used to be there is exactly the name a reviewer needs.\n const present = new Set(after.map((entry) => entry.component));\n for (const entry of before) {\n if (entry.component === UNATTRIBUTED) continue;\n if (!present.has(entry.component)) causes.push(entry.component);\n }\n\n return causes.sort();\n}\n\n/**\n * Whether a component's *own content* differs, ignoring where its box ended up.\n *\n * Deliberately not phrased in bands, because it is not a band question. Both\n * `structure` and `geometry` map to the `geometry` band, and this has to keep\n * them apart: a component whose tree changed edited itself, and a component\n * whose rect moved was pushed. That distinction is the entire cause/collateral\n * result, and asking it through the band mapping would need the digests back\n * again to answer it.\n *\n * The list is every digest a component owns except `geometry`. It grew by two on\n * 2026-08-06 without changing meaning: `semantics` and `text` used to be inside\n * `structure`.\n */\nfunction ownContentMoved(before: BandDigests, after: BandDigests): boolean {\n return (\n before.structure !== after.structure ||\n before.semantics !== after.semantics ||\n before.text !== after.text ||\n before.style !== after.style\n );\n}\n\n/**\n * Which frequency bands moved between one component's two hashes.\n *\n * The reason the digests were split. A baseline carries hashes and not\n * documents, so \"what changed here\" used to be answerable only as a boolean —\n * and a boolean cannot serve a route-level test, whose entire request is *tell\n * me when the page stops assembling and never when it is repainted*.\n *\n * The mapping is exact and it is the same one `bandOf` applies to a delta, which\n * is the property that matters: a subject relaxed to `layout` must absorb the\n * same things whether the run held two documents or two sidecars. Two mappings\n * would be one drift away from a config key meaning different things on the two\n * paths, discovered as a regression somebody let through.\n *\n * | digest | band | what it covers |\n * |---|---|---|\n * | `semantics` | `a11y` | role, accessible name, ARIA state |\n * | `text` | `content` | text runs |\n * | `structure` | `geometry` | tags, aliases, attributes, child boundaries |\n * | `geometry` | `geometry` | rects and computed layout output |\n * | `style` | `token` | declared values and custom properties |\n *\n * `texture` never appears. It is raster residue by definition, and a component\n * hash is built from a document — so the band a comparison of hashes cannot\n * decide is *absent* from the answer rather than reported as unmoved, which is\n * ADR-0002's rule applied to a narrower question.\n *\n * A missing `geometry` on either side is the profile saying it has no layout\n * engine, and is not a difference. Treating absent as a change would report\n * every component as having moved the moment a jsdom baseline met a Chromium\n * run — which the environment key already refuses as `incomparable`, so this\n * would be a second, wronger answer to a question already settled.\n */\nexport function movedBands(before: BandDigests, after: BandDigests): readonly Band[] {\n const moved = new Set<Band>();\n\n if (before.semantics !== after.semantics) moved.add('a11y');\n if (before.text !== after.text) moved.add('content');\n if (before.structure !== after.structure) moved.add('geometry');\n if (before.style !== after.style) moved.add('token');\n if (\n before.geometry !== undefined &&\n after.geometry !== undefined &&\n before.geometry !== after.geometry\n ) {\n moved.add('geometry');\n }\n\n return BANDS.filter((band) => moved.has(band));\n}\n\n/**\n * Every band that moved anywhere in the subject, given both revisions' hashes.\n *\n * A component present on one side only contributes `geometry`: something was\n * added or removed, which is the structural half of that band however the rest\n * of it compares. It deliberately does not contribute `a11y` or `content` as\n * well — a component that is simply not there did not *rename* anything, and\n * inflating the answer would make a level that absorbs nothing look like the\n * only safe choice.\n */\nexport function bandsBetween(\n before: readonly ComponentHash[],\n after: readonly ComponentHash[],\n): readonly Band[] {\n const previous = new Map(before.map((entry) => [entry.component, entry]));\n const present = new Set(after.map((entry) => entry.component));\n const moved = new Set<Band>();\n\n for (const entry of after) {\n const was = previous.get(entry.component);\n if (was === undefined) moved.add('geometry');\n else for (const band of movedBands(was, entry)) moved.add(band);\n }\n\n for (const entry of before) {\n if (!present.has(entry.component)) moved.add('geometry');\n }\n\n return BANDS.filter((band) => moved.has(band));\n}\n\n/**\n * Which bands moved, kept per component instead of folded into one list.\n *\n * {@link bandsBetween} answers *what kind of change is in this subject*, which is\n * the question a sensitivity level asks. It cannot answer the one a reviewer\n * asks — *what changed, and where* — because the fold is lossy in exactly the\n * place attribution lives: a subject reporting `content, geometry, token` has\n * told you a colour and a string and a size all moved somewhere in it, and left\n * you to guess which of the forty components on the page owns which.\n *\n * Unfolded, the same two sidecars say `Button — geometry, token` and\n * `CardFooter — a11y, content`, and that is a sentence a page can print beside a\n * picture. It is also the record that survives when the raster tier loses the\n * name: a difference that reflowed its neighbours merges into one blob whose box\n * fits no component, so the region resolves to the document root and the edit\n * arrives unattributed — while the hashes, which never looked at a pixel, still\n * hold the component that moved and the sense in which it moved.\n *\n * `cause` repeats the {@link causesBetween} predicate rather than being derived\n * from `bands`, and the repetition is the point: `structure` and `geometry` both\n * map to the `geometry` band, so a component that edited its own tree and one\n * that was merely pushed by a neighbour are indistinguishable *after* the band\n * mapping. Losing that here would make every reflowed container a culprit.\n */\nexport interface ComponentBands {\n readonly component: string;\n /** Non-empty: a component whose digests all matched has no entry at all. */\n readonly bands: readonly Band[];\n /** Its own content moved, as opposed to only its rect. `causesBetween`'s test. */\n readonly cause: boolean;\n /** Set only when the component is on one side alone, which `bands` cannot say. */\n readonly presence?: 'added' | 'removed';\n\n /**\n * How much bigger its own box got, when every instance agrees on the answer.\n *\n * The band a reviewer actually wanted. `geometry` says a rect under here is\n * not the rect it was; this says the control is eight pixels taller and\n * thirty-six wider, which is the padding somebody edited, arriving as a\n * measurement rather than as a guess about which property produced it.\n *\n * Absent for four different reasons and they are one reason: nobody can say.\n * No boxes on one side, a different number of instances, instances that\n * disagree about the delta, or a box that did not change size. Present is\n * always non-zero on at least one axis.\n */\n readonly grew?: { readonly width: number; readonly height: number };\n\n /**\n * Which declared values changed, by property, when both sides recorded them.\n *\n * `token` says a declaration under here moved; this names it. `from` holds the\n * values only the baseline had and `to` the values only this run has, so a\n * component rendered as primary and secondary that changed one background\n * reports that background and not the other. An empty side is a property no\n * node declared on that side, which was measured and is not the same as absent.\n *\n * Absent when either sidecar predates the record, and when the style digest\n * moved but no property's set of values did — a value that moved from one\n * node to another is a real change this cannot describe, and an empty list\n * would say there was none. Present is never empty.\n */\n readonly changed?: readonly ValueChange[];\n}\n\n/** One property whose set of declared values differs between two revisions. */\nexport interface ValueChange {\n readonly property: string;\n readonly from: readonly string[];\n readonly to: readonly string[];\n}\n\n/**\n * Every component whose hashes differ, with the bands it differs in.\n *\n * One-sided components contribute `geometry` and nothing else, which is\n * {@link bandsBetween}'s rule and must stay identical to it: a component that is\n * simply not there did not rename anything, and inflating the answer would put\n * an `a11y` claim on a page for a component nobody can look at. `presence` is\n * what carries the rest of that meaning, so no reader has to infer *appeared*\n * from a lone `geometry`.\n *\n * `(unattributed)` is excluded for the reason {@link causesBetween} excludes it:\n * it is a bucket for nodes whose provenance chain broke, so it collects\n * unrelated parts of a page under one name and nothing downstream could act on\n * it. The broken chain is reported where it means something, not here as a\n * component that moved.\n */\nexport function movedBandsBetween(\n before: readonly ComponentHash[],\n after: readonly ComponentHash[],\n): readonly ComponentBands[] {\n const previous = new Map(before.map((entry) => [entry.component, entry]));\n const present = new Set(after.map((entry) => entry.component));\n const moved: ComponentBands[] = [];\n\n for (const entry of after) {\n if (entry.component === UNATTRIBUTED) continue;\n const was = previous.get(entry.component);\n if (was === undefined) {\n moved.push({ component: entry.component, bands: ['geometry'], cause: true, presence: 'added' });\n continue;\n }\n const bands = movedBands(was, entry);\n if (bands.length === 0) continue;\n const grew = grewBetween(was.boxes, entry.boxes);\n const changed = was.style === entry.style ? undefined : changedBetween(was.values, entry.values);\n moved.push({\n component: entry.component,\n bands,\n cause: ownContentMoved(was, entry),\n ...(grew === undefined ? {} : { grew }),\n ...(changed === undefined ? {} : { changed }),\n });\n }\n\n for (const entry of before) {\n if (entry.component === UNATTRIBUTED) continue;\n if (present.has(entry.component)) continue;\n moved.push({ component: entry.component, bands: ['geometry'], cause: true, presence: 'removed' });\n }\n\n return moved.sort((left, right) => byCodeUnit(left.component, right.component));\n}\n\n/**\n * Every property whose values differ, or nothing when that cannot be said.\n *\n * Read only when the `style` digest moved, which is the digest these values\n * were declared into: a component whose declarations hashed the same has\n * nothing here to name, whatever else about it changed.\n */\nfunction changedBetween(\n before: ComponentHash['values'],\n after: ComponentHash['values'],\n): readonly ValueChange[] | undefined {\n if (before === undefined || after === undefined) return undefined;\n\n const changed: ValueChange[] = [];\n const properties = new Set([...Object.keys(before), ...Object.keys(after)]);\n for (const property of [...properties].sort(byCodeUnit)) {\n const was = before[property] ?? [];\n const now = after[property] ?? [];\n const from = was.filter((value) => !now.includes(value));\n const to = now.filter((value) => !was.includes(value));\n if (from.length > 0 || to.length > 0) changed.push({ property, from, to });\n }\n return changed.length === 0 ? undefined : changed;\n}\n\n/**\n * One size delta both sides agree on, or nothing.\n *\n * Instances are paired by document order, which is the only order either side\n * has. That pairing is sound exactly while the counts match: a component that\n * gained an instance shifted every index after the insertion, and the deltas\n * that fell out would be measurements of one instance against a different one.\n * So a changed count answers nothing rather than answering wrongly — the count\n * itself already moved `structure`, and the reviewer is told that instead.\n *\n * Instances that disagree also answer nothing. Three buttons where one grew and\n * two did not is a real finding and it is not *this* one, and printing the first\n * or the largest would be the page picking a representative and not saying so.\n *\n * Position is deliberately not read. Everything below a control that got taller\n * moved down, and a delta drawn from `x`/`y` would name every one of them.\n */\nfunction grewBetween(\n before: readonly (Rect | null)[] | undefined,\n after: readonly (Rect | null)[] | undefined,\n): { readonly width: number; readonly height: number } | undefined {\n if (before === undefined || after === undefined) return undefined;\n if (before.length === 0 || before.length !== after.length) return undefined;\n\n let agreed: { width: number; height: number } | undefined;\n for (const [index, was] of before.entries()) {\n const now = after[index];\n if (was === null || was === undefined || now === null || now === undefined) return undefined;\n\n const width = now.width - was.width;\n const height = now.height - was.height;\n if (agreed === undefined) agreed = { width, height };\n else if (agreed.width !== width || agreed.height !== height) return undefined;\n }\n\n return agreed === undefined || (agreed.width === 0 && agreed.height === 0) ? undefined : agreed;\n}\n"]}
|
|
@@ -28,7 +28,7 @@ export type { SourceMap, OriginalPosition } from './source-map.js';
|
|
|
28
28
|
export { createCallSiteResolver, locateProvenance, locateSites } from './call-site.js';
|
|
29
29
|
export type { CallSiteResolver, CallSiteStats, FetchModule, LocatableSite } from './call-site.js';
|
|
30
30
|
export { hashComponents, causesBetween, movedBands, movedBandsBetween, bandsBetween, UNATTRIBUTED, } from './component-hash.js';
|
|
31
|
-
export type { BandDigests, ComponentBands, ComponentHash } from './component-hash.js';
|
|
31
|
+
export type { BandDigests, ComponentBands, ComponentHash, ValueChange } from './component-hash.js';
|
|
32
32
|
export { componentInstances, attributed } from './instances.js';
|
|
33
33
|
export type { ComponentInstance } from './instances.js';
|
|
34
34
|
export { nodeClosures, sharedClosures } from './closure.js';
|
|
@@ -36,10 +36,10 @@ export type { ClosureSite, NodeClosure, SharedClosure, SharedClosureOptions, Sub
|
|
|
36
36
|
export { composeSubjects } from './composition.js';
|
|
37
37
|
export type { Composition, ComponentEntry, Echo, PropsClass, Rendering, Site, SubjectComposition, } from './composition.js';
|
|
38
38
|
export type { Divergence, DivergenceParting } from './divergence.js';
|
|
39
|
-
export { lexiconOf, structureOf, LEXICON_CAP } from './lexicon.js';
|
|
39
|
+
export { lexiconOf, lexiconOfValues, lexiconValuesOf, structureOf, withExamples, LEXICON_CAP } from './lexicon.js';
|
|
40
40
|
export { LANDMARK_CAP } from './landmark.js';
|
|
41
41
|
export type { Landmark } from './landmark.js';
|
|
42
|
-
export type { BoundaryRow, LexiconField, LexiconOptions, SubjectLexicon } from './lexicon.js';
|
|
42
|
+
export type { BoundaryRow, LexiconField, LexiconOptions, LexiconValues, SubjectLexicon } from './lexicon.js';
|
|
43
43
|
export { attributeMovement } from './movement.js';
|
|
44
44
|
export type { Attribution, Cause, Evidence, Moved, Movement } from './movement.js';
|
|
45
45
|
export { coverageOf, alsoCovering, summarizeCoverage } from './coverage.js';
|
package/dist/attribute/index.js
CHANGED
|
@@ -24,7 +24,7 @@ export { hashComponents, causesBetween, movedBands, movedBandsBetween, bandsBetw
|
|
|
24
24
|
export { componentInstances, attributed } from './instances.js';
|
|
25
25
|
export { nodeClosures, sharedClosures } from './closure.js';
|
|
26
26
|
export { composeSubjects } from './composition.js';
|
|
27
|
-
export { lexiconOf, structureOf, LEXICON_CAP } from './lexicon.js';
|
|
27
|
+
export { lexiconOf, lexiconOfValues, lexiconValuesOf, structureOf, withExamples, LEXICON_CAP } from './lexicon.js';
|
|
28
28
|
export { LANDMARK_CAP } from './landmark.js';
|
|
29
29
|
export { attributeMovement } from './movement.js';
|
|
30
30
|
export { coverageOf, alsoCovering, summarizeCoverage } from './coverage.js';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/attribute/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAGrC,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAU3E,OAAO,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAG5D,OAAO,EACL,aAAa,EACb,YAAY,EACZ,WAAW,EACX,kBAAkB,EAClB,kBAAkB,GACnB,MAAM,aAAa,CAAC;AAGrB,OAAO,EAAE,gBAAgB,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAG9E,OAAO,EACL,cAAc,EACd,mBAAmB,EACnB,kBAAkB,EAClB,iBAAiB,GAClB,MAAM,iBAAiB,CAAC;AAGzB,OAAO,EAAE,sBAAsB,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAGvF,OAAO,EACL,cAAc,EACd,aAAa,EACb,UAAU,EACV,iBAAiB,EACjB,YAAY,EACZ,YAAY,GACb,MAAM,qBAAqB,CAAC;AAG7B,OAAO,EAAE,kBAAkB,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAGhE,OAAO,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAG5D,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAanD,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/attribute/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAGrC,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAU3E,OAAO,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAG5D,OAAO,EACL,aAAa,EACb,YAAY,EACZ,WAAW,EACX,kBAAkB,EAClB,kBAAkB,GACnB,MAAM,aAAa,CAAC;AAGrB,OAAO,EAAE,gBAAgB,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAG9E,OAAO,EACL,cAAc,EACd,mBAAmB,EACnB,kBAAkB,EAClB,iBAAiB,GAClB,MAAM,iBAAiB,CAAC;AAGzB,OAAO,EAAE,sBAAsB,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAGvF,OAAO,EACL,cAAc,EACd,aAAa,EACb,UAAU,EACV,iBAAiB,EACjB,YAAY,EACZ,YAAY,GACb,MAAM,qBAAqB,CAAC;AAG7B,OAAO,EAAE,kBAAkB,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAGhE,OAAO,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAG5D,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAanD,OAAO,EAAE,SAAS,EAAE,eAAe,EAAE,eAAe,EAAE,WAAW,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AACnH,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAI7C,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAGlD,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAG5E,OAAO,EAAE,iBAAiB,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC","sourcesContent":["/**\n * `@variance-authority/core/attribute` — from a position to a name to a file.\n *\n * The group that makes a difference actionable. Pixels cluster into regions,\n * regions join the box tree, the tree knows which component produced each node,\n * and the component resolves to a source location. The same machinery answers\n * \"which component is not holding still\" and \"which subjects cover this\n * component\".\n *\n * This is the answer to *\"a pixel differ reports 1530 pixels changed\"* — and the\n * reason it is a group rather than a function is that every step of the journey\n * is separately useful, and separately capable of refusing. `locateInstability`\n * will not name a component for a difference that is beneath the semantic tier;\n * `coverage` will not recommend deleting anything.\n */\n\nexport { locate } from './locate.js';\nexport type { Location, LocationStep } from './locate.js';\n\nexport { isolateRegions, subtractRegions, excludedBoxes } from './mask.js';\nexport type {\n ChangeMask,\n DiffRegion,\n ExcludedBox,\n Isolation,\n IsolationOptions,\n Subtraction,\n} from './mask.js';\n\nexport { attributeRegions, rankRegions } from './region.js';\nexport type { AttributedRegion, AttributionOptions, RankedRegion } from './region.js';\n\nexport {\n resolveSource,\n formatSource,\n indexSource,\n mergeSourceIndexes,\n overlaySourceIndex,\n} from './source.js';\nexport type { SourceIndex, SourceRef, Resolution } from './source.js';\n\nexport { parseStackFrames, isVendorPath, writerLocationOf } from './stack.js';\nexport type { StackFrame } from './stack.js';\n\nexport {\n parseSourceMap,\n originalPositionFor,\n sourceMappingUrlOf,\n inlineSourceMapOf,\n} from './source-map.js';\nexport type { SourceMap, OriginalPosition } from './source-map.js';\n\nexport { createCallSiteResolver, locateProvenance, locateSites } from './call-site.js';\nexport type { CallSiteResolver, CallSiteStats, FetchModule, LocatableSite } from './call-site.js';\n\nexport {\n hashComponents,\n causesBetween,\n movedBands,\n movedBandsBetween,\n bandsBetween,\n UNATTRIBUTED,\n} from './component-hash.js';\nexport type { BandDigests, ComponentBands, ComponentHash, ValueChange } from './component-hash.js';\n\nexport { componentInstances, attributed } from './instances.js';\nexport type { ComponentInstance } from './instances.js';\n\nexport { nodeClosures, sharedClosures } from './closure.js';\nexport type { ClosureSite, NodeClosure, SharedClosure, SharedClosureOptions, SubjectTree } from './closure.js';\n\nexport { composeSubjects } from './composition.js';\nexport type {\n Composition,\n ComponentEntry,\n Echo,\n PropsClass,\n Rendering,\n Site,\n SubjectComposition,\n} from './composition.js';\n\nexport type { Divergence, DivergenceParting } from './divergence.js';\n\nexport { lexiconOf, lexiconOfValues, lexiconValuesOf, structureOf, withExamples, LEXICON_CAP } from './lexicon.js';\nexport { LANDMARK_CAP } from './landmark.js';\nexport type { Landmark } from './landmark.js';\nexport type { BoundaryRow, LexiconField, LexiconOptions, LexiconValues, SubjectLexicon } from './lexicon.js';\n\nexport { attributeMovement } from './movement.js';\nexport type { Attribution, Cause, Evidence, Moved, Movement } from './movement.js';\n\nexport { coverageOf, alsoCovering, summarizeCoverage } from './coverage.js';\nexport type { Coverage, ComponentCoverage, SubjectCoverage, SubjectValue } from './coverage.js';\n\nexport { locateInstability, summarizeInstability } from './instability.js';\nexport type {\n Instability,\n InstabilityBand,\n UnstableLocation,\n UnstableProperty,\n} from './instability.js';\n"]}
|
|
@@ -88,6 +88,43 @@ export declare const LEXICON_CAP = 200;
|
|
|
88
88
|
* always measured by the same instrument.
|
|
89
89
|
*/
|
|
90
90
|
export declare function lexiconOf(subjects: readonly SubjectComposition[], options?: LexiconOptions): readonly SubjectLexicon[];
|
|
91
|
+
/**
|
|
92
|
+
* The fold over readings already taken: the suite-wide count, then the cap.
|
|
93
|
+
*
|
|
94
|
+
* Exported for the reader that holds readings and no subjects — the shards of
|
|
95
|
+
* one build, each of which read its own subjects and none of which saw how
|
|
96
|
+
* widely the whole suite holds a value. Handed every shard's readings in plan
|
|
97
|
+
* order, it writes the lexicon one unsharded run would have.
|
|
98
|
+
*/
|
|
99
|
+
export declare function lexiconOfValues(rows: readonly LexiconValues[]): readonly SubjectLexicon[];
|
|
100
|
+
/**
|
|
101
|
+
* One subject's values per field before any cap, its landmarks, its boundary
|
|
102
|
+
* count. Plain data, so a shard can write it down and another machine fold it.
|
|
103
|
+
*/
|
|
104
|
+
export interface LexiconValues {
|
|
105
|
+
readonly subject: string;
|
|
106
|
+
/** Distinct values per field, code-unit sorted; a field with none is absent. */
|
|
107
|
+
readonly fields: Partial<Record<LexiconField, readonly string[]>>;
|
|
108
|
+
readonly landmarks: readonly Landmark[];
|
|
109
|
+
readonly boundaries: number;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Every value a subject holds, per field, with nothing cut.
|
|
113
|
+
*
|
|
114
|
+
* Split from the fold above because a cap that keeps the values worth keeping
|
|
115
|
+
* has to know how widely the *suite* holds each one, and that is not a fact any
|
|
116
|
+
* single subject carries. So the pass that reads a subject and the pass that
|
|
117
|
+
* decides what survives are two passes, with the counting in between.
|
|
118
|
+
*/
|
|
119
|
+
export declare function lexiconValuesOf(subject: SubjectComposition, options?: LexiconOptions): LexiconValues;
|
|
120
|
+
/**
|
|
121
|
+
* A reading with its `example` field taken again from a census.
|
|
122
|
+
*
|
|
123
|
+
* Which subject is the narrow example of a component is decided against every
|
|
124
|
+
* subject that renders it, so a shard's reading answers it for the shard alone.
|
|
125
|
+
* The build that holds every shard's census replaces the field with this.
|
|
126
|
+
*/
|
|
127
|
+
export declare function withExamples(row: LexiconValues, examples: readonly string[]): LexiconValues;
|
|
91
128
|
/** Boundaries of one component at one place in a subject's tree. */
|
|
92
129
|
export interface BoundaryRow {
|
|
93
130
|
readonly component: string;
|
|
@@ -34,16 +34,24 @@ export const LEXICON_CAP = 200;
|
|
|
34
34
|
* always measured by the same instrument.
|
|
35
35
|
*/
|
|
36
36
|
export function lexiconOf(subjects, options = {}) {
|
|
37
|
-
|
|
37
|
+
return lexiconOfValues(subjects.map((subject) => lexiconValuesOf(subject, options)));
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* The fold over readings already taken: the suite-wide count, then the cap.
|
|
41
|
+
*
|
|
42
|
+
* Exported for the reader that holds readings and no subjects — the shards of
|
|
43
|
+
* one build, each of which read its own subjects and none of which saw how
|
|
44
|
+
* widely the whole suite holds a value. Handed every shard's readings in plan
|
|
45
|
+
* order, it writes the lexicon one unsharded run would have.
|
|
46
|
+
*/
|
|
47
|
+
export function lexiconOfValues(rows) {
|
|
38
48
|
const holders = holdersOf(rows);
|
|
39
|
-
return rows.map((row
|
|
40
|
-
const subject = subjects[at];
|
|
41
|
-
const fields = row.fields;
|
|
49
|
+
return rows.map((row) => {
|
|
42
50
|
const terms = {};
|
|
43
51
|
const elided = {};
|
|
44
52
|
for (const field of FIELDS) {
|
|
45
|
-
const values = fields
|
|
46
|
-
if (values === undefined || values.
|
|
53
|
+
const values = row.fields[field];
|
|
54
|
+
if (values === undefined || values.length === 0)
|
|
47
55
|
continue;
|
|
48
56
|
// Code-unit first, so the result is byte-stable, and only then by how
|
|
49
57
|
// widely the suite holds the value — a `sort` that keeps the order of
|
|
@@ -62,8 +70,8 @@ export function lexiconOf(subjects, options = {}) {
|
|
|
62
70
|
const found = row.landmarks;
|
|
63
71
|
const landmarks = found.length <= LANDMARK_CAP ? found : capLandmarks(found);
|
|
64
72
|
return {
|
|
65
|
-
subject:
|
|
66
|
-
boundaries:
|
|
73
|
+
subject: row.subject,
|
|
74
|
+
boundaries: row.boundaries,
|
|
67
75
|
terms,
|
|
68
76
|
...(Object.keys(elided).length === 0 ? {} : { elided }),
|
|
69
77
|
...(landmarks.length === 0 ? {} : { landmarks }),
|
|
@@ -79,7 +87,7 @@ export function lexiconOf(subjects, options = {}) {
|
|
|
79
87
|
* single subject carries. So the pass that reads a subject and the pass that
|
|
80
88
|
* decides what survives are two passes, with the counting in between.
|
|
81
89
|
*/
|
|
82
|
-
function
|
|
90
|
+
export function lexiconValuesOf(subject, options = {}) {
|
|
83
91
|
{
|
|
84
92
|
const fields = new Map();
|
|
85
93
|
const add = (field, value) => {
|
|
@@ -129,9 +137,29 @@ function valuesOf(subject, options) {
|
|
|
129
137
|
return at;
|
|
130
138
|
});
|
|
131
139
|
}
|
|
132
|
-
|
|
140
|
+
const read = {};
|
|
141
|
+
for (const field of FIELDS) {
|
|
142
|
+
const values = fields.get(field);
|
|
143
|
+
if (values !== undefined && values.size > 0)
|
|
144
|
+
read[field] = [...values].sort(codeUnit);
|
|
145
|
+
}
|
|
146
|
+
return { subject: subject.subject, fields: read, landmarks, boundaries: held.length };
|
|
133
147
|
}
|
|
134
148
|
}
|
|
149
|
+
/**
|
|
150
|
+
* A reading with its `example` field taken again from a census.
|
|
151
|
+
*
|
|
152
|
+
* Which subject is the narrow example of a component is decided against every
|
|
153
|
+
* subject that renders it, so a shard's reading answers it for the shard alone.
|
|
154
|
+
* The build that holds every shard's census replaces the field with this.
|
|
155
|
+
*/
|
|
156
|
+
export function withExamples(row, examples) {
|
|
157
|
+
const kept = [...new Set(examples.map((value) => value.trim()))]
|
|
158
|
+
.filter((value) => value !== '' && !isDigest(value))
|
|
159
|
+
.sort(codeUnit);
|
|
160
|
+
const { example: _dropped, ...rest } = row.fields;
|
|
161
|
+
return { ...row, fields: kept.length === 0 ? rest : { ...rest, example: kept } };
|
|
162
|
+
}
|
|
135
163
|
/**
|
|
136
164
|
* How many subjects hold each value, per field.
|
|
137
165
|
*
|
|
@@ -144,8 +172,8 @@ function valuesOf(subject, options) {
|
|
|
144
172
|
function holdersOf(rows) {
|
|
145
173
|
const holders = new Map();
|
|
146
174
|
for (const row of rows) {
|
|
147
|
-
for (const [field, values] of row.fields) {
|
|
148
|
-
for (const value of values) {
|
|
175
|
+
for (const [field, values] of Object.entries(row.fields)) {
|
|
176
|
+
for (const value of values ?? []) {
|
|
149
177
|
const key = `${field} ${value}`;
|
|
150
178
|
holders.set(key, (holders.get(key) ?? 0) + 1);
|
|
151
179
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lexicon.js","sourceRoot":"","sources":["../../src/attribute/lexicon.ts"],"names":[],"mappings":"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","sourcesContent":["import { attributed, type ComponentInstance } from './instances.js';\nimport type { SubjectComposition } from './composition.js';\nimport {\n LANDMARK_CAP,\n capLandmarks,\n isDigest,\n landmarkFor,\n walk,\n type Landmark,\n} from './landmark.js';\n\n/**\n * The subject-first folds: every name a subject carries, and its tree as rows.\n *\n * `composeSubjects` folds the suite component-first, which is the axis a\n * *finding* wants: which component, in how many subjects, mounted by whom. The\n * question an agent asks on arrival runs the other way: *which subject is the\n * one I mean*, and *what is that subject made of*. Both are one pass over the\n * same instance lists, done here so that whoever holds the lists, the run or a\n * test holding snapshots, gets the same rows the report carries.\n *\n * Neither fold ranks anything. The lexicon is values per field and the structure\n * is rows in document order; what a reader does with them is the reader's rule,\n * and it is printed where it is applied.\n */\n\n/** The vocabularies a subject is indexed under. Mirrors the report's `LexiconField`. */\nexport type LexiconField =\n | 'example'\n | 'names'\n | 'text'\n | 'components'\n | 'createdBy'\n | 'regions'\n | 'files'\n | 'roles'\n | 'tokens';\n\n\n/** One subject's names, per field, and where each thing was. */\nexport interface SubjectLexicon {\n readonly subject: string;\n /** Attributed boundaries. The only number a rank may read. */\n readonly boundaries: number;\n /** Distinct values per field, code-unit sorted; a field found empty is absent. */\n readonly terms: Partial<Record<LexiconField, readonly string[]>>;\n /** Distinct values a field's cap left out, where it cut. */\n readonly elided?: Partial<Record<LexiconField, number>>;\n\n /**\n * The same reading in the arrangement it was read in, document order.\n *\n * The fields above are what the subject *says*; this is where it said it. A\n * bag can answer that the subject holds the word `carrier` and the word\n * `contract` and can never answer that the second sits under the first, which\n * is the question somebody actually arrives with. Both come out of one walk\n * over one tree, because two walks eventually disagree about what was there.\n *\n * Absent on a subject with no snapshot. There is a real difference between a\n * dialog with nothing on it and a run that never looked.\n */\n readonly landmarks?: readonly Landmark[];\n /** Landmarks the cap left out. */\n readonly elidedLandmarks?: number;\n}\n\n/** What the fold cannot read off the instances and the snapshot. */\nexport interface LexiconOptions {\n /**\n * Subject → the components it is the narrow example of, from the composition.\n * Taken rather than re-derived so the lexicon and the census cannot disagree\n * about which story shows what.\n */\n readonly examples?: ReadonlyMap<string, readonly string[]>;\n /** Component → files declaring it, from the source index. */\n readonly declaredIn?: ReadonlyMap<string, readonly string[]>;\n /** Subject → lexical names of the regions its journey entered, from the journal. */\n readonly regions?: ReadonlyMap<string, readonly string[]>;\n}\n\n/**\n * Distinct values kept per field. Generous, and what it cuts is counted in\n * `elided`, because a cap that says nothing reads as coverage.\n *\n * What it keeps is the narrower promise. Where a cap fires it keeps the values\n * the fewest other subjects hold, not the ones that sort first, because the\n * alternative is a rule about spelling: a subject whose tree is six hundred\n * boundaries deep under an application's worth of wrappers would keep\n * `Anonymous` and `Connect(Account)` and drop the one component it is about,\n * on no better ground than the alphabet. Ties keep code-unit order, so equal\n * values still land in the same place on every machine.\n */\nexport const LEXICON_CAP = 200;\n\n\n/**\n * Every name each subject carries, per field.\n *\n * Fields come from three places and the option that supplies each says which:\n * the instances give `components`, `createdBy` and `tokens`; the snapshot, when\n * the subject carries one, gives `roles`, `names`, `text` and the call-site\n * `files`; the options give `example`, declared `files` and `regions`. A subject\n * with no snapshot still indexes under what its instances hold, which is the\n * honest shape for a sidecar read back without its document.\n *\n * Digests are never values. A text the policy declared volatile reaches the\n * snapshot as `v1:…`, and a reader that matched on it would be matching a\n * coordinate, not a word.\n *\n * Values are kept as they were read and never split. Splitting is a rule, and a\n * rule applied here would be applied to one side of a later comparison only —\n * the reader's query would meet tokens it did not produce. So the fold stores\n * words and the reader owns the rule, which is how a term and a value are\n * always measured by the same instrument.\n */\nexport function lexiconOf(\n subjects: readonly SubjectComposition[],\n options: LexiconOptions = {},\n): readonly SubjectLexicon[] {\n const rows = subjects.map((subject) => valuesOf(subject, options));\n const holders = holdersOf(rows);\n\n return rows.map((row, at) => {\n const subject = subjects[at]!;\n const fields = row.fields;\n const terms: Partial<Record<LexiconField, readonly string[]>> = {};\n const elided: Partial<Record<LexiconField, number>> = {};\n for (const field of FIELDS) {\n const values = fields.get(field);\n if (values === undefined || values.size === 0) continue;\n\n // Code-unit first, so the result is byte-stable, and only then by how\n // widely the suite holds the value — a `sort` that keeps the order of\n // equal elements, which every engine's does since ES2019.\n const sorted = [...values].sort(codeUnit);\n const kept = sorted.length <= LEXICON_CAP\n ? sorted\n : [...sorted]\n .sort((left, right) => (holders.get(`${field} ${left}`) ?? 0) - (holders.get(`${field} ${right}`) ?? 0))\n .slice(0, LEXICON_CAP)\n .sort(codeUnit);\n\n terms[field] = kept;\n if (sorted.length > LEXICON_CAP) elided[field] = sorted.length - LEXICON_CAP;\n }\n\n const found = row.landmarks;\n const landmarks = found.length <= LANDMARK_CAP ? found : capLandmarks(found);\n\n return {\n subject: subject.subject,\n boundaries: rows[at]!.boundaries,\n terms,\n ...(Object.keys(elided).length === 0 ? {} : { elided }),\n ...(landmarks.length === 0 ? {} : { landmarks }),\n ...(found.length > LANDMARK_CAP ? { elidedLandmarks: found.length - LANDMARK_CAP } : {}),\n };\n });\n}\n\n/** One subject's values per field before any cap, its landmarks, its boundary count. */\ninterface SubjectValues {\n readonly fields: ReadonlyMap<LexiconField, Set<string>>;\n readonly landmarks: readonly Landmark[];\n readonly boundaries: number;\n}\n\n/**\n * Every value a subject holds, per field, with nothing cut.\n *\n * Split from the fold above because a cap that keeps the values worth keeping\n * has to know how widely the *suite* holds each one, and that is not a fact any\n * single subject carries. So the pass that reads a subject and the pass that\n * decides what survives are two passes, with the counting in between.\n */\nfunction valuesOf(subject: SubjectComposition, options: LexiconOptions): SubjectValues {\n {\n const fields = new Map<LexiconField, Set<string>>();\n const add = (field: LexiconField, value: string | undefined) => {\n if (value === undefined) return;\n const trimmed = value.trim();\n if (trimmed === '' || isDigest(trimmed)) return;\n let set = fields.get(field);\n if (set === undefined) fields.set(field, (set = new Set()));\n set.add(trimmed);\n };\n\n const held = subject.instances.filter(attributed);\n for (const instance of held) {\n add('components', instance.component);\n add('createdBy', instance.createdBy);\n for (const token of instance.tokens) add('tokens', token);\n for (const file of options.declaredIn?.get(instance.component) ?? []) add('files', file);\n }\n for (const component of options.examples?.get(subject.subject) ?? []) add('example', component);\n for (const region of options.regions?.get(subject.subject) ?? []) add('regions', region);\n\n const landmarks: Landmark[] = [];\n if (subject.snapshot !== undefined) {\n walk(subject.snapshot.root, undefined, (node, within) => {\n add('roles', node.role);\n add('names', node.name);\n add('names', node.description);\n // The three attributes that are a label for a person rather than a\n // value for a machine. `value` is state, `href` is a coordinate, and\n // `class` never survives normalization at all.\n add('names', node.attributes['placeholder']);\n add('names', node.attributes['alt']);\n add('names', node.attributes['title']);\n add('text', node.text);\n add('files', node.provenance?.source?.file);\n\n // The same visit, keeping what the bags drop. See `landmarkFor`.\n const landmark = landmarkFor(node, within);\n if (landmark === undefined) return within;\n const at = landmarks.length;\n landmarks.push(landmark);\n return at;\n });\n }\n\n return { fields, landmarks, boundaries: held.length };\n }\n}\n\n/**\n * How many subjects hold each value, per field.\n *\n * The one number that separates a name from a fixture. A value nearly every\n * subject carries is structure — a harness wrapper, a provider, an HOC, a\n * chunk every page imports — and a value one subject carries is what\n * distinguishes it. Counted rather than listed, so it costs nothing to be\n * wrong about which framework wrote the wrapper.\n */\nfunction holdersOf(rows: readonly SubjectValues[]): ReadonlyMap<string, number> {\n const holders = new Map<string, number>();\n for (const row of rows) {\n for (const [field, values] of row.fields) {\n for (const value of values) {\n const key = `${field} ${value}`;\n holders.set(key, (holders.get(key) ?? 0) + 1);\n }\n }\n }\n return holders;\n}\n\n/** Boundaries of one component at one place in a subject's tree. */\nexport interface BoundaryRow {\n readonly component: string;\n readonly depth: number;\n readonly within?: string;\n readonly createdBy?: string;\n /** Boundaries folded into this row. */\n readonly count: number;\n /** Distinct props digests among them; unknown props count as one. */\n readonly variants: number;\n}\n\n/**\n * One subject's boundaries as rows, document order, identical rows folded.\n *\n * A row's key is where it sits and who put it there: component, depth,\n * enclosing boundary, creator. Not what it rendered, because the question this\n * answers is *what is this subject made of* and three chips under one stack are\n * one answer to it. What differed among them survives as `variants`, so the\n * fold loses the digests and keeps the fact that there were several.\n */\nexport function structureOf(subject: SubjectComposition): readonly BoundaryRow[] {\n const rows = new Map<string, { row: BoundaryRow; props: Set<string> }>();\n\n for (const instance of subject.instances) {\n if (!attributed(instance)) continue;\n const key = [instance.component, instance.depth, instance.within ?? '', instance.createdBy ?? '']\n .join(' ');\n const props = instance.props ?? UNKNOWN;\n const held = rows.get(key);\n if (held === undefined) {\n rows.set(key, { row: rowOf(instance), props: new Set([props]) });\n } else {\n held.props.add(props);\n held.row = { ...held.row, count: held.row.count + 1 };\n }\n }\n\n return [...rows.values()].map(({ row, props }) => ({ ...row, variants: props.size }));\n}\n\nfunction rowOf(instance: ComponentInstance): BoundaryRow {\n return {\n component: instance.component,\n depth: instance.depth,\n ...(instance.within === undefined ? {} : { within: instance.within }),\n ...(instance.createdBy === undefined ? {} : { createdBy: instance.createdBy }),\n count: 1,\n variants: 1,\n };\n}\n\nconst FIELDS: readonly LexiconField[] = [\n 'example',\n 'names',\n 'text',\n 'components',\n 'createdBy',\n 'regions',\n 'files',\n 'roles',\n 'tokens',\n];\n\nconst UNKNOWN = '(unknown props)';\n\n\n\n\n\n\n\n/** Code-unit order: the same bytes on every machine, whatever `LANG` says. */\nfunction codeUnit(left: string, right: string): number {\n return left < right ? -1 : left > right ? 1 : 0;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"lexicon.js","sourceRoot":"","sources":["../../src/attribute/lexicon.ts"],"names":[],"mappings":"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","sourcesContent":["import { attributed, type ComponentInstance } from './instances.js';\nimport type { SubjectComposition } from './composition.js';\nimport {\n LANDMARK_CAP,\n capLandmarks,\n isDigest,\n landmarkFor,\n walk,\n type Landmark,\n} from './landmark.js';\n\n/**\n * The subject-first folds: every name a subject carries, and its tree as rows.\n *\n * `composeSubjects` folds the suite component-first, which is the axis a\n * *finding* wants: which component, in how many subjects, mounted by whom. The\n * question an agent asks on arrival runs the other way: *which subject is the\n * one I mean*, and *what is that subject made of*. Both are one pass over the\n * same instance lists, done here so that whoever holds the lists, the run or a\n * test holding snapshots, gets the same rows the report carries.\n *\n * Neither fold ranks anything. The lexicon is values per field and the structure\n * is rows in document order; what a reader does with them is the reader's rule,\n * and it is printed where it is applied.\n */\n\n/** The vocabularies a subject is indexed under. Mirrors the report's `LexiconField`. */\nexport type LexiconField =\n | 'example'\n | 'names'\n | 'text'\n | 'components'\n | 'createdBy'\n | 'regions'\n | 'files'\n | 'roles'\n | 'tokens';\n\n\n/** One subject's names, per field, and where each thing was. */\nexport interface SubjectLexicon {\n readonly subject: string;\n /** Attributed boundaries. The only number a rank may read. */\n readonly boundaries: number;\n /** Distinct values per field, code-unit sorted; a field found empty is absent. */\n readonly terms: Partial<Record<LexiconField, readonly string[]>>;\n /** Distinct values a field's cap left out, where it cut. */\n readonly elided?: Partial<Record<LexiconField, number>>;\n\n /**\n * The same reading in the arrangement it was read in, document order.\n *\n * The fields above are what the subject *says*; this is where it said it. A\n * bag can answer that the subject holds the word `carrier` and the word\n * `contract` and can never answer that the second sits under the first, which\n * is the question somebody actually arrives with. Both come out of one walk\n * over one tree, because two walks eventually disagree about what was there.\n *\n * Absent on a subject with no snapshot. There is a real difference between a\n * dialog with nothing on it and a run that never looked.\n */\n readonly landmarks?: readonly Landmark[];\n /** Landmarks the cap left out. */\n readonly elidedLandmarks?: number;\n}\n\n/** What the fold cannot read off the instances and the snapshot. */\nexport interface LexiconOptions {\n /**\n * Subject → the components it is the narrow example of, from the composition.\n * Taken rather than re-derived so the lexicon and the census cannot disagree\n * about which story shows what.\n */\n readonly examples?: ReadonlyMap<string, readonly string[]>;\n /** Component → files declaring it, from the source index. */\n readonly declaredIn?: ReadonlyMap<string, readonly string[]>;\n /** Subject → lexical names of the regions its journey entered, from the journal. */\n readonly regions?: ReadonlyMap<string, readonly string[]>;\n}\n\n/**\n * Distinct values kept per field. Generous, and what it cuts is counted in\n * `elided`, because a cap that says nothing reads as coverage.\n *\n * What it keeps is the narrower promise. Where a cap fires it keeps the values\n * the fewest other subjects hold, not the ones that sort first, because the\n * alternative is a rule about spelling: a subject whose tree is six hundred\n * boundaries deep under an application's worth of wrappers would keep\n * `Anonymous` and `Connect(Account)` and drop the one component it is about,\n * on no better ground than the alphabet. Ties keep code-unit order, so equal\n * values still land in the same place on every machine.\n */\nexport const LEXICON_CAP = 200;\n\n\n/**\n * Every name each subject carries, per field.\n *\n * Fields come from three places and the option that supplies each says which:\n * the instances give `components`, `createdBy` and `tokens`; the snapshot, when\n * the subject carries one, gives `roles`, `names`, `text` and the call-site\n * `files`; the options give `example`, declared `files` and `regions`. A subject\n * with no snapshot still indexes under what its instances hold, which is the\n * honest shape for a sidecar read back without its document.\n *\n * Digests are never values. A text the policy declared volatile reaches the\n * snapshot as `v1:…`, and a reader that matched on it would be matching a\n * coordinate, not a word.\n *\n * Values are kept as they were read and never split. Splitting is a rule, and a\n * rule applied here would be applied to one side of a later comparison only —\n * the reader's query would meet tokens it did not produce. So the fold stores\n * words and the reader owns the rule, which is how a term and a value are\n * always measured by the same instrument.\n */\nexport function lexiconOf(\n subjects: readonly SubjectComposition[],\n options: LexiconOptions = {},\n): readonly SubjectLexicon[] {\n return lexiconOfValues(subjects.map((subject) => lexiconValuesOf(subject, options)));\n}\n\n/**\n * The fold over readings already taken: the suite-wide count, then the cap.\n *\n * Exported for the reader that holds readings and no subjects — the shards of\n * one build, each of which read its own subjects and none of which saw how\n * widely the whole suite holds a value. Handed every shard's readings in plan\n * order, it writes the lexicon one unsharded run would have.\n */\nexport function lexiconOfValues(rows: readonly LexiconValues[]): readonly SubjectLexicon[] {\n const holders = holdersOf(rows);\n\n return rows.map((row) => {\n const terms: Partial<Record<LexiconField, readonly string[]>> = {};\n const elided: Partial<Record<LexiconField, number>> = {};\n for (const field of FIELDS) {\n const values = row.fields[field];\n if (values === undefined || values.length === 0) continue;\n\n // Code-unit first, so the result is byte-stable, and only then by how\n // widely the suite holds the value — a `sort` that keeps the order of\n // equal elements, which every engine's does since ES2019.\n const sorted = [...values].sort(codeUnit);\n const kept = sorted.length <= LEXICON_CAP\n ? sorted\n : [...sorted]\n .sort((left, right) => (holders.get(`${field} ${left}`) ?? 0) - (holders.get(`${field} ${right}`) ?? 0))\n .slice(0, LEXICON_CAP)\n .sort(codeUnit);\n\n terms[field] = kept;\n if (sorted.length > LEXICON_CAP) elided[field] = sorted.length - LEXICON_CAP;\n }\n\n const found = row.landmarks;\n const landmarks = found.length <= LANDMARK_CAP ? found : capLandmarks(found);\n\n return {\n subject: row.subject,\n boundaries: row.boundaries,\n terms,\n ...(Object.keys(elided).length === 0 ? {} : { elided }),\n ...(landmarks.length === 0 ? {} : { landmarks }),\n ...(found.length > LANDMARK_CAP ? { elidedLandmarks: found.length - LANDMARK_CAP } : {}),\n };\n });\n}\n\n/**\n * One subject's values per field before any cap, its landmarks, its boundary\n * count. Plain data, so a shard can write it down and another machine fold it.\n */\nexport interface LexiconValues {\n readonly subject: string;\n /** Distinct values per field, code-unit sorted; a field with none is absent. */\n readonly fields: Partial<Record<LexiconField, readonly string[]>>;\n readonly landmarks: readonly Landmark[];\n readonly boundaries: number;\n}\n\n/**\n * Every value a subject holds, per field, with nothing cut.\n *\n * Split from the fold above because a cap that keeps the values worth keeping\n * has to know how widely the *suite* holds each one, and that is not a fact any\n * single subject carries. So the pass that reads a subject and the pass that\n * decides what survives are two passes, with the counting in between.\n */\nexport function lexiconValuesOf(subject: SubjectComposition, options: LexiconOptions = {}): LexiconValues {\n {\n const fields = new Map<LexiconField, Set<string>>();\n const add = (field: LexiconField, value: string | undefined) => {\n if (value === undefined) return;\n const trimmed = value.trim();\n if (trimmed === '' || isDigest(trimmed)) return;\n let set = fields.get(field);\n if (set === undefined) fields.set(field, (set = new Set()));\n set.add(trimmed);\n };\n\n const held = subject.instances.filter(attributed);\n for (const instance of held) {\n add('components', instance.component);\n add('createdBy', instance.createdBy);\n for (const token of instance.tokens) add('tokens', token);\n for (const file of options.declaredIn?.get(instance.component) ?? []) add('files', file);\n }\n for (const component of options.examples?.get(subject.subject) ?? []) add('example', component);\n for (const region of options.regions?.get(subject.subject) ?? []) add('regions', region);\n\n const landmarks: Landmark[] = [];\n if (subject.snapshot !== undefined) {\n walk(subject.snapshot.root, undefined, (node, within) => {\n add('roles', node.role);\n add('names', node.name);\n add('names', node.description);\n // The three attributes that are a label for a person rather than a\n // value for a machine. `value` is state, `href` is a coordinate, and\n // `class` never survives normalization at all.\n add('names', node.attributes['placeholder']);\n add('names', node.attributes['alt']);\n add('names', node.attributes['title']);\n add('text', node.text);\n add('files', node.provenance?.source?.file);\n\n // The same visit, keeping what the bags drop. See `landmarkFor`.\n const landmark = landmarkFor(node, within);\n if (landmark === undefined) return within;\n const at = landmarks.length;\n landmarks.push(landmark);\n return at;\n });\n }\n\n const read: Partial<Record<LexiconField, readonly string[]>> = {};\n for (const field of FIELDS) {\n const values = fields.get(field);\n if (values !== undefined && values.size > 0) read[field] = [...values].sort(codeUnit);\n }\n return { subject: subject.subject, fields: read, landmarks, boundaries: held.length };\n }\n}\n\n/**\n * A reading with its `example` field taken again from a census.\n *\n * Which subject is the narrow example of a component is decided against every\n * subject that renders it, so a shard's reading answers it for the shard alone.\n * The build that holds every shard's census replaces the field with this.\n */\nexport function withExamples(row: LexiconValues, examples: readonly string[]): LexiconValues {\n const kept = [...new Set(examples.map((value) => value.trim()))]\n .filter((value) => value !== '' && !isDigest(value))\n .sort(codeUnit);\n const { example: _dropped, ...rest } = row.fields;\n return { ...row, fields: kept.length === 0 ? rest : { ...rest, example: kept } };\n}\n\n/**\n * How many subjects hold each value, per field.\n *\n * The one number that separates a name from a fixture. A value nearly every\n * subject carries is structure — a harness wrapper, a provider, an HOC, a\n * chunk every page imports — and a value one subject carries is what\n * distinguishes it. Counted rather than listed, so it costs nothing to be\n * wrong about which framework wrote the wrapper.\n */\nfunction holdersOf(rows: readonly LexiconValues[]): ReadonlyMap<string, number> {\n const holders = new Map<string, number>();\n for (const row of rows) {\n for (const [field, values] of Object.entries(row.fields)) {\n for (const value of values ?? []) {\n const key = `${field} ${value}`;\n holders.set(key, (holders.get(key) ?? 0) + 1);\n }\n }\n }\n return holders;\n}\n\n/** Boundaries of one component at one place in a subject's tree. */\nexport interface BoundaryRow {\n readonly component: string;\n readonly depth: number;\n readonly within?: string;\n readonly createdBy?: string;\n /** Boundaries folded into this row. */\n readonly count: number;\n /** Distinct props digests among them; unknown props count as one. */\n readonly variants: number;\n}\n\n/**\n * One subject's boundaries as rows, document order, identical rows folded.\n *\n * A row's key is where it sits and who put it there: component, depth,\n * enclosing boundary, creator. Not what it rendered, because the question this\n * answers is *what is this subject made of* and three chips under one stack are\n * one answer to it. What differed among them survives as `variants`, so the\n * fold loses the digests and keeps the fact that there were several.\n */\nexport function structureOf(subject: SubjectComposition): readonly BoundaryRow[] {\n const rows = new Map<string, { row: BoundaryRow; props: Set<string> }>();\n\n for (const instance of subject.instances) {\n if (!attributed(instance)) continue;\n const key = [instance.component, instance.depth, instance.within ?? '', instance.createdBy ?? '']\n .join(' ');\n const props = instance.props ?? UNKNOWN;\n const held = rows.get(key);\n if (held === undefined) {\n rows.set(key, { row: rowOf(instance), props: new Set([props]) });\n } else {\n held.props.add(props);\n held.row = { ...held.row, count: held.row.count + 1 };\n }\n }\n\n return [...rows.values()].map(({ row, props }) => ({ ...row, variants: props.size }));\n}\n\nfunction rowOf(instance: ComponentInstance): BoundaryRow {\n return {\n component: instance.component,\n depth: instance.depth,\n ...(instance.within === undefined ? {} : { within: instance.within }),\n ...(instance.createdBy === undefined ? {} : { createdBy: instance.createdBy }),\n count: 1,\n variants: 1,\n };\n}\n\nconst FIELDS: readonly LexiconField[] = [\n 'example',\n 'names',\n 'text',\n 'components',\n 'createdBy',\n 'regions',\n 'files',\n 'roles',\n 'tokens',\n];\n\nconst UNKNOWN = '(unknown props)';\n\n\n\n\n\n\n\n/** Code-unit order: the same bytes on every machine, whatever `LANG` says. */\nfunction codeUnit(left: string, right: string): number {\n return left < right ? -1 : left > right ? 1 : 0;\n}\n"]}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { SemanticNode } from '../format/snapshot.js';
|
|
2
|
+
/**
|
|
3
|
+
* How a value that may carry a structural alias is rewritten before hashing.
|
|
4
|
+
*
|
|
5
|
+
* `undefined` for the per-name hashes, which have always hashed the alias the
|
|
6
|
+
* normalizer assigned and must keep doing so byte for byte. Supplied by the
|
|
7
|
+
* per-instance hashes, which need a boundary-local alias space — see
|
|
8
|
+
* {@link ./instances.js}, where the argument for it lives.
|
|
9
|
+
*/
|
|
10
|
+
export interface Rename {
|
|
11
|
+
readonly attribute: (name: string, value: string) => string;
|
|
12
|
+
readonly style: (value: string) => string;
|
|
13
|
+
readonly alias: (alias: string) => string;
|
|
14
|
+
}
|
|
15
|
+
export declare function renamedAlias(node: SemanticNode, rename?: Rename): string | undefined;
|
|
16
|
+
export declare function renamedAttributes(node: SemanticNode, rename?: Rename): Readonly<Record<string, string>>;
|
|
17
|
+
export declare function renamedTokens(node: SemanticNode, rename?: Rename): Readonly<Record<string, string>> | undefined;
|
|
18
|
+
//# sourceMappingURL=rename.d.ts.map
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export function renamedAlias(node, rename) {
|
|
2
|
+
if (rename === undefined || node.alias === undefined)
|
|
3
|
+
return node.alias;
|
|
4
|
+
return rename.alias(node.alias);
|
|
5
|
+
}
|
|
6
|
+
export function renamedAttributes(node, rename) {
|
|
7
|
+
if (rename === undefined)
|
|
8
|
+
return node.attributes;
|
|
9
|
+
const renamed = {};
|
|
10
|
+
for (const [name, value] of Object.entries(node.attributes)) {
|
|
11
|
+
renamed[name] = rename.attribute(name, value);
|
|
12
|
+
}
|
|
13
|
+
return renamed;
|
|
14
|
+
}
|
|
15
|
+
export function renamedTokens(node, rename) {
|
|
16
|
+
if (rename === undefined || node.tokens === undefined)
|
|
17
|
+
return node.tokens;
|
|
18
|
+
const renamed = {};
|
|
19
|
+
for (const [name, value] of Object.entries(node.tokens)) {
|
|
20
|
+
renamed[name] = rename.style(value);
|
|
21
|
+
}
|
|
22
|
+
return renamed;
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=rename.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rename.js","sourceRoot":"","sources":["../../src/attribute/rename.ts"],"names":[],"mappings":"AAgBA,MAAM,UAAU,YAAY,CAAC,IAAkB,EAAE,MAAe;IAC9D,IAAI,MAAM,KAAK,SAAS,IAAI,IAAI,CAAC,KAAK,KAAK,SAAS;QAAE,OAAO,IAAI,CAAC,KAAK,CAAC;IACxE,OAAO,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;AAClC,CAAC;AAED,MAAM,UAAU,iBAAiB,CAC/B,IAAkB,EAClB,MAAe;IAEf,IAAI,MAAM,KAAK,SAAS;QAAE,OAAO,IAAI,CAAC,UAAU,CAAC;IAEjD,MAAM,OAAO,GAA2B,EAAE,CAAC;IAC3C,KAAK,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,UAAU,CAAC,EAAE,CAAC;QAC5D,OAAO,CAAC,IAAI,CAAC,GAAG,MAAM,CAAC,SAAS,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;IAChD,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,MAAM,UAAU,aAAa,CAC3B,IAAkB,EAClB,MAAe;IAEf,IAAI,MAAM,KAAK,SAAS,IAAI,IAAI,CAAC,MAAM,KAAK,SAAS;QAAE,OAAO,IAAI,CAAC,MAAM,CAAC;IAE1E,MAAM,OAAO,GAA2B,EAAE,CAAC;IAC3C,KAAK,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC;QACxD,OAAO,CAAC,IAAI,CAAC,GAAG,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IACtC,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC","sourcesContent":["import type { SemanticNode } from '../format/snapshot.js';\n\n/**\n * How a value that may carry a structural alias is rewritten before hashing.\n *\n * `undefined` for the per-name hashes, which have always hashed the alias the\n * normalizer assigned and must keep doing so byte for byte. Supplied by the\n * per-instance hashes, which need a boundary-local alias space — see\n * {@link ./instances.js}, where the argument for it lives.\n */\nexport interface Rename {\n readonly attribute: (name: string, value: string) => string;\n readonly style: (value: string) => string;\n readonly alias: (alias: string) => string;\n}\n\nexport function renamedAlias(node: SemanticNode, rename?: Rename): string | undefined {\n if (rename === undefined || node.alias === undefined) return node.alias;\n return rename.alias(node.alias);\n}\n\nexport function renamedAttributes(\n node: SemanticNode,\n rename?: Rename,\n): Readonly<Record<string, string>> {\n if (rename === undefined) return node.attributes;\n\n const renamed: Record<string, string> = {};\n for (const [name, value] of Object.entries(node.attributes)) {\n renamed[name] = rename.attribute(name, value);\n }\n return renamed;\n}\n\nexport function renamedTokens(\n node: SemanticNode,\n rename?: Rename,\n): Readonly<Record<string, string>> | undefined {\n if (rename === undefined || node.tokens === undefined) return node.tokens;\n\n const renamed: Record<string, string> = {};\n for (const [name, value] of Object.entries(node.tokens)) {\n renamed[name] = rename.style(value);\n }\n return renamed;\n}\n"]}
|
|
@@ -17,6 +17,8 @@ import type { MovedInput } from './parting.js';
|
|
|
17
17
|
* from somewhere else — which is the whole reading below.
|
|
18
18
|
*/
|
|
19
19
|
export declare function declaredIn(snapshot: SemanticSnapshot): ReadonlySet<string>;
|
|
20
|
+
/** Whether a declaration set `property` at the node at `path`, per {@link declaredIn}. */
|
|
21
|
+
export declare function declares(declared: ReadonlySet<string>, path: string, property: string): boolean;
|
|
20
22
|
/**
|
|
21
23
|
* What an ancestor decided, and the rung that stops this being called a flake.
|
|
22
24
|
*
|
package/dist/compare/cascade.js
CHANGED
|
@@ -18,6 +18,10 @@ import { digestValue } from '../format/hash.js';
|
|
|
18
18
|
export function declaredIn(snapshot) {
|
|
19
19
|
return new Set(snapshot.styleProvenance.map(keyOf));
|
|
20
20
|
}
|
|
21
|
+
/** Whether a declaration set `property` at the node at `path`, per {@link declaredIn}. */
|
|
22
|
+
export function declares(declared, path, property) {
|
|
23
|
+
return declared.has(`${path}\u0000${property}`);
|
|
24
|
+
}
|
|
21
25
|
function keyOf(entry) {
|
|
22
26
|
return `${entry.path}\u0000${entry.property}`;
|
|
23
27
|
}
|
|
@@ -62,7 +66,7 @@ export function cascadeInputs(left, right, there, here) {
|
|
|
62
66
|
function inheritedAt(node, declared) {
|
|
63
67
|
const found = new Map();
|
|
64
68
|
for (const [property, value] of Object.entries(node.style)) {
|
|
65
|
-
if (declared
|
|
69
|
+
if (declares(declared, node.path, property))
|
|
66
70
|
continue;
|
|
67
71
|
found.set(property, digestValue(value));
|
|
68
72
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cascade.js","sourceRoot":"","sources":["../../src/compare/cascade.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAe,MAAM,mBAAmB,CAAC;AAI7D;;;;;;;;GAQG;AAEH;;;;;;GAMG;AACH,MAAM,UAAU,UAAU,CAAC,QAA0B;IACnD,OAAO,IAAI,GAAG,CAAC,QAAQ,CAAC,eAAe,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC;AACtD,CAAC;AAED,SAAS,KAAK,CAAC,KAA2B;IACxC,OAAO,GAAG,KAAK,CAAC,IAAI,SAAS,KAAK,CAAC,QAAQ,EAAE,CAAC;AAChD,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,UAAU,aAAa,CAC3B,IAA8B,EAC9B,KAAmB,EACnB,KAA0B,EAC1B,IAAyB;IAEzB,IAAI,IAAI,KAAK,SAAS;QAAE,OAAO,EAAE,CAAC;IAElC,MAAM,MAAM,GAAG,WAAW,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;IACxC,MAAM,KAAK,GAAG,WAAW,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;IACvC,MAAM,KAAK,GAAiB,EAAE,CAAC;IAE/B,KAAK,MAAM,IAAI,IAAI,CAAC,GAAG,IAAI,GAAG,CAAC,CAAC,GAAG,MAAM,CAAC,IAAI,EAAE,EAAE,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC;QAC5E,MAAM,IAAI,GAAG,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QAC9B,MAAM,EAAE,GAAG,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QAC3B,IAAI,IAAI,KAAK,EAAE;YAAE,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC,CAAC;IACrE,CAAC;IAED,OAAO,KAAK,CAAC;AACf,CAAC;AAED,SAAS,WAAW,CAAC,IAAkB,EAAE,QAA6B;IACpE,MAAM,KAAK,GAAG,IAAI,GAAG,EAAkB,CAAC;IACxC,KAAK,MAAM,CAAC,QAAQ,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;QAC3D,IAAI,QAAQ,CAAC,
|
|
1
|
+
{"version":3,"file":"cascade.js","sourceRoot":"","sources":["../../src/compare/cascade.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAe,MAAM,mBAAmB,CAAC;AAI7D;;;;;;;;GAQG;AAEH;;;;;;GAMG;AACH,MAAM,UAAU,UAAU,CAAC,QAA0B;IACnD,OAAO,IAAI,GAAG,CAAC,QAAQ,CAAC,eAAe,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC;AACtD,CAAC;AAED,0FAA0F;AAC1F,MAAM,UAAU,QAAQ,CAAC,QAA6B,EAAE,IAAY,EAAE,QAAgB;IACpF,OAAO,QAAQ,CAAC,GAAG,CAAC,GAAG,IAAI,SAAS,QAAQ,EAAE,CAAC,CAAC;AAClD,CAAC;AAED,SAAS,KAAK,CAAC,KAA2B;IACxC,OAAO,GAAG,KAAK,CAAC,IAAI,SAAS,KAAK,CAAC,QAAQ,EAAE,CAAC;AAChD,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,UAAU,aAAa,CAC3B,IAA8B,EAC9B,KAAmB,EACnB,KAA0B,EAC1B,IAAyB;IAEzB,IAAI,IAAI,KAAK,SAAS;QAAE,OAAO,EAAE,CAAC;IAElC,MAAM,MAAM,GAAG,WAAW,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;IACxC,MAAM,KAAK,GAAG,WAAW,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;IACvC,MAAM,KAAK,GAAiB,EAAE,CAAC;IAE/B,KAAK,MAAM,IAAI,IAAI,CAAC,GAAG,IAAI,GAAG,CAAC,CAAC,GAAG,MAAM,CAAC,IAAI,EAAE,EAAE,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC;QAC5E,MAAM,IAAI,GAAG,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QAC9B,MAAM,EAAE,GAAG,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QAC3B,IAAI,IAAI,KAAK,EAAE;YAAE,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC,CAAC;IACrE,CAAC;IAED,OAAO,KAAK,CAAC;AACf,CAAC;AAED,SAAS,WAAW,CAAC,IAAkB,EAAE,QAA6B;IACpE,MAAM,KAAK,GAAG,IAAI,GAAG,EAAkB,CAAC;IACxC,KAAK,MAAM,CAAC,QAAQ,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;QAC3D,IAAI,QAAQ,CAAC,QAAQ,EAAE,IAAI,CAAC,IAAI,EAAE,QAAQ,CAAC;YAAE,SAAS;QACtD,KAAK,CAAC,GAAG,CAAC,QAAQ,EAAE,WAAW,CAAC,KAAK,CAAC,CAAC,CAAC;IAC1C,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC","sourcesContent":["import { digestValue, type Digest } from '../format/hash.js';\nimport type { SemanticNode, SemanticSnapshot, StyleProvenanceEntry } from '../format/snapshot.js';\nimport type { MovedInput } from './parting.js';\n\n/**\n * The input nobody passes.\n *\n * Every rung in `parting.ts` above this one reads something a boundary\n * *received* — a prop, a context, a hook cell — and all three arrive through a\n * framework adapter. This one reads what the boundary did not receive and did\n * not declare, out of the snapshot itself, which is why it is the only rung a\n * run with no adapter attached can reach.\n */\n\n/**\n * Every property some declaration set *at* a node, keyed by node and name.\n *\n * `styleProvenance` records where each winning declaration came from, so the\n * properties it does not mention at a node are exactly the ones that arrived\n * from somewhere else — which is the whole reading below.\n */\nexport function declaredIn(snapshot: SemanticSnapshot): ReadonlySet<string> {\n return new Set(snapshot.styleProvenance.map(keyOf));\n}\n\n/** Whether a declaration set `property` at the node at `path`, per {@link declaredIn}. */\nexport function declares(declared: ReadonlySet<string>, path: string, property: string): boolean {\n return declared.has(`${path}\\u0000${property}`);\n}\n\nfunction keyOf(entry: StyleProvenanceEntry): string {\n return `${entry.path}\\u0000${entry.property}`;\n}\n\n/**\n * What an ancestor decided, and the rung that stops this being called a flake.\n *\n * A component that sets its own font and no colour paints white on a dark panel\n * and near-black on a light card from one set of props. Every input the holding\n * carries — props, contexts, hook cells — is byte-identical across the two, so\n * before this existed the boundary settled at `undetermined` and `explainParting`\n * called a perfectly deterministic component *nondeterministic*. That is the\n * accusation ADR-0002 forbids making about an input nobody read, and the input\n * was there to be read: the resolved value is on the node and `styleProvenance`\n * says no declaration here produced it.\n *\n * So the reading is the difference between those two facts — **in the style,\n * not in the provenance** — and a property that differs on that test came from\n * an ancestor's cascade. It ranks with `handed` and `provided` because it says\n * the same thing they do: the cause is above this boundary, not in it.\n *\n * Values are digested rather than carried, so that every `MovedInput` means one\n * thing regardless of kind. The property name is the part a reader acts on, and\n * the deltas already name what it turned into on the page.\n *\n * Unpaired boundaries contribute nothing: a component present on one side only\n * has not inherited differently, it has not been compared.\n */\nexport function cascadeInputs(\n left: SemanticNode | undefined,\n right: SemanticNode,\n there: ReadonlySet<string>,\n here: ReadonlySet<string>,\n): readonly MovedInput[] {\n if (left === undefined) return [];\n\n const before = inheritedAt(left, there);\n const after = inheritedAt(right, here);\n const moved: MovedInput[] = [];\n\n for (const name of [...new Set([...before.keys(), ...after.keys()])].sort()) {\n const from = before.get(name);\n const to = after.get(name);\n if (from !== to) moved.push({ kind: 'inherited', name, from, to });\n }\n\n return moved;\n}\n\nfunction inheritedAt(node: SemanticNode, declared: ReadonlySet<string>): Map<string, Digest> {\n const found = new Map<string, Digest>();\n for (const [property, value] of Object.entries(node.style)) {\n if (declares(declared, node.path, property)) continue;\n found.set(property, digestValue(value));\n }\n return found;\n}\n"]}
|
package/dist/format/hash.d.ts
CHANGED
|
@@ -68,4 +68,24 @@ export declare function digestCombine(domain: string, parts: readonly Digest[]):
|
|
|
68
68
|
* earns a digest of the whole id, which is what actually distinguishes it.
|
|
69
69
|
*/
|
|
70
70
|
export declare function fileNameFor(id: string): string;
|
|
71
|
+
/**
|
|
72
|
+
* A digest spelled so that a filesystem accepts it: `v1-<hex>` for `v1:<hex>`.
|
|
73
|
+
*
|
|
74
|
+
* The colon is the one character in a digest that is not portable. NTFS refuses
|
|
75
|
+
* it in a name, so a directory named by a raw digest is a repository that cannot
|
|
76
|
+
* be checked out on Windows, and `actions/upload-artifact` refuses the path for
|
|
77
|
+
* the same reason. Every name made from a digest goes through here, so the
|
|
78
|
+
* spelling has one owner and {@link digestOfFileName} can read it back.
|
|
79
|
+
*/
|
|
80
|
+
export declare function digestFileName(digest: Digest): string;
|
|
81
|
+
/**
|
|
82
|
+
* The digest a name spells, or `undefined` because it spells none.
|
|
83
|
+
*
|
|
84
|
+
* Reads the raw `v1:<hex>` spelling as well as {@link digestFileName}'s. The
|
|
85
|
+
* file-backed store named its identity partitions with the raw digest before
|
|
86
|
+
* this spelling existed, and a baseline kept under that name is still a
|
|
87
|
+
* baseline: a reader that knew only the new spelling would call it `new` and
|
|
88
|
+
* record over it.
|
|
89
|
+
*/
|
|
90
|
+
export declare function digestOfFileName(name: string): Digest | undefined;
|
|
71
91
|
//# sourceMappingURL=hash.d.ts.map
|
package/dist/format/hash.js
CHANGED
|
@@ -77,6 +77,32 @@ export function fileNameFor(id) {
|
|
|
77
77
|
const encoded = encodeURIComponent(id);
|
|
78
78
|
if (encoded.length <= NAME_BUDGET)
|
|
79
79
|
return encoded;
|
|
80
|
-
return `${encoded.slice(0, NAME_BUDGET)}~${digestString(id)
|
|
80
|
+
return `${encoded.slice(0, NAME_BUDGET)}~${digestFileName(digestString(id))}`;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* A digest spelled so that a filesystem accepts it: `v1-<hex>` for `v1:<hex>`.
|
|
84
|
+
*
|
|
85
|
+
* The colon is the one character in a digest that is not portable. NTFS refuses
|
|
86
|
+
* it in a name, so a directory named by a raw digest is a repository that cannot
|
|
87
|
+
* be checked out on Windows, and `actions/upload-artifact` refuses the path for
|
|
88
|
+
* the same reason. Every name made from a digest goes through here, so the
|
|
89
|
+
* spelling has one owner and {@link digestOfFileName} can read it back.
|
|
90
|
+
*/
|
|
91
|
+
export function digestFileName(digest) {
|
|
92
|
+
return digest.replace(':', '-');
|
|
93
|
+
}
|
|
94
|
+
const FILE_NAME = new RegExp(`^${PREFIX}[-:]([0-9a-f]{${HEX_LENGTH}})$`);
|
|
95
|
+
/**
|
|
96
|
+
* The digest a name spells, or `undefined` because it spells none.
|
|
97
|
+
*
|
|
98
|
+
* Reads the raw `v1:<hex>` spelling as well as {@link digestFileName}'s. The
|
|
99
|
+
* file-backed store named its identity partitions with the raw digest before
|
|
100
|
+
* this spelling existed, and a baseline kept under that name is still a
|
|
101
|
+
* baseline: a reader that knew only the new spelling would call it `new` and
|
|
102
|
+
* record over it.
|
|
103
|
+
*/
|
|
104
|
+
export function digestOfFileName(name) {
|
|
105
|
+
const hex = FILE_NAME.exec(name)?.[1];
|
|
106
|
+
return hex === undefined ? undefined : `${PREFIX}:${hex}`;
|
|
81
107
|
}
|
|
82
108
|
//# sourceMappingURL=hash.js.map
|