@michaelmagan/dnumb 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 (133) hide show
  1. package/AGENTS.md +180 -0
  2. package/LICENSE +21 -0
  3. package/README.md +297 -0
  4. package/dist/analysis/cluster.cjs +114 -0
  5. package/dist/analysis/cluster.d.cts +68 -0
  6. package/dist/analysis/cluster.d.ts +68 -0
  7. package/dist/analysis/cluster.js +109 -0
  8. package/dist/analysis/distances.cjs +124 -0
  9. package/dist/analysis/distances.d.cts +58 -0
  10. package/dist/analysis/distances.d.ts +58 -0
  11. package/dist/analysis/distances.js +119 -0
  12. package/dist/analysis/geometry.cjs +60 -0
  13. package/dist/analysis/geometry.d.cts +34 -0
  14. package/dist/analysis/geometry.d.ts +34 -0
  15. package/dist/analysis/geometry.js +53 -0
  16. package/dist/analysis/layout-tree.cjs +245 -0
  17. package/dist/analysis/layout-tree.d.cts +45 -0
  18. package/dist/analysis/layout-tree.d.ts +45 -0
  19. package/dist/analysis/layout-tree.js +242 -0
  20. package/dist/analysis/region-query.cjs +71 -0
  21. package/dist/analysis/region-query.d.cts +25 -0
  22. package/dist/analysis/region-query.d.ts +25 -0
  23. package/dist/analysis/region-query.js +67 -0
  24. package/dist/analysis/types.cjs +7 -0
  25. package/dist/analysis/types.d.cts +143 -0
  26. package/dist/analysis/types.d.ts +143 -0
  27. package/dist/analysis/types.js +6 -0
  28. package/dist/bundle.cjs +182 -0
  29. package/dist/bundle.d.cts +91 -0
  30. package/dist/bundle.d.ts +91 -0
  31. package/dist/bundle.js +175 -0
  32. package/dist/cli.cjs +755 -0
  33. package/dist/cli.d.cts +2 -0
  34. package/dist/cli.d.ts +2 -0
  35. package/dist/cli.js +719 -0
  36. package/dist/diff/changes.cjs +656 -0
  37. package/dist/diff/changes.d.cts +2 -0
  38. package/dist/diff/changes.d.ts +2 -0
  39. package/dist/diff/changes.js +653 -0
  40. package/dist/diff/identity.cjs +167 -0
  41. package/dist/diff/identity.d.cts +62 -0
  42. package/dist/diff/identity.d.ts +62 -0
  43. package/dist/diff/identity.js +159 -0
  44. package/dist/diff/index.cjs +84 -0
  45. package/dist/diff/index.d.cts +76 -0
  46. package/dist/diff/index.d.ts +76 -0
  47. package/dist/diff/index.js +68 -0
  48. package/dist/diff/match.cjs +156 -0
  49. package/dist/diff/match.d.cts +17 -0
  50. package/dist/diff/match.d.ts +17 -0
  51. package/dist/diff/match.js +152 -0
  52. package/dist/diff/render.cjs +540 -0
  53. package/dist/diff/render.d.cts +2 -0
  54. package/dist/diff/render.d.ts +2 -0
  55. package/dist/diff/render.js +537 -0
  56. package/dist/diff/types.cjs +2 -0
  57. package/dist/diff/types.d.cts +211 -0
  58. package/dist/diff/types.d.ts +211 -0
  59. package/dist/diff/types.js +1 -0
  60. package/dist/digest/index.cjs +8 -0
  61. package/dist/digest/index.d.cts +4 -0
  62. package/dist/digest/index.d.ts +4 -0
  63. package/dist/digest/index.js +2 -0
  64. package/dist/digest/parse.cjs +163 -0
  65. package/dist/digest/parse.d.cts +80 -0
  66. package/dist/digest/parse.d.ts +80 -0
  67. package/dist/digest/parse.js +160 -0
  68. package/dist/digest/render.cjs +1575 -0
  69. package/dist/digest/render.d.cts +41 -0
  70. package/dist/digest/render.d.ts +41 -0
  71. package/dist/digest/render.js +1571 -0
  72. package/dist/digest/types.cjs +10 -0
  73. package/dist/digest/types.d.cts +86 -0
  74. package/dist/digest/types.d.ts +86 -0
  75. package/dist/digest/types.js +9 -0
  76. package/dist/index.cjs +61 -0
  77. package/dist/index.d.cts +27 -0
  78. package/dist/index.d.ts +27 -0
  79. package/dist/index.js +19 -0
  80. package/dist/page.cjs +35 -0
  81. package/dist/page.d.cts +40 -0
  82. package/dist/page.d.ts +40 -0
  83. package/dist/page.js +31 -0
  84. package/dist/playwright.cjs +138 -0
  85. package/dist/playwright.d.cts +69 -0
  86. package/dist/playwright.d.ts +69 -0
  87. package/dist/playwright.js +131 -0
  88. package/dist/query.cjs +1152 -0
  89. package/dist/query.d.cts +47 -0
  90. package/dist/query.d.ts +47 -0
  91. package/dist/query.js +1146 -0
  92. package/dist/snapshot/capture.cjs +80 -0
  93. package/dist/snapshot/capture.d.cts +34 -0
  94. package/dist/snapshot/capture.d.ts +34 -0
  95. package/dist/snapshot/capture.js +77 -0
  96. package/dist/snapshot/cdp.cjs +450 -0
  97. package/dist/snapshot/cdp.d.cts +36 -0
  98. package/dist/snapshot/cdp.d.ts +36 -0
  99. package/dist/snapshot/cdp.js +447 -0
  100. package/dist/snapshot/clip.cjs +100 -0
  101. package/dist/snapshot/clip.d.cts +26 -0
  102. package/dist/snapshot/clip.d.ts +26 -0
  103. package/dist/snapshot/clip.js +96 -0
  104. package/dist/snapshot/collect.cjs +358 -0
  105. package/dist/snapshot/collect.d.cts +18 -0
  106. package/dist/snapshot/collect.d.ts +18 -0
  107. package/dist/snapshot/collect.js +355 -0
  108. package/dist/snapshot/color.cjs +1039 -0
  109. package/dist/snapshot/color.d.cts +316 -0
  110. package/dist/snapshot/color.d.ts +316 -0
  111. package/dist/snapshot/color.js +1026 -0
  112. package/dist/snapshot/facts.cjs +167 -0
  113. package/dist/snapshot/facts.d.cts +48 -0
  114. package/dist/snapshot/facts.d.ts +48 -0
  115. package/dist/snapshot/facts.js +161 -0
  116. package/dist/snapshot/limits.cjs +69 -0
  117. package/dist/snapshot/limits.d.cts +59 -0
  118. package/dist/snapshot/limits.d.ts +59 -0
  119. package/dist/snapshot/limits.js +65 -0
  120. package/dist/snapshot/not-measured.cjs +44 -0
  121. package/dist/snapshot/not-measured.d.cts +14 -0
  122. package/dist/snapshot/not-measured.d.ts +14 -0
  123. package/dist/snapshot/not-measured.js +41 -0
  124. package/dist/snapshot/types.cjs +2 -0
  125. package/dist/snapshot/types.d.cts +238 -0
  126. package/dist/snapshot/types.d.ts +238 -0
  127. package/dist/snapshot/types.js +1 -0
  128. package/dist/spec/index.cjs +9 -0
  129. package/dist/spec/index.d.cts +15 -0
  130. package/dist/spec/index.d.ts +15 -0
  131. package/dist/spec/index.js +6 -0
  132. package/package.json +141 -0
  133. package/skills/dnumb/SKILL.md +136 -0
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+ /**
3
+ * Shared types for `src/digest/*`.
4
+ *
5
+ * The digest reads `ElementRecord[]` and the facts derived from them, and
6
+ * nothing else — no CLI types, no Playwright, no check engine. The prototype
7
+ * enforced the same boundary and it is the reason the digest and the diff read
8
+ * one geometry implementation rather than two that quietly diverge.
9
+ */
10
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Shared types for `src/digest/*`.
3
+ *
4
+ * The digest reads `ElementRecord[]` and the facts derived from them, and
5
+ * nothing else — no CLI types, no Playwright, no check engine. The prototype
6
+ * enforced the same boundary and it is the reason the digest and the diff read
7
+ * one geometry implementation rather than two that quietly diverge.
8
+ */
9
+ export interface DigestMeta {
10
+ url: string;
11
+ viewport: {
12
+ name?: string;
13
+ width: number;
14
+ height: number;
15
+ };
16
+ /**
17
+ * The state this capture was taken in — `menu-open`, `dark`, `after-submit`.
18
+ * New in v2, and the reason the format can describe more than one moment of a
19
+ * page: without it, two captures of the same URL are indistinguishable in
20
+ * their own headers.
21
+ */
22
+ state?: string;
23
+ capturedAt?: string;
24
+ /**
25
+ * Which capture path produced this snapshot. Surfaced in the header because
26
+ * `cdp` and `walk` do not see the same page on any page using shadow DOM —
27
+ * `walk` cannot enter a shadow root at all, `cdp` flattens one in — so a
28
+ * reader comparing two digests needs to know which capability tier each one
29
+ * came from, not just what it found.
30
+ */
31
+ strategy?: 'cdp' | 'walk';
32
+ /**
33
+ * True when `strategy` is `'walk'` only because the CDP fast path was tried
34
+ * on a Chromium page and its own protocol call failed — an unplanned
35
+ * downgrade, not a deliberate portable capture. See
36
+ * `CaptureResult.cdpFellBack`.
37
+ */
38
+ cdpFellBack?: boolean;
39
+ }
40
+ export interface RenderDigestOptions {
41
+ /**
42
+ * Soft target for total body characters. Default 32768 (~11.5K tokens at
43
+ * the corpus's measured 2.84 chars/token) -- see `DEFAULT_BUDGET_CHARS`'s
44
+ * own doc comment in `digest/render.ts` and `docs/notes/06-budget.md` §2
45
+ * and §5 for why characters replaced lines as the meter, and why that
46
+ * number. Exceeding it makes folding progressively more aggressive, then
47
+ * collapses subtrees leaf-up (`collapseCandidates`, shallowest subtree
48
+ * first), rather than truncating -- a digest that stops mid-tree is worse
49
+ * than one that summarises the repetitive parts. Not applied under
50
+ * `--focus`/`options.focus` -- see that option's doc comment.
51
+ */
52
+ budgetChars?: number;
53
+ /**
54
+ * Soft target for total body *lines* -- the budget's original unit, before
55
+ * `docs/notes/06-budget.md` replaced it with `budgetChars` (lines predict
56
+ * a reader's real token cost within a factor of ~15 on ordinary bodies,
57
+ * characters within ±20%). Still honored when a caller passes it
58
+ * explicitly: `src/cli.ts`'s `--budget` flag and `src/query.ts`'s
59
+ * `QueryOptions` both still type and forward it, and this package is not
60
+ * the sole owner of either tonight, so this stays a real, working option
61
+ * rather than a silently-dropped one. It carries no default of its own,
62
+ * though -- a bare `renderDigest(snapshot, meta)` call is bounded by
63
+ * `budgetChars` alone -- and when both are set, the render stays under
64
+ * whichever limit is stricter.
65
+ */
66
+ budgetLines?: number;
67
+ /**
68
+ * Scope to one element's subtree, by element id or tag. Neither budget
69
+ * option applies once focused: stage 2 (fold) and stage 3 (collapse) are
70
+ * both disabled entirely under `--focus`, so a focused render can exceed
71
+ * `budgetChars`/`budgetLines` when the focused subtree itself is large.
72
+ * That is deliberate, not an oversight -- see `renderDigest`'s own doc
73
+ * comment on the stage-3 loop for why re-applying a budget-driven collapse
74
+ * inside an already-focused subtree would reopen the exact non-convergence
75
+ * bug disabling it there was written to fix (chasing a `(--focus <id>)`
76
+ * hint that lands on another partial, re-collapsed view of the same
77
+ * subtree instead of the full content the caller asked for).
78
+ */
79
+ focus?: string;
80
+ /**
81
+ * Emit the edge-cluster histogram header. Off by default: it is a page-wide
82
+ * summary that costs several dense lines and answers a question most captures
83
+ * are not asking.
84
+ */
85
+ edges?: boolean;
86
+ }
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Shared types for `src/digest/*`.
3
+ *
4
+ * The digest reads `ElementRecord[]` and the facts derived from them, and
5
+ * nothing else — no CLI types, no Playwright, no check engine. The prototype
6
+ * enforced the same boundary and it is the reason the digest and the diff read
7
+ * one geometry implementation rather than two that quietly diverge.
8
+ */
9
+ export interface DigestMeta {
10
+ url: string;
11
+ viewport: {
12
+ name?: string;
13
+ width: number;
14
+ height: number;
15
+ };
16
+ /**
17
+ * The state this capture was taken in — `menu-open`, `dark`, `after-submit`.
18
+ * New in v2, and the reason the format can describe more than one moment of a
19
+ * page: without it, two captures of the same URL are indistinguishable in
20
+ * their own headers.
21
+ */
22
+ state?: string;
23
+ capturedAt?: string;
24
+ /**
25
+ * Which capture path produced this snapshot. Surfaced in the header because
26
+ * `cdp` and `walk` do not see the same page on any page using shadow DOM —
27
+ * `walk` cannot enter a shadow root at all, `cdp` flattens one in — so a
28
+ * reader comparing two digests needs to know which capability tier each one
29
+ * came from, not just what it found.
30
+ */
31
+ strategy?: 'cdp' | 'walk';
32
+ /**
33
+ * True when `strategy` is `'walk'` only because the CDP fast path was tried
34
+ * on a Chromium page and its own protocol call failed — an unplanned
35
+ * downgrade, not a deliberate portable capture. See
36
+ * `CaptureResult.cdpFellBack`.
37
+ */
38
+ cdpFellBack?: boolean;
39
+ }
40
+ export interface RenderDigestOptions {
41
+ /**
42
+ * Soft target for total body characters. Default 32768 (~11.5K tokens at
43
+ * the corpus's measured 2.84 chars/token) -- see `DEFAULT_BUDGET_CHARS`'s
44
+ * own doc comment in `digest/render.ts` and `docs/notes/06-budget.md` §2
45
+ * and §5 for why characters replaced lines as the meter, and why that
46
+ * number. Exceeding it makes folding progressively more aggressive, then
47
+ * collapses subtrees leaf-up (`collapseCandidates`, shallowest subtree
48
+ * first), rather than truncating -- a digest that stops mid-tree is worse
49
+ * than one that summarises the repetitive parts. Not applied under
50
+ * `--focus`/`options.focus` -- see that option's doc comment.
51
+ */
52
+ budgetChars?: number;
53
+ /**
54
+ * Soft target for total body *lines* -- the budget's original unit, before
55
+ * `docs/notes/06-budget.md` replaced it with `budgetChars` (lines predict
56
+ * a reader's real token cost within a factor of ~15 on ordinary bodies,
57
+ * characters within ±20%). Still honored when a caller passes it
58
+ * explicitly: `src/cli.ts`'s `--budget` flag and `src/query.ts`'s
59
+ * `QueryOptions` both still type and forward it, and this package is not
60
+ * the sole owner of either tonight, so this stays a real, working option
61
+ * rather than a silently-dropped one. It carries no default of its own,
62
+ * though -- a bare `renderDigest(snapshot, meta)` call is bounded by
63
+ * `budgetChars` alone -- and when both are set, the render stays under
64
+ * whichever limit is stricter.
65
+ */
66
+ budgetLines?: number;
67
+ /**
68
+ * Scope to one element's subtree, by element id or tag. Neither budget
69
+ * option applies once focused: stage 2 (fold) and stage 3 (collapse) are
70
+ * both disabled entirely under `--focus`, so a focused render can exceed
71
+ * `budgetChars`/`budgetLines` when the focused subtree itself is large.
72
+ * That is deliberate, not an oversight -- see `renderDigest`'s own doc
73
+ * comment on the stage-3 loop for why re-applying a budget-driven collapse
74
+ * inside an already-focused subtree would reopen the exact non-convergence
75
+ * bug disabling it there was written to fix (chasing a `(--focus <id>)`
76
+ * hint that lands on another partial, re-collapsed view of the same
77
+ * subtree instead of the full content the caller asked for).
78
+ */
79
+ focus?: string;
80
+ /**
81
+ * Emit the edge-cluster histogram header. Off by default: it is a page-wide
82
+ * summary that costs several dense lines and answers a question most captures
83
+ * are not asking.
84
+ */
85
+ edges?: boolean;
86
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Shared types for `src/digest/*`.
3
+ *
4
+ * The digest reads `ElementRecord[]` and the facts derived from them, and
5
+ * nothing else — no CLI types, no Playwright, no check engine. The prototype
6
+ * enforced the same boundary and it is the reason the digest and the diff read
7
+ * one geometry implementation rather than two that quietly diverge.
8
+ */
9
+ export {};
package/dist/index.cjs ADDED
@@ -0,0 +1,61 @@
1
+ "use strict";
2
+ /**
3
+ * Public library entrypoint.
4
+ *
5
+ * The capture bundle, not the browser session, is the load-bearing object here.
6
+ * Everything in this package is a pure function over `ElementRecord[]` plus a
7
+ * thin host that produces them.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.SPEC_VERSION = exports.formatNotMeasured = exports.renderFacts = exports.factsForAll = exports.factsFor = exports.toHex = exports.resolveBackdrop = exports.renderColorFact = exports.relativeLuminance = exports.parseCssColor = exports.groupColorFacts = exports.detectOverlay = exports.contrastRatio = exports.colorFactsForAll = exports.colorFactFor = exports.backdropLabel = exports.collectSnapshot = exports.clipFactFor = exports.attachClipFacts = exports.capture = exports.viewportOf = exports.isCDPCapable = exports.SPARSE_CAPTURE_MARKER = exports.renderDigest = exports.parseDigestLine = exports.renderDiff = exports.prepare = exports.match = exports.diffCaptures = exports.computeChanges = exports.serializeBundle = exports.parseBundle = exports.buildBundle = exports.BundleFormatError = exports.intersect = exports.gapAlong = exports.contains = exports.center = exports.area = void 0;
11
+ var geometry_js_1 = require("./analysis/geometry.cjs");
12
+ Object.defineProperty(exports, "area", { enumerable: true, get: function () { return geometry_js_1.area; } });
13
+ Object.defineProperty(exports, "center", { enumerable: true, get: function () { return geometry_js_1.center; } });
14
+ Object.defineProperty(exports, "contains", { enumerable: true, get: function () { return geometry_js_1.contains; } });
15
+ Object.defineProperty(exports, "gapAlong", { enumerable: true, get: function () { return geometry_js_1.gapAlong; } });
16
+ Object.defineProperty(exports, "intersect", { enumerable: true, get: function () { return geometry_js_1.intersect; } });
17
+ var bundle_js_1 = require("./bundle.cjs");
18
+ Object.defineProperty(exports, "BundleFormatError", { enumerable: true, get: function () { return bundle_js_1.BundleFormatError; } });
19
+ Object.defineProperty(exports, "buildBundle", { enumerable: true, get: function () { return bundle_js_1.buildBundle; } });
20
+ Object.defineProperty(exports, "parseBundle", { enumerable: true, get: function () { return bundle_js_1.parseBundle; } });
21
+ Object.defineProperty(exports, "serializeBundle", { enumerable: true, get: function () { return bundle_js_1.serializeBundle; } });
22
+ var index_js_1 = require("./diff/index.cjs");
23
+ Object.defineProperty(exports, "computeChanges", { enumerable: true, get: function () { return index_js_1.computeChanges; } });
24
+ Object.defineProperty(exports, "diffCaptures", { enumerable: true, get: function () { return index_js_1.diffCaptures; } });
25
+ Object.defineProperty(exports, "match", { enumerable: true, get: function () { return index_js_1.match; } });
26
+ Object.defineProperty(exports, "prepare", { enumerable: true, get: function () { return index_js_1.prepare; } });
27
+ Object.defineProperty(exports, "renderDiff", { enumerable: true, get: function () { return index_js_1.renderDiff; } });
28
+ var index_js_2 = require("./digest/index.cjs");
29
+ Object.defineProperty(exports, "parseDigestLine", { enumerable: true, get: function () { return index_js_2.parseDigestLine; } });
30
+ Object.defineProperty(exports, "renderDigest", { enumerable: true, get: function () { return index_js_2.renderDigest; } });
31
+ Object.defineProperty(exports, "SPARSE_CAPTURE_MARKER", { enumerable: true, get: function () { return index_js_2.SPARSE_CAPTURE_MARKER; } });
32
+ var page_js_1 = require("./page.cjs");
33
+ Object.defineProperty(exports, "isCDPCapable", { enumerable: true, get: function () { return page_js_1.isCDPCapable; } });
34
+ Object.defineProperty(exports, "viewportOf", { enumerable: true, get: function () { return page_js_1.viewportOf; } });
35
+ var capture_js_1 = require("./snapshot/capture.cjs");
36
+ Object.defineProperty(exports, "capture", { enumerable: true, get: function () { return capture_js_1.capture; } });
37
+ var clip_js_1 = require("./snapshot/clip.cjs");
38
+ Object.defineProperty(exports, "attachClipFacts", { enumerable: true, get: function () { return clip_js_1.attachClipFacts; } });
39
+ Object.defineProperty(exports, "clipFactFor", { enumerable: true, get: function () { return clip_js_1.clipFactFor; } });
40
+ var collect_js_1 = require("./snapshot/collect.cjs");
41
+ Object.defineProperty(exports, "collectSnapshot", { enumerable: true, get: function () { return collect_js_1.collectSnapshot; } });
42
+ var color_js_1 = require("./snapshot/color.cjs");
43
+ Object.defineProperty(exports, "backdropLabel", { enumerable: true, get: function () { return color_js_1.backdropLabel; } });
44
+ Object.defineProperty(exports, "colorFactFor", { enumerable: true, get: function () { return color_js_1.colorFactFor; } });
45
+ Object.defineProperty(exports, "colorFactsForAll", { enumerable: true, get: function () { return color_js_1.colorFactsForAll; } });
46
+ Object.defineProperty(exports, "contrastRatio", { enumerable: true, get: function () { return color_js_1.contrastRatio; } });
47
+ Object.defineProperty(exports, "detectOverlay", { enumerable: true, get: function () { return color_js_1.detectOverlay; } });
48
+ Object.defineProperty(exports, "groupColorFacts", { enumerable: true, get: function () { return color_js_1.groupColorFacts; } });
49
+ Object.defineProperty(exports, "parseCssColor", { enumerable: true, get: function () { return color_js_1.parseCssColor; } });
50
+ Object.defineProperty(exports, "relativeLuminance", { enumerable: true, get: function () { return color_js_1.relativeLuminance; } });
51
+ Object.defineProperty(exports, "renderColorFact", { enumerable: true, get: function () { return color_js_1.renderColorFact; } });
52
+ Object.defineProperty(exports, "resolveBackdrop", { enumerable: true, get: function () { return color_js_1.resolveBackdrop; } });
53
+ Object.defineProperty(exports, "toHex", { enumerable: true, get: function () { return color_js_1.toHex; } });
54
+ var facts_js_1 = require("./snapshot/facts.cjs");
55
+ Object.defineProperty(exports, "factsFor", { enumerable: true, get: function () { return facts_js_1.factsFor; } });
56
+ Object.defineProperty(exports, "factsForAll", { enumerable: true, get: function () { return facts_js_1.factsForAll; } });
57
+ Object.defineProperty(exports, "renderFacts", { enumerable: true, get: function () { return facts_js_1.renderFacts; } });
58
+ var not_measured_js_1 = require("./snapshot/not-measured.cjs");
59
+ Object.defineProperty(exports, "formatNotMeasured", { enumerable: true, get: function () { return not_measured_js_1.formatNotMeasured; } });
60
+ var index_js_3 = require("./spec/index.cjs");
61
+ Object.defineProperty(exports, "SPEC_VERSION", { enumerable: true, get: function () { return index_js_3.SPEC_VERSION; } });
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Public library entrypoint.
3
+ *
4
+ * The capture bundle, not the browser session, is the load-bearing object here.
5
+ * Everything in this package is a pure function over `ElementRecord[]` plus a
6
+ * thin host that produces them.
7
+ */
8
+ export { area, center, contains, gapAlong, intersect } from "./analysis/geometry.cjs";
9
+ export type { BuildBundleOptions, Bundle } from "./bundle.cjs";
10
+ export { BundleFormatError, buildBundle, parseBundle, serializeBundle } from "./bundle.cjs";
11
+ export type { BoxDelta, Change, ChangeKind, DiffCapturesOptions, DiffMeta, DiffOptions, DiffResult, Match, Prepared, } from "./diff/index.cjs";
12
+ export { computeChanges, diffCaptures, match, prepare, renderDiff } from "./diff/index.cjs";
13
+ export type { DigestMeta, ParsedElementLine, ParsedLine, RenderDigestOptions, } from "./digest/index.cjs";
14
+ export { parseDigestLine, renderDigest, SPARSE_CAPTURE_MARKER } from "./digest/index.cjs";
15
+ export type { CDPCapablePage, CDPSessionLike, PageLike } from "./page.cjs";
16
+ export { isCDPCapable, viewportOf } from "./page.cjs";
17
+ export type { CaptureOptions, CaptureResult } from "./snapshot/capture.cjs";
18
+ export { capture } from "./snapshot/capture.cjs";
19
+ export { attachClipFacts, clipFactFor } from "./snapshot/clip.cjs";
20
+ export { collectSnapshot } from "./snapshot/collect.cjs";
21
+ export type { Backdrop, BackdropLayer, ColorFact, ColorPair, ParsedColor, RatioReason, ResolvedBackdrop, RGBA, } from "./snapshot/color.cjs";
22
+ export { backdropLabel, colorFactFor, colorFactsForAll, contrastRatio, detectOverlay, groupColorFacts, parseCssColor, relativeLuminance, renderColorFact, resolveBackdrop, toHex, } from "./snapshot/color.cjs";
23
+ export type { Fact, FactKind } from "./snapshot/facts.cjs";
24
+ export { factsFor, factsForAll, renderFacts } from "./snapshot/facts.cjs";
25
+ export { formatNotMeasured } from "./snapshot/not-measured.cjs";
26
+ export type { BoxSides, ClipFact, ElementRecord, NotMeasured, Rect, Snapshot, } from "./snapshot/types.cjs";
27
+ export { SPEC_VERSION } from "./spec/index.cjs";
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Public library entrypoint.
3
+ *
4
+ * The capture bundle, not the browser session, is the load-bearing object here.
5
+ * Everything in this package is a pure function over `ElementRecord[]` plus a
6
+ * thin host that produces them.
7
+ */
8
+ export { area, center, contains, gapAlong, intersect } from "./analysis/geometry.js";
9
+ export type { BuildBundleOptions, Bundle } from "./bundle.js";
10
+ export { BundleFormatError, buildBundle, parseBundle, serializeBundle } from "./bundle.js";
11
+ export type { BoxDelta, Change, ChangeKind, DiffCapturesOptions, DiffMeta, DiffOptions, DiffResult, Match, Prepared, } from "./diff/index.js";
12
+ export { computeChanges, diffCaptures, match, prepare, renderDiff } from "./diff/index.js";
13
+ export type { DigestMeta, ParsedElementLine, ParsedLine, RenderDigestOptions, } from "./digest/index.js";
14
+ export { parseDigestLine, renderDigest, SPARSE_CAPTURE_MARKER } from "./digest/index.js";
15
+ export type { CDPCapablePage, CDPSessionLike, PageLike } from "./page.js";
16
+ export { isCDPCapable, viewportOf } from "./page.js";
17
+ export type { CaptureOptions, CaptureResult } from "./snapshot/capture.js";
18
+ export { capture } from "./snapshot/capture.js";
19
+ export { attachClipFacts, clipFactFor } from "./snapshot/clip.js";
20
+ export { collectSnapshot } from "./snapshot/collect.js";
21
+ export type { Backdrop, BackdropLayer, ColorFact, ColorPair, ParsedColor, RatioReason, ResolvedBackdrop, RGBA, } from "./snapshot/color.js";
22
+ export { backdropLabel, colorFactFor, colorFactsForAll, contrastRatio, detectOverlay, groupColorFacts, parseCssColor, relativeLuminance, renderColorFact, resolveBackdrop, toHex, } from "./snapshot/color.js";
23
+ export type { Fact, FactKind } from "./snapshot/facts.js";
24
+ export { factsFor, factsForAll, renderFacts } from "./snapshot/facts.js";
25
+ export { formatNotMeasured } from "./snapshot/not-measured.js";
26
+ export type { BoxSides, ClipFact, ElementRecord, NotMeasured, Rect, Snapshot, } from "./snapshot/types.js";
27
+ export { SPEC_VERSION } from "./spec/index.js";
package/dist/index.js ADDED
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Public library entrypoint.
3
+ *
4
+ * The capture bundle, not the browser session, is the load-bearing object here.
5
+ * Everything in this package is a pure function over `ElementRecord[]` plus a
6
+ * thin host that produces them.
7
+ */
8
+ export { area, center, contains, gapAlong, intersect } from "./analysis/geometry.js";
9
+ export { BundleFormatError, buildBundle, parseBundle, serializeBundle } from "./bundle.js";
10
+ export { computeChanges, diffCaptures, match, prepare, renderDiff } from "./diff/index.js";
11
+ export { parseDigestLine, renderDigest, SPARSE_CAPTURE_MARKER } from "./digest/index.js";
12
+ export { isCDPCapable, viewportOf } from "./page.js";
13
+ export { capture } from "./snapshot/capture.js";
14
+ export { attachClipFacts, clipFactFor } from "./snapshot/clip.js";
15
+ export { collectSnapshot } from "./snapshot/collect.js";
16
+ export { backdropLabel, colorFactFor, colorFactsForAll, contrastRatio, detectOverlay, groupColorFacts, parseCssColor, relativeLuminance, renderColorFact, resolveBackdrop, toHex, } from "./snapshot/color.js";
17
+ export { factsFor, factsForAll, renderFacts } from "./snapshot/facts.js";
18
+ export { formatNotMeasured } from "./snapshot/not-measured.js";
19
+ export { SPEC_VERSION } from "./spec/index.js";
package/dist/page.cjs ADDED
@@ -0,0 +1,35 @@
1
+ "use strict";
2
+ /**
3
+ * The driver interface, defined structurally so the core of this package needs
4
+ * no Playwright at all.
5
+ *
6
+ * A caller passes whatever `Page` they already have — Playwright's, or anything
7
+ * that answers `evaluate`. `playwright-core` is an *optional* peer precisely so
8
+ * that a consumer who never captures live (offline `dnumb --from <bundle>`)
9
+ * pays nothing for it.
10
+ */
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.isCDPCapable = isCDPCapable;
13
+ exports.viewportOf = viewportOf;
14
+ function isCDPCapable(page) {
15
+ const context = page.context;
16
+ if (typeof context !== 'function')
17
+ return false;
18
+ try {
19
+ return typeof context.call(page)?.newCDPSession === 'function';
20
+ }
21
+ catch {
22
+ // Some drivers throw rather than returning a context. Not CDP-capable.
23
+ return false;
24
+ }
25
+ }
26
+ /**
27
+ * The viewport, read from inside the page.
28
+ *
29
+ * Playwright's `page.viewportSize()` returns **null** over `connectOverCDP`,
30
+ * which is exactly the mode the interactive path uses — so it cannot be the
31
+ * source of truth. `innerWidth`/`innerHeight` are always right.
32
+ */
33
+ async function viewportOf(page) {
34
+ return page.evaluate(() => ({ width: window.innerWidth, height: window.innerHeight }));
35
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * The driver interface, defined structurally so the core of this package needs
3
+ * no Playwright at all.
4
+ *
5
+ * A caller passes whatever `Page` they already have — Playwright's, or anything
6
+ * that answers `evaluate`. `playwright-core` is an *optional* peer precisely so
7
+ * that a consumer who never captures live (offline `dnumb --from <bundle>`)
8
+ * pays nothing for it.
9
+ */
10
+ /** The minimum a page must answer for the portable capture path. */
11
+ export interface PageLike {
12
+ evaluate<R>(pageFunction: () => R): Promise<R>;
13
+ }
14
+ /** A Chrome DevTools Protocol session. Chromium only. */
15
+ export interface CDPSessionLike {
16
+ send(method: string, params?: Record<string, unknown>): Promise<unknown>;
17
+ detach(): Promise<void>;
18
+ }
19
+ /**
20
+ * A page that can open a CDP session. Chromium only, and the shape is loose on
21
+ * purpose: `newCDPSession` takes the page itself, and we do not want this
22
+ * package's types to depend on Playwright's.
23
+ */
24
+ export interface CDPCapablePage extends PageLike {
25
+ context(): {
26
+ newCDPSession(page: unknown): Promise<CDPSessionLike>;
27
+ };
28
+ }
29
+ export declare function isCDPCapable(page: PageLike): page is CDPCapablePage;
30
+ /**
31
+ * The viewport, read from inside the page.
32
+ *
33
+ * Playwright's `page.viewportSize()` returns **null** over `connectOverCDP`,
34
+ * which is exactly the mode the interactive path uses — so it cannot be the
35
+ * source of truth. `innerWidth`/`innerHeight` are always right.
36
+ */
37
+ export declare function viewportOf(page: PageLike): Promise<{
38
+ width: number;
39
+ height: number;
40
+ }>;
package/dist/page.d.ts ADDED
@@ -0,0 +1,40 @@
1
+ /**
2
+ * The driver interface, defined structurally so the core of this package needs
3
+ * no Playwright at all.
4
+ *
5
+ * A caller passes whatever `Page` they already have — Playwright's, or anything
6
+ * that answers `evaluate`. `playwright-core` is an *optional* peer precisely so
7
+ * that a consumer who never captures live (offline `dnumb --from <bundle>`)
8
+ * pays nothing for it.
9
+ */
10
+ /** The minimum a page must answer for the portable capture path. */
11
+ export interface PageLike {
12
+ evaluate<R>(pageFunction: () => R): Promise<R>;
13
+ }
14
+ /** A Chrome DevTools Protocol session. Chromium only. */
15
+ export interface CDPSessionLike {
16
+ send(method: string, params?: Record<string, unknown>): Promise<unknown>;
17
+ detach(): Promise<void>;
18
+ }
19
+ /**
20
+ * A page that can open a CDP session. Chromium only, and the shape is loose on
21
+ * purpose: `newCDPSession` takes the page itself, and we do not want this
22
+ * package's types to depend on Playwright's.
23
+ */
24
+ export interface CDPCapablePage extends PageLike {
25
+ context(): {
26
+ newCDPSession(page: unknown): Promise<CDPSessionLike>;
27
+ };
28
+ }
29
+ export declare function isCDPCapable(page: PageLike): page is CDPCapablePage;
30
+ /**
31
+ * The viewport, read from inside the page.
32
+ *
33
+ * Playwright's `page.viewportSize()` returns **null** over `connectOverCDP`,
34
+ * which is exactly the mode the interactive path uses — so it cannot be the
35
+ * source of truth. `innerWidth`/`innerHeight` are always right.
36
+ */
37
+ export declare function viewportOf(page: PageLike): Promise<{
38
+ width: number;
39
+ height: number;
40
+ }>;
package/dist/page.js ADDED
@@ -0,0 +1,31 @@
1
+ /**
2
+ * The driver interface, defined structurally so the core of this package needs
3
+ * no Playwright at all.
4
+ *
5
+ * A caller passes whatever `Page` they already have — Playwright's, or anything
6
+ * that answers `evaluate`. `playwright-core` is an *optional* peer precisely so
7
+ * that a consumer who never captures live (offline `dnumb --from <bundle>`)
8
+ * pays nothing for it.
9
+ */
10
+ export function isCDPCapable(page) {
11
+ const context = page.context;
12
+ if (typeof context !== 'function')
13
+ return false;
14
+ try {
15
+ return typeof context.call(page)?.newCDPSession === 'function';
16
+ }
17
+ catch {
18
+ // Some drivers throw rather than returning a context. Not CDP-capable.
19
+ return false;
20
+ }
21
+ }
22
+ /**
23
+ * The viewport, read from inside the page.
24
+ *
25
+ * Playwright's `page.viewportSize()` returns **null** over `connectOverCDP`,
26
+ * which is exactly the mode the interactive path uses — so it cannot be the
27
+ * source of truth. `innerWidth`/`innerHeight` are always right.
28
+ */
29
+ export async function viewportOf(page) {
30
+ return page.evaluate(() => ({ width: window.innerWidth, height: window.innerHeight }));
31
+ }
@@ -0,0 +1,138 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DEFAULT_OUTPUT_DIR = void 0;
4
+ exports.safeName = safeName;
5
+ exports.bundlePath = bundlePath;
6
+ exports.writeBundle = writeBundle;
7
+ exports.capture = capture;
8
+ /**
9
+ * `dnumb/playwright` — one import and one call inside a suite that already has
10
+ * a running app, an authenticated session, and a `page` in scope.
11
+ *
12
+ * This is the wedge: the cost of a capture has to be a single line in a test
13
+ * someone already wrote, or the bundle never gets produced and nothing
14
+ * downstream of it matters.
15
+ *
16
+ * Nothing here imports Playwright, not even as a type. `playwright-core` is an
17
+ * *optional* peer, so a `import type { Page } from 'playwright-core'` in this
18
+ * file would break typechecking for a consumer who installed dnumb for the
19
+ * offline `--from <bundle>` path alone. The interfaces are structural instead,
20
+ * and a real `Page` / `TestInfo` satisfies them by shape.
21
+ */
22
+ const node_fs_1 = require("node:fs");
23
+ const node_path_1 = require("node:path");
24
+ const bundle_js_1 = require("./bundle.cjs");
25
+ const capture_js_1 = require("./snapshot/capture.cjs");
26
+ /** Mirrors Playwright's own `.playwright-artifacts` habit: hidden, gitignorable, per-repo. */
27
+ exports.DEFAULT_OUTPUT_DIR = '.dnumb';
28
+ /** Everything a filename can carry without quoting on any of the three platforms. */
29
+ const UNSAFE = /[^a-zA-Z0-9._-]+/g;
30
+ /** ext4 and APFS both cap a name at 255 bytes; leave room for the extension. */
31
+ const MAX_NAME = 120;
32
+ /**
33
+ * A capture name is caller-chosen text that becomes a path segment, so it is
34
+ * the one untrusted input here. Separators collapse into `-` rather than being
35
+ * rejected, because `settings/desktop` is a reasonable thing to type and
36
+ * failing a test over it would be hostile — but a leading run of dots or dashes
37
+ * is stripped outright, which is what turns `../../etc/passwd` into
38
+ * `etc-passwd` instead of a traversal.
39
+ */
40
+ function safeName(name) {
41
+ const flat = name
42
+ .trim()
43
+ .replace(UNSAFE, '-')
44
+ .replace(/^[-.]+/, '')
45
+ .replace(/[-.]+$/, '');
46
+ if (!flat) {
47
+ throw new Error(`dnumb: capture name ${JSON.stringify(name)} has no filesystem-safe characters in it. ` +
48
+ 'Names become filenames; use letters, digits, "." "_" or "-".');
49
+ }
50
+ return flat.slice(0, MAX_NAME);
51
+ }
52
+ /** Where `writeBundle` puts a bundle of this name. Exported so a caller can predict it. */
53
+ function bundlePath(dir, name) {
54
+ return (0, node_path_1.join)(dir, `${safeName(name)}.json`);
55
+ }
56
+ /**
57
+ * The bundle keeps the caller's name verbatim — it is the state label the
58
+ * digest header prints — and only the *path* is sanitized. Two names that
59
+ * differ solely in punctuation therefore land on one file: `settings/desktop`
60
+ * and `settings-desktop` both write `settings-desktop.json`.
61
+ *
62
+ * Re-capturing the same name overwrites, which is what a test re-run should do.
63
+ * A *different* name landing on an existing file is a collision, and silently
64
+ * discarding one of two captures a suite meant to keep is the kind of data loss
65
+ * that gets noticed weeks later, on the run that mattered. So that case throws,
66
+ * and the message names both captures and the fix.
67
+ */
68
+ function writeBundle(bundle, dir = exports.DEFAULT_OUTPUT_DIR) {
69
+ const path = bundlePath(dir, bundle.name);
70
+ (0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(path), { recursive: true });
71
+ if ((0, node_fs_1.existsSync)(path)) {
72
+ const existing = readExistingName(path);
73
+ if (existing !== null && existing !== bundle.name) {
74
+ throw new Error(`dnumb: capture ${JSON.stringify(bundle.name)} and capture ${JSON.stringify(existing)} ` +
75
+ `both write to ${path}, because names become filenames. Rename one of them.`);
76
+ }
77
+ }
78
+ (0, node_fs_1.writeFileSync)(path, (0, bundle_js_1.serializeBundle)(bundle), 'utf8');
79
+ return path;
80
+ }
81
+ /**
82
+ * The `name` of the bundle already at `path`, or `null` if it cannot be read as
83
+ * one. Unreadable is not a collision — an unrelated file in the output
84
+ * directory should not stop a capture — so it falls through to the overwrite.
85
+ */
86
+ function readExistingName(path) {
87
+ try {
88
+ const parsed = JSON.parse((0, node_fs_1.readFileSync)(path, 'utf8'));
89
+ if (typeof parsed !== 'object' || parsed === null)
90
+ return null;
91
+ const name = parsed.name;
92
+ return typeof name === 'string' ? name : null;
93
+ }
94
+ catch {
95
+ return null;
96
+ }
97
+ }
98
+ /**
99
+ * Capture `page` as a bundle on disk.
100
+ *
101
+ * ```ts
102
+ * import { capture } from '@michaelmagan/dnumb/playwright';
103
+ *
104
+ * test('settings page', async ({ page }, testInfo) => {
105
+ * await page.goto('/settings');
106
+ * await capture(page, 'settings-desktop', { testInfo });
107
+ * });
108
+ * ```
109
+ */
110
+ async function capture(page, name, options = {}) {
111
+ const dir = options.outputDir ?? exports.DEFAULT_OUTPUT_DIR;
112
+ // Fail on an unusable name before spending a capture on it.
113
+ safeName(name);
114
+ const result = await (0, capture_js_1.capture)(page, options.strategy ? { strategy: options.strategy } : {});
115
+ // The URL is read from inside the page rather than taken from the caller for
116
+ // the same reason the viewport is (`page.viewportSize()` is null over
117
+ // `connectOverCDP`): the page is the only thing that knows where it ended up
118
+ // after redirects, and a bundle that names the URL the test *asked* for is a
119
+ // bundle that lies six months later.
120
+ const url = await page.evaluate(() => location.href);
121
+ const bundle = (0, bundle_js_1.buildBundle)(result, {
122
+ name,
123
+ url,
124
+ capturedAt: new Date().toISOString(),
125
+ ...(options.viewportName ? { viewportName: options.viewportName } : {}),
126
+ ...(options.labels ? { labels: options.labels } : {}),
127
+ });
128
+ const path = writeBundle(bundle, dir);
129
+ if (options.testInfo) {
130
+ // `body`, not `path`: the attachment has to survive the output directory
131
+ // being cleaned between the run and the report being read in CI.
132
+ await options.testInfo.attach(`${safeName(name)}.dnumb.json`, {
133
+ body: (0, bundle_js_1.serializeBundle)(bundle),
134
+ contentType: 'application/json',
135
+ });
136
+ }
137
+ return { bundle, path };
138
+ }