@variance-authority/core 0.9.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/README.md +46 -20
  3. package/dist/attribute/boundary.d.ts +21 -14
  4. package/dist/attribute/boundary.js +23 -29
  5. package/dist/attribute/boundary.js.map +1 -1
  6. package/dist/attribute/component-hash.d.ts +21 -0
  7. package/dist/attribute/component-hash.js +48 -3
  8. package/dist/attribute/component-hash.js.map +1 -1
  9. package/dist/attribute/index.d.ts +3 -3
  10. package/dist/attribute/index.js +1 -1
  11. package/dist/attribute/index.js.map +1 -1
  12. package/dist/attribute/lexicon.d.ts +37 -0
  13. package/dist/attribute/lexicon.js +40 -12
  14. package/dist/attribute/lexicon.js.map +1 -1
  15. package/dist/attribute/rename.d.ts +18 -0
  16. package/dist/attribute/rename.js +24 -0
  17. package/dist/attribute/rename.js.map +1 -0
  18. package/dist/compare/cascade.d.ts +2 -0
  19. package/dist/compare/cascade.js +5 -1
  20. package/dist/compare/cascade.js.map +1 -1
  21. package/dist/compare/observability.d.ts +1 -1
  22. package/dist/compare/observability.js +1 -1
  23. package/dist/compare/observability.js.map +1 -1
  24. package/dist/format/hash.d.ts +20 -0
  25. package/dist/format/hash.js +27 -1
  26. package/dist/format/hash.js.map +1 -1
  27. package/dist/format/index.d.ts +1 -1
  28. package/dist/format/index.js +1 -1
  29. package/dist/format/index.js.map +1 -1
  30. package/dist/format/snapshot.d.ts +15 -0
  31. package/dist/format/snapshot.js.map +1 -1
  32. package/dist/share/http-line.d.ts +30 -0
  33. package/dist/share/http-line.js +164 -0
  34. package/dist/share/http-line.js.map +1 -0
  35. package/dist/share/index.d.ts +9 -126
  36. package/dist/share/index.js +9 -168
  37. package/dist/share/index.js.map +1 -1
  38. package/dist/share/line.d.ts +153 -0
  39. package/dist/share/line.js +184 -0
  40. package/dist/share/line.js.map +1 -0
  41. package/dist/share/publish.d.ts +79 -0
  42. package/dist/share/publish.js +231 -0
  43. package/dist/share/publish.js.map +1 -0
  44. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"observability.js","sourceRoot":"","sources":["../../src/compare/observability.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAGH,OAAO,EAAE,KAAK,EAAa,MAAM,WAAW,CAAC;AAK7C;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,UAAU,eAAe,CAAC,OAA2B;IACzD,OAAO;QACL,IAAI,EAAE,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM;QACxC,QAAQ,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,iBAAiB,CAAC,CAAC,CAAC,MAAM;QACjF,KAAK,EAAE,OAAO,CAAC,aAAa,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,aAAa,CAAC,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,MAAM;QACxF,0EAA0E;QAC1E,gDAAgD;QAChD,OAAO,EAAE,MAAM;QACf,OAAO,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM;KAC1C,CAAC;AACJ,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,WAAW,CAAC,IAAU,EAAE,KAAoB;IAC1D,OAAO,IAAI,KAAK,UAAU,CAAC,CAAC,CAAC,KAAK,KAAK,MAAM,CAAC,CAAC,CAAC,KAAK,KAAK,MAAM,CAAC;AACnE,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,IAAI,GAA4C;IACpD,IAAI,EAAE,CAAC;IACP,iBAAiB,EAAE,CAAC;IACpB,eAAe,EAAE,CAAC;IAClB,IAAI,EAAE,CAAC;CACR,CAAC;AAEF,MAAM,UAAU,MAAM,CAAC,CAAgB,EAAE,CAAgB;IACvD,OAAO,IAAI,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AACpC,CAAC;AAED,0DAA0D;AAC1D,MAAM,UAAU,mBAAmB,CACjC,CAAqB,EACrB,CAAqB;IAErB,MAAM,IAAI,GAAG,eAAe,CAAC,CAAC,CAAC,CAAC;IAChC,MAAM,KAAK,GAAG,eAAe,CAAC,CAAC,CAAC,CAAC;IAEjC,OAAO,MAAM,CAAC,WAAW,CACvB,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAC9B,CAAC;AACnC,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,eAAe,CAAC,MAA6C;IAC3E,OAAO,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,WAAW,CAAC,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClE,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,aAAa,CAAC,MAA6C;IACzE,OAAO,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,WAAW,CAAC,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,KAAK,MAAM,CAAC,CAAC;AAC5F,CAAC","sourcesContent":["/**\n * What a profile can decide, band by band.\n *\n * This lives in `compare` rather than beside `ObservationProfile` in `format`\n * because it is a statement about *bands*, and a band classifies a delta. Core's\n * entrypoints run format → rules → compare → attribute → judge → plan, and a\n * `format` module importing `Band` would point that arrow backwards. The profile\n * describes a collector's capability; mapping that capability onto what can be\n * settled is the comparison layer's question.\n */\n\nimport type { ObservationProfile } from '../format/profile.js';\nimport { BANDS, type Band } from './band.js';\n\n/** How completely a profile can decide one band. */\nexport type Observability = 'full' | 'structural-only' | 'declared-only' | 'none';\n\n/**\n * Bands a profile is able to decide. A band this returns `none` for can never\n * resolve to `unchanged` under that profile — it resolves to `unobserved`.\n *\n * JSDOM sees structural geometry (nodes appearing, disappearing, reordering)\n * because that is tree shape, but not metric geometry (moves, resizes) because\n * that requires an engine. `geometry` is therefore only partially observable,\n * and partial observation of a blocking band is reported, never assumed away.\n *\n * The two bands that need no engine are worth stating rather than leaving to be\n * inferred: **`a11y` and `content` are fully decidable in the unit-test\n * process.** A role, an accessible name, an ARIA state and a text node are facts\n * about a document, so a dropped `aria-label` and a changed string are settled at\n * the cheapest tier this project has — no browser, no screenshot, no baseline\n * image. That is the concrete form of `docs/comparison.md` §3.3: the category no\n * raster comparison can reach is also the category that costs least to reach.\n *\n * The return type is `Record<Band, …>` on purpose. Declaring a new band and\n * forgetting to say whether a profile can observe it is a compile error, not a\n * silent `undefined` that a truthiness check reads as \"no\".\n */\nexport function observableBands(profile: ObservationProfile): Record<Band, Observability> {\n return {\n a11y: profile.ariaTree ? 'full' : 'none',\n geometry: profile.layout ? 'full' : profile.ariaTree ? 'structural-only' : 'none',\n token: profile.computedStyle ? 'full' : profile.declaredStyle ? 'declared-only' : 'none',\n // Text is the one band no collector can fail at: having a document is the\n // precondition for producing a snapshot at all.\n content: 'full',\n texture: profile.raster ? 'full' : 'none',\n };\n}\n\n/**\n * Whether a level is enough to settle its band, or whether the band must be\n * reported `unobserved`.\n *\n * The asymmetry between `geometry` and everything else is ADR-0002 and is\n * deliberate. `structural-only` geometry is *not* enough: a profile that sees\n * tree shape but not metrics would report a box that moved as `unchanged`, which\n * is the one failure this product must never produce. `declared-only` style *is*\n * enough — an author's declaration is a real answer about the token band, just a\n * narrower one than a resolved cascade.\n */\nexport function decidesBand(band: Band, level: Observability): boolean {\n return band === 'geometry' ? level === 'full' : level !== 'none';\n}\n\n/**\n * How completely a band was decided when two profiles both had to decide it.\n *\n * The weaker reading wins, which is the same union rule `unobserved` is built\n * from carried one value further. A pair is only as well observed as its blinder\n * side, and reporting the better side's level would describe a reading nobody\n * took.\n *\n * The two partial levels never meet: `structural-only` is a geometry answer and\n * `declared-only` a token one, and a band produces at most one of them. So the\n * ranking needs no tie-break between them, and a tie is two spellings of one\n * level.\n */\nconst RANK: Readonly<Record<Observability, number>> = {\n none: 0,\n 'structural-only': 1,\n 'declared-only': 1,\n full: 2,\n};\n\nexport function weaker(a: Observability, b: Observability): Observability {\n return RANK[a] <= RANK[b] ? a : b;\n}\n\n/** The weaker of two profiles' readings, band by band. */\nexport function sharedObservability(\n a: ObservationProfile,\n b: ObservationProfile,\n): Record<Band, Observability> {\n const here = observableBands(a);\n const there = observableBands(b);\n\n return Object.fromEntries(\n BANDS.map((band) => [band, weaker(here[band], there[band])]),\n ) as Record<Band, Observability>;\n}\n\n/**\n * Bands a comparison could not decide at all.\n *\n * Derived from {@link sharedObservability} rather than computed beside it: a band\n * is unobserved exactly when the weaker of the two levels does not decide it, and\n * two expressions of one rule are two things that can disagree.\n */\nexport function unobservedBands(levels: Readonly<Record<Band, Observability>>): readonly Band[] {\n return BANDS.filter((band) => !decidesBand(band, levels[band]));\n}\n\n/**\n * Bands that were decided, but on less than the evidence the band is made of.\n *\n * The half of observability that has no verdict to hide behind. `unobserved` is\n * loud — a band with no answer is reported as having none — while a band decided\n * `declared-only` returns `unchanged`, in the same word a resolved cascade\n * returns, over a narrower question: JSDOM compares what an author wrote, so\n * `padding: 1rem` stays `1rem` and a root font-size that moved underneath it is\n * not in the comparison. That is a real answer and the reason the level decides\n * its band at all — but it is not the answer the word `unchanged` promises, and\n * a reader given the word without the level cannot tell which one they have.\n */\nexport function narrowedBands(levels: Readonly<Record<Band, Observability>>): readonly Band[] {\n return BANDS.filter((band) => decidesBand(band, levels[band]) && levels[band] !== 'full');\n}\n"]}
1
+ {"version":3,"file":"observability.js","sourceRoot":"","sources":["../../src/compare/observability.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAGH,OAAO,EAAE,KAAK,EAAa,MAAM,WAAW,CAAC;AAK7C;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,UAAU,eAAe,CAAC,OAA2B;IACzD,OAAO;QACL,IAAI,EAAE,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM;QACxC,QAAQ,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,iBAAiB,CAAC,CAAC,CAAC,MAAM;QACjF,KAAK,EAAE,OAAO,CAAC,aAAa,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,aAAa,CAAC,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,MAAM;QACxF,0EAA0E;QAC1E,gDAAgD;QAChD,OAAO,EAAE,MAAM;QACf,OAAO,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM;KAC1C,CAAC;AACJ,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,WAAW,CAAC,IAAU,EAAE,KAAoB;IAC1D,OAAO,IAAI,KAAK,UAAU,CAAC,CAAC,CAAC,KAAK,KAAK,MAAM,CAAC,CAAC,CAAC,KAAK,KAAK,MAAM,CAAC;AACnE,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,IAAI,GAA4C;IACpD,IAAI,EAAE,CAAC;IACP,iBAAiB,EAAE,CAAC;IACpB,eAAe,EAAE,CAAC;IAClB,IAAI,EAAE,CAAC;CACR,CAAC;AAEF,MAAM,UAAU,MAAM,CAAC,CAAgB,EAAE,CAAgB;IACvD,OAAO,IAAI,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AACpC,CAAC;AAED,0DAA0D;AAC1D,MAAM,UAAU,mBAAmB,CACjC,CAAqB,EACrB,CAAqB;IAErB,MAAM,IAAI,GAAG,eAAe,CAAC,CAAC,CAAC,CAAC;IAChC,MAAM,KAAK,GAAG,eAAe,CAAC,CAAC,CAAC,CAAC;IAEjC,OAAO,MAAM,CAAC,WAAW,CACvB,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAC9B,CAAC;AACnC,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,eAAe,CAAC,MAA6C;IAC3E,OAAO,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,WAAW,CAAC,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClE,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,aAAa,CAAC,MAA6C;IACzE,OAAO,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,WAAW,CAAC,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,KAAK,MAAM,CAAC,CAAC;AAC5F,CAAC","sourcesContent":["/**\n * What a profile can decide, band by band.\n *\n * This lives in `compare` rather than beside `ObservationProfile` in `format`\n * because it is a statement about *bands*, and a band classifies a delta. Core's\n * entrypoints run format → rules → compare → attribute → judge → plan, and a\n * `format` module importing `Band` would point that arrow backwards. The profile\n * describes a collector's capability; mapping that capability onto what can be\n * settled is the comparison layer's question.\n */\n\nimport type { ObservationProfile } from '../format/profile.js';\nimport { BANDS, type Band } from './band.js';\n\n/** How completely a profile can decide one band. */\nexport type Observability = 'full' | 'structural-only' | 'declared-only' | 'none';\n\n/**\n * Bands a profile is able to decide. A band this returns `none` for can never\n * resolve to `unchanged` under that profile — it resolves to `unobserved`.\n *\n * JSDOM sees structural geometry (nodes appearing, disappearing, reordering)\n * because that is tree shape, but not metric geometry (moves, resizes) because\n * that requires an engine. `geometry` is therefore only partially observable,\n * and partial observation of a blocking band is reported, never assumed away.\n *\n * The two bands that need no engine are worth stating rather than leaving to be\n * inferred: **`a11y` and `content` are fully decidable in the unit-test\n * process.** A role, an accessible name, an ARIA state and a text node are facts\n * about a document, so a dropped `aria-label` and a changed string are settled at\n * the cheapest tier this project has — no browser, no screenshot, no baseline\n * image. That is the concrete form of `docs/compare-visual-review.md` §3.3: the category no\n * raster comparison can reach is also the category that costs least to reach.\n *\n * The return type is `Record<Band, …>` on purpose. Declaring a new band and\n * forgetting to say whether a profile can observe it is a compile error, not a\n * silent `undefined` that a truthiness check reads as \"no\".\n */\nexport function observableBands(profile: ObservationProfile): Record<Band, Observability> {\n return {\n a11y: profile.ariaTree ? 'full' : 'none',\n geometry: profile.layout ? 'full' : profile.ariaTree ? 'structural-only' : 'none',\n token: profile.computedStyle ? 'full' : profile.declaredStyle ? 'declared-only' : 'none',\n // Text is the one band no collector can fail at: having a document is the\n // precondition for producing a snapshot at all.\n content: 'full',\n texture: profile.raster ? 'full' : 'none',\n };\n}\n\n/**\n * Whether a level is enough to settle its band, or whether the band must be\n * reported `unobserved`.\n *\n * The asymmetry between `geometry` and everything else is ADR-0002 and is\n * deliberate. `structural-only` geometry is *not* enough: a profile that sees\n * tree shape but not metrics would report a box that moved as `unchanged`, which\n * is the one failure this product must never produce. `declared-only` style *is*\n * enough — an author's declaration is a real answer about the token band, just a\n * narrower one than a resolved cascade.\n */\nexport function decidesBand(band: Band, level: Observability): boolean {\n return band === 'geometry' ? level === 'full' : level !== 'none';\n}\n\n/**\n * How completely a band was decided when two profiles both had to decide it.\n *\n * The weaker reading wins, which is the same union rule `unobserved` is built\n * from carried one value further. A pair is only as well observed as its blinder\n * side, and reporting the better side's level would describe a reading nobody\n * took.\n *\n * The two partial levels never meet: `structural-only` is a geometry answer and\n * `declared-only` a token one, and a band produces at most one of them. So the\n * ranking needs no tie-break between them, and a tie is two spellings of one\n * level.\n */\nconst RANK: Readonly<Record<Observability, number>> = {\n none: 0,\n 'structural-only': 1,\n 'declared-only': 1,\n full: 2,\n};\n\nexport function weaker(a: Observability, b: Observability): Observability {\n return RANK[a] <= RANK[b] ? a : b;\n}\n\n/** The weaker of two profiles' readings, band by band. */\nexport function sharedObservability(\n a: ObservationProfile,\n b: ObservationProfile,\n): Record<Band, Observability> {\n const here = observableBands(a);\n const there = observableBands(b);\n\n return Object.fromEntries(\n BANDS.map((band) => [band, weaker(here[band], there[band])]),\n ) as Record<Band, Observability>;\n}\n\n/**\n * Bands a comparison could not decide at all.\n *\n * Derived from {@link sharedObservability} rather than computed beside it: a band\n * is unobserved exactly when the weaker of the two levels does not decide it, and\n * two expressions of one rule are two things that can disagree.\n */\nexport function unobservedBands(levels: Readonly<Record<Band, Observability>>): readonly Band[] {\n return BANDS.filter((band) => !decidesBand(band, levels[band]));\n}\n\n/**\n * Bands that were decided, but on less than the evidence the band is made of.\n *\n * The half of observability that has no verdict to hide behind. `unobserved` is\n * loud — a band with no answer is reported as having none — while a band decided\n * `declared-only` returns `unchanged`, in the same word a resolved cascade\n * returns, over a narrower question: JSDOM compares what an author wrote, so\n * `padding: 1rem` stays `1rem` and a root font-size that moved underneath it is\n * not in the comparison. That is a real answer and the reason the level decides\n * its band at all — but it is not the answer the word `unchanged` promises, and\n * a reader given the word without the level cannot tell which one they have.\n */\nexport function narrowedBands(levels: Readonly<Record<Band, Observability>>): readonly Band[] {\n return BANDS.filter((band) => decidesBand(band, levels[band]) && levels[band] !== 'full');\n}\n"]}
@@ -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
@@ -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).replace(':', '-')}`;
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
@@ -1 +1 @@
1
- {"version":3,"file":"hash.js","sourceRoot":"","sources":["../../src/format/hash.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAuB,MAAM,gBAAgB,CAAC;AACnE,OAAO,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAkBxD,MAAM,MAAM,GAAG,IAAI,CAAC;AACpB,MAAM,UAAU,GAAG,EAAE,CAAC;AAEtB;;;;;;;GAOG;AACH,MAAM,UAAU,YAAY,CAAC,KAAa;IACxC,OAAO,GAAG,MAAM,IAAI,SAAS,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,UAAU,CAAC,EAAE,CAAC;AAC9D,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,cAAc,CAAC,GAAW;IACxC,OAAO,GAAG,MAAM,IAAI,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,UAAU,CAAC,EAAE,CAAC;AACjD,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,WAAW,CAAC,KAAiB;IAC3C,OAAO,GAAG,MAAM,IAAI,cAAc,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,UAAU,CAAC,EAAE,CAAC;AACnE,CAAC;AAED,wEAAwE;AACxE,MAAM,UAAU,WAAW,CAAC,KAAqB;IAC/C,OAAO,YAAY,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC,CAAC;AAC3C,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,aAAa,CAAC,MAAc,EAAE,KAAwB;IACpE,OAAO,YAAY,CAAC,GAAG,MAAM,SAAS,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC;AAChE,CAAC;AAED;;;;;;GAMG;AACH,MAAM,WAAW,GAAG,GAAG,CAAC;AAExB;;;;;;;;;GASG;AACH,MAAM,UAAU,WAAW,CAAC,EAAU;IACpC,MAAM,OAAO,GAAG,kBAAkB,CAAC,EAAE,CAAC,CAAC;IACvC,IAAI,OAAO,CAAC,MAAM,IAAI,WAAW;QAAE,OAAO,OAAO,CAAC;IAClD,OAAO,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,WAAW,CAAC,IAAI,YAAY,CAAC,EAAE,CAAC,CAAC,OAAO,CAAC,GAAG,EAAE,GAAG,CAAC,EAAE,CAAC;AAClF,CAAC","sourcesContent":["import { canonicalize, type CanonicalValue } from './canonical.js';\nimport { sha256Hex, sha256HexBytes } from './sha256.js';\n\n/**\n * Content addressing (Principle 4).\n *\n * Hashes are the system's only identity. A baseline is addressed by what it *is*,\n * never by the branch or commit it arrived on, which is why a rebase cannot\n * invalidate one.\n *\n * Digests are prefixed with their algorithm and truncated to 128 bits. The prefix\n * makes a future algorithm change a visible, greppable migration rather than a\n * silent collision domain; 128 bits is far beyond collision risk for a corpus\n * bounded by the number of subjects a repository has, and keeps `variance.lock`\n * readable as a diff — a manifest a human refuses to read is a manifest nobody\n * reviews.\n */\nexport type Digest = string;\n\nconst PREFIX = 'v1';\nconst HEX_LENGTH = 32;\n\n/**\n * Hash an already-canonical string.\n *\n * Synchronous, and must stay so. `propsDigest` runs inside the rendering page,\n * where a prop may be a function or an element that cannot be serialized out —\n * so the digest is taken where the value still exists. `crypto.subtle` is async\n * and would colour the whole normalizer; see `sha256.ts`.\n */\nexport function digestString(input: string): Digest {\n return `${PREFIX}:${sha256Hex(input).slice(0, HEX_LENGTH)}`;\n}\n\n/**\n * The digest of a SHA-256 some other implementation already computed.\n *\n * The algorithm in `sha256.ts` is portable TypeScript because `core` has to run\n * inside the page. A host that is not a page — a transform, a scanner, anything\n * reading files off a disk — has the same hash natively and an order of\n * magnitude faster, and FIPS 180-4 leaves no room for the two to disagree.\n *\n * What such a host must not also own is the shape. The prefix is what makes an\n * algorithm change a greppable migration, and the truncation is what keeps a\n * manifest readable; a second place deciding either is a digest domain that\n * splits without anybody noticing. So the host brings the hex and this brings\n * the identity.\n */\nexport function digestOfSha256(hex: string): Digest {\n return `${PREFIX}:${hex.slice(0, HEX_LENGTH)}`;\n}\n\n/**\n * Hash raw bytes — an image, a font file, anything whose identity is its octets.\n *\n * Same prefix and same truncation as every other digest here, so an asset hash\n * and a render hash are the same kind of value and neither can be mistaken for\n * the other's domain. What it is for is `EnvironmentInputs.assets`: a URL is not\n * an identity, and the same `url(...)` can resolve to different bytes tomorrow.\n */\nexport function digestBytes(bytes: Uint8Array): Digest {\n return `${PREFIX}:${sha256HexBytes(bytes).slice(0, HEX_LENGTH)}`;\n}\n\n/** Canonicalize then hash. The only correct way to hash a structure. */\nexport function digestValue(value: CanonicalValue): Digest {\n return digestString(canonicalize(value));\n}\n\n/**\n * Combine digests under a domain label.\n *\n * The label prevents cross-domain collision: an environment key and a render hash\n * built from identical component digests must not be equal, or an environment\n * change could masquerade as a content match.\n */\nexport function digestCombine(domain: string, parts: readonly Digest[]): Digest {\n return digestString(`${domain}\\u0000${parts.join('\\u0000')}`);\n}\n\n/**\n * How much of an encoded id a filename may carry.\n *\n * A filename is capped at 255 bytes on every filesystem anybody runs a suite on,\n * and a caller's own suffix -- an extension, a temporary marker with a pid and a\n * UUID in it -- comes out of the same 255. What is left is the budget.\n */\nconst NAME_BUDGET = 140;\n\n/**\n * A filesystem name for an id that was not written with filesystems in mind.\n *\n * An id long enough to overrun a filename is not an exotic case: a suite that\n * names subjects after the test that produced them -- a file path and a full\n * test name -- passes 255 bytes on ordinary tests, and the whole point of that\n * convention is that the id says where the subject came from. Truncating alone\n * would put two tests in one file, so a long id keeps a readable prefix and\n * earns a digest of the whole id, which is what actually distinguishes it.\n */\nexport function fileNameFor(id: string): string {\n const encoded = encodeURIComponent(id);\n if (encoded.length <= NAME_BUDGET) return encoded;\n return `${encoded.slice(0, NAME_BUDGET)}~${digestString(id).replace(':', '-')}`;\n}\n"]}
1
+ {"version":3,"file":"hash.js","sourceRoot":"","sources":["../../src/format/hash.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAuB,MAAM,gBAAgB,CAAC;AACnE,OAAO,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAkBxD,MAAM,MAAM,GAAG,IAAI,CAAC;AACpB,MAAM,UAAU,GAAG,EAAE,CAAC;AAEtB;;;;;;;GAOG;AACH,MAAM,UAAU,YAAY,CAAC,KAAa;IACxC,OAAO,GAAG,MAAM,IAAI,SAAS,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,UAAU,CAAC,EAAE,CAAC;AAC9D,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,cAAc,CAAC,GAAW;IACxC,OAAO,GAAG,MAAM,IAAI,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,UAAU,CAAC,EAAE,CAAC;AACjD,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,WAAW,CAAC,KAAiB;IAC3C,OAAO,GAAG,MAAM,IAAI,cAAc,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,UAAU,CAAC,EAAE,CAAC;AACnE,CAAC;AAED,wEAAwE;AACxE,MAAM,UAAU,WAAW,CAAC,KAAqB;IAC/C,OAAO,YAAY,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC,CAAC;AAC3C,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,aAAa,CAAC,MAAc,EAAE,KAAwB;IACpE,OAAO,YAAY,CAAC,GAAG,MAAM,SAAS,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC;AAChE,CAAC;AAED;;;;;;GAMG;AACH,MAAM,WAAW,GAAG,GAAG,CAAC;AAExB;;;;;;;;;GASG;AACH,MAAM,UAAU,WAAW,CAAC,EAAU;IACpC,MAAM,OAAO,GAAG,kBAAkB,CAAC,EAAE,CAAC,CAAC;IACvC,IAAI,OAAO,CAAC,MAAM,IAAI,WAAW;QAAE,OAAO,OAAO,CAAC;IAClD,OAAO,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,WAAW,CAAC,IAAI,cAAc,CAAC,YAAY,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC;AAChF,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,cAAc,CAAC,MAAc;IAC3C,OAAO,MAAM,CAAC,OAAO,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;AAClC,CAAC;AAED,MAAM,SAAS,GAAG,IAAI,MAAM,CAAC,IAAI,MAAM,iBAAiB,UAAU,KAAK,CAAC,CAAC;AAEzE;;;;;;;;GAQG;AACH,MAAM,UAAU,gBAAgB,CAAC,IAAY;IAC3C,MAAM,GAAG,GAAG,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;IACtC,OAAO,GAAG,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,GAAG,MAAM,IAAI,GAAG,EAAE,CAAC;AAC5D,CAAC","sourcesContent":["import { canonicalize, type CanonicalValue } from './canonical.js';\nimport { sha256Hex, sha256HexBytes } from './sha256.js';\n\n/**\n * Content addressing (Principle 4).\n *\n * Hashes are the system's only identity. A baseline is addressed by what it *is*,\n * never by the branch or commit it arrived on, which is why a rebase cannot\n * invalidate one.\n *\n * Digests are prefixed with their algorithm and truncated to 128 bits. The prefix\n * makes a future algorithm change a visible, greppable migration rather than a\n * silent collision domain; 128 bits is far beyond collision risk for a corpus\n * bounded by the number of subjects a repository has, and keeps `variance.lock`\n * readable as a diff — a manifest a human refuses to read is a manifest nobody\n * reviews.\n */\nexport type Digest = string;\n\nconst PREFIX = 'v1';\nconst HEX_LENGTH = 32;\n\n/**\n * Hash an already-canonical string.\n *\n * Synchronous, and must stay so. `propsDigest` runs inside the rendering page,\n * where a prop may be a function or an element that cannot be serialized out —\n * so the digest is taken where the value still exists. `crypto.subtle` is async\n * and would colour the whole normalizer; see `sha256.ts`.\n */\nexport function digestString(input: string): Digest {\n return `${PREFIX}:${sha256Hex(input).slice(0, HEX_LENGTH)}`;\n}\n\n/**\n * The digest of a SHA-256 some other implementation already computed.\n *\n * The algorithm in `sha256.ts` is portable TypeScript because `core` has to run\n * inside the page. A host that is not a page — a transform, a scanner, anything\n * reading files off a disk — has the same hash natively and an order of\n * magnitude faster, and FIPS 180-4 leaves no room for the two to disagree.\n *\n * What such a host must not also own is the shape. The prefix is what makes an\n * algorithm change a greppable migration, and the truncation is what keeps a\n * manifest readable; a second place deciding either is a digest domain that\n * splits without anybody noticing. So the host brings the hex and this brings\n * the identity.\n */\nexport function digestOfSha256(hex: string): Digest {\n return `${PREFIX}:${hex.slice(0, HEX_LENGTH)}`;\n}\n\n/**\n * Hash raw bytes — an image, a font file, anything whose identity is its octets.\n *\n * Same prefix and same truncation as every other digest here, so an asset hash\n * and a render hash are the same kind of value and neither can be mistaken for\n * the other's domain. What it is for is `EnvironmentInputs.assets`: a URL is not\n * an identity, and the same `url(...)` can resolve to different bytes tomorrow.\n */\nexport function digestBytes(bytes: Uint8Array): Digest {\n return `${PREFIX}:${sha256HexBytes(bytes).slice(0, HEX_LENGTH)}`;\n}\n\n/** Canonicalize then hash. The only correct way to hash a structure. */\nexport function digestValue(value: CanonicalValue): Digest {\n return digestString(canonicalize(value));\n}\n\n/**\n * Combine digests under a domain label.\n *\n * The label prevents cross-domain collision: an environment key and a render hash\n * built from identical component digests must not be equal, or an environment\n * change could masquerade as a content match.\n */\nexport function digestCombine(domain: string, parts: readonly Digest[]): Digest {\n return digestString(`${domain}\\u0000${parts.join('\\u0000')}`);\n}\n\n/**\n * How much of an encoded id a filename may carry.\n *\n * A filename is capped at 255 bytes on every filesystem anybody runs a suite on,\n * and a caller's own suffix -- an extension, a temporary marker with a pid and a\n * UUID in it -- comes out of the same 255. What is left is the budget.\n */\nconst NAME_BUDGET = 140;\n\n/**\n * A filesystem name for an id that was not written with filesystems in mind.\n *\n * An id long enough to overrun a filename is not an exotic case: a suite that\n * names subjects after the test that produced them -- a file path and a full\n * test name -- passes 255 bytes on ordinary tests, and the whole point of that\n * convention is that the id says where the subject came from. Truncating alone\n * would put two tests in one file, so a long id keeps a readable prefix and\n * earns a digest of the whole id, which is what actually distinguishes it.\n */\nexport function fileNameFor(id: string): string {\n const encoded = encodeURIComponent(id);\n if (encoded.length <= NAME_BUDGET) return encoded;\n return `${encoded.slice(0, NAME_BUDGET)}~${digestFileName(digestString(id))}`;\n}\n\n/**\n * A digest spelled so that a filesystem accepts it: `v1-<hex>` for `v1:<hex>`.\n *\n * The colon is the one character in a digest that is not portable. NTFS refuses\n * it in a name, so a directory named by a raw digest is a repository that cannot\n * be checked out on Windows, and `actions/upload-artifact` refuses the path for\n * the same reason. Every name made from a digest goes through here, so the\n * spelling has one owner and {@link digestOfFileName} can read it back.\n */\nexport function digestFileName(digest: Digest): string {\n return digest.replace(':', '-');\n}\n\nconst FILE_NAME = new RegExp(`^${PREFIX}[-:]([0-9a-f]{${HEX_LENGTH}})$`);\n\n/**\n * The digest a name spells, or `undefined` because it spells none.\n *\n * Reads the raw `v1:<hex>` spelling as well as {@link digestFileName}'s. The\n * file-backed store named its identity partitions with the raw digest before\n * this spelling existed, and a baseline kept under that name is still a\n * baseline: a reader that knew only the new spelling would call it `new` and\n * record over it.\n */\nexport function digestOfFileName(name: string): Digest | undefined {\n const hex = FILE_NAME.exec(name)?.[1];\n return hex === undefined ? undefined : `${PREFIX}:${hex}`;\n}\n"]}
@@ -16,7 +16,7 @@ export { canonicalize, canonicalNumber } from './canonical.js';
16
16
  export type { CapturedValue, ShapeValueOptions, ValueShaping } from './value.js';
17
17
  export { shapeValue, pointerShape, pointerToken, DROPPED, VALUE_RECIPE } from './value.js';
18
18
  export type { Digest } from './hash.js';
19
- export { digestString, digestBytes, digestValue, digestCombine, digestOfSha256, fileNameFor } from './hash.js';
19
+ export { digestString, digestBytes, digestValue, digestCombine, digestOfSha256, fileNameFor, digestFileName, digestOfFileName } from './hash.js';
20
20
  export type { AccessibilitySnapshot } from './accessibility.js';
21
21
  export { accessibilitySnapshot } from './accessibility.js';
22
22
  export type { ObservationProfile, ProfileId } from './profile.js';
@@ -13,7 +13,7 @@
13
13
  */
14
14
  export { canonicalize, canonicalNumber } from './canonical.js';
15
15
  export { shapeValue, pointerShape, pointerToken, DROPPED, VALUE_RECIPE } from './value.js';
16
- export { digestString, digestBytes, digestValue, digestCombine, digestOfSha256, fileNameFor } from './hash.js';
16
+ export { digestString, digestBytes, digestValue, digestCombine, digestOfSha256, fileNameFor, digestFileName, digestOfFileName } from './hash.js';
17
17
  export { accessibilitySnapshot } from './accessibility.js';
18
18
  export { JSDOM_PROFILE, CHROMIUM_PROFILE, profileById, tierOfProfile } from './profile.js';
19
19
  export { tierReaches } from './tier.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/format/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAKH,OAAO,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAG/D,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,YAAY,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAG3F,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,WAAW,EAAE,aAAa,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAG/G,OAAO,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAG3D,OAAO,EAAE,aAAa,EAAE,gBAAgB,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAG3F,OAAO,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AASxC,OAAO,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAEpE,OAAO,EACL,aAAa,EACb,eAAe,EACf,cAAc,EACd,aAAa,EACb,aAAa,EACb,cAAc,EACd,aAAa,EACb,SAAS,EACT,cAAc,EACd,wBAAwB,EACxB,YAAY,EACZ,aAAa,EACb,OAAO,EACP,SAAS,EACT,gBAAgB,EAChB,QAAQ,EACR,SAAS,EACT,gBAAgB,EAChB,YAAY,EACZ,YAAY,EACZ,cAAc,GACf,MAAM,gBAAgB,CAAC;AAUxB,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,WAAW,EAAE,gBAAgB,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAGrG,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAY9C,OAAO,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAWhD,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,cAAc,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC","sourcesContent":["/**\n * `@variance-authority/core/format` — what a subject *is*, as a value.\n *\n * The shapes every other group reads and none of them may redefine: a raw\n * capture as a collector hands it over, the normalized snapshot it becomes, the\n * document a renderer paints, the identity that addresses the result, and the\n * hashing that makes any of it comparable.\n *\n * A consumer writing their own collector needs exactly this and nothing else.\n * That is the entrypoint's reason to exist: the format is a contract other people\n * implement, and implementing it should not require reading the rules that\n * interpret it or the verdicts drawn from it.\n */\n\n// compass: variance-authority.normalization\n\nexport type { CanonicalValue } from './canonical.js';\nexport { canonicalize, canonicalNumber } from './canonical.js';\n\nexport type { CapturedValue, ShapeValueOptions, ValueShaping } from './value.js';\nexport { shapeValue, pointerShape, pointerToken, DROPPED, VALUE_RECIPE } from './value.js';\n\nexport type { Digest } from './hash.js';\nexport { digestString, digestBytes, digestValue, digestCombine, digestOfSha256, fileNameFor } from './hash.js';\n\nexport type { AccessibilitySnapshot } from './accessibility.js';\nexport { accessibilitySnapshot } from './accessibility.js';\n\nexport type { ObservationProfile, ProfileId } from './profile.js';\nexport { JSDOM_PROFILE, CHROMIUM_PROFILE, profileById, tierOfProfile } from './profile.js';\n\nexport type { Tier } from './tier.js';\nexport { tierReaches } from './tier.js';\n\nexport type {\n EnvironmentInputs,\n EnvironmentKey,\n EnvironmentDelta,\n EnvironmentField,\n Viewport,\n} from './environment.js';\nexport { environmentKey, diffEnvironments } from './environment.js';\n\nexport {\n INTERVENTIONS,\n SEMANTIC_RECIPE,\n COLLECT_RECIPE,\n LAYOUT_RECIPE,\n RASTER_RECIPE,\n holdAnimations,\n pinAnimations,\n hideCaret,\n hideScrollbars,\n hidePresentationalImages,\n waitForFonts,\n waitForImages,\n forTier,\n conflicts,\n interventionById,\n recipeOf,\n recipeCss,\n recipeScreenshot,\n recipeDigest,\n settleRecipe,\n describeRecipe,\n} from './stabilize.js';\nexport type {\n Intervention,\n Recipe,\n Trick,\n ScreenshotOptions,\n SettleTarget,\n} from './stabilize.js';\n\nexport type { Provenance, OwnerFrame, SourceLocation, StackFrame } from './provenance.js';\nexport { propsDigest, heldDigest, jsxSourceOf, relativizeSource, JSX_SOURCE } from './provenance.js';\nexport type { Holding, HeldCell, HeldValue } from './holding.js';\nexport type { Wiring } from './wiring.js';\nexport { keyedByPosition } from './wiring.js';\n\nexport type {\n RawCapture,\n RawNode,\n RawAria,\n MatchedRule,\n Declaration,\n Rect,\n SubjectRef,\n Diagnostic,\n} from './capture.js';\nexport { mergeDiagnostics } from './capture.js';\n\nexport type {\n SemanticSnapshot,\n SemanticNode,\n NodePath,\n StyleProvenanceEntry,\n IgnoreSite,\n ComponentHash,\n} from './snapshot.js';\n\nexport { documentDigest, identityDigest, occupiesPixels, pictured, picture } from './document.js';\nexport type {\n RenderDocument,\n RenderFrame,\n FrameElement,\n RenderResource,\n RenderIdentity,\n Raster,\n Pictured,\n} from './document.js';\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/format/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAKH,OAAO,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAG/D,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,YAAY,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAG3F,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,WAAW,EAAE,aAAa,EAAE,cAAc,EAAE,WAAW,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAGjJ,OAAO,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAG3D,OAAO,EAAE,aAAa,EAAE,gBAAgB,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAG3F,OAAO,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AASxC,OAAO,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAEpE,OAAO,EACL,aAAa,EACb,eAAe,EACf,cAAc,EACd,aAAa,EACb,aAAa,EACb,cAAc,EACd,aAAa,EACb,SAAS,EACT,cAAc,EACd,wBAAwB,EACxB,YAAY,EACZ,aAAa,EACb,OAAO,EACP,SAAS,EACT,gBAAgB,EAChB,QAAQ,EACR,SAAS,EACT,gBAAgB,EAChB,YAAY,EACZ,YAAY,EACZ,cAAc,GACf,MAAM,gBAAgB,CAAC;AAUxB,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,WAAW,EAAE,gBAAgB,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAGrG,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAY9C,OAAO,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAWhD,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,cAAc,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC","sourcesContent":["/**\n * `@variance-authority/core/format` — what a subject *is*, as a value.\n *\n * The shapes every other group reads and none of them may redefine: a raw\n * capture as a collector hands it over, the normalized snapshot it becomes, the\n * document a renderer paints, the identity that addresses the result, and the\n * hashing that makes any of it comparable.\n *\n * A consumer writing their own collector needs exactly this and nothing else.\n * That is the entrypoint's reason to exist: the format is a contract other people\n * implement, and implementing it should not require reading the rules that\n * interpret it or the verdicts drawn from it.\n */\n\n// compass: variance-authority.normalization\n\nexport type { CanonicalValue } from './canonical.js';\nexport { canonicalize, canonicalNumber } from './canonical.js';\n\nexport type { CapturedValue, ShapeValueOptions, ValueShaping } from './value.js';\nexport { shapeValue, pointerShape, pointerToken, DROPPED, VALUE_RECIPE } from './value.js';\n\nexport type { Digest } from './hash.js';\nexport { digestString, digestBytes, digestValue, digestCombine, digestOfSha256, fileNameFor, digestFileName, digestOfFileName } from './hash.js';\n\nexport type { AccessibilitySnapshot } from './accessibility.js';\nexport { accessibilitySnapshot } from './accessibility.js';\n\nexport type { ObservationProfile, ProfileId } from './profile.js';\nexport { JSDOM_PROFILE, CHROMIUM_PROFILE, profileById, tierOfProfile } from './profile.js';\n\nexport type { Tier } from './tier.js';\nexport { tierReaches } from './tier.js';\n\nexport type {\n EnvironmentInputs,\n EnvironmentKey,\n EnvironmentDelta,\n EnvironmentField,\n Viewport,\n} from './environment.js';\nexport { environmentKey, diffEnvironments } from './environment.js';\n\nexport {\n INTERVENTIONS,\n SEMANTIC_RECIPE,\n COLLECT_RECIPE,\n LAYOUT_RECIPE,\n RASTER_RECIPE,\n holdAnimations,\n pinAnimations,\n hideCaret,\n hideScrollbars,\n hidePresentationalImages,\n waitForFonts,\n waitForImages,\n forTier,\n conflicts,\n interventionById,\n recipeOf,\n recipeCss,\n recipeScreenshot,\n recipeDigest,\n settleRecipe,\n describeRecipe,\n} from './stabilize.js';\nexport type {\n Intervention,\n Recipe,\n Trick,\n ScreenshotOptions,\n SettleTarget,\n} from './stabilize.js';\n\nexport type { Provenance, OwnerFrame, SourceLocation, StackFrame } from './provenance.js';\nexport { propsDigest, heldDigest, jsxSourceOf, relativizeSource, JSX_SOURCE } from './provenance.js';\nexport type { Holding, HeldCell, HeldValue } from './holding.js';\nexport type { Wiring } from './wiring.js';\nexport { keyedByPosition } from './wiring.js';\n\nexport type {\n RawCapture,\n RawNode,\n RawAria,\n MatchedRule,\n Declaration,\n Rect,\n SubjectRef,\n Diagnostic,\n} from './capture.js';\nexport { mergeDiagnostics } from './capture.js';\n\nexport type {\n SemanticSnapshot,\n SemanticNode,\n NodePath,\n StyleProvenanceEntry,\n IgnoreSite,\n ComponentHash,\n} from './snapshot.js';\n\nexport { documentDigest, identityDigest, occupiesPixels, pictured, picture } from './document.js';\nexport type {\n RenderDocument,\n RenderFrame,\n FrameElement,\n RenderResource,\n RenderIdentity,\n Raster,\n Pictured,\n} from './document.js';\n"]}
@@ -247,6 +247,21 @@ export interface ComponentHash {
247
247
  * kept in place so the list stays parallel to the instances it counts.
248
248
  */
249
249
  readonly boxes?: readonly (Rect | null)[];
250
+ /**
251
+ * Every distinct value the component's own nodes declared, per property, in
252
+ * code-unit order; a custom property appears under its `--name` with the
253
+ * values it resolved to. Never hashed.
254
+ *
255
+ * `boxes` answers *by how much*; this answers *which declaration*. `style`
256
+ * moved says a `Button` is not styled as it was, and a reviewer holding only
257
+ * that opens the file and diffs by eye. With both revisions' values the run
258
+ * says `padding-left 8px → 14px`, which is the line somebody edited.
259
+ *
260
+ * Absent for a baseline written before this existed. That means *nobody
261
+ * recorded it*, and the comparison says nothing about properties rather than
262
+ * reporting every one of them as new.
263
+ */
264
+ readonly values?: Readonly<Record<string, readonly string[]>>;
250
265
  }
251
266
  export interface StyleProvenanceEntry {
252
267
  readonly path: NodePath;
@@ -1 +1 @@
1
- {"version":3,"file":"snapshot.js","sourceRoot":"","sources":["../../src/format/snapshot.ts"],"names":[],"mappings":"","sourcesContent":["import type { SubjectRef, Rect, Diagnostic } from './capture.js';\nimport type { EnvironmentKey } from './environment.js';\nimport type { Digest } from './hash.js';\nimport type { ObservationProfile } from './profile.js';\nimport type { Provenance } from './provenance.js';\nimport type { Wiring } from './wiring.js';\nimport type { Holding } from './holding.js';\n\n/**\n * The normalized semantic snapshot: the verdict's input, and the thing a render\n * hash addresses.\n *\n * Everything volatile is gone by this point. Ids have become structural aliases,\n * class attributes have been dropped entirely, inapplicable CSS has been pruned,\n * and the cascade has been resolved to winning values (ADR-0003). What remains is\n * meant to be *read*: a snapshot a reviewer refuses to open is a snapshot that\n * gets rubber-stamped.\n */\nexport interface SemanticSnapshot {\n readonly formatVersion: 1;\n readonly subject: SubjectRef;\n readonly profile: ObservationProfile;\n readonly environment: EnvironmentKey;\n\n /** Content hash of `root` under `environment`. The identity of this state. */\n readonly renderHash: Digest;\n\n /**\n * Structure and applicable style, hashed separately.\n *\n * These back the tier below full semantic comparison (ADR-0003): matching both\n * settles a subject without resolving layout or taking a screenshot. Split\n * rather than combined because the docket wants to say *which* held — \"the DOM\n * is identical, only styling moved\" is the sentence that turns a diff into a\n * token-band root.\n */\n readonly structureHash: Digest;\n readonly styleHash: Digest;\n\n readonly root: SemanticNode;\n\n /**\n * Where each winning declaration came from. Deliberately outside the hash.\n *\n * Pruning discards exactly what attribution needs — *which rule set this?* — so\n * it is preserved here instead. Keeping it out of the hash is the point: moving\n * a rule between files renames a source without changing a render, and must not\n * invalidate a baseline.\n */\n readonly styleProvenance: readonly StyleProvenanceEntry[];\n\n /**\n * Subtrees the operator declared out of scope, resolved against *this* tree.\n *\n * Deliberately outside the hash, for the reason `styleProvenance` is: an ignore\n * changes what a run says about a render, never the render. Folding it into the\n * identity would invalidate every baseline in the repository the first time\n * somebody masked a clock, which is the surest way to make a safety feature the\n * thing people turn off.\n *\n * Present only when something was excluded, so a snapshot from a run with no\n * ignores is byte-identical to one produced before this field existed.\n */\n readonly ignoreSites?: readonly IgnoreSite[];\n\n /**\n * What the collector or the normalizer could not do, carried forward.\n *\n * Outside the hash: a diagnostic describes the *observation*, not the render,\n * and a snapshot that hashed its own warnings would invalidate whenever the\n * warning text was reworded. But it must survive to the verdict — a subject\n * whose capture half-failed is not a subject that legitimately `unchanged`,\n * and silently discarding that fact is how a broken collector reads as a clean\n * build.\n */\n readonly diagnostics: readonly Diagnostic[];\n}\n\n/**\n * A node's stable address: child indices from the root, e.g. `0/2/1`.\n *\n * Positional because nothing else survives. Ids are volatile (that is why they\n * were aliased away), and class names have been dropped. Position is honest about\n * what it costs: inserting a sibling renumbers everything after it, so the differ\n * matches nodes by shape and provenance before falling back to path (see\n * `diff.ts`), and treats a path change alone as evidence of nothing.\n */\nexport type NodePath = string;\n\nexport interface SemanticNode {\n readonly path: NodePath;\n readonly tag: string;\n\n /** Structural alias, e.g. `#a0`. Absent when the node carried no id. */\n readonly alias?: string;\n\n readonly role?: string;\n readonly name?: string;\n\n /**\n * Accessible description, resolved. See `RawAria.description` for why this is\n * a field of its own rather than an attribute.\n */\n readonly description?: string;\n\n readonly state?: Readonly<Record<string, string | boolean | number>>;\n\n /** Semantic attributes surviving normalization. `class` is never among them. */\n readonly attributes: Readonly<Record<string, string>>;\n\n /** Winning values for allowlisted properties, canonicalized. */\n readonly style: Readonly<Record<string, string>>;\n\n /**\n * Custom properties this node's winning declarations resolved through, as\n * name → resolved value.\n *\n * Recording the name alongside the value is what lets a design-token change\n * surface as one root with counted collateral instead of hundreds of unrelated\n * colour diffs (spec §5, `token` band).\n */\n readonly tokens?: Readonly<Record<string, string>>;\n\n /**\n * Which token each styled property resolved through, including by inheritance.\n *\n * Outside the hash, like `styleProvenance` and for the same reason: swapping a\n * literal for a token of the same value renames a source without changing a\n * render. It exists so the differ can tell *which* property a token edit\n * explains — `tokens` alone says a node uses tokens, not which of its values\n * one drove.\n */\n readonly styleTokens?: Readonly<Record<string, string>>;\n\n /** Present only under a profile with layout. Absent, never zeroed (ADR-0002). */\n readonly rect?: Rect;\n\n /** Text content, or its digest when policy declares the region volatile. */\n readonly text?: string;\n\n readonly provenance?: Provenance;\n\n /**\n * How the framework holds this node's component, when a framework adapter\n * supplied it. See {@link Wiring}.\n *\n * On the node rather than on `Provenance`, though both come from the same\n * adapter and the same fiber, because they answer to different readers.\n * Provenance exists so a diff arrives carrying a name; wiring is a *dimension\n * of the subject*, hashed into a band of its own, and a band's inputs belong\n * beside the other bands' inputs — next to `style` and `attributes`, which is\n * where `shapeOf` goes looking.\n */\n readonly wiring?: Wiring;\n\n /**\n * What that component was holding. **Outside every hash**, always.\n *\n * The field beside it is the contrast worth reading: `wiring` is a band and\n * this is evidence. `Wiring` refuses state values because a hook's value is\n * exactly what may legitimately differ between two readings of one page, and\n * that refusal is right and is not softened here — a `holding` reaches the\n * snapshot and reaches no digest, so a subject whose clock ticked has the same\n * `renderHash` it had before this field existed.\n *\n * It rides outside on the same terms as `styleProvenance`, `styleTokens` and\n * `ignoredBy`, by the same mechanism: `structureOf`, `styleOf` and `shapeOf`\n * project the fields they hash **by name**, so a field none of them names\n * cannot reach an identity. `holding.hash.test.ts` asserts that rather than\n * trusting it, because the cost of being wrong is every baseline in a\n * repository invalidating the first time a component held a timestamp.\n */\n readonly holding?: Holding;\n\n /**\n * `true` on the root of a subtree rendered through a portal.\n *\n * Recorded in the structure hash so that \"the dialog moved from inline to\n * portalled\" is a change rather than a coincidence of identical content, and\n * so the docket can say *where* something rendered, not only that it exists.\n */\n readonly portalled?: boolean;\n\n /**\n * Ignore rules this node was found under. Outside every hash, like its origin\n * on `RawNode`. See {@link SemanticSnapshot.ignoreSites}.\n */\n readonly ignoredBy?: readonly string[];\n\n readonly children: readonly SemanticNode[];\n}\n\n/**\n * A subtree the operator excluded, resolved to a path and a box (spec 0024).\n *\n * Lives in `format` rather than beside the rules that consume it, because it is\n * part of what a snapshot *is*: the collector produced it, it travels with the\n * document over every wire this project has, and `core/judge` is only its first\n * reader. A type the wire format needs cannot live in the policy layer.\n */\nexport interface IgnoreSite {\n /** Root of the excluded subtree, in this snapshot's own path space. */\n readonly path: NodePath;\n\n /** Which rule put it here, matched exactly against `IgnoreRule.id`. */\n readonly rule: string;\n\n /**\n * The box it occupied, for the raster tier.\n *\n * Absent under a profile with no layout — which is ADR-0002 rather than a\n * failure: a profile that cannot measure a box cannot decide pixels either, so\n * there is nothing for the box to subtract from.\n */\n readonly rect?: Rect;\n}\n\n/**\n * A component's own content, hashed per band (ADR-0018).\n *\n * In `format` rather than beside `hashComponents`, which computes it, because a\n * baseline carries these: a stored image plus the component hashes of the\n * document that painted it is what lets a later run tell the component that\n * *caused* a change from the components the change merely moved. That makes this\n * part of the wire format every store and every transport has to preserve, and a\n * type the wire needs cannot live in the layer that derives it.\n */\nexport interface ComponentHash {\n readonly component: string;\n\n /** Boundaries of this component in the subject, counted in document order. */\n readonly instances: number;\n\n /**\n * Tree shape: tags, structural aliases, portalling, allowlisted attributes,\n * and which child boundaries sit where. The `geometry` band's structural half.\n *\n * **Split out of a single `structure` digest on 2026-08-06.** It used to carry\n * the accessible semantics and the text too, and the fusion was invisible\n * until something needed to ask *which band moved* — at which point a baseline\n * could say \"this component changed\" and never say whether a heading was\n * renamed, a paragraph reworded, or a node inserted. Those are three different\n * bands, three different readers, and one of them is the band a route-level\n * test exists to ignore.\n */\n readonly structure: Digest;\n\n /**\n * Role, accessible name, and ARIA state. The `a11y` band.\n *\n * Its own digest because it is the band that must never be absorbed. Every\n * sensitivity level asserts on it, including the ones that ignore everything\n * else — and a level cannot assert on a band that is fused into another one.\n */\n readonly semantics: Digest;\n\n /** Text runs, in document order. The `content` band. */\n readonly text: Digest;\n\n readonly style: Digest;\n\n /**\n * Absent under a profile without layout — absent, never empty (ADR-0002).\n *\n * An empty geometry digest would compare equal between a run that observed no\n * movement and a run that could not observe movement at all, which is the\n * false `unchanged` this system must never produce.\n */\n readonly geometry?: Digest;\n\n /**\n * The box each instance occupies, in document order. Never hashed.\n *\n * A digest can say a component is not what it was and can never say by how\n * much. That is the gap a reviewer falls into: `Button — geometry, token` is\n * true of a control that lost a shadow and of one that grew eight pixels and\n * pushed the page down, and only the second explains why the subject is a\n * different height. Four numbers per instance close it.\n *\n * Absent for a baseline written before this existed and for a profile with no\n * layout — the same absence, and both mean *nobody measured*, never *it did\n * not move*. A `null` entry is a boundary the browser laid out nothing for,\n * kept in place so the list stays parallel to the instances it counts.\n */\n readonly boxes?: readonly (Rect | null)[];\n}\n\nexport interface StyleProvenanceEntry {\n readonly path: NodePath;\n readonly property: string;\n readonly sheet: string;\n /** Generated segments replaced by placeholders, so it can name a root safely. */\n readonly selector: string;\n readonly tokenName?: string;\n readonly source?: { readonly file: string; readonly line: number };\n}\n"]}
1
+ {"version":3,"file":"snapshot.js","sourceRoot":"","sources":["../../src/format/snapshot.ts"],"names":[],"mappings":"","sourcesContent":["import type { SubjectRef, Rect, Diagnostic } from './capture.js';\nimport type { EnvironmentKey } from './environment.js';\nimport type { Digest } from './hash.js';\nimport type { ObservationProfile } from './profile.js';\nimport type { Provenance } from './provenance.js';\nimport type { Wiring } from './wiring.js';\nimport type { Holding } from './holding.js';\n\n/**\n * The normalized semantic snapshot: the verdict's input, and the thing a render\n * hash addresses.\n *\n * Everything volatile is gone by this point. Ids have become structural aliases,\n * class attributes have been dropped entirely, inapplicable CSS has been pruned,\n * and the cascade has been resolved to winning values (ADR-0003). What remains is\n * meant to be *read*: a snapshot a reviewer refuses to open is a snapshot that\n * gets rubber-stamped.\n */\nexport interface SemanticSnapshot {\n readonly formatVersion: 1;\n readonly subject: SubjectRef;\n readonly profile: ObservationProfile;\n readonly environment: EnvironmentKey;\n\n /** Content hash of `root` under `environment`. The identity of this state. */\n readonly renderHash: Digest;\n\n /**\n * Structure and applicable style, hashed separately.\n *\n * These back the tier below full semantic comparison (ADR-0003): matching both\n * settles a subject without resolving layout or taking a screenshot. Split\n * rather than combined because the docket wants to say *which* held — \"the DOM\n * is identical, only styling moved\" is the sentence that turns a diff into a\n * token-band root.\n */\n readonly structureHash: Digest;\n readonly styleHash: Digest;\n\n readonly root: SemanticNode;\n\n /**\n * Where each winning declaration came from. Deliberately outside the hash.\n *\n * Pruning discards exactly what attribution needs — *which rule set this?* — so\n * it is preserved here instead. Keeping it out of the hash is the point: moving\n * a rule between files renames a source without changing a render, and must not\n * invalidate a baseline.\n */\n readonly styleProvenance: readonly StyleProvenanceEntry[];\n\n /**\n * Subtrees the operator declared out of scope, resolved against *this* tree.\n *\n * Deliberately outside the hash, for the reason `styleProvenance` is: an ignore\n * changes what a run says about a render, never the render. Folding it into the\n * identity would invalidate every baseline in the repository the first time\n * somebody masked a clock, which is the surest way to make a safety feature the\n * thing people turn off.\n *\n * Present only when something was excluded, so a snapshot from a run with no\n * ignores is byte-identical to one produced before this field existed.\n */\n readonly ignoreSites?: readonly IgnoreSite[];\n\n /**\n * What the collector or the normalizer could not do, carried forward.\n *\n * Outside the hash: a diagnostic describes the *observation*, not the render,\n * and a snapshot that hashed its own warnings would invalidate whenever the\n * warning text was reworded. But it must survive to the verdict — a subject\n * whose capture half-failed is not a subject that legitimately `unchanged`,\n * and silently discarding that fact is how a broken collector reads as a clean\n * build.\n */\n readonly diagnostics: readonly Diagnostic[];\n}\n\n/**\n * A node's stable address: child indices from the root, e.g. `0/2/1`.\n *\n * Positional because nothing else survives. Ids are volatile (that is why they\n * were aliased away), and class names have been dropped. Position is honest about\n * what it costs: inserting a sibling renumbers everything after it, so the differ\n * matches nodes by shape and provenance before falling back to path (see\n * `diff.ts`), and treats a path change alone as evidence of nothing.\n */\nexport type NodePath = string;\n\nexport interface SemanticNode {\n readonly path: NodePath;\n readonly tag: string;\n\n /** Structural alias, e.g. `#a0`. Absent when the node carried no id. */\n readonly alias?: string;\n\n readonly role?: string;\n readonly name?: string;\n\n /**\n * Accessible description, resolved. See `RawAria.description` for why this is\n * a field of its own rather than an attribute.\n */\n readonly description?: string;\n\n readonly state?: Readonly<Record<string, string | boolean | number>>;\n\n /** Semantic attributes surviving normalization. `class` is never among them. */\n readonly attributes: Readonly<Record<string, string>>;\n\n /** Winning values for allowlisted properties, canonicalized. */\n readonly style: Readonly<Record<string, string>>;\n\n /**\n * Custom properties this node's winning declarations resolved through, as\n * name → resolved value.\n *\n * Recording the name alongside the value is what lets a design-token change\n * surface as one root with counted collateral instead of hundreds of unrelated\n * colour diffs (spec §5, `token` band).\n */\n readonly tokens?: Readonly<Record<string, string>>;\n\n /**\n * Which token each styled property resolved through, including by inheritance.\n *\n * Outside the hash, like `styleProvenance` and for the same reason: swapping a\n * literal for a token of the same value renames a source without changing a\n * render. It exists so the differ can tell *which* property a token edit\n * explains — `tokens` alone says a node uses tokens, not which of its values\n * one drove.\n */\n readonly styleTokens?: Readonly<Record<string, string>>;\n\n /** Present only under a profile with layout. Absent, never zeroed (ADR-0002). */\n readonly rect?: Rect;\n\n /** Text content, or its digest when policy declares the region volatile. */\n readonly text?: string;\n\n readonly provenance?: Provenance;\n\n /**\n * How the framework holds this node's component, when a framework adapter\n * supplied it. See {@link Wiring}.\n *\n * On the node rather than on `Provenance`, though both come from the same\n * adapter and the same fiber, because they answer to different readers.\n * Provenance exists so a diff arrives carrying a name; wiring is a *dimension\n * of the subject*, hashed into a band of its own, and a band's inputs belong\n * beside the other bands' inputs — next to `style` and `attributes`, which is\n * where `shapeOf` goes looking.\n */\n readonly wiring?: Wiring;\n\n /**\n * What that component was holding. **Outside every hash**, always.\n *\n * The field beside it is the contrast worth reading: `wiring` is a band and\n * this is evidence. `Wiring` refuses state values because a hook's value is\n * exactly what may legitimately differ between two readings of one page, and\n * that refusal is right and is not softened here — a `holding` reaches the\n * snapshot and reaches no digest, so a subject whose clock ticked has the same\n * `renderHash` it had before this field existed.\n *\n * It rides outside on the same terms as `styleProvenance`, `styleTokens` and\n * `ignoredBy`, by the same mechanism: `structureOf`, `styleOf` and `shapeOf`\n * project the fields they hash **by name**, so a field none of them names\n * cannot reach an identity. `holding.hash.test.ts` asserts that rather than\n * trusting it, because the cost of being wrong is every baseline in a\n * repository invalidating the first time a component held a timestamp.\n */\n readonly holding?: Holding;\n\n /**\n * `true` on the root of a subtree rendered through a portal.\n *\n * Recorded in the structure hash so that \"the dialog moved from inline to\n * portalled\" is a change rather than a coincidence of identical content, and\n * so the docket can say *where* something rendered, not only that it exists.\n */\n readonly portalled?: boolean;\n\n /**\n * Ignore rules this node was found under. Outside every hash, like its origin\n * on `RawNode`. See {@link SemanticSnapshot.ignoreSites}.\n */\n readonly ignoredBy?: readonly string[];\n\n readonly children: readonly SemanticNode[];\n}\n\n/**\n * A subtree the operator excluded, resolved to a path and a box (spec 0024).\n *\n * Lives in `format` rather than beside the rules that consume it, because it is\n * part of what a snapshot *is*: the collector produced it, it travels with the\n * document over every wire this project has, and `core/judge` is only its first\n * reader. A type the wire format needs cannot live in the policy layer.\n */\nexport interface IgnoreSite {\n /** Root of the excluded subtree, in this snapshot's own path space. */\n readonly path: NodePath;\n\n /** Which rule put it here, matched exactly against `IgnoreRule.id`. */\n readonly rule: string;\n\n /**\n * The box it occupied, for the raster tier.\n *\n * Absent under a profile with no layout — which is ADR-0002 rather than a\n * failure: a profile that cannot measure a box cannot decide pixels either, so\n * there is nothing for the box to subtract from.\n */\n readonly rect?: Rect;\n}\n\n/**\n * A component's own content, hashed per band (ADR-0018).\n *\n * In `format` rather than beside `hashComponents`, which computes it, because a\n * baseline carries these: a stored image plus the component hashes of the\n * document that painted it is what lets a later run tell the component that\n * *caused* a change from the components the change merely moved. That makes this\n * part of the wire format every store and every transport has to preserve, and a\n * type the wire needs cannot live in the layer that derives it.\n */\nexport interface ComponentHash {\n readonly component: string;\n\n /** Boundaries of this component in the subject, counted in document order. */\n readonly instances: number;\n\n /**\n * Tree shape: tags, structural aliases, portalling, allowlisted attributes,\n * and which child boundaries sit where. The `geometry` band's structural half.\n *\n * **Split out of a single `structure` digest on 2026-08-06.** It used to carry\n * the accessible semantics and the text too, and the fusion was invisible\n * until something needed to ask *which band moved* — at which point a baseline\n * could say \"this component changed\" and never say whether a heading was\n * renamed, a paragraph reworded, or a node inserted. Those are three different\n * bands, three different readers, and one of them is the band a route-level\n * test exists to ignore.\n */\n readonly structure: Digest;\n\n /**\n * Role, accessible name, and ARIA state. The `a11y` band.\n *\n * Its own digest because it is the band that must never be absorbed. Every\n * sensitivity level asserts on it, including the ones that ignore everything\n * else — and a level cannot assert on a band that is fused into another one.\n */\n readonly semantics: Digest;\n\n /** Text runs, in document order. The `content` band. */\n readonly text: Digest;\n\n readonly style: Digest;\n\n /**\n * Absent under a profile without layout — absent, never empty (ADR-0002).\n *\n * An empty geometry digest would compare equal between a run that observed no\n * movement and a run that could not observe movement at all, which is the\n * false `unchanged` this system must never produce.\n */\n readonly geometry?: Digest;\n\n /**\n * The box each instance occupies, in document order. Never hashed.\n *\n * A digest can say a component is not what it was and can never say by how\n * much. That is the gap a reviewer falls into: `Button — geometry, token` is\n * true of a control that lost a shadow and of one that grew eight pixels and\n * pushed the page down, and only the second explains why the subject is a\n * different height. Four numbers per instance close it.\n *\n * Absent for a baseline written before this existed and for a profile with no\n * layout — the same absence, and both mean *nobody measured*, never *it did\n * not move*. A `null` entry is a boundary the browser laid out nothing for,\n * kept in place so the list stays parallel to the instances it counts.\n */\n readonly boxes?: readonly (Rect | null)[];\n\n /**\n * Every distinct value the component's own nodes declared, per property, in\n * code-unit order; a custom property appears under its `--name` with the\n * values it resolved to. Never hashed.\n *\n * `boxes` answers *by how much*; this answers *which declaration*. `style`\n * moved says a `Button` is not styled as it was, and a reviewer holding only\n * that opens the file and diffs by eye. With both revisions' values the run\n * says `padding-left 8px → 14px`, which is the line somebody edited.\n *\n * Absent for a baseline written before this existed. That means *nobody\n * recorded it*, and the comparison says nothing about properties rather than\n * reporting every one of them as new.\n */\n readonly values?: Readonly<Record<string, readonly string[]>>;\n}\n\nexport interface StyleProvenanceEntry {\n readonly path: NodePath;\n readonly property: string;\n readonly sheet: string;\n /** Generated segments replaced by placeholders, so it can name a root safely. */\n readonly selector: string;\n readonly tokenName?: string;\n readonly source?: { readonly file: string; readonly line: number };\n}\n"]}
@@ -0,0 +1,30 @@
1
+ /**
2
+ * A line over HTTP: a bucket, a signed base, a tribunal deployment.
3
+ *
4
+ * The manifest is replaced with `If-Match` on the `ETag` it was read with, or
5
+ * `If-None-Match: *` when there was none, which is the conditional write S3,
6
+ * GCS, R2 and tribunal all honour. Blobs are addressed by digest and written
7
+ * without a condition, first, so a manifest never names a blob that is not
8
+ * there yet.
9
+ */
10
+ import { type LineCell } from './line.js';
11
+ export interface HttpLineOptions {
12
+ /** Base URL. Every path is appended to it, with exactly one `/` between. */
13
+ readonly endpoint: string;
14
+ /** Sent on every request: a bucket's `Authorization`, a deployment's token. */
15
+ readonly headers?: Readonly<Record<string, string>>;
16
+ /** The verb a write uses. `PUT` for a bucket, `POST` for a deployment that routes on it. */
17
+ readonly method?: 'PUT' | 'POST';
18
+ /**
19
+ * Milliseconds one request may take, from sending it to the last byte of its
20
+ * body, before the store is `unreachable`. 60 000 when absent.
21
+ */
22
+ readonly timeoutMs?: number;
23
+ }
24
+ /**
25
+ * `<endpoint>/<kind>/<name>/manifest.json`, the line's entries beside it, and
26
+ * `<endpoint>/images/<digest>` shared by every line, so an image two lines name
27
+ * is stored once.
28
+ */
29
+ export declare function httpLineCell(options: HttpLineOptions): LineCell;
30
+ //# sourceMappingURL=http-line.d.ts.map
@@ -0,0 +1,164 @@
1
+ /**
2
+ * A line over HTTP: a bucket, a signed base, a tribunal deployment.
3
+ *
4
+ * The manifest is replaced with `If-Match` on the `ETag` it was read with, or
5
+ * `If-None-Match: *` when there was none, which is the conditional write S3,
6
+ * GCS, R2 and tribunal all honour. Blobs are addressed by digest and written
7
+ * without a condition, first, so a manifest never names a blob that is not
8
+ * there yet.
9
+ */
10
+ import { linePath } from './line.js';
11
+ /** The most of a store's reason one miss carries, so a long body does not become the whole message. */
12
+ const REASON_LIMIT = 1000;
13
+ /**
14
+ * How long one request may take, its body included, when the options do not
15
+ * say. A store that accepts a connection and never answers would otherwise
16
+ * hold `ask`, `serve` and a publish open with nothing said.
17
+ */
18
+ const DEFAULT_TIMEOUT_MS = 60_000;
19
+ /**
20
+ * `<endpoint>/<kind>/<name>/manifest.json`, the line's entries beside it, and
21
+ * `<endpoint>/images/<digest>` shared by every line, so an image two lines name
22
+ * is stored once.
23
+ */
24
+ export function httpLineCell(options) {
25
+ const base = options.endpoint.replace(/\/+$/, '');
26
+ const headers = options.headers ?? {};
27
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
28
+ const where = (line, path) => path.startsWith('images/') ? `${base}/${path}` : `${base}/${linePath(line)}/${path}`;
29
+ // The signal is made per request and covers reading the body, so a store
30
+ // that sends its headers and then stalls times out like one that never
31
+ // answers at all.
32
+ async function get(url) {
33
+ try {
34
+ const response = await fetch(url, { headers: { ...headers }, signal: AbortSignal.timeout(timeoutMs) });
35
+ if (!response.ok)
36
+ return await missOf(url, response);
37
+ return { bytes: new Uint8Array(await response.arrayBuffer()), etag: response.headers.get('etag') };
38
+ }
39
+ catch (error) {
40
+ return thrown(url, error, timeoutMs);
41
+ }
42
+ }
43
+ async function put(url, body, condition) {
44
+ let response;
45
+ try {
46
+ response = await fetch(url, {
47
+ method: options.method ?? 'PUT',
48
+ headers: { 'content-type': 'application/octet-stream', ...headers, ...condition },
49
+ body,
50
+ signal: AbortSignal.timeout(timeoutMs),
51
+ });
52
+ }
53
+ catch (error) {
54
+ return thrown(url, error, timeoutMs);
55
+ }
56
+ if (response.ok)
57
+ return 'written';
58
+ if (response.status === 412 || response.status === 409)
59
+ return 'conflict';
60
+ // FIXME: a write answered 404 is read as `absent`, so the publish prints
61
+ // "nothing is published there" for a store that would not take the write.
62
+ return missOf(url, response);
63
+ }
64
+ return {
65
+ async load(line) {
66
+ const held = await get(where(line, 'manifest.json'));
67
+ if (!('bytes' in held))
68
+ return held;
69
+ // FIXME: a store that sends no ETag cannot be written conditionally, so
70
+ // two publishers racing on it can lose an entry. The empty version makes
71
+ // the next write unconditional rather than refused.
72
+ return { manifest: held.bytes, version: held.etag ?? '' };
73
+ },
74
+ async blob(line, path) {
75
+ const held = await get(where(line, path));
76
+ return 'bytes' in held ? held.bytes : held;
77
+ },
78
+ async store(line, write) {
79
+ // TODO: an entry or image no manifest names any more stays in the store,
80
+ // and a branch's line outlives the branch; nothing here deletes, and a
81
+ // bucket's lifecycle rule is the collector.
82
+ for (const [path, bytes] of write.blobs) {
83
+ const outcome = await put(where(line, path), bytes, {});
84
+ if (outcome === 'conflict') {
85
+ return { kind: 'unreachable', detail: `${path}: the store answered an unconditional write with a conflict` };
86
+ }
87
+ if (outcome !== 'written')
88
+ return outcome;
89
+ }
90
+ const condition = write.expected === undefined
91
+ ? { 'if-none-match': '*' }
92
+ : write.expected === ''
93
+ ? {}
94
+ : { 'if-match': write.expected };
95
+ return put(where(line, 'manifest.json'), write.manifest, condition);
96
+ },
97
+ };
98
+ }
99
+ /**
100
+ * A request that threw, as the miss it is. A timeout says how long it waited;
101
+ * anything else says the platform's message and, when it wraps one, the
102
+ * reason underneath, which is where a refused connection or an unknown host
103
+ * is named.
104
+ */
105
+ function thrown(url, error, timeoutMs) {
106
+ const failed = error;
107
+ if (failed.name === 'TimeoutError') {
108
+ return { kind: 'unreachable', detail: `${url}: timed out after ${String(timeoutMs / 1000)} s` };
109
+ }
110
+ const cause = typeof failed.cause?.message === 'string' ? `: ${failed.cause.message}` : '';
111
+ return { kind: 'unreachable', detail: `${url}: ${String(failed.message)}${cause}` };
112
+ }
113
+ /**
114
+ * A status the store answered with, as the miss it is. 404 is nothing
115
+ * published. 408, 429 and a 5xx say the store could not take the request now,
116
+ * so they are `unreachable`. Every other 4xx is the store saying no — a token
117
+ * it does not take, a key it will not hold, a path it does not route — so it
118
+ * is `refused`, and the detail carries the store's own reason when it sent one.
119
+ */
120
+ async function missOf(url, response) {
121
+ const { status } = response;
122
+ if (status === 404)
123
+ return { kind: 'absent' };
124
+ const reason = await reasonOf(response);
125
+ const detail = `${url}: HTTP ${String(status)}${reason === undefined ? '' : `: ${reason}`}`;
126
+ if (status >= 400 && status < 500 && status !== 408 && status !== 429)
127
+ return { kind: 'refused', detail };
128
+ return { kind: 'unreachable', detail };
129
+ }
130
+ /**
131
+ * The reason a store gave with a status: the `error` of a JSON body, as
132
+ * tribunal and most APIs send it, or a plain-text body, on one line. Undefined
133
+ * for any other body, which is a page for a person rather than a reason, and
134
+ * for a body that does not read.
135
+ */
136
+ async function reasonOf(response) {
137
+ const type = response.headers.get('content-type')?.split(';')[0]?.trim().toLowerCase();
138
+ if (type !== 'application/json' && type !== 'text/plain')
139
+ return undefined;
140
+ let body;
141
+ try {
142
+ body = await response.text();
143
+ }
144
+ catch {
145
+ return undefined;
146
+ }
147
+ let reason = body;
148
+ if (type === 'application/json') {
149
+ try {
150
+ const parsed = JSON.parse(body);
151
+ if (typeof parsed.error !== 'string')
152
+ return undefined;
153
+ reason = parsed.error;
154
+ }
155
+ catch {
156
+ return undefined;
157
+ }
158
+ }
159
+ const line = reason.replace(/\s+/g, ' ').trim();
160
+ if (line === '')
161
+ return undefined;
162
+ return line.length > REASON_LIMIT ? `${line.slice(0, REASON_LIMIT)}…` : line;
163
+ }
164
+ //# sourceMappingURL=http-line.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"http-line.js","sourceRoot":"","sources":["../../src/share/http-line.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * A line over HTTP: a bucket, a signed base, a tribunal deployment.\n *\n * The manifest is replaced with `If-Match` on the `ETag` it was read with, or\n * `If-None-Match: *` when there was none, which is the conditional write S3,\n * GCS, R2 and tribunal all honour. Blobs are addressed by digest and written\n * without a condition, first, so a manifest never names a blob that is not\n * there yet.\n */\n\nimport { linePath, type BlobPath, type LineCell, type ShareLine, type ShareMiss } from './line.js';\n\n// Declared rather than imported, as elsewhere in this package: `core` takes no\n// platform's types. Only the members used are named.\ndeclare const fetch: (\n url: string,\n init?: {\n method?: string;\n headers?: Record<string, string>;\n body?: Uint8Array;\n signal?: unknown;\n },\n) => Promise<{\n readonly ok: boolean;\n readonly status: number;\n readonly headers: { get(name: string): string | null };\n arrayBuffer(): Promise<ArrayBuffer>;\n text(): Promise<string>;\n}>;\ndeclare const AbortSignal: { timeout(ms: number): unknown };\n\ntype Answer = Awaited<ReturnType<typeof fetch>>;\n\n/** The most of a store's reason one miss carries, so a long body does not become the whole message. */\nconst REASON_LIMIT = 1000;\n\n/**\n * How long one request may take, its body included, when the options do not\n * say. A store that accepts a connection and never answers would otherwise\n * hold `ask`, `serve` and a publish open with nothing said.\n */\nconst DEFAULT_TIMEOUT_MS = 60_000;\n\nexport interface HttpLineOptions {\n /** Base URL. Every path is appended to it, with exactly one `/` between. */\n readonly endpoint: string;\n /** Sent on every request: a bucket's `Authorization`, a deployment's token. */\n readonly headers?: Readonly<Record<string, string>>;\n /** The verb a write uses. `PUT` for a bucket, `POST` for a deployment that routes on it. */\n readonly method?: 'PUT' | 'POST';\n /**\n * Milliseconds one request may take, from sending it to the last byte of its\n * body, before the store is `unreachable`. 60 000 when absent.\n */\n readonly timeoutMs?: number;\n}\n\n/**\n * `<endpoint>/<kind>/<name>/manifest.json`, the line's entries beside it, and\n * `<endpoint>/images/<digest>` shared by every line, so an image two lines name\n * is stored once.\n */\nexport function httpLineCell(options: HttpLineOptions): LineCell {\n const base = options.endpoint.replace(/\\/+$/, '');\n const headers = options.headers ?? {};\n const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n const where = (line: ShareLine, path: 'manifest.json' | BlobPath): string =>\n path.startsWith('images/') ? `${base}/${path}` : `${base}/${linePath(line)}/${path}`;\n\n // The signal is made per request and covers reading the body, so a store\n // that sends its headers and then stalls times out like one that never\n // answers at all.\n async function get(url: string): Promise<{ bytes: Uint8Array; etag: string | null } | ShareMiss> {\n try {\n const response = await fetch(url, { headers: { ...headers }, signal: AbortSignal.timeout(timeoutMs) });\n if (!response.ok) return await missOf(url, response);\n return { bytes: new Uint8Array(await response.arrayBuffer()), etag: response.headers.get('etag') };\n } catch (error) {\n return thrown(url, error, timeoutMs);\n }\n }\n\n async function put(url: string, body: Uint8Array, condition: Record<string, string>): Promise<'written' | 'conflict' | ShareMiss> {\n let response: Answer;\n try {\n response = await fetch(url, {\n method: options.method ?? 'PUT',\n headers: { 'content-type': 'application/octet-stream', ...headers, ...condition },\n body,\n signal: AbortSignal.timeout(timeoutMs),\n });\n } catch (error) {\n return thrown(url, error, timeoutMs);\n }\n if (response.ok) return 'written';\n if (response.status === 412 || response.status === 409) return 'conflict';\n // FIXME: a write answered 404 is read as `absent`, so the publish prints\n // \"nothing is published there\" for a store that would not take the write.\n return missOf(url, response);\n }\n\n return {\n async load(line) {\n const held = await get(where(line, 'manifest.json'));\n if (!('bytes' in held)) return held;\n // FIXME: a store that sends no ETag cannot be written conditionally, so\n // two publishers racing on it can lose an entry. The empty version makes\n // the next write unconditional rather than refused.\n return { manifest: held.bytes, version: held.etag ?? '' };\n },\n async blob(line, path) {\n const held = await get(where(line, path));\n return 'bytes' in held ? held.bytes : held;\n },\n async store(line, write) {\n // TODO: an entry or image no manifest names any more stays in the store,\n // and a branch's line outlives the branch; nothing here deletes, and a\n // bucket's lifecycle rule is the collector.\n for (const [path, bytes] of write.blobs) {\n const outcome = await put(where(line, path), bytes, {});\n if (outcome === 'conflict') {\n return { kind: 'unreachable', detail: `${path}: the store answered an unconditional write with a conflict` };\n }\n if (outcome !== 'written') return outcome;\n }\n const condition =\n write.expected === undefined\n ? { 'if-none-match': '*' }\n : write.expected === ''\n ? {}\n : { 'if-match': write.expected };\n return put(where(line, 'manifest.json'), write.manifest, condition);\n },\n };\n}\n\n/**\n * A request that threw, as the miss it is. A timeout says how long it waited;\n * anything else says the platform's message and, when it wraps one, the\n * reason underneath, which is where a refused connection or an unknown host\n * is named.\n */\nfunction thrown(url: string, error: unknown, timeoutMs: number): ShareMiss {\n const failed = error as { readonly name?: unknown; readonly message?: unknown; readonly cause?: { readonly message?: unknown } };\n if (failed.name === 'TimeoutError') {\n return { kind: 'unreachable', detail: `${url}: timed out after ${String(timeoutMs / 1000)} s` };\n }\n const cause = typeof failed.cause?.message === 'string' ? `: ${failed.cause.message}` : '';\n return { kind: 'unreachable', detail: `${url}: ${String(failed.message)}${cause}` };\n}\n\n/**\n * A status the store answered with, as the miss it is. 404 is nothing\n * published. 408, 429 and a 5xx say the store could not take the request now,\n * so they are `unreachable`. Every other 4xx is the store saying no — a token\n * it does not take, a key it will not hold, a path it does not route — so it\n * is `refused`, and the detail carries the store's own reason when it sent one.\n */\nasync function missOf(url: string, response: Answer): Promise<ShareMiss> {\n const { status } = response;\n if (status === 404) return { kind: 'absent' };\n const reason = await reasonOf(response);\n const detail = `${url}: HTTP ${String(status)}${reason === undefined ? '' : `: ${reason}`}`;\n if (status >= 400 && status < 500 && status !== 408 && status !== 429) return { kind: 'refused', detail };\n\n return { kind: 'unreachable', detail };\n}\n\n/**\n * The reason a store gave with a status: the `error` of a JSON body, as\n * tribunal and most APIs send it, or a plain-text body, on one line. Undefined\n * for any other body, which is a page for a person rather than a reason, and\n * for a body that does not read.\n */\nasync function reasonOf(response: Answer): Promise<string | undefined> {\n const type = response.headers.get('content-type')?.split(';')[0]?.trim().toLowerCase();\n if (type !== 'application/json' && type !== 'text/plain') return undefined;\n let body: string;\n try {\n body = await response.text();\n } catch {\n return undefined;\n }\n let reason = body;\n if (type === 'application/json') {\n try {\n const parsed = JSON.parse(body) as { readonly error?: unknown };\n if (typeof parsed.error !== 'string') return undefined;\n reason = parsed.error;\n } catch {\n return undefined;\n }\n }\n const line = reason.replace(/\\s+/g, ' ').trim();\n if (line === '') return undefined;\n\n return line.length > REASON_LIMIT ? `${line.slice(0, REASON_LIMIT)}…` : line;\n}\n"]}