@variance-authority/core 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (260) hide show
  1. package/CHANGELOG.md +79 -0
  2. package/LICENSE +21 -0
  3. package/README.md +169 -0
  4. package/dist/artifact.d.ts +47 -0
  5. package/dist/artifact.js +2 -0
  6. package/dist/artifact.js.map +1 -0
  7. package/dist/attribute/because.d.ts +43 -0
  8. package/dist/attribute/because.js +64 -0
  9. package/dist/attribute/because.js.map +1 -0
  10. package/dist/attribute/boundary.d.ts +191 -0
  11. package/dist/attribute/boundary.js +328 -0
  12. package/dist/attribute/boundary.js.map +1 -0
  13. package/dist/attribute/call-site.d.ts +96 -0
  14. package/dist/attribute/call-site.js +183 -0
  15. package/dist/attribute/call-site.js.map +1 -0
  16. package/dist/attribute/component-hash.d.ts +188 -0
  17. package/dist/attribute/component-hash.js +312 -0
  18. package/dist/attribute/component-hash.js.map +1 -0
  19. package/dist/attribute/composition-fixture.d.ts +18 -0
  20. package/dist/attribute/composition-fixture.js +53 -0
  21. package/dist/attribute/composition-fixture.js.map +1 -0
  22. package/dist/attribute/composition.d.ts +182 -0
  23. package/dist/attribute/composition.js +169 -0
  24. package/dist/attribute/composition.js.map +1 -0
  25. package/dist/attribute/control.d.ts +43 -0
  26. package/dist/attribute/control.js +54 -0
  27. package/dist/attribute/control.js.map +1 -0
  28. package/dist/attribute/coverage.d.ts +99 -0
  29. package/dist/attribute/coverage.js +86 -0
  30. package/dist/attribute/coverage.js.map +1 -0
  31. package/dist/attribute/divergence.d.ts +102 -0
  32. package/dist/attribute/divergence.js +152 -0
  33. package/dist/attribute/divergence.js.map +1 -0
  34. package/dist/attribute/index.d.ts +43 -0
  35. package/dist/attribute/index.js +29 -0
  36. package/dist/attribute/index.js.map +1 -0
  37. package/dist/attribute/instability.d.ts +89 -0
  38. package/dist/attribute/instability.js +183 -0
  39. package/dist/attribute/instability.js.map +1 -0
  40. package/dist/attribute/instances.d.ts +145 -0
  41. package/dist/attribute/instances.js +140 -0
  42. package/dist/attribute/instances.js.map +1 -0
  43. package/dist/attribute/locate.d.ts +22 -0
  44. package/dist/attribute/locate.js +100 -0
  45. package/dist/attribute/locate.js.map +1 -0
  46. package/dist/attribute/mask.d.ts +156 -0
  47. package/dist/attribute/mask.js +201 -0
  48. package/dist/attribute/mask.js.map +1 -0
  49. package/dist/attribute/movement.d.ts +211 -0
  50. package/dist/attribute/movement.js +181 -0
  51. package/dist/attribute/movement.js.map +1 -0
  52. package/dist/attribute/region.d.ts +188 -0
  53. package/dist/attribute/region.js +144 -0
  54. package/dist/attribute/region.js.map +1 -0
  55. package/dist/attribute/source-map.d.ts +98 -0
  56. package/dist/attribute/source-map.js +264 -0
  57. package/dist/attribute/source-map.js.map +1 -0
  58. package/dist/attribute/source.d.ts +77 -0
  59. package/dist/attribute/source.js +95 -0
  60. package/dist/attribute/source.js.map +1 -0
  61. package/dist/attribute/stack.d.ts +80 -0
  62. package/dist/attribute/stack.js +108 -0
  63. package/dist/attribute/stack.js.map +1 -0
  64. package/dist/compare/band.d.ts +85 -0
  65. package/dist/compare/band.js +70 -0
  66. package/dist/compare/band.js.map +1 -0
  67. package/dist/compare/cascade.d.ts +45 -0
  68. package/dist/compare/cascade.js +71 -0
  69. package/dist/compare/cascade.js.map +1 -0
  70. package/dist/compare/derive.d.ts +78 -0
  71. package/dist/compare/derive.js +39 -0
  72. package/dist/compare/derive.js.map +1 -0
  73. package/dist/compare/diff/attribution.d.ts +23 -0
  74. package/dist/compare/diff/attribution.js +244 -0
  75. package/dist/compare/diff/attribution.js.map +1 -0
  76. package/dist/compare/diff/compare-nodes.d.ts +15 -0
  77. package/dist/compare/diff/compare-nodes.js +227 -0
  78. package/dist/compare/diff/compare-nodes.js.map +1 -0
  79. package/dist/compare/diff/components.d.ts +20 -0
  80. package/dist/compare/diff/components.js +76 -0
  81. package/dist/compare/diff/components.js.map +1 -0
  82. package/dist/compare/diff/delta.d.ts +165 -0
  83. package/dist/compare/diff/delta.js +12 -0
  84. package/dist/compare/diff/delta.js.map +1 -0
  85. package/dist/compare/diff/index.d.ts +69 -0
  86. package/dist/compare/diff/index.js +101 -0
  87. package/dist/compare/diff/index.js.map +1 -0
  88. package/dist/compare/diff/match.d.ts +37 -0
  89. package/dist/compare/diff/match.js +167 -0
  90. package/dist/compare/diff/match.js.map +1 -0
  91. package/dist/compare/explain.d.ts +23 -0
  92. package/dist/compare/explain.js +190 -0
  93. package/dist/compare/explain.js.map +1 -0
  94. package/dist/compare/holding-diff.d.ts +25 -0
  95. package/dist/compare/holding-diff.js +74 -0
  96. package/dist/compare/holding-diff.js.map +1 -0
  97. package/dist/compare/impact.d.ts +56 -0
  98. package/dist/compare/impact.js +124 -0
  99. package/dist/compare/impact.js.map +1 -0
  100. package/dist/compare/index.d.ts +30 -0
  101. package/dist/compare/index.js +22 -0
  102. package/dist/compare/index.js.map +1 -0
  103. package/dist/compare/instance.d.ts +30 -0
  104. package/dist/compare/instance.js +145 -0
  105. package/dist/compare/instance.js.map +1 -0
  106. package/dist/compare/observability.d.ts +49 -0
  107. package/dist/compare/observability.js +57 -0
  108. package/dist/compare/observability.js.map +1 -0
  109. package/dist/compare/parting.d.ts +179 -0
  110. package/dist/compare/parting.js +244 -0
  111. package/dist/compare/parting.js.map +1 -0
  112. package/dist/compare/slice.d.ts +74 -0
  113. package/dist/compare/slice.js +79 -0
  114. package/dist/compare/slice.js.map +1 -0
  115. package/dist/compare/value.d.ts +60 -0
  116. package/dist/compare/value.js +111 -0
  117. package/dist/compare/value.js.map +1 -0
  118. package/dist/format/accessibility.d.ts +27 -0
  119. package/dist/format/accessibility.js +17 -0
  120. package/dist/format/accessibility.js.map +1 -0
  121. package/dist/format/canonical.d.ts +38 -0
  122. package/dist/format/canonical.js +65 -0
  123. package/dist/format/canonical.js.map +1 -0
  124. package/dist/format/capture.d.ts +212 -0
  125. package/dist/format/capture.js +2 -0
  126. package/dist/format/capture.js.map +1 -0
  127. package/dist/format/document.d.ts +279 -0
  128. package/dist/format/document.js +65 -0
  129. package/dist/format/document.js.map +1 -0
  130. package/dist/format/environment.d.ts +120 -0
  131. package/dist/format/environment.js +53 -0
  132. package/dist/format/environment.js.map +1 -0
  133. package/dist/format/hash.d.ts +45 -0
  134. package/dist/format/hash.js +41 -0
  135. package/dist/format/hash.js.map +1 -0
  136. package/dist/format/holding.d.ts +134 -0
  137. package/dist/format/holding.js +2 -0
  138. package/dist/format/holding.js.map +1 -0
  139. package/dist/format/index.d.ts +39 -0
  140. package/dist/format/index.js +25 -0
  141. package/dist/format/index.js.map +1 -0
  142. package/dist/format/intervention.d.ts +87 -0
  143. package/dist/format/intervention.js +11 -0
  144. package/dist/format/intervention.js.map +1 -0
  145. package/dist/format/profile.d.ts +59 -0
  146. package/dist/format/profile.js +66 -0
  147. package/dist/format/profile.js.map +1 -0
  148. package/dist/format/provenance.d.ts +179 -0
  149. package/dist/format/provenance.js +222 -0
  150. package/dist/format/provenance.js.map +1 -0
  151. package/dist/format/sha256.d.ts +34 -0
  152. package/dist/format/sha256.js +152 -0
  153. package/dist/format/sha256.js.map +1 -0
  154. package/dist/format/snapshot.d.ts +263 -0
  155. package/dist/format/snapshot.js +2 -0
  156. package/dist/format/snapshot.js.map +1 -0
  157. package/dist/format/stabilize.d.ts +190 -0
  158. package/dist/format/stabilize.js +333 -0
  159. package/dist/format/stabilize.js.map +1 -0
  160. package/dist/format/tier.d.ts +19 -0
  161. package/dist/format/tier.js +11 -0
  162. package/dist/format/tier.js.map +1 -0
  163. package/dist/format/value.d.ts +115 -0
  164. package/dist/format/value.js +182 -0
  165. package/dist/format/value.js.map +1 -0
  166. package/dist/format/wiring.d.ts +115 -0
  167. package/dist/format/wiring.js +60 -0
  168. package/dist/format/wiring.js.map +1 -0
  169. package/dist/index.d.ts +35 -0
  170. package/dist/index.js +35 -0
  171. package/dist/index.js.map +1 -0
  172. package/dist/judge/docket.d.ts +85 -0
  173. package/dist/judge/docket.js +162 -0
  174. package/dist/judge/docket.js.map +1 -0
  175. package/dist/judge/fingerprint.d.ts +53 -0
  176. package/dist/judge/fingerprint.js +144 -0
  177. package/dist/judge/fingerprint.js.map +1 -0
  178. package/dist/judge/ignore.d.ts +206 -0
  179. package/dist/judge/ignore.js +237 -0
  180. package/dist/judge/ignore.js.map +1 -0
  181. package/dist/judge/index.d.ts +44 -0
  182. package/dist/judge/index.js +34 -0
  183. package/dist/judge/index.js.map +1 -0
  184. package/dist/judge/inspect.d.ts +148 -0
  185. package/dist/judge/inspect.js +208 -0
  186. package/dist/judge/inspect.js.map +1 -0
  187. package/dist/judge/intent.d.ts +146 -0
  188. package/dist/judge/intent.js +150 -0
  189. package/dist/judge/intent.js.map +1 -0
  190. package/dist/judge/locale.d.ts +111 -0
  191. package/dist/judge/locale.js +247 -0
  192. package/dist/judge/locale.js.map +1 -0
  193. package/dist/judge/scope.d.ts +41 -0
  194. package/dist/judge/scope.js +47 -0
  195. package/dist/judge/scope.js.map +1 -0
  196. package/dist/judge/sensitivity.d.ts +202 -0
  197. package/dist/judge/sensitivity.js +152 -0
  198. package/dist/judge/sensitivity.js.map +1 -0
  199. package/dist/judge/trail.d.ts +122 -0
  200. package/dist/judge/trail.js +96 -0
  201. package/dist/judge/trail.js.map +1 -0
  202. package/dist/judge/verdict.d.ts +39 -0
  203. package/dist/judge/verdict.js +32 -0
  204. package/dist/judge/verdict.js.map +1 -0
  205. package/dist/plan/index.d.ts +12 -0
  206. package/dist/plan/index.js +11 -0
  207. package/dist/plan/index.js.map +1 -0
  208. package/dist/plan/tool.d.ts +85 -0
  209. package/dist/plan/tool.js +87 -0
  210. package/dist/plan/tool.js.map +1 -0
  211. package/dist/relate/graph.d.ts +190 -0
  212. package/dist/relate/graph.js +234 -0
  213. package/dist/relate/graph.js.map +1 -0
  214. package/dist/relate/index.d.ts +16 -0
  215. package/dist/relate/index.js +16 -0
  216. package/dist/relate/index.js.map +1 -0
  217. package/dist/relate/merkle.d.ts +110 -0
  218. package/dist/relate/merkle.js +246 -0
  219. package/dist/relate/merkle.js.map +1 -0
  220. package/dist/relate/reach.d.ts +65 -0
  221. package/dist/relate/reach.js +111 -0
  222. package/dist/relate/reach.js.map +1 -0
  223. package/dist/relate/records.d.ts +129 -0
  224. package/dist/relate/records.js +112 -0
  225. package/dist/relate/records.js.map +1 -0
  226. package/dist/rules/index.d.ts +19 -0
  227. package/dist/rules/index.js +17 -0
  228. package/dist/rules/index.js.map +1 -0
  229. package/dist/rules/normalize/alias.d.ts +50 -0
  230. package/dist/rules/normalize/alias.js +111 -0
  231. package/dist/rules/normalize/alias.js.map +1 -0
  232. package/dist/rules/normalize/cascade.d.ts +85 -0
  233. package/dist/rules/normalize/cascade.js +324 -0
  234. package/dist/rules/normalize/cascade.js.map +1 -0
  235. package/dist/rules/normalize/color.d.ts +30 -0
  236. package/dist/rules/normalize/color.js +185 -0
  237. package/dist/rules/normalize/color.js.map +1 -0
  238. package/dist/rules/normalize/fixture.d.ts +83 -0
  239. package/dist/rules/normalize/fixture.js +74 -0
  240. package/dist/rules/normalize/fixture.js.map +1 -0
  241. package/dist/rules/normalize/index.d.ts +39 -0
  242. package/dist/rules/normalize/index.js +268 -0
  243. package/dist/rules/normalize/index.js.map +1 -0
  244. package/dist/rules/normalize/project.d.ts +42 -0
  245. package/dist/rules/normalize/project.js +83 -0
  246. package/dist/rules/normalize/project.js.map +1 -0
  247. package/dist/rules/normalize/shorthand.d.ts +38 -0
  248. package/dist/rules/normalize/shorthand.js +388 -0
  249. package/dist/rules/normalize/shorthand.js.map +1 -0
  250. package/dist/rules/normalize/value.d.ts +23 -0
  251. package/dist/rules/normalize/value.js +162 -0
  252. package/dist/rules/normalize/value.js.map +1 -0
  253. package/dist/rules/normalize/wrapper.d.ts +16 -0
  254. package/dist/rules/normalize/wrapper.js +137 -0
  255. package/dist/rules/normalize/wrapper.js.map +1 -0
  256. package/dist/rules/ruleset.d.ts +93 -0
  257. package/dist/rules/ruleset.js +175 -0
  258. package/dist/rules/ruleset.js.map +1 -0
  259. package/mark.svg +30 -0
  260. package/package.json +69 -0
@@ -0,0 +1,60 @@
1
+ import { type CapturedValue } from '../format/value.js';
2
+ import { type Digest } from '../format/hash.js';
3
+ /**
4
+ * Two values become deltas — and no verdict (spec 0031).
5
+ *
6
+ * `compare/diff` does this for two rendered documents. This is the same shape at
7
+ * the floor of the subject list, where there is no schema, no position and
8
+ * therefore nothing that can decide whether a caller survives. What is left is
9
+ * real and smaller: which paths appeared, which vanished, which changed type,
10
+ * and which changed value.
11
+ *
12
+ * ## Why no library
13
+ *
14
+ * The untyped case is usually reached for `jsondiffpatch`, whose value is a
15
+ * longest-common-subsequence match over arrays and an optional text diff. Neither
16
+ * is wanted here: `ValueShaping.arrayKey` turns an array into an object
17
+ * before this function ever sees it, which is a better answer than guessing at
18
+ * moves, and a text diff of a leaf is a picture of a difference rather than a
19
+ * name for it. What remains is arithmetic, and `core` carries no dependency.
20
+ *
21
+ * ## What a delta does not carry
22
+ *
23
+ * No values. The two canonical texts are the baseline and the candidate and a
24
+ * person reads them side by side; a delta that quoted them would put a payload
25
+ * into the docket, the changelog and every record that accumulates — which is a
26
+ * retention decision this project must not make on an adopter's behalf.
27
+ */
28
+ export type ValueChange = 'added' | 'removed' | 'type-changed' | 'value-changed';
29
+ export interface ValueDelta {
30
+ readonly change: ValueChange;
31
+ /** RFC 6901 pointer to what moved. The empty string is the whole value. */
32
+ readonly pointer: string;
33
+ /**
34
+ * The shape of this difference, with position and values removed.
35
+ *
36
+ * Taken over the dialect, the *wildcarded* pointer and the kind of change — so
37
+ * the same edit at row 4 and at row 900 is one shape happening twice, and two
38
+ * subjects carrying it can be settled by one decision. Deliberately not over
39
+ * the subject: a fingerprint that named its subject could never group two.
40
+ */
41
+ readonly fingerprint: Digest;
42
+ }
43
+ /**
44
+ * Every difference between two shaped values, one delta per site.
45
+ *
46
+ * A subtree that appeared, vanished or changed kind is **one** delta at its root
47
+ * and is not descended into. A removed object reported as one removal per leaf is
48
+ * the forty-red-rectangles failure in a medium where nobody can see it at a
49
+ * glance, and the root is the thing a person would fix.
50
+ */
51
+ export declare function compareValues(baseline: CapturedValue, candidate: CapturedValue): readonly ValueDelta[];
52
+ /**
53
+ * The fingerprint a delta would carry, without having to produce the delta.
54
+ *
55
+ * Exported so that a rule scoped to a shape is written by copying a digest out of
56
+ * a report rather than by deriving one — the same property region fingerprints
57
+ * have, for the same reason.
58
+ */
59
+ export declare function fingerprintOfValueDelta(dialect: string, pointer: string, change: ValueChange, keyed?: readonly string[]): Digest;
60
+ //# sourceMappingURL=value.d.ts.map
@@ -0,0 +1,111 @@
1
+ import { pointerShape, pointerToken } from '../format/value.js';
2
+ import { digestCombine, digestString } from '../format/hash.js';
3
+ /**
4
+ * Every difference between two shaped values, one delta per site.
5
+ *
6
+ * A subtree that appeared, vanished or changed kind is **one** delta at its root
7
+ * and is not descended into. A removed object reported as one removal per leaf is
8
+ * the forty-red-rectangles failure in a medium where nobody can see it at a
9
+ * glance, and the root is the thing a person would fix.
10
+ */
11
+ export function compareValues(baseline, candidate) {
12
+ if (baseline.dialect !== candidate.dialect) {
13
+ throw new Error(`cannot compare a ${baseline.dialect} value against a ${candidate.dialect} one`);
14
+ }
15
+ if (baseline.recipe !== candidate.recipe) {
16
+ throw new Error(`these values were shaped under different rules (${baseline.recipe} and ` +
17
+ `${candidate.recipe}), so a difference between them may be the rules and not the value`);
18
+ }
19
+ const keyed = (baseline.keyed ?? []).join(' ');
20
+ if (keyed !== (candidate.keyed ?? []).join(' ')) {
21
+ throw new Error('these values keyed different arrays, so a row named by its identity on one side is ' +
22
+ 'named by its position on the other');
23
+ }
24
+ const found = [];
25
+ walk(parse(baseline), parse(candidate), '', baseline.dialect, baseline.keyed ?? [], found);
26
+ // Code-unit ordering. Never `localeCompare`: this ordering reaches a report.
27
+ return found.sort((left, right) => {
28
+ if (left.pointer !== right.pointer)
29
+ return left.pointer < right.pointer ? -1 : 1;
30
+ if (left.change === right.change)
31
+ return 0;
32
+ return left.change < right.change ? -1 : 1;
33
+ });
34
+ }
35
+ /**
36
+ * The fingerprint a delta would carry, without having to produce the delta.
37
+ *
38
+ * Exported so that a rule scoped to a shape is written by copying a digest out of
39
+ * a report rather than by deriving one — the same property region fingerprints
40
+ * have, for the same reason.
41
+ */
42
+ export function fingerprintOfValueDelta(dialect, pointer, change, keyed = []) {
43
+ const site = digestCombine('value-site/v1', [
44
+ digestString(dialect),
45
+ digestString(pointerShape(pointer, keyed)),
46
+ ]);
47
+ return digestCombine('value-change/v1', [site, digestString(change)]);
48
+ }
49
+ function parse(value) {
50
+ try {
51
+ return JSON.parse(value.text);
52
+ }
53
+ catch (error) {
54
+ throw new Error(`a ${value.dialect} value at ${value.digest} is not readable`, {
55
+ cause: error,
56
+ });
57
+ }
58
+ }
59
+ function kindOf(value) {
60
+ if (value === null)
61
+ return 'null';
62
+ if (Array.isArray(value))
63
+ return 'array';
64
+ return typeof value;
65
+ }
66
+ function walk(before, after, pointer, dialect, keyed, found) {
67
+ const was = kindOf(before);
68
+ const now = kindOf(after);
69
+ const record = (change, at) => {
70
+ found.push({
71
+ change,
72
+ pointer: at,
73
+ fingerprint: fingerprintOfValueDelta(dialect, at, change, keyed),
74
+ });
75
+ };
76
+ if (was !== now) {
77
+ record('type-changed', pointer);
78
+ return;
79
+ }
80
+ if (was === 'array') {
81
+ const olds = before;
82
+ const news = after;
83
+ for (let index = 0; index < Math.max(olds.length, news.length); index += 1) {
84
+ const at = `${pointer}/${index}`;
85
+ if (index >= news.length)
86
+ record('removed', at);
87
+ else if (index >= olds.length)
88
+ record('added', at);
89
+ else
90
+ walk(olds[index], news[index], at, dialect, keyed, found);
91
+ }
92
+ return;
93
+ }
94
+ if (was === 'object') {
95
+ const olds = before;
96
+ const news = after;
97
+ for (const key of new Set([...Object.keys(olds), ...Object.keys(news)])) {
98
+ const at = `${pointer}/${pointerToken(key)}`;
99
+ if (!(key in news))
100
+ record('removed', at);
101
+ else if (!(key in olds))
102
+ record('added', at);
103
+ else
104
+ walk(olds[key], news[key], at, dialect, keyed, found);
105
+ }
106
+ return;
107
+ }
108
+ if (before !== after)
109
+ record('value-changed', pointer);
110
+ }
111
+ //# sourceMappingURL=value.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"value.js","sourceRoot":"","sources":["../../src/compare/value.ts"],"names":[],"mappings":"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","sourcesContent":["import { pointerShape, pointerToken, type CapturedValue } from '../format/value.js';\nimport { digestCombine, digestString, type Digest } from '../format/hash.js';\n\n/**\n * Two values become deltas — and no verdict (spec 0031).\n *\n * `compare/diff` does this for two rendered documents. This is the same shape at\n * the floor of the subject list, where there is no schema, no position and\n * therefore nothing that can decide whether a caller survives. What is left is\n * real and smaller: which paths appeared, which vanished, which changed type,\n * and which changed value.\n *\n * ## Why no library\n *\n * The untyped case is usually reached for `jsondiffpatch`, whose value is a\n * longest-common-subsequence match over arrays and an optional text diff. Neither\n * is wanted here: `ValueShaping.arrayKey` turns an array into an object\n * before this function ever sees it, which is a better answer than guessing at\n * moves, and a text diff of a leaf is a picture of a difference rather than a\n * name for it. What remains is arithmetic, and `core` carries no dependency.\n *\n * ## What a delta does not carry\n *\n * No values. The two canonical texts are the baseline and the candidate and a\n * person reads them side by side; a delta that quoted them would put a payload\n * into the docket, the changelog and every record that accumulates — which is a\n * retention decision this project must not make on an adopter's behalf.\n */\n\nexport type ValueChange = 'added' | 'removed' | 'type-changed' | 'value-changed';\n\nexport interface ValueDelta {\n readonly change: ValueChange;\n\n /** RFC 6901 pointer to what moved. The empty string is the whole value. */\n readonly pointer: string;\n\n /**\n * The shape of this difference, with position and values removed.\n *\n * Taken over the dialect, the *wildcarded* pointer and the kind of change — so\n * the same edit at row 4 and at row 900 is one shape happening twice, and two\n * subjects carrying it can be settled by one decision. Deliberately not over\n * the subject: a fingerprint that named its subject could never group two.\n */\n readonly fingerprint: Digest;\n}\n\n/** What kind of thing a canonical value is. A change between two of these is structural. */\ntype ValueKind = 'null' | 'boolean' | 'number' | 'string' | 'array' | 'object';\n\n/**\n * Every difference between two shaped values, one delta per site.\n *\n * A subtree that appeared, vanished or changed kind is **one** delta at its root\n * and is not descended into. A removed object reported as one removal per leaf is\n * the forty-red-rectangles failure in a medium where nobody can see it at a\n * glance, and the root is the thing a person would fix.\n */\nexport function compareValues(\n baseline: CapturedValue,\n candidate: CapturedValue,\n): readonly ValueDelta[] {\n if (baseline.dialect !== candidate.dialect) {\n throw new Error(\n `cannot compare a ${baseline.dialect} value against a ${candidate.dialect} one`,\n );\n }\n if (baseline.recipe !== candidate.recipe) {\n throw new Error(\n `these values were shaped under different rules (${baseline.recipe} and ` +\n `${candidate.recipe}), so a difference between them may be the rules and not the value`,\n );\n }\n\n const keyed = (baseline.keyed ?? []).join(' ');\n if (keyed !== (candidate.keyed ?? []).join(' ')) {\n throw new Error(\n 'these values keyed different arrays, so a row named by its identity on one side is ' +\n 'named by its position on the other',\n );\n }\n\n const found: ValueDelta[] = [];\n walk(parse(baseline), parse(candidate), '', baseline.dialect, baseline.keyed ?? [], found);\n\n // Code-unit ordering. Never `localeCompare`: this ordering reaches a report.\n return found.sort((left, right) => {\n if (left.pointer !== right.pointer) return left.pointer < right.pointer ? -1 : 1;\n if (left.change === right.change) return 0;\n return left.change < right.change ? -1 : 1;\n });\n}\n\n/**\n * The fingerprint a delta would carry, without having to produce the delta.\n *\n * Exported so that a rule scoped to a shape is written by copying a digest out of\n * a report rather than by deriving one — the same property region fingerprints\n * have, for the same reason.\n */\nexport function fingerprintOfValueDelta(\n dialect: string,\n pointer: string,\n change: ValueChange,\n keyed: readonly string[] = [],\n): Digest {\n const site = digestCombine('value-site/v1', [\n digestString(dialect),\n digestString(pointerShape(pointer, keyed)),\n ]);\n return digestCombine('value-change/v1', [site, digestString(change)]);\n}\n\nfunction parse(value: CapturedValue): unknown {\n try {\n return JSON.parse(value.text);\n } catch (error) {\n throw new Error(`a ${value.dialect} value at ${value.digest} is not readable`, {\n cause: error,\n });\n }\n}\n\nfunction kindOf(value: unknown): ValueKind {\n if (value === null) return 'null';\n if (Array.isArray(value)) return 'array';\n return typeof value as ValueKind;\n}\n\nfunction walk(\n before: unknown,\n after: unknown,\n pointer: string,\n dialect: string,\n keyed: readonly string[],\n found: ValueDelta[],\n): void {\n const was = kindOf(before);\n const now = kindOf(after);\n\n const record = (change: ValueChange, at: string): void => {\n found.push({\n change,\n pointer: at,\n fingerprint: fingerprintOfValueDelta(dialect, at, change, keyed),\n });\n };\n\n if (was !== now) {\n record('type-changed', pointer);\n return;\n }\n\n if (was === 'array') {\n const olds = before as readonly unknown[];\n const news = after as readonly unknown[];\n for (let index = 0; index < Math.max(olds.length, news.length); index += 1) {\n const at = `${pointer}/${index}`;\n if (index >= news.length) record('removed', at);\n else if (index >= olds.length) record('added', at);\n else walk(olds[index], news[index], at, dialect, keyed, found);\n }\n return;\n }\n\n if (was === 'object') {\n const olds = before as Record<string, unknown>;\n const news = after as Record<string, unknown>;\n for (const key of new Set([...Object.keys(olds), ...Object.keys(news)])) {\n const at = `${pointer}/${pointerToken(key)}`;\n if (!(key in news)) record('removed', at);\n else if (!(key in olds)) record('added', at);\n else walk(olds[key], news[key], at, dialect, keyed, found);\n }\n return;\n }\n\n if (before !== after) record('value-changed', pointer);\n}\n"]}
@@ -0,0 +1,27 @@
1
+ import { type Digest } from './hash.js';
2
+ /**
3
+ * The browser accessibility trees exposed by one Playwright acquisition.
4
+ *
5
+ * `roots[0]` is the subject locator. Remaining roots are React portal contents,
6
+ * in component-tree order, because a portal remains part of the subject even
7
+ * though its DOM lives elsewhere (ADR-0007). The text is Playwright's ARIA
8
+ * snapshot format rather than this project's ARIA approximation: browser
9
+ * visibility, naming and role computation are the boundary being observed.
10
+ */
11
+ export interface AccessibilitySnapshot {
12
+ readonly snapshotVersion: 1;
13
+ readonly producer: 'playwright-aria@1';
14
+ readonly engine: string;
15
+ /**
16
+ * One boundary-relative Playwright snapshot per requested root.
17
+ *
18
+ * An empty string is an observed root with no exposed accessibility nodes,
19
+ * not missing evidence. A root also need not name a parent or any children:
20
+ * boundary truncation is part of the reading and is hashed without repair.
21
+ */
22
+ readonly roots: readonly string[];
23
+ readonly digest: Digest;
24
+ }
25
+ /** Build the canonical, content-addressed form stored beside a raster. */
26
+ export declare function accessibilitySnapshot(engine: string, roots: readonly string[]): AccessibilitySnapshot;
27
+ //# sourceMappingURL=accessibility.d.ts.map
@@ -0,0 +1,17 @@
1
+ import { digestValue } from './hash.js';
2
+ /** Build the canonical, content-addressed form stored beside a raster. */
3
+ export function accessibilitySnapshot(engine, roots) {
4
+ const stableRoots = [...roots];
5
+ return {
6
+ snapshotVersion: 1,
7
+ producer: 'playwright-aria@1',
8
+ engine,
9
+ roots: stableRoots,
10
+ digest: digestValue({
11
+ producer: 'playwright-aria@1',
12
+ engine,
13
+ roots: stableRoots,
14
+ }),
15
+ };
16
+ }
17
+ //# sourceMappingURL=accessibility.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"accessibility.js","sourceRoot":"","sources":["../../src/format/accessibility.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAe,MAAM,WAAW,CAAC;AA0BrD,0EAA0E;AAC1E,MAAM,UAAU,qBAAqB,CACnC,MAAc,EACd,KAAwB;IAExB,MAAM,WAAW,GAAG,CAAC,GAAG,KAAK,CAAC,CAAC;IAC/B,OAAO;QACL,eAAe,EAAE,CAAC;QAClB,QAAQ,EAAE,mBAAmB;QAC7B,MAAM;QACN,KAAK,EAAE,WAAW;QAClB,MAAM,EAAE,WAAW,CAAC;YAClB,QAAQ,EAAE,mBAAmB;YAC7B,MAAM;YACN,KAAK,EAAE,WAAW;SACnB,CAAC;KACH,CAAC;AACJ,CAAC","sourcesContent":["import { digestValue, type Digest } from './hash.js';\n\n/**\n * The browser accessibility trees exposed by one Playwright acquisition.\n *\n * `roots[0]` is the subject locator. Remaining roots are React portal contents,\n * in component-tree order, because a portal remains part of the subject even\n * though its DOM lives elsewhere (ADR-0007). The text is Playwright's ARIA\n * snapshot format rather than this project's ARIA approximation: browser\n * visibility, naming and role computation are the boundary being observed.\n */\nexport interface AccessibilitySnapshot {\n readonly snapshotVersion: 1;\n readonly producer: 'playwright-aria@1';\n readonly engine: string;\n /**\n * One boundary-relative Playwright snapshot per requested root.\n *\n * An empty string is an observed root with no exposed accessibility nodes,\n * not missing evidence. A root also need not name a parent or any children:\n * boundary truncation is part of the reading and is hashed without repair.\n */\n readonly roots: readonly string[];\n readonly digest: Digest;\n}\n\n/** Build the canonical, content-addressed form stored beside a raster. */\nexport function accessibilitySnapshot(\n engine: string,\n roots: readonly string[],\n): AccessibilitySnapshot {\n const stableRoots = [...roots];\n return {\n snapshotVersion: 1,\n producer: 'playwright-aria@1',\n engine,\n roots: stableRoots,\n digest: digestValue({\n producer: 'playwright-aria@1',\n engine,\n roots: stableRoots,\n }),\n };\n}\n"]}
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Canonical serialization.
3
+ *
4
+ * Determinism (Principle 2) means byte-identical output for identical inputs, on
5
+ * any machine. `JSON.stringify` does not provide that: key order follows
6
+ * insertion order, and float formatting is the one place where "obvious" and
7
+ * "identical everywhere" diverge.
8
+ *
9
+ * Everything hashed by this system passes through `canonicalize` first.
10
+ */
11
+ export type CanonicalValue = string | number | boolean | null | readonly CanonicalValue[] | {
12
+ readonly [key: string]: CanonicalValue | undefined;
13
+ };
14
+ /**
15
+ * Serialize to a canonical string: object keys sorted by code unit, `undefined`
16
+ * members omitted (never emitted as `null`), and numbers formatted per
17
+ * {@link canonicalNumber}.
18
+ *
19
+ * Omitting rather than nulling `undefined` is what makes "a dimension this
20
+ * profile cannot observe" structurally absent from the hash input rather than
21
+ * present-and-empty (ADR-0002). Absence and emptiness must not collide.
22
+ *
23
+ * @throws {RangeError} on `NaN` or `Infinity` — a non-finite value in a hash
24
+ * input means an upstream measurement failed, and silently encoding it would
25
+ * produce a stable hash for a broken observation.
26
+ */
27
+ export declare function canonicalize(value: CanonicalValue): string;
28
+ /**
29
+ * Format a number identically across platforms.
30
+ *
31
+ * `-0` normalizes to `0`: it is a legitimate output of layout arithmetic and is
32
+ * indistinguishable from `0` to every consumer, but stringifies differently.
33
+ * Integers are emitted without a decimal point; other values keep full round-trip
34
+ * precision, having already been rounded to their dimension's tolerance by the
35
+ * ruleset (lengths to px at fixed precision) before reaching here.
36
+ */
37
+ export declare function canonicalNumber(value: number): string;
38
+ //# sourceMappingURL=canonical.d.ts.map
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Canonical serialization.
3
+ *
4
+ * Determinism (Principle 2) means byte-identical output for identical inputs, on
5
+ * any machine. `JSON.stringify` does not provide that: key order follows
6
+ * insertion order, and float formatting is the one place where "obvious" and
7
+ * "identical everywhere" diverge.
8
+ *
9
+ * Everything hashed by this system passes through `canonicalize` first.
10
+ */
11
+ /**
12
+ * Serialize to a canonical string: object keys sorted by code unit, `undefined`
13
+ * members omitted (never emitted as `null`), and numbers formatted per
14
+ * {@link canonicalNumber}.
15
+ *
16
+ * Omitting rather than nulling `undefined` is what makes "a dimension this
17
+ * profile cannot observe" structurally absent from the hash input rather than
18
+ * present-and-empty (ADR-0002). Absence and emptiness must not collide.
19
+ *
20
+ * @throws {RangeError} on `NaN` or `Infinity` — a non-finite value in a hash
21
+ * input means an upstream measurement failed, and silently encoding it would
22
+ * produce a stable hash for a broken observation.
23
+ */
24
+ export function canonicalize(value) {
25
+ if (value === null)
26
+ return 'null';
27
+ switch (typeof value) {
28
+ case 'boolean':
29
+ return value ? 'true' : 'false';
30
+ case 'number':
31
+ return canonicalNumber(value);
32
+ case 'string':
33
+ return JSON.stringify(value);
34
+ }
35
+ if (Array.isArray(value)) {
36
+ return `[${value.map((item) => canonicalize(item)).join(',')}]`;
37
+ }
38
+ const record = value;
39
+ const parts = [];
40
+ for (const key of Object.keys(record).sort()) {
41
+ const member = record[key];
42
+ if (member === undefined)
43
+ continue;
44
+ parts.push(`${JSON.stringify(key)}:${canonicalize(member)}`);
45
+ }
46
+ return `{${parts.join(',')}}`;
47
+ }
48
+ /**
49
+ * Format a number identically across platforms.
50
+ *
51
+ * `-0` normalizes to `0`: it is a legitimate output of layout arithmetic and is
52
+ * indistinguishable from `0` to every consumer, but stringifies differently.
53
+ * Integers are emitted without a decimal point; other values keep full round-trip
54
+ * precision, having already been rounded to their dimension's tolerance by the
55
+ * ruleset (lengths to px at fixed precision) before reaching here.
56
+ */
57
+ export function canonicalNumber(value) {
58
+ if (!Number.isFinite(value)) {
59
+ throw new RangeError(`canonical serialization requires a finite number, received ${value}`);
60
+ }
61
+ if (Object.is(value, -0))
62
+ return '0';
63
+ return String(value);
64
+ }
65
+ //# sourceMappingURL=canonical.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"canonical.js","sourceRoot":"","sources":["../../src/format/canonical.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAUH;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,YAAY,CAAC,KAAqB;IAChD,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,MAAM,CAAC;IAElC,QAAQ,OAAO,KAAK,EAAE,CAAC;QACrB,KAAK,SAAS;YACZ,OAAO,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC;QAClC,KAAK,QAAQ;YACX,OAAO,eAAe,CAAC,KAAK,CAAC,CAAC;QAChC,KAAK,QAAQ;YACX,OAAO,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;IACjC,CAAC;IAED,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACzB,OAAO,IAAI,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC;IAClE,CAAC;IAED,MAAM,MAAM,GAAG,KAA+D,CAAC;IAC/E,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC;QAC7C,MAAM,MAAM,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC;QAC3B,IAAI,MAAM,KAAK,SAAS;YAAE,SAAS;QACnC,KAAK,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,IAAI,YAAY,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;IAC/D,CAAC;IACD,OAAO,IAAI,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC;AAChC,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,eAAe,CAAC,KAAa;IAC3C,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;QAC5B,MAAM,IAAI,UAAU,CAAC,8DAA8D,KAAK,EAAE,CAAC,CAAC;IAC9F,CAAC;IACD,IAAI,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;QAAE,OAAO,GAAG,CAAC;IACrC,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC;AACvB,CAAC","sourcesContent":["/**\n * Canonical serialization.\n *\n * Determinism (Principle 2) means byte-identical output for identical inputs, on\n * any machine. `JSON.stringify` does not provide that: key order follows\n * insertion order, and float formatting is the one place where \"obvious\" and\n * \"identical everywhere\" diverge.\n *\n * Everything hashed by this system passes through `canonicalize` first.\n */\n\nexport type CanonicalValue =\n | string\n | number\n | boolean\n | null\n | readonly CanonicalValue[]\n | { readonly [key: string]: CanonicalValue | undefined };\n\n/**\n * Serialize to a canonical string: object keys sorted by code unit, `undefined`\n * members omitted (never emitted as `null`), and numbers formatted per\n * {@link canonicalNumber}.\n *\n * Omitting rather than nulling `undefined` is what makes \"a dimension this\n * profile cannot observe\" structurally absent from the hash input rather than\n * present-and-empty (ADR-0002). Absence and emptiness must not collide.\n *\n * @throws {RangeError} on `NaN` or `Infinity` — a non-finite value in a hash\n * input means an upstream measurement failed, and silently encoding it would\n * produce a stable hash for a broken observation.\n */\nexport function canonicalize(value: CanonicalValue): string {\n if (value === null) return 'null';\n\n switch (typeof value) {\n case 'boolean':\n return value ? 'true' : 'false';\n case 'number':\n return canonicalNumber(value);\n case 'string':\n return JSON.stringify(value);\n }\n\n if (Array.isArray(value)) {\n return `[${value.map((item) => canonicalize(item)).join(',')}]`;\n }\n\n const record = value as { readonly [key: string]: CanonicalValue | undefined };\n const parts: string[] = [];\n for (const key of Object.keys(record).sort()) {\n const member = record[key];\n if (member === undefined) continue;\n parts.push(`${JSON.stringify(key)}:${canonicalize(member)}`);\n }\n return `{${parts.join(',')}}`;\n}\n\n/**\n * Format a number identically across platforms.\n *\n * `-0` normalizes to `0`: it is a legitimate output of layout arithmetic and is\n * indistinguishable from `0` to every consumer, but stringifies differently.\n * Integers are emitted without a decimal point; other values keep full round-trip\n * precision, having already been rounded to their dimension's tolerance by the\n * ruleset (lengths to px at fixed precision) before reaching here.\n */\nexport function canonicalNumber(value: number): string {\n if (!Number.isFinite(value)) {\n throw new RangeError(`canonical serialization requires a finite number, received ${value}`);\n }\n if (Object.is(value, -0)) return '0';\n return String(value);\n}\n"]}
@@ -0,0 +1,212 @@
1
+ import type { EnvironmentInputs } from './environment.js';
2
+ import type { ObservationProfile } from './profile.js';
3
+ import type { Provenance } from './provenance.js';
4
+ import type { Wiring } from './wiring.js';
5
+ import type { Holding } from './holding.js';
6
+ /**
7
+ * The collector/core boundary.
8
+ *
9
+ * Collectors **extract**; `core` **normalizes**. Nothing in this file is a live
10
+ * object — a `RawCapture` is plain data, fully serializable, produced by touching
11
+ * a DOM and consumed by code that has never seen one.
12
+ *
13
+ * Two properties fall out of that split, and both are the reason for it:
14
+ *
15
+ * 1. **One ruleset, by construction.** Normalization is the moat (ADR-0003) and
16
+ * the thing most likely to drift between two implementations. If the JSDOM and
17
+ * Chromium collectors each normalized their own captures, "the same rules ran"
18
+ * would be a claim maintained by discipline. Here it is a fact of the call
19
+ * graph: there is one normalizer and both profiles enter it.
20
+ * 2. **The sub-renderer can be anywhere.** A capture crossing a worker boundary,
21
+ * a pipe, or a network hop to a device farm is the same value it was in
22
+ * process. No code may assume the collector is local.
23
+ */
24
+ export interface RawCapture {
25
+ readonly captureVersion: 1;
26
+ readonly subject: SubjectRef;
27
+ readonly profile: ObservationProfile;
28
+ /**
29
+ * Environment inputs the *collector* is positioned to know — engine version,
30
+ * fonts actually loaded, resolved conditions, asset hashes. The ruleset and
31
+ * allowlist versions are `core`'s to supply, so they are absent here and the
32
+ * key is completed during normalization.
33
+ */
34
+ readonly environment: Omit<EnvironmentInputs, 'ruleset' | 'allowlist'>;
35
+ readonly root: RawNode;
36
+ /**
37
+ * Inherited values in force at the subject root.
38
+ *
39
+ * Mandatory, not an optimization. CSS applicability pruning (ADR-0003) drops
40
+ * every rule that matches nothing inside the subtree — including rules on
41
+ * ancestors *outside* it whose inheritable properties still reach in. Without
42
+ * this seed the cheap tier is unsound and will report false `unchanged`.
43
+ */
44
+ readonly inheritedSeed: Readonly<Record<string, string>>;
45
+ /**
46
+ * Subtrees this subject renders through portals, in fiber traversal order.
47
+ *
48
+ * A subject's boundary is a component-tree question, not a DOM-containment
49
+ * one: `createPortal` renders elsewhere in the document while remaining part
50
+ * of the tree rooted here. Omitting these makes an opening modal report
51
+ * `unchanged`, because its own container is byte-identical (ADR-0007).
52
+ */
53
+ readonly portals?: readonly RawNode[];
54
+ /**
55
+ * Shared root state this subject is *latently* coupled to.
56
+ *
57
+ * Rules like `html.dark .card` contribute nothing today and everything the
58
+ * moment `dark` lands on `<html>`. In a session that reuses one document across
59
+ * subjects, that moment may arrive because a *different* subject put it there —
60
+ * turning this subject's baseline order-dependent with no visible cause.
61
+ * Recorded so the coupling can be reported before it bites.
62
+ */
63
+ readonly couplings?: readonly string[];
64
+ /** Anything the collector could not do. Empty is the expected case. */
65
+ readonly diagnostics: readonly Diagnostic[];
66
+ }
67
+ export interface SubjectRef {
68
+ /** Stable across renames of the file, e.g. `story:components-button--primary`. */
69
+ readonly id: string;
70
+ /**
71
+ * What kind of thing this is, which is not the same question as what produced
72
+ * it.
73
+ *
74
+ * `value` is the one that is not a rendering: a JSON body, a schema, a route
75
+ * table. It is a fourth member rather than a reuse of `route`, which already
76
+ * means *a page rendered at a URL* — a route table is a value about the same
77
+ * paths, and a subject list where one word meant both is one nobody can filter.
78
+ */
79
+ readonly kind: 'story' | 'route' | 'fixture' | 'value';
80
+ readonly title?: string;
81
+ }
82
+ /**
83
+ * A node exactly as observed, before any rule is applied.
84
+ *
85
+ * Raw means raw: `attributes` still holds generated ids and hashed class names,
86
+ * `matchedRules` still holds cascade losers. Normalization needs the unedited
87
+ * input — a collector that helpfully pre-cleaned would be a second, invisible
88
+ * ruleset, versioned by nothing.
89
+ */
90
+ export interface RawNode {
91
+ readonly tag: string;
92
+ readonly attributes: Readonly<Record<string, string>>;
93
+ /** Resolved accessibility properties, when the profile provides them. */
94
+ readonly aria?: RawAria;
95
+ /**
96
+ * Rules matching this node, as authored.
97
+ *
98
+ * Losing declarations are retained: `core` resolves the cascade for profiles
99
+ * whose engine did not (ADR-0003 step 5), and cannot do that from winners
100
+ * alone. Shorthands are retained *unexpanded* — expansion is `core`'s job, so
101
+ * that it is versioned by the ruleset rather than by each collector.
102
+ *
103
+ * A collector MAY drop a declaration whose property is neither allowlisted nor
104
+ * a shorthand of an allowlisted property, and SHOULD when the capture crosses
105
+ * a network hop. It MUST NOT normalize values; a helpfully pre-cleaned capture
106
+ * is a second, invisible ruleset versioned by nothing.
107
+ */
108
+ readonly matchedRules: readonly MatchedRule[];
109
+ readonly inlineStyle?: Readonly<Record<string, string>>;
110
+ /** Engine-resolved computed style. Present only when `profile.computedStyle`. */
111
+ readonly computedStyle?: Readonly<Record<string, string>>;
112
+ /** Present only when `profile.layout`. */
113
+ readonly rect?: Rect;
114
+ /** Literal text of a text node. Digested during normalization, per policy. */
115
+ readonly text?: string;
116
+ readonly provenance?: Provenance;
117
+ /**
118
+ * How the framework holds this node's component, when an adapter supplied it.
119
+ *
120
+ * Beside `provenance` because it arrives from the same seam and under the same
121
+ * rule: the collector carries no framework dependency and both are injected.
122
+ * See {@link Wiring} for why it is a dimension rather than another attribution
123
+ * field.
124
+ */
125
+ readonly wiring?: Wiring;
126
+ /**
127
+ * What the component that authored this node was holding, when an adapter
128
+ * supplied it.
129
+ *
130
+ * A third injection at the same seam, and deliberately not folded into
131
+ * `wiring`: wiring is hashed into a band and this may never be, so a caller
132
+ * that wants the band must be able to have it without also carrying values,
133
+ * and a caller that wants explanations must be able to ask for them without
134
+ * changing a single digest. See {@link Holding}.
135
+ */
136
+ readonly holding?: Holding;
137
+ readonly children: readonly RawNode[];
138
+ /** Nodes inside a shadow root, kept distinct from light-DOM children. */
139
+ readonly shadowChildren?: readonly RawNode[];
140
+ /**
141
+ * Ignore rules the collector found this element under (spec 0024).
142
+ *
143
+ * A mark, not a removal. The collector is the only thing holding a live DOM, so
144
+ * it is the only thing that can run a selector or read a marker attribute — but
145
+ * it must not act on what it finds. An element deleted from the capture would
146
+ * be invisible to every count downstream, and the difference between "absorbed
147
+ * by the `carousel` rule" and "was never there" is the difference between an
148
+ * ignore and a blind spot.
149
+ *
150
+ * Never hashed. Normalization turns these into
151
+ * {@link import('./snapshot.js').SemanticSnapshot.ignoreSites}, which sits
152
+ * beside `styleProvenance` outside the render hash for the same reason: masking
153
+ * a clock changes what a run *says*, not what it *renders*, and must not
154
+ * re-baseline the repository.
155
+ */
156
+ readonly ignoredBy?: readonly string[];
157
+ }
158
+ export interface RawAria {
159
+ readonly role: string | null;
160
+ readonly name: string | null;
161
+ /**
162
+ * Accessible description — what `aria-describedby` and `title` resolve to.
163
+ *
164
+ * Captured because nothing else captures it, and its absence was a hole. The
165
+ * attribute allowlist drops every `aria-*` attribute on the stated grounds
166
+ * that they "are resolved into role/name/state" — true of `aria-label` and
167
+ * `aria-selected`, and false of `aria-describedby`, which resolves into a
168
+ * description and had nowhere to land. A field whose `aria-describedby`
169
+ * pointed at a deleted error message compared **equal**: the reference was
170
+ * dropped by the allowlist, the name was unaffected, and the regression was
171
+ * silent on every tier including raster.
172
+ */
173
+ readonly description?: string | null;
174
+ /** `checked`, `disabled`, `expanded`, `selected`, … */
175
+ readonly state: Readonly<Record<string, string | boolean | number>>;
176
+ }
177
+ export interface MatchedRule {
178
+ /** Identifies the origin sheet for attribution: href, or a synthetic id. */
179
+ readonly sheet: string;
180
+ readonly selector: string;
181
+ /** `[idCount, classCount, typeCount]` — CSS specificity, most significant first. */
182
+ readonly specificity: readonly [number, number, number];
183
+ /** Document order of the rule. Breaks specificity ties, per the cascade. */
184
+ readonly order: number;
185
+ readonly declarations: readonly Declaration[];
186
+ /** Source position, when the sheet exposes one. Turns a diff into a file:line. */
187
+ readonly source?: {
188
+ readonly file: string;
189
+ readonly line: number;
190
+ };
191
+ }
192
+ export interface Declaration {
193
+ readonly property: string;
194
+ readonly value: string;
195
+ readonly important: boolean;
196
+ /** Custom properties referenced by the value, for token-keyed attribution. */
197
+ readonly references?: readonly string[];
198
+ }
199
+ /** Browser-layout coordinates retained as evidence, without a preferred layout policy. */
200
+ export interface Rect {
201
+ readonly x: number;
202
+ readonly y: number;
203
+ readonly width: number;
204
+ readonly height: number;
205
+ }
206
+ export interface Diagnostic {
207
+ readonly severity: 'warn' | 'error';
208
+ readonly code: string;
209
+ readonly message: string;
210
+ readonly nodePath?: string;
211
+ }
212
+ //# sourceMappingURL=capture.d.ts.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=capture.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"capture.js","sourceRoot":"","sources":["../../src/format/capture.ts"],"names":[],"mappings":"","sourcesContent":["import type { EnvironmentInputs } from './environment.js';\nimport type { ObservationProfile } from './profile.js';\nimport type { Provenance } from './provenance.js';\nimport type { Wiring } from './wiring.js';\nimport type { Holding } from './holding.js';\n\n/**\n * The collector/core boundary.\n *\n * Collectors **extract**; `core` **normalizes**. Nothing in this file is a live\n * object — a `RawCapture` is plain data, fully serializable, produced by touching\n * a DOM and consumed by code that has never seen one.\n *\n * Two properties fall out of that split, and both are the reason for it:\n *\n * 1. **One ruleset, by construction.** Normalization is the moat (ADR-0003) and\n * the thing most likely to drift between two implementations. If the JSDOM and\n * Chromium collectors each normalized their own captures, \"the same rules ran\"\n * would be a claim maintained by discipline. Here it is a fact of the call\n * graph: there is one normalizer and both profiles enter it.\n * 2. **The sub-renderer can be anywhere.** A capture crossing a worker boundary,\n * a pipe, or a network hop to a device farm is the same value it was in\n * process. No code may assume the collector is local.\n */\nexport interface RawCapture {\n readonly captureVersion: 1;\n readonly subject: SubjectRef;\n readonly profile: ObservationProfile;\n\n /**\n * Environment inputs the *collector* is positioned to know — engine version,\n * fonts actually loaded, resolved conditions, asset hashes. The ruleset and\n * allowlist versions are `core`'s to supply, so they are absent here and the\n * key is completed during normalization.\n */\n readonly environment: Omit<EnvironmentInputs, 'ruleset' | 'allowlist'>;\n\n readonly root: RawNode;\n\n /**\n * Inherited values in force at the subject root.\n *\n * Mandatory, not an optimization. CSS applicability pruning (ADR-0003) drops\n * every rule that matches nothing inside the subtree — including rules on\n * ancestors *outside* it whose inheritable properties still reach in. Without\n * this seed the cheap tier is unsound and will report false `unchanged`.\n */\n readonly inheritedSeed: Readonly<Record<string, string>>;\n\n /**\n * Subtrees this subject renders through portals, in fiber traversal order.\n *\n * A subject's boundary is a component-tree question, not a DOM-containment\n * one: `createPortal` renders elsewhere in the document while remaining part\n * of the tree rooted here. Omitting these makes an opening modal report\n * `unchanged`, because its own container is byte-identical (ADR-0007).\n */\n readonly portals?: readonly RawNode[];\n\n /**\n * Shared root state this subject is *latently* coupled to.\n *\n * Rules like `html.dark .card` contribute nothing today and everything the\n * moment `dark` lands on `<html>`. In a session that reuses one document across\n * subjects, that moment may arrive because a *different* subject put it there —\n * turning this subject's baseline order-dependent with no visible cause.\n * Recorded so the coupling can be reported before it bites.\n */\n readonly couplings?: readonly string[];\n\n /** Anything the collector could not do. Empty is the expected case. */\n readonly diagnostics: readonly Diagnostic[];\n}\n\nexport interface SubjectRef {\n /** Stable across renames of the file, e.g. `story:components-button--primary`. */\n readonly id: string;\n\n /**\n * What kind of thing this is, which is not the same question as what produced\n * it.\n *\n * `value` is the one that is not a rendering: a JSON body, a schema, a route\n * table. It is a fourth member rather than a reuse of `route`, which already\n * means *a page rendered at a URL* — a route table is a value about the same\n * paths, and a subject list where one word meant both is one nobody can filter.\n */\n readonly kind: 'story' | 'route' | 'fixture' | 'value';\n readonly title?: string;\n}\n\n/**\n * A node exactly as observed, before any rule is applied.\n *\n * Raw means raw: `attributes` still holds generated ids and hashed class names,\n * `matchedRules` still holds cascade losers. Normalization needs the unedited\n * input — a collector that helpfully pre-cleaned would be a second, invisible\n * ruleset, versioned by nothing.\n */\nexport interface RawNode {\n readonly tag: string;\n readonly attributes: Readonly<Record<string, string>>;\n\n /** Resolved accessibility properties, when the profile provides them. */\n readonly aria?: RawAria;\n\n /**\n * Rules matching this node, as authored.\n *\n * Losing declarations are retained: `core` resolves the cascade for profiles\n * whose engine did not (ADR-0003 step 5), and cannot do that from winners\n * alone. Shorthands are retained *unexpanded* — expansion is `core`'s job, so\n * that it is versioned by the ruleset rather than by each collector.\n *\n * A collector MAY drop a declaration whose property is neither allowlisted nor\n * a shorthand of an allowlisted property, and SHOULD when the capture crosses\n * a network hop. It MUST NOT normalize values; a helpfully pre-cleaned capture\n * is a second, invisible ruleset versioned by nothing.\n */\n readonly matchedRules: readonly MatchedRule[];\n\n readonly inlineStyle?: Readonly<Record<string, string>>;\n\n /** Engine-resolved computed style. Present only when `profile.computedStyle`. */\n readonly computedStyle?: Readonly<Record<string, string>>;\n\n /** Present only when `profile.layout`. */\n readonly rect?: Rect;\n\n /** Literal text of a text node. Digested during normalization, per policy. */\n readonly text?: string;\n\n readonly provenance?: Provenance;\n\n /**\n * How the framework holds this node's component, when an adapter supplied it.\n *\n * Beside `provenance` because it arrives from the same seam and under the same\n * rule: the collector carries no framework dependency and both are injected.\n * See {@link Wiring} for why it is a dimension rather than another attribution\n * field.\n */\n readonly wiring?: Wiring;\n\n /**\n * What the component that authored this node was holding, when an adapter\n * supplied it.\n *\n * A third injection at the same seam, and deliberately not folded into\n * `wiring`: wiring is hashed into a band and this may never be, so a caller\n * that wants the band must be able to have it without also carrying values,\n * and a caller that wants explanations must be able to ask for them without\n * changing a single digest. See {@link Holding}.\n */\n readonly holding?: Holding;\n\n readonly children: readonly RawNode[];\n\n /** Nodes inside a shadow root, kept distinct from light-DOM children. */\n readonly shadowChildren?: readonly RawNode[];\n\n /**\n * Ignore rules the collector found this element under (spec 0024).\n *\n * A mark, not a removal. The collector is the only thing holding a live DOM, so\n * it is the only thing that can run a selector or read a marker attribute — but\n * it must not act on what it finds. An element deleted from the capture would\n * be invisible to every count downstream, and the difference between \"absorbed\n * by the `carousel` rule\" and \"was never there\" is the difference between an\n * ignore and a blind spot.\n *\n * Never hashed. Normalization turns these into\n * {@link import('./snapshot.js').SemanticSnapshot.ignoreSites}, which sits\n * beside `styleProvenance` outside the render hash for the same reason: masking\n * a clock changes what a run *says*, not what it *renders*, and must not\n * re-baseline the repository.\n */\n readonly ignoredBy?: readonly string[];\n}\n\nexport interface RawAria {\n readonly role: string | null;\n readonly name: string | null;\n\n /**\n * Accessible description — what `aria-describedby` and `title` resolve to.\n *\n * Captured because nothing else captures it, and its absence was a hole. The\n * attribute allowlist drops every `aria-*` attribute on the stated grounds\n * that they \"are resolved into role/name/state\" — true of `aria-label` and\n * `aria-selected`, and false of `aria-describedby`, which resolves into a\n * description and had nowhere to land. A field whose `aria-describedby`\n * pointed at a deleted error message compared **equal**: the reference was\n * dropped by the allowlist, the name was unaffected, and the regression was\n * silent on every tier including raster.\n */\n readonly description?: string | null;\n\n /** `checked`, `disabled`, `expanded`, `selected`, … */\n readonly state: Readonly<Record<string, string | boolean | number>>;\n}\n\nexport interface MatchedRule {\n /** Identifies the origin sheet for attribution: href, or a synthetic id. */\n readonly sheet: string;\n readonly selector: string;\n /** `[idCount, classCount, typeCount]` — CSS specificity, most significant first. */\n readonly specificity: readonly [number, number, number];\n /** Document order of the rule. Breaks specificity ties, per the cascade. */\n readonly order: number;\n readonly declarations: readonly Declaration[];\n /** Source position, when the sheet exposes one. Turns a diff into a file:line. */\n readonly source?: { readonly file: string; readonly line: number };\n}\n\nexport interface Declaration {\n readonly property: string;\n readonly value: string;\n readonly important: boolean;\n /** Custom properties referenced by the value, for token-keyed attribution. */\n readonly references?: readonly string[];\n}\n\n/** Browser-layout coordinates retained as evidence, without a preferred layout policy. */\nexport interface Rect {\n readonly x: number;\n readonly y: number;\n readonly width: number;\n readonly height: number;\n}\n\nexport interface Diagnostic {\n readonly severity: 'warn' | 'error';\n readonly code: string;\n readonly message: string;\n readonly nodePath?: string;\n}\n"]}