@ambitresearch/genie 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -0
- package/dist/auth/bearer.d.ts +49 -0
- package/dist/auth/bearer.d.ts.map +1 -0
- package/dist/auth/bearer.js +159 -0
- package/dist/auth/bearer.js.map +1 -0
- package/dist/auth/oauth/authorize.d.ts +19 -0
- package/dist/auth/oauth/authorize.d.ts.map +1 -0
- package/dist/auth/oauth/authorize.js +111 -0
- package/dist/auth/oauth/authorize.js.map +1 -0
- package/dist/auth/oauth/config.d.ts +10 -0
- package/dist/auth/oauth/config.d.ts.map +1 -0
- package/dist/auth/oauth/config.js +16 -0
- package/dist/auth/oauth/config.js.map +1 -0
- package/dist/auth/oauth/dcr.d.ts +15 -0
- package/dist/auth/oauth/dcr.d.ts.map +1 -0
- package/dist/auth/oauth/dcr.js +38 -0
- package/dist/auth/oauth/dcr.js.map +1 -0
- package/dist/auth/oauth/jwt.d.ts +13 -0
- package/dist/auth/oauth/jwt.d.ts.map +1 -0
- package/dist/auth/oauth/jwt.js +46 -0
- package/dist/auth/oauth/jwt.js.map +1 -0
- package/dist/auth/oauth/metadata.d.ts +9 -0
- package/dist/auth/oauth/metadata.d.ts.map +1 -0
- package/dist/auth/oauth/metadata.js +19 -0
- package/dist/auth/oauth/metadata.js.map +1 -0
- package/dist/auth/oauth/router.d.ts +32 -0
- package/dist/auth/oauth/router.d.ts.map +1 -0
- package/dist/auth/oauth/router.js +159 -0
- package/dist/auth/oauth/router.js.map +1 -0
- package/dist/auth/oauth/store.d.ts +55 -0
- package/dist/auth/oauth/store.d.ts.map +1 -0
- package/dist/auth/oauth/store.js +63 -0
- package/dist/auth/oauth/store.js.map +1 -0
- package/dist/auth/oauth/token.d.ts +21 -0
- package/dist/auth/oauth/token.d.ts.map +1 -0
- package/dist/auth/oauth/token.js +77 -0
- package/dist/auth/oauth/token.js.map +1 -0
- package/dist/auth/oidc/group-policy.d.ts +43 -0
- package/dist/auth/oidc/group-policy.d.ts.map +1 -0
- package/dist/auth/oidc/group-policy.js +54 -0
- package/dist/auth/oidc/group-policy.js.map +1 -0
- package/dist/auth/oidc/verifier.d.ts +42 -0
- package/dist/auth/oidc/verifier.d.ts.map +1 -0
- package/dist/auth/oidc/verifier.js +145 -0
- package/dist/auth/oidc/verifier.js.map +1 -0
- package/dist/auth/token-cli.d.ts +9 -0
- package/dist/auth/token-cli.d.ts.map +1 -0
- package/dist/auth/token-cli.js +98 -0
- package/dist/auth/token-cli.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +149 -0
- package/dist/cli.js.map +1 -0
- package/dist/config/redact.d.ts +20 -0
- package/dist/config/redact.d.ts.map +1 -0
- package/dist/config/redact.js +77 -0
- package/dist/config/redact.js.map +1 -0
- package/dist/config/secrets.d.ts +100 -0
- package/dist/config/secrets.d.ts.map +1 -0
- package/dist/config/secrets.js +130 -0
- package/dist/config/secrets.js.map +1 -0
- package/dist/framework/html.d.ts +103 -0
- package/dist/framework/html.d.ts.map +1 -0
- package/dist/framework/html.js +271 -0
- package/dist/framework/html.js.map +1 -0
- package/dist/framework/interface.d.ts +130 -0
- package/dist/framework/interface.d.ts.map +1 -0
- package/dist/framework/interface.js +112 -0
- package/dist/framework/interface.js.map +1 -0
- package/dist/framework/react.d.ts +133 -0
- package/dist/framework/react.d.ts.map +1 -0
- package/dist/framework/react.js +243 -0
- package/dist/framework/react.js.map +1 -0
- package/dist/framework/vue.d.ts +99 -0
- package/dist/framework/vue.d.ts.map +1 -0
- package/dist/framework/vue.js +375 -0
- package/dist/framework/vue.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/llm/client.d.ts +81 -0
- package/dist/llm/client.d.ts.map +1 -0
- package/dist/llm/client.js +141 -0
- package/dist/llm/client.js.map +1 -0
- package/dist/llm/component-response.d.ts +111 -0
- package/dist/llm/component-response.d.ts.map +1 -0
- package/dist/llm/component-response.js +167 -0
- package/dist/llm/component-response.js.map +1 -0
- package/dist/llm/generated-files.d.ts +10 -0
- package/dist/llm/generated-files.d.ts.map +1 -0
- package/dist/llm/generated-files.js +26 -0
- package/dist/llm/generated-files.js.map +1 -0
- package/dist/llm/prompts/CHANGELOG.md +70 -0
- package/dist/llm/prompts/generate-component.system.md +99 -0
- package/dist/llm/prompts/refine-component.system.md +112 -0
- package/dist/llm/prompts.d.ts +66 -0
- package/dist/llm/prompts.d.ts.map +1 -0
- package/dist/llm/prompts.js +78 -0
- package/dist/llm/prompts.js.map +1 -0
- package/dist/llm/retry.d.ts +183 -0
- package/dist/llm/retry.d.ts.map +1 -0
- package/dist/llm/retry.js +475 -0
- package/dist/llm/retry.js.map +1 -0
- package/dist/llm/schema.d.ts +129 -0
- package/dist/llm/schema.d.ts.map +1 -0
- package/dist/llm/schema.js +147 -0
- package/dist/llm/schema.js.map +1 -0
- package/dist/llm/validate.d.ts +107 -0
- package/dist/llm/validate.d.ts.map +1 -0
- package/dist/llm/validate.js +174 -0
- package/dist/llm/validate.js.map +1 -0
- package/dist/manifest/compiler.d.ts +112 -0
- package/dist/manifest/compiler.d.ts.map +1 -0
- package/dist/manifest/compiler.js +306 -0
- package/dist/manifest/compiler.js.map +1 -0
- package/dist/manifest/ensure.d.ts +46 -0
- package/dist/manifest/ensure.d.ts.map +1 -0
- package/dist/manifest/ensure.js +49 -0
- package/dist/manifest/ensure.js.map +1 -0
- package/dist/manifest/index.d.ts +13 -0
- package/dist/manifest/index.d.ts.map +1 -0
- package/dist/manifest/index.js +13 -0
- package/dist/manifest/index.js.map +1 -0
- package/dist/middleware/plan-guard.d.ts +167 -0
- package/dist/middleware/plan-guard.d.ts.map +1 -0
- package/dist/middleware/plan-guard.js +190 -0
- package/dist/middleware/plan-guard.js.map +1 -0
- package/dist/plans/index.d.ts +71 -0
- package/dist/plans/index.d.ts.map +1 -0
- package/dist/plans/index.js +252 -0
- package/dist/plans/index.js.map +1 -0
- package/dist/schemas/component.schema.json +114 -0
- package/dist/server.d.ts +96 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +258 -0
- package/dist/server.js.map +1 -0
- package/dist/store/empty-manifest.d.ts +58 -0
- package/dist/store/empty-manifest.d.ts.map +1 -0
- package/dist/store/empty-manifest.js +59 -0
- package/dist/store/empty-manifest.js.map +1 -0
- package/dist/store/git-host.d.ts +120 -0
- package/dist/store/git-host.d.ts.map +1 -0
- package/dist/store/git-host.js +796 -0
- package/dist/store/git-host.js.map +1 -0
- package/dist/store/index.d.ts +5 -0
- package/dist/store/index.d.ts.map +1 -0
- package/dist/store/index.js +4 -0
- package/dist/store/index.js.map +1 -0
- package/dist/store/interface.d.ts +322 -0
- package/dist/store/interface.d.ts.map +1 -0
- package/dist/store/interface.js +100 -0
- package/dist/store/interface.js.map +1 -0
- package/dist/store/kit-files.d.ts +88 -0
- package/dist/store/kit-files.d.ts.map +1 -0
- package/dist/store/kit-files.js +199 -0
- package/dist/store/kit-files.js.map +1 -0
- package/dist/store/local.d.ts +70 -0
- package/dist/store/local.d.ts.map +1 -0
- package/dist/store/local.js +651 -0
- package/dist/store/local.js.map +1 -0
- package/dist/store/manifest.d.ts +44 -0
- package/dist/store/manifest.d.ts.map +1 -0
- package/dist/store/manifest.js +116 -0
- package/dist/store/manifest.js.map +1 -0
- package/dist/store/viewer-assets.d.ts +20 -0
- package/dist/store/viewer-assets.d.ts.map +1 -0
- package/dist/store/viewer-assets.js +118 -0
- package/dist/store/viewer-assets.js.map +1 -0
- package/dist/sync/anchor.d.ts +125 -0
- package/dist/sync/anchor.d.ts.map +1 -0
- package/dist/sync/anchor.js +210 -0
- package/dist/sync/anchor.js.map +1 -0
- package/dist/sync/index.d.ts +15 -0
- package/dist/sync/index.d.ts.map +1 -0
- package/dist/sync/index.js +15 -0
- package/dist/sync/index.js.map +1 -0
- package/dist/sync/orchestrator.d.ts +160 -0
- package/dist/sync/orchestrator.d.ts.map +1 -0
- package/dist/sync/orchestrator.js +288 -0
- package/dist/sync/orchestrator.js.map +1 -0
- package/dist/tools/bind_kit.d.ts +29 -0
- package/dist/tools/bind_kit.d.ts.map +1 -0
- package/dist/tools/bind_kit.js +70 -0
- package/dist/tools/bind_kit.js.map +1 -0
- package/dist/tools/conjure.d.ts +141 -0
- package/dist/tools/conjure.d.ts.map +1 -0
- package/dist/tools/conjure.js +655 -0
- package/dist/tools/conjure.js.map +1 -0
- package/dist/tools/conjure_screen.d.ts +174 -0
- package/dist/tools/conjure_screen.d.ts.map +1 -0
- package/dist/tools/conjure_screen.js +458 -0
- package/dist/tools/conjure_screen.js.map +1 -0
- package/dist/tools/create_kit.d.ts +32 -0
- package/dist/tools/create_kit.d.ts.map +1 -0
- package/dist/tools/create_kit.js +131 -0
- package/dist/tools/create_kit.js.map +1 -0
- package/dist/tools/create_project.d.ts +267 -0
- package/dist/tools/create_project.d.ts.map +1 -0
- package/dist/tools/create_project.js +546 -0
- package/dist/tools/create_project.js.map +1 -0
- package/dist/tools/delete_files.d.ts +93 -0
- package/dist/tools/delete_files.d.ts.map +1 -0
- package/dist/tools/delete_files.js +276 -0
- package/dist/tools/delete_files.js.map +1 -0
- package/dist/tools/delete_project.d.ts +43 -0
- package/dist/tools/delete_project.d.ts.map +1 -0
- package/dist/tools/delete_project.js +118 -0
- package/dist/tools/delete_project.js.map +1 -0
- package/dist/tools/get_kit.d.ts +28 -0
- package/dist/tools/get_kit.d.ts.map +1 -0
- package/dist/tools/get_kit.js +95 -0
- package/dist/tools/get_kit.js.map +1 -0
- package/dist/tools/get_project.d.ts +37 -0
- package/dist/tools/get_project.d.ts.map +1 -0
- package/dist/tools/get_project.js +71 -0
- package/dist/tools/get_project.js.map +1 -0
- package/dist/tools/list_components.d.ts +42 -0
- package/dist/tools/list_components.d.ts.map +1 -0
- package/dist/tools/list_components.js +125 -0
- package/dist/tools/list_components.js.map +1 -0
- package/dist/tools/list_files.d.ts +32 -0
- package/dist/tools/list_files.d.ts.map +1 -0
- package/dist/tools/list_files.js +109 -0
- package/dist/tools/list_files.js.map +1 -0
- package/dist/tools/list_kits.d.ts +14 -0
- package/dist/tools/list_kits.d.ts.map +1 -0
- package/dist/tools/list_kits.js +41 -0
- package/dist/tools/list_kits.js.map +1 -0
- package/dist/tools/list_projects.d.ts +23 -0
- package/dist/tools/list_projects.d.ts.map +1 -0
- package/dist/tools/list_projects.js +66 -0
- package/dist/tools/list_projects.js.map +1 -0
- package/dist/tools/plan.d.ts +16 -0
- package/dist/tools/plan.d.ts.map +1 -0
- package/dist/tools/plan.js +146 -0
- package/dist/tools/plan.js.map +1 -0
- package/dist/tools/preview.d.ts +289 -0
- package/dist/tools/preview.d.ts.map +1 -0
- package/dist/tools/preview.js +672 -0
- package/dist/tools/preview.js.map +1 -0
- package/dist/tools/read_file.d.ts +40 -0
- package/dist/tools/read_file.d.ts.map +1 -0
- package/dist/tools/read_file.js +114 -0
- package/dist/tools/read_file.js.map +1 -0
- package/dist/tools/refine.d.ts +220 -0
- package/dist/tools/refine.d.ts.map +1 -0
- package/dist/tools/refine.js +501 -0
- package/dist/tools/refine.js.map +1 -0
- package/dist/tools/validate.d.ts +74 -0
- package/dist/tools/validate.d.ts.map +1 -0
- package/dist/tools/validate.js +224 -0
- package/dist/tools/validate.js.map +1 -0
- package/dist/tools/write_files.d.ts +158 -0
- package/dist/tools/write_files.d.ts.map +1 -0
- package/dist/tools/write_files.js +541 -0
- package/dist/tools/write_files.js.map +1 -0
- package/dist/transport.d.ts +77 -0
- package/dist/transport.d.ts.map +1 -0
- package/dist/transport.js +448 -0
- package/dist/transport.js.map +1 -0
- package/dist/ui/card-asset-broker.d.ts +42 -0
- package/dist/ui/card-asset-broker.d.ts.map +1 -0
- package/dist/ui/card-asset-broker.js +479 -0
- package/dist/ui/card-asset-broker.js.map +1 -0
- package/dist/ui/grid-resource.d.ts +252 -0
- package/dist/ui/grid-resource.d.ts.map +1 -0
- package/dist/ui/grid-resource.js +680 -0
- package/dist/ui/grid-resource.js.map +1 -0
- package/dist/ui/viewer-static/index.html +95 -0
- package/dist/ui/viewer-static/viewer.css +451 -0
- package/dist/ui/viewer-static/viewer.js +1405 -0
- package/dist/validate/full-scan.d.ts +87 -0
- package/dist/validate/full-scan.d.ts.map +1 -0
- package/dist/validate/full-scan.js +299 -0
- package/dist/validate/full-scan.js.map +1 -0
- package/dist/validate/index.d.ts +15 -0
- package/dist/validate/index.d.ts.map +1 -0
- package/dist/validate/index.js +18 -0
- package/dist/validate/index.js.map +1 -0
- package/dist/validate/marker.d.ts +89 -0
- package/dist/validate/marker.d.ts.map +1 -0
- package/dist/validate/marker.js +87 -0
- package/dist/validate/marker.js.map +1 -0
- package/dist/validate/phash.d.ts +135 -0
- package/dist/validate/phash.d.ts.map +1 -0
- package/dist/validate/phash.js +307 -0
- package/dist/validate/phash.js.map +1 -0
- package/dist/validate/render.d.ts +38 -0
- package/dist/validate/render.d.ts.map +1 -0
- package/dist/validate/render.js +155 -0
- package/dist/validate/render.js.map +1 -0
- package/dist/watch/index.d.ts +12 -0
- package/dist/watch/index.d.ts.map +1 -0
- package/dist/watch/index.js +12 -0
- package/dist/watch/index.js.map +1 -0
- package/dist/watch/watcher.d.ts +65 -0
- package/dist/watch/watcher.d.ts.map +1 -0
- package/dist/watch/watcher.js +238 -0
- package/dist/watch/watcher.js.map +1 -0
- package/package.json +70 -0
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/** Decoded RGBA pixel buffer — what a PNG decoder (`pngjs`) hands back, and
|
|
2
|
+
* what {@link computePHash} (via `blockhash-core`'s `bmvbhash`) consumes. */
|
|
3
|
+
export interface RGBAImage {
|
|
4
|
+
/** Raw pixel bytes, 4 per pixel (R,G,B,A), row-major. */
|
|
5
|
+
data: Uint8Array;
|
|
6
|
+
width: number;
|
|
7
|
+
height: number;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Perceptual hashes within this many bits (out of 256, i.e. ~1.5%) are
|
|
11
|
+
* treated as "the same rendered card" for AC5's `variantsIdentical` check.
|
|
12
|
+
* See the module doc above for the empirical calibration behind this number.
|
|
13
|
+
*/
|
|
14
|
+
export declare const DEFAULT_TOLERANCE_BITS = 4;
|
|
15
|
+
/**
|
|
16
|
+
* Perceptual-hash an already-decoded RGBA image via the blockhash algorithm
|
|
17
|
+
* (AC5). Returns a hex string; two structurally-identical images always hash
|
|
18
|
+
* identically (verified deterministic), and the hash space is the fixed
|
|
19
|
+
* `HASH_BITS²` bits `bmvbhash` always emits for a given `bits` argument, so
|
|
20
|
+
* every hash this module produces is directly comparable via {@link
|
|
21
|
+
* hammingDistance}.
|
|
22
|
+
*/
|
|
23
|
+
export declare function computePHash(image: RGBAImage): string;
|
|
24
|
+
/**
|
|
25
|
+
* A card's mean "ink" color (DRO-717) — the second signal the color veto in
|
|
26
|
+
* {@link findDuplicateClusters} compares alongside the blockhash. Mean R/G/B
|
|
27
|
+
* over the saturated, non-background pixels of a render (see {@link
|
|
28
|
+
* computeColorSignature} for the exclusion rule). `undefined` when a render has
|
|
29
|
+
* no ink pixels at all (a blank/near-empty card), in which case the veto
|
|
30
|
+
* abstains and the pairing falls back to blockhash alone.
|
|
31
|
+
*/
|
|
32
|
+
export interface ColorSignature {
|
|
33
|
+
r: number;
|
|
34
|
+
g: number;
|
|
35
|
+
b: number;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* L1 (Manhattan) distance below which two {@link ColorSignature}s are treated
|
|
39
|
+
* as "the same fill color" for the hue-veto in {@link findDuplicateClusters}
|
|
40
|
+
* (DRO-717). Calibrated on real DRO-717 renders (see the module doc's
|
|
41
|
+
* "Hue-aware color veto" section): label variants of a single fill drift ≤ ~4.5,
|
|
42
|
+
* every genuinely-distinct hue pair measured ≥ ~33 — a threshold of 20 sits
|
|
43
|
+
* centered in that gap, ~4× above label-variant noise and comfortably below the
|
|
44
|
+
* tightest real hue difference.
|
|
45
|
+
*/
|
|
46
|
+
export declare const DEFAULT_COLOR_TOLERANCE_L1 = 20;
|
|
47
|
+
/**
|
|
48
|
+
* Compute a render's mean "ink" color (DRO-717) — the hue signal the color veto
|
|
49
|
+
* in {@link findDuplicateClusters} pairs with the blockhash. Averages R/G/B over
|
|
50
|
+
* only the saturated, non-background pixels (see {@link INK_ALPHA_MIN} et al.
|
|
51
|
+
* for the exclusion rule and the module doc for why background + white text are
|
|
52
|
+
* dropped rather than averaged in). Returns `undefined` when NO pixel qualifies
|
|
53
|
+
* as ink (a fully blank or near-white card) — the caller then compares such a
|
|
54
|
+
* card on blockhash alone, since it has no hue to veto on.
|
|
55
|
+
*
|
|
56
|
+
* Pure arithmetic over the same decoded RGBA buffer {@link computePHash}
|
|
57
|
+
* consumes — no I/O, one linear pass, so it adds negligibly to AC7's per-render
|
|
58
|
+
* budget.
|
|
59
|
+
*/
|
|
60
|
+
export declare function computeColorSignature(image: RGBAImage): ColorSignature | undefined;
|
|
61
|
+
/**
|
|
62
|
+
* L1 (Manhattan) distance between two {@link ColorSignature}s — the metric the
|
|
63
|
+
* color veto thresholds against {@link DEFAULT_COLOR_TOLERANCE_L1}. Manhattan,
|
|
64
|
+
* not Euclidean, because the calibration gap is wide (≤4.5 vs ≥33) so the
|
|
65
|
+
* cheaper sum-of-abs-differences is more than discriminating enough, and it
|
|
66
|
+
* keeps the whole comparison integer-friendly.
|
|
67
|
+
*/
|
|
68
|
+
export declare function colorDistanceL1(a: ColorSignature, b: ColorSignature): number;
|
|
69
|
+
/**
|
|
70
|
+
* Count differing BITS (not hex characters) between two equal-length hex hash
|
|
71
|
+
* strings (AC5). Both {@link computePHash}'s output and this function operate
|
|
72
|
+
* over the same fixed-width hash space, so a caller never mixes hashes from a
|
|
73
|
+
* different `HASH_BITS` setting — but the length check below still guards the
|
|
74
|
+
* case defensively rather than silently truncating.
|
|
75
|
+
*
|
|
76
|
+
* Hex, not raw bit-strings, because that's the wire/storage-friendly form
|
|
77
|
+
* `computePHash` returns; comparing correctly still requires decoding to bits
|
|
78
|
+
* per nibble (a naive per-CHARACTER diff would undercount — e.g. `0x0` vs
|
|
79
|
+
* `0xf` differ in ALL 4 bits of that nibble but are only "1 character" apart).
|
|
80
|
+
*/
|
|
81
|
+
export declare function hammingDistance(hexA: string, hexB: string): number;
|
|
82
|
+
/** One kit file's path plus its precomputed perceptual hash — the input
|
|
83
|
+
* {@link findDuplicateClusters} clusters. `color` is the DRO-717 hue-veto
|
|
84
|
+
* signal ({@link computeColorSignature}); OPTIONAL and backward-compatible — an
|
|
85
|
+
* entry without it is compared on blockhash alone, exactly as before the veto
|
|
86
|
+
* existed. Production (`full-scan.ts`) always supplies it; arithmetic-only tests
|
|
87
|
+
* may omit it. */
|
|
88
|
+
export interface HashedEntry {
|
|
89
|
+
path: string;
|
|
90
|
+
hash: string;
|
|
91
|
+
color?: ColorSignature;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Group `entries` into near-duplicate clusters (AC5: pairs — generalized here
|
|
95
|
+
* to clusters, since 3+ near-identical renders is exactly the failure mode
|
|
96
|
+
* the AC's own example describes) and return the FLAT, deduplicated,
|
|
97
|
+
* sorted list of every path that landed in a cluster of size >= 2. A path
|
|
98
|
+
* with no near-duplicate (a singleton) is omitted entirely — `variantsIdentical`
|
|
99
|
+
* is "these paths render the same as some other path", not a roster of every
|
|
100
|
+
* file scanned.
|
|
101
|
+
*
|
|
102
|
+
* Clustering is transitive (union-find): if A is within tolerance of B, and B
|
|
103
|
+
* is within tolerance of C, all three land in ONE cluster even if A and C
|
|
104
|
+
* alone exceed tolerance — the natural reading of "these components all look
|
|
105
|
+
* the same" when three-plus near-duplicates form a chain rather than every
|
|
106
|
+
* pair being mutually within tolerance.
|
|
107
|
+
*
|
|
108
|
+
* ── Two-signal near-duplicate test (DRO-717) ─────────────────────────────────
|
|
109
|
+
* Two entries are unioned only when they are near-duplicates on BOTH signals:
|
|
110
|
+
* 1. blockhash Hamming distance <= `toleranceBits` (layout/luminance), AND
|
|
111
|
+
* 2. `colorDistanceL1` <= `colorToleranceL1` (hue) — the DRO-717 veto that
|
|
112
|
+
* stops the hue-blind blockhash from clustering a primary/danger/success
|
|
113
|
+
* button set (identical layout, distinct fills) as identical.
|
|
114
|
+
* The color check is a pure VETO: it can only PREVENT a union the blockhash
|
|
115
|
+
* would have made, never create one. It abstains (does not veto) when EITHER
|
|
116
|
+
* entry lacks a color signature — so a caller that supplies no colors (e.g. an
|
|
117
|
+
* arithmetic-only test) gets the exact pre-DRO-717 blockhash-only behavior.
|
|
118
|
+
*
|
|
119
|
+
* Why the veto doesn't cause false NEGATIVES (dropping a real duplicate): for a
|
|
120
|
+
* SAME-fill pair, the ink-color distance co-varies with the button's coverage,
|
|
121
|
+
* which co-varies with the blockhash. Two same-fill cards only drift apart in
|
|
122
|
+
* ink color once their rendered widths differ enough — and by then their
|
|
123
|
+
* blockhash has ALREADY diverged past `toleranceBits`, so the union was never on
|
|
124
|
+
* the table for the veto to remove (measured on real dark- and light-background
|
|
125
|
+
* renders: a same-fill pair reaching colorL1 ≈ 39 was already ~35 blockhash bits
|
|
126
|
+
* apart). The veto therefore only ever bites the case it targets — blockhash
|
|
127
|
+
* says "close" while hue says "different", i.e. the hue-blind false positive.
|
|
128
|
+
*
|
|
129
|
+
* O(n²) pairwise comparisons — acceptable for AC7's ~50-component budget (at
|
|
130
|
+
* most ~1225 comparisons, each a cheap XOR+popcount over a 256-bit BigInt plus
|
|
131
|
+
* a three-term color subtraction); would need a smarter index (e.g. an LSH
|
|
132
|
+
* bucket) at kit sizes multiple orders of magnitude larger than genie's target.
|
|
133
|
+
*/
|
|
134
|
+
export declare function findDuplicateClusters(entries: HashedEntry[], toleranceBits?: number, colorToleranceL1?: number): string[];
|
|
135
|
+
//# sourceMappingURL=phash.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"phash.d.ts","sourceRoot":"","sources":["../../src/validate/phash.ts"],"names":[],"mappings":"AAmFA;6EAC6E;AAC7E,MAAM,WAAW,SAAS;IACxB,yDAAyD;IACzD,IAAI,EAAE,UAAU,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AAUD;;;;GAIG;AACH,eAAO,MAAM,sBAAsB,IAAI,CAAC;AAExC;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,SAAS,GAAG,MAAM,CAErD;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,cAAc;IAC7B,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;CACX;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,0BAA0B,KAAK,CAAC;AAmB7C;;;;;;;;;;;;GAYG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,SAAS,GAAG,cAAc,GAAG,SAAS,CAsBlF;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,CAAC,EAAE,cAAc,EAAE,CAAC,EAAE,cAAc,GAAG,MAAM,CAE5E;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAelE;AAED;;;;;kBAKkB;AAClB,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,cAAc,CAAC;CACxB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AACH,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,WAAW,EAAE,EACtB,aAAa,GAAE,MAA+B,EAC9C,gBAAgB,GAAE,MAAmC,GACpD,MAAM,EAAE,CAuDV"}
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Perceptual-hash helpers for the M3-04 (DRO-260) `validate` full-scan facet's
|
|
3
|
+
* `variantsIdentical` check (AC5) — "the model generated 3 buttons that all
|
|
4
|
+
* look the same".
|
|
5
|
+
*
|
|
6
|
+
* Pure arithmetic, no I/O: {@link computePHash} takes decoded RGBA pixels (the
|
|
7
|
+
* caller renders + decodes the PNG — see `render.ts`), and {@link
|
|
8
|
+
* findDuplicateClusters} takes already-computed hashes. Kept pure/offline so
|
|
9
|
+
* this module — and its test suite — never touches Playwright, a browser, or
|
|
10
|
+
* the filesystem, mirroring `validate/marker.ts`'s own "pure and offline" design
|
|
11
|
+
* note.
|
|
12
|
+
*
|
|
13
|
+
* ── Algorithm choice: blockhash, not a DCT pHash ──────────────────────────────
|
|
14
|
+
* `bmvbhash` (the `blockhash-core` package, the reference implementation of
|
|
15
|
+
* blockhash.io's algorithm) divides the image into an NxN grid of blocks,
|
|
16
|
+
* takes each block's average brightness, and bits the result against the
|
|
17
|
+
* per-quadrant median. It is simpler and faster than a DCT-based pHash and
|
|
18
|
+
* needs no native build step (no `sharp`/libvips) — genie's server stays a
|
|
19
|
+
* pure-JS install, matching the rest of `dependencies` in package.json. The
|
|
20
|
+
* accuracy tradeoff is fine for this AC: the goal is "flag near-identical
|
|
21
|
+
* card renders", not general-purpose reverse image search.
|
|
22
|
+
*
|
|
23
|
+
* ── Tolerance calibration ─────────────────────────────────────────────────────
|
|
24
|
+
* `DEFAULT_TOLERANCE_BITS` was calibrated empirically (real Chromium renders +
|
|
25
|
+
* `bmvbhash(…, 16)`, i.e. a 256-bit hash), most recently re-measured post-#154
|
|
26
|
+
* WITH fonts loaded (DRO-717 — an early fontless run misleadingly showed every
|
|
27
|
+
* label variant at distance 0, because a browser with no font renders every
|
|
28
|
+
* button at the same zero-glyph-advance width; the numbers below are the real,
|
|
29
|
+
* font-loaded measurements):
|
|
30
|
+
* - Byte-identical HTML (the model re-emitting the exact same card twice)
|
|
31
|
+
* hashes to a Hamming distance of **0**.
|
|
32
|
+
* - A legitimately distinct restyle of the same component — an outline vs.
|
|
33
|
+
* filled button, a secondary size/radius variant — produces **~11–27 bits**.
|
|
34
|
+
* - Two cards differing ONLY in their label TEXT ("Save" vs "Delete account")
|
|
35
|
+
* also produce **~8–34 bits** — because a wider label lays the whole button
|
|
36
|
+
* out at a different width, which genuinely changes the block-grid
|
|
37
|
+
* structure. (This corrects an earlier version of this comment, which
|
|
38
|
+
* claimed label-only differences hash to distance 0 / cluster together —
|
|
39
|
+
* true only in the degenerate fontless render; false under real text.)
|
|
40
|
+
* Those last two ranges OVERLAP, so no single `DEFAULT_TOLERANCE_BITS` can both
|
|
41
|
+
* cluster every label variant AND avoid clustering every genuine restyle. We
|
|
42
|
+
* therefore keep the tolerance TIGHT (**4 bits out of 256**, just above pure
|
|
43
|
+
* render noise — antialiasing jitter, sub-pixel rounding — and far below the
|
|
44
|
+
* smallest genuinely-different pair), and treat `variantsIdentical` as its
|
|
45
|
+
* literal name: renders that are STRUCTURALLY identical (a true duplicate card
|
|
46
|
+
* — same markup, or whitespace-only differences), not merely two buttons of the
|
|
47
|
+
* same shape wearing different words. This preserves the doc's own long-standing
|
|
48
|
+
* bias — a false negative just means a human's eyes catch it in the rendered
|
|
49
|
+
* grid, while a false positive makes `validate` cry wolf on a legitimate
|
|
50
|
+
* "Save"/"Cancel" pair.
|
|
51
|
+
*
|
|
52
|
+
* ── Hue-aware color veto (DRO-717, fixes finding 1) ───────────────────────────
|
|
53
|
+
* blockhash is a LUMINANCE-structure hash: `bmvbhash` reduces each block to a
|
|
54
|
+
* single brightness value (the `R+G+B` sum), with no hue term at all. Three
|
|
55
|
+
* buttons of identical layout but visually distinct fills — clay `#c87c5e`,
|
|
56
|
+
* blue `#345197`, red `#c5372f` — therefore hash to Hamming distance **0**
|
|
57
|
+
* (confirmed on real renders), which would falsely cluster a primary/danger/
|
|
58
|
+
* success button SET as `variantsIdentical`. This is structural, not a
|
|
59
|
+
* tolerance-tuning problem: no `DEFAULT_TOLERANCE_BITS` recovers hue the hash
|
|
60
|
+
* never encoded.
|
|
61
|
+
*
|
|
62
|
+
* The fix is an ADDITIVE second signal, not a different hash: alongside each
|
|
63
|
+
* card's blockhash we carry a cheap {@link computeColorSignature} — the mean
|
|
64
|
+
* RGB of its "ink" pixels (saturated, non-background — see the function doc for
|
|
65
|
+
* why page background and white glyph fill are excluded). Two cards are treated
|
|
66
|
+
* as near-duplicates only when they are BOTH blockhash-close AND color-close
|
|
67
|
+
* (L1 within {@link DEFAULT_COLOR_TOLERANCE_L1}). The color signature was
|
|
68
|
+
* calibrated on the same DRO-717 real renders: label variants of one fill drift
|
|
69
|
+
* at most **~4.5** (blank space and white text excluded, so a growing button
|
|
70
|
+
* doesn't move it), while every genuinely-different hue pair measured — the
|
|
71
|
+
* issue trio included — sits at **≥33** (the tightest real pair, teal/green,
|
|
72
|
+
* was 33.5; the issue trio spanned 115–269). A threshold of **20** sits
|
|
73
|
+
* centered in that gap. The veto is strictly SUBTRACTIVE — it can only DROP a
|
|
74
|
+
* pairing the blockhash alone would have made, never add one — so an entry
|
|
75
|
+
* carrying no color signature falls back to the exact pre-DRO-717 behavior.
|
|
76
|
+
*
|
|
77
|
+
* `blockhash-core` ships its own `index.d.ts` (a small, pure-JS package, no
|
|
78
|
+
* native build step — genie's server stays a pure-JS install, matching the
|
|
79
|
+
* rest of `dependencies` in package.json), so the named import below needs no
|
|
80
|
+
* ambient shim.
|
|
81
|
+
*/
|
|
82
|
+
import { bmvbhash } from "blockhash-core";
|
|
83
|
+
/**
|
|
84
|
+
* Grid resolution `bmvbhash` divides the image into (an N×N block grid,
|
|
85
|
+
* yielding an N²-bit hash). 16 → a 256-bit hash — fine-grained enough to
|
|
86
|
+
* distinguish real layout differences while staying cheap to compute for a
|
|
87
|
+
* batch of dozens of components (AC7's < 5 s / 50-component budget).
|
|
88
|
+
*/
|
|
89
|
+
const HASH_BITS = 16;
|
|
90
|
+
/**
|
|
91
|
+
* Perceptual hashes within this many bits (out of 256, i.e. ~1.5%) are
|
|
92
|
+
* treated as "the same rendered card" for AC5's `variantsIdentical` check.
|
|
93
|
+
* See the module doc above for the empirical calibration behind this number.
|
|
94
|
+
*/
|
|
95
|
+
export const DEFAULT_TOLERANCE_BITS = 4;
|
|
96
|
+
/**
|
|
97
|
+
* Perceptual-hash an already-decoded RGBA image via the blockhash algorithm
|
|
98
|
+
* (AC5). Returns a hex string; two structurally-identical images always hash
|
|
99
|
+
* identically (verified deterministic), and the hash space is the fixed
|
|
100
|
+
* `HASH_BITS²` bits `bmvbhash` always emits for a given `bits` argument, so
|
|
101
|
+
* every hash this module produces is directly comparable via {@link
|
|
102
|
+
* hammingDistance}.
|
|
103
|
+
*/
|
|
104
|
+
export function computePHash(image) {
|
|
105
|
+
return bmvbhash({ data: image.data, width: image.width, height: image.height }, HASH_BITS);
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* L1 (Manhattan) distance below which two {@link ColorSignature}s are treated
|
|
109
|
+
* as "the same fill color" for the hue-veto in {@link findDuplicateClusters}
|
|
110
|
+
* (DRO-717). Calibrated on real DRO-717 renders (see the module doc's
|
|
111
|
+
* "Hue-aware color veto" section): label variants of a single fill drift ≤ ~4.5,
|
|
112
|
+
* every genuinely-distinct hue pair measured ≥ ~33 — a threshold of 20 sits
|
|
113
|
+
* centered in that gap, ~4× above label-variant noise and comfortably below the
|
|
114
|
+
* tightest real hue difference.
|
|
115
|
+
*/
|
|
116
|
+
export const DEFAULT_COLOR_TOLERANCE_L1 = 20;
|
|
117
|
+
/**
|
|
118
|
+
* Thresholds (DRO-717) for {@link computeColorSignature}'s "is this pixel ink?"
|
|
119
|
+
* test. A pixel is EXCLUDED from the mean when it is either barely-opaque
|
|
120
|
+
* (`alpha < ALPHA_MIN` — antialiased edges, transparent margin) or
|
|
121
|
+
* bright-and-low-chroma (`min(R,G,B) > BRIGHT_MIN` AND `max−min < CHROMA_MAX`).
|
|
122
|
+
* The second clause drops both the light page background a preview renders
|
|
123
|
+
* against AND the white glyph fill of the label text — the two things that,
|
|
124
|
+
* left in, would (a) dominate a small button's mean and (b) shift it as the
|
|
125
|
+
* label grows, which is exactly the width-sensitivity that disqualifies a
|
|
126
|
+
* whole-canvas mean as a hue signal. What survives is the saturated button
|
|
127
|
+
* fill, whose mean is stable across label-length changes (measured drift ≤ ~4.5)
|
|
128
|
+
* yet cleanly separates distinct hues (≥ ~33).
|
|
129
|
+
*/
|
|
130
|
+
const INK_ALPHA_MIN = 128;
|
|
131
|
+
const INK_BRIGHT_MIN = 200;
|
|
132
|
+
const INK_CHROMA_MAX = 24;
|
|
133
|
+
/**
|
|
134
|
+
* Compute a render's mean "ink" color (DRO-717) — the hue signal the color veto
|
|
135
|
+
* in {@link findDuplicateClusters} pairs with the blockhash. Averages R/G/B over
|
|
136
|
+
* only the saturated, non-background pixels (see {@link INK_ALPHA_MIN} et al.
|
|
137
|
+
* for the exclusion rule and the module doc for why background + white text are
|
|
138
|
+
* dropped rather than averaged in). Returns `undefined` when NO pixel qualifies
|
|
139
|
+
* as ink (a fully blank or near-white card) — the caller then compares such a
|
|
140
|
+
* card on blockhash alone, since it has no hue to veto on.
|
|
141
|
+
*
|
|
142
|
+
* Pure arithmetic over the same decoded RGBA buffer {@link computePHash}
|
|
143
|
+
* consumes — no I/O, one linear pass, so it adds negligibly to AC7's per-render
|
|
144
|
+
* budget.
|
|
145
|
+
*/
|
|
146
|
+
export function computeColorSignature(image) {
|
|
147
|
+
const d = image.data;
|
|
148
|
+
let r = 0;
|
|
149
|
+
let g = 0;
|
|
150
|
+
let b = 0;
|
|
151
|
+
let n = 0;
|
|
152
|
+
for (let i = 0; i + 3 < d.length; i += 4) {
|
|
153
|
+
const R = d[i];
|
|
154
|
+
const G = d[i + 1];
|
|
155
|
+
const B = d[i + 2];
|
|
156
|
+
const A = d[i + 3];
|
|
157
|
+
if (A < INK_ALPHA_MIN)
|
|
158
|
+
continue;
|
|
159
|
+
const mn = Math.min(R, G, B);
|
|
160
|
+
const mx = Math.max(R, G, B);
|
|
161
|
+
if (mn > INK_BRIGHT_MIN && mx - mn < INK_CHROMA_MAX)
|
|
162
|
+
continue; // background / white text
|
|
163
|
+
r += R;
|
|
164
|
+
g += G;
|
|
165
|
+
b += B;
|
|
166
|
+
n += 1;
|
|
167
|
+
}
|
|
168
|
+
if (n === 0)
|
|
169
|
+
return undefined;
|
|
170
|
+
return { r: r / n, g: g / n, b: b / n };
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* L1 (Manhattan) distance between two {@link ColorSignature}s — the metric the
|
|
174
|
+
* color veto thresholds against {@link DEFAULT_COLOR_TOLERANCE_L1}. Manhattan,
|
|
175
|
+
* not Euclidean, because the calibration gap is wide (≤4.5 vs ≥33) so the
|
|
176
|
+
* cheaper sum-of-abs-differences is more than discriminating enough, and it
|
|
177
|
+
* keeps the whole comparison integer-friendly.
|
|
178
|
+
*/
|
|
179
|
+
export function colorDistanceL1(a, b) {
|
|
180
|
+
return Math.abs(a.r - b.r) + Math.abs(a.g - b.g) + Math.abs(a.b - b.b);
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Count differing BITS (not hex characters) between two equal-length hex hash
|
|
184
|
+
* strings (AC5). Both {@link computePHash}'s output and this function operate
|
|
185
|
+
* over the same fixed-width hash space, so a caller never mixes hashes from a
|
|
186
|
+
* different `HASH_BITS` setting — but the length check below still guards the
|
|
187
|
+
* case defensively rather than silently truncating.
|
|
188
|
+
*
|
|
189
|
+
* Hex, not raw bit-strings, because that's the wire/storage-friendly form
|
|
190
|
+
* `computePHash` returns; comparing correctly still requires decoding to bits
|
|
191
|
+
* per nibble (a naive per-CHARACTER diff would undercount — e.g. `0x0` vs
|
|
192
|
+
* `0xf` differ in ALL 4 bits of that nibble but are only "1 character" apart).
|
|
193
|
+
*/
|
|
194
|
+
export function hammingDistance(hexA, hexB) {
|
|
195
|
+
if (hexA.length !== hexB.length) {
|
|
196
|
+
throw new Error(`hammingDistance: hash length mismatch (${hexA.length} vs ${hexB.length}) — hashes must come from the same hash configuration.`);
|
|
197
|
+
}
|
|
198
|
+
const a = BigInt(`0x${hexA}`);
|
|
199
|
+
const b = BigInt(`0x${hexB}`);
|
|
200
|
+
let xor = a ^ b;
|
|
201
|
+
let count = 0;
|
|
202
|
+
while (xor > 0n) {
|
|
203
|
+
count += Number(xor & 1n);
|
|
204
|
+
xor >>= 1n;
|
|
205
|
+
}
|
|
206
|
+
return count;
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Group `entries` into near-duplicate clusters (AC5: pairs — generalized here
|
|
210
|
+
* to clusters, since 3+ near-identical renders is exactly the failure mode
|
|
211
|
+
* the AC's own example describes) and return the FLAT, deduplicated,
|
|
212
|
+
* sorted list of every path that landed in a cluster of size >= 2. A path
|
|
213
|
+
* with no near-duplicate (a singleton) is omitted entirely — `variantsIdentical`
|
|
214
|
+
* is "these paths render the same as some other path", not a roster of every
|
|
215
|
+
* file scanned.
|
|
216
|
+
*
|
|
217
|
+
* Clustering is transitive (union-find): if A is within tolerance of B, and B
|
|
218
|
+
* is within tolerance of C, all three land in ONE cluster even if A and C
|
|
219
|
+
* alone exceed tolerance — the natural reading of "these components all look
|
|
220
|
+
* the same" when three-plus near-duplicates form a chain rather than every
|
|
221
|
+
* pair being mutually within tolerance.
|
|
222
|
+
*
|
|
223
|
+
* ── Two-signal near-duplicate test (DRO-717) ─────────────────────────────────
|
|
224
|
+
* Two entries are unioned only when they are near-duplicates on BOTH signals:
|
|
225
|
+
* 1. blockhash Hamming distance <= `toleranceBits` (layout/luminance), AND
|
|
226
|
+
* 2. `colorDistanceL1` <= `colorToleranceL1` (hue) — the DRO-717 veto that
|
|
227
|
+
* stops the hue-blind blockhash from clustering a primary/danger/success
|
|
228
|
+
* button set (identical layout, distinct fills) as identical.
|
|
229
|
+
* The color check is a pure VETO: it can only PREVENT a union the blockhash
|
|
230
|
+
* would have made, never create one. It abstains (does not veto) when EITHER
|
|
231
|
+
* entry lacks a color signature — so a caller that supplies no colors (e.g. an
|
|
232
|
+
* arithmetic-only test) gets the exact pre-DRO-717 blockhash-only behavior.
|
|
233
|
+
*
|
|
234
|
+
* Why the veto doesn't cause false NEGATIVES (dropping a real duplicate): for a
|
|
235
|
+
* SAME-fill pair, the ink-color distance co-varies with the button's coverage,
|
|
236
|
+
* which co-varies with the blockhash. Two same-fill cards only drift apart in
|
|
237
|
+
* ink color once their rendered widths differ enough — and by then their
|
|
238
|
+
* blockhash has ALREADY diverged past `toleranceBits`, so the union was never on
|
|
239
|
+
* the table for the veto to remove (measured on real dark- and light-background
|
|
240
|
+
* renders: a same-fill pair reaching colorL1 ≈ 39 was already ~35 blockhash bits
|
|
241
|
+
* apart). The veto therefore only ever bites the case it targets — blockhash
|
|
242
|
+
* says "close" while hue says "different", i.e. the hue-blind false positive.
|
|
243
|
+
*
|
|
244
|
+
* O(n²) pairwise comparisons — acceptable for AC7's ~50-component budget (at
|
|
245
|
+
* most ~1225 comparisons, each a cheap XOR+popcount over a 256-bit BigInt plus
|
|
246
|
+
* a three-term color subtraction); would need a smarter index (e.g. an LSH
|
|
247
|
+
* bucket) at kit sizes multiple orders of magnitude larger than genie's target.
|
|
248
|
+
*/
|
|
249
|
+
export function findDuplicateClusters(entries, toleranceBits = DEFAULT_TOLERANCE_BITS, colorToleranceL1 = DEFAULT_COLOR_TOLERANCE_L1) {
|
|
250
|
+
const n = entries.length;
|
|
251
|
+
if (n < 2)
|
|
252
|
+
return [];
|
|
253
|
+
/**
|
|
254
|
+
* The DRO-717 hue veto: `true` when the two entries' fills are close enough
|
|
255
|
+
* to be "the same color". Abstains (returns `true`) when either entry has no
|
|
256
|
+
* color signature, so a color-less caller keeps blockhash-only behavior.
|
|
257
|
+
*/
|
|
258
|
+
function colorAgrees(a, b) {
|
|
259
|
+
if (!a.color || !b.color)
|
|
260
|
+
return true;
|
|
261
|
+
return colorDistanceL1(a.color, b.color) <= colorToleranceL1;
|
|
262
|
+
}
|
|
263
|
+
// Union-find over entry INDICES (not paths) so duplicate paths in the input
|
|
264
|
+
// can never collide as map keys.
|
|
265
|
+
const parent = Array.from({ length: n }, (_, i) => i);
|
|
266
|
+
function find(i) {
|
|
267
|
+
while (parent[i] !== i) {
|
|
268
|
+
parent[i] = parent[parent[i]]; // path-halving
|
|
269
|
+
i = parent[i];
|
|
270
|
+
}
|
|
271
|
+
return i;
|
|
272
|
+
}
|
|
273
|
+
function union(i, j) {
|
|
274
|
+
const ri = find(i);
|
|
275
|
+
const rj = find(j);
|
|
276
|
+
if (ri !== rj)
|
|
277
|
+
parent[ri] = rj;
|
|
278
|
+
}
|
|
279
|
+
for (let i = 0; i < n; i++) {
|
|
280
|
+
for (let j = i + 1; j < n; j++) {
|
|
281
|
+
const a = entries[i];
|
|
282
|
+
const b = entries[j];
|
|
283
|
+
if (hammingDistance(a.hash, b.hash) <= toleranceBits && colorAgrees(a, b)) {
|
|
284
|
+
union(i, j);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
// Group entry indices by cluster root; keep only clusters with >= 2 members.
|
|
289
|
+
const clusters = new Map();
|
|
290
|
+
for (let i = 0; i < n; i++) {
|
|
291
|
+
const root = find(i);
|
|
292
|
+
const members = clusters.get(root);
|
|
293
|
+
if (members)
|
|
294
|
+
members.push(i);
|
|
295
|
+
else
|
|
296
|
+
clusters.set(root, [i]);
|
|
297
|
+
}
|
|
298
|
+
const result = [];
|
|
299
|
+
for (const members of clusters.values()) {
|
|
300
|
+
if (members.length < 2)
|
|
301
|
+
continue;
|
|
302
|
+
for (const idx of members)
|
|
303
|
+
result.push(entries[idx].path);
|
|
304
|
+
}
|
|
305
|
+
return result.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
|
|
306
|
+
}
|
|
307
|
+
//# sourceMappingURL=phash.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"phash.js","sourceRoot":"","sources":["../../src/validate/phash.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/** One render's measurements: content height (AC4) + a decoded screenshot for
|
|
2
|
+
* perceptual hashing (AC5). */
|
|
3
|
+
export interface RenderedCard {
|
|
4
|
+
/** Union bounding-box height, in CSS px, of `document.body`'s direct
|
|
5
|
+
* children — the rendered content's real vertical extent. */
|
|
6
|
+
contentHeight: number;
|
|
7
|
+
/** Decoded RGBA screenshot, ready for `validate/phash.ts`'s `computePHash`. */
|
|
8
|
+
image: {
|
|
9
|
+
data: Uint8Array;
|
|
10
|
+
width: number;
|
|
11
|
+
height: number;
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* The render seam `full-scan.ts` depends on (DI, same pattern as `refine.ts`'s
|
|
16
|
+
* `RegionCropper`) — tests inject a stub so no browser is ever launched in the
|
|
17
|
+
* unit suite; production gets {@link createDefaultRenderer}'s real Playwright
|
|
18
|
+
* instance.
|
|
19
|
+
*/
|
|
20
|
+
export interface Renderer {
|
|
21
|
+
/** Render `html` at `viewport` and return its measurements. */
|
|
22
|
+
render(html: string, viewport: {
|
|
23
|
+
width: number;
|
|
24
|
+
height: number;
|
|
25
|
+
}): Promise<RenderedCard>;
|
|
26
|
+
/** Shut down the underlying browser. Idempotent-safe to call once per scan. */
|
|
27
|
+
close(): Promise<void>;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Create the default (production) {@link Renderer}: one headless Chromium
|
|
31
|
+
* instance (via lazy Playwright), reused across every `render()` call until
|
|
32
|
+
* `close()`. Returns `null` when Playwright cannot be loaded — the caller
|
|
33
|
+
* (`full-scan.ts`) degrades AC4/AC5 gracefully rather than failing the whole
|
|
34
|
+
* `validate` call over an environment gap (same posture as `refine.ts`'s AC7
|
|
35
|
+
* region crop).
|
|
36
|
+
*/
|
|
37
|
+
export declare function createDefaultRenderer(): Promise<Renderer | null>;
|
|
38
|
+
//# sourceMappingURL=render.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"render.d.ts","sourceRoot":"","sources":["../../src/validate/render.ts"],"names":[],"mappings":"AAkDA;+BAC+B;AAC/B,MAAM,WAAW,YAAY;IAC3B;iEAC6D;IAC7D,aAAa,EAAE,MAAM,CAAC;IACtB,+EAA+E;IAC/E,KAAK,EAAE;QAAE,IAAI,EAAE,UAAU,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;CAC5D;AAED;;;;;GAKG;AACH,MAAM,WAAW,QAAQ;IACvB,+DAA+D;IAC/D,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC;IACzF,+EAA+E;IAC/E,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AA8ED;;;;;;;GAOG;AACH,wBAAsB,qBAAqB,IAAI,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,CAgDtE"}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lazy-Playwright renderer for the M3-04 (DRO-260) `validate` full-scan facet.
|
|
3
|
+
*
|
|
4
|
+
* Renders a component preview's raw HTML in headless Chromium and reports two
|
|
5
|
+
* things per render: the rendered content's bounding-box height (AC4's "thin"
|
|
6
|
+
* check) and a decoded RGBA screenshot buffer (AC5's perceptual-hash input,
|
|
7
|
+
* via `validate/phash.ts`). One {@link Renderer} instance owns ONE browser
|
|
8
|
+
* process reused across every file in a scan — launching a fresh browser per
|
|
9
|
+
* file would blow AC7's < 5 s / 50-component budget on launch overhead alone.
|
|
10
|
+
*
|
|
11
|
+
* ── Playwright is an OPTIONAL peer dependency, same precedent as `refine.ts` ──
|
|
12
|
+
* `refine.ts`'s AC7 region-crop already established this pattern for genie:
|
|
13
|
+
* Playwright is not in `dependencies` (a real browser download is too heavy to
|
|
14
|
+
* force on every install), so it is resolved via a runtime, non-literal
|
|
15
|
+
* `import()` that degrades to `null` (never throws) when the package isn't
|
|
16
|
+
* installed. `createDefaultRenderer` here follows the exact same contract —
|
|
17
|
+
* `packages/server/package.json` DOES declare it this time (M3-04's own issue
|
|
18
|
+
* asks for it: "Reuse Playwright instance from M2-04's region cropper"), but
|
|
19
|
+
* the caller (`full-scan.ts`) still must not assume it is always resolvable
|
|
20
|
+
* (a stripped install, an offline `npm ci --omit=optional`-style environment,
|
|
21
|
+
* or a sandboxed CI runner missing the OS-level shared libraries Chromium
|
|
22
|
+
* itself needs — independent of whether the npm package is present).
|
|
23
|
+
*
|
|
24
|
+
* ── Degradation is logged, not silent (PR #152 review) ────────────────────────
|
|
25
|
+
* A `null` renderer makes the full-scan facet skip `thin`/`variantsIdentical`
|
|
26
|
+
* entirely, so its `{ thin: [], variantsIdentical: [] }` would otherwise be
|
|
27
|
+
* indistinguishable from a genuinely clean kit — neither an operator nor the
|
|
28
|
+
* model consuming `validate` could tell the checks were skipped for an
|
|
29
|
+
* environment reason rather than a real "nothing wrong" result. Both
|
|
30
|
+
* degradation paths below (import-missing, launch-failure) emit a structured
|
|
31
|
+
* stderr line via the same `logStderr` (`llm/component-response.ts`) `refine.ts`
|
|
32
|
+
* uses for its own region-cropper degradation — stderr, never stdout, since
|
|
33
|
+
* stdout is the stdio transport's JSON-RPC framing.
|
|
34
|
+
*
|
|
35
|
+
* ── Bare-fragment previews render in quirks mode — measure children, not body ──
|
|
36
|
+
* A genie `<Name>.html` preview is a bare fragment: `<!-- @genie … -->`
|
|
37
|
+
* followed directly by markup, with NO `<!DOCTYPE html>`/`<html>`/`<body>`
|
|
38
|
+
* wrapper (confirmed against `validate/marker.test.ts`'s own fixtures and
|
|
39
|
+
* `refine.test.ts`'s component fixtures). Loaded via `page.setContent`, that
|
|
40
|
+
* makes the page quirks-mode, where `document.body` stretches to fill the
|
|
41
|
+
* viewport regardless of content (`document.body.scrollHeight` reports the
|
|
42
|
+
* VIEWPORT height, not the content's real extent — verified empirically).
|
|
43
|
+
* `measureContentHeight` instead unions the bounding boxes of `body`'s direct
|
|
44
|
+
* children (skipping any zero-area element, e.g. a `display:none` node) —
|
|
45
|
+
* this reports the actual rendered content extent independent of quirks-mode
|
|
46
|
+
* body stretching, and needs no synthetic doctype/wrapper injection that
|
|
47
|
+
* could itself perturb the very layout AC4 is trying to measure.
|
|
48
|
+
*/
|
|
49
|
+
import { logStderr } from "../llm/component-response.js";
|
|
50
|
+
/** Load Playwright at runtime, or `null` if it is not installed / its OS-level
|
|
51
|
+
* dependencies are missing. The non-literal specifier keeps `tsc` from trying
|
|
52
|
+
* to resolve types for it at build time (mirrors `refine.ts`'s
|
|
53
|
+
* `importPlaywright`, kept as a separate copy here rather than a shared
|
|
54
|
+
* export — the two call sites have no other coupling, and duplicating one
|
|
55
|
+
* seven-line try/catch is cheaper than introducing a cross-tool dependency
|
|
56
|
+
* for it). */
|
|
57
|
+
async function importPlaywright() {
|
|
58
|
+
const specifier = "playwright";
|
|
59
|
+
try {
|
|
60
|
+
return (await import(specifier));
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
// Logged (not silent, PR #152 review) — see the module doc's "Degradation
|
|
64
|
+
// is logged, not silent" section for why an operator/model needs this
|
|
65
|
+
// signal. Same event-naming convention as `refine.ts`'s own
|
|
66
|
+
// `refine.region.unavailable` for the identical Playwright-absent case.
|
|
67
|
+
logStderr({
|
|
68
|
+
event: "validate.render.unavailable",
|
|
69
|
+
reason: "playwright-not-installed",
|
|
70
|
+
error: String(error),
|
|
71
|
+
});
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* The script `measureContentHeight` evaluates in-page (AC4). Returns the union
|
|
77
|
+
* bounding-box height of `document.body`'s direct children, skipping any
|
|
78
|
+
* zero-area element (`display:none`, a collapsed empty node, …) — see the
|
|
79
|
+
* module doc for why this measures real content instead of quirks-mode body
|
|
80
|
+
* stretch. A `<Name>.html` with literally no rendered content (a marker line
|
|
81
|
+
* with nothing after it) correctly yields `0`.
|
|
82
|
+
*/
|
|
83
|
+
const MEASURE_CONTENT_HEIGHT_SCRIPT = `(() => {
|
|
84
|
+
const children = Array.from(document.body.children);
|
|
85
|
+
if (children.length === 0) return 0;
|
|
86
|
+
let top = Infinity;
|
|
87
|
+
let bottom = -Infinity;
|
|
88
|
+
for (const el of children) {
|
|
89
|
+
const rect = el.getBoundingClientRect();
|
|
90
|
+
if (rect.width === 0 && rect.height === 0) continue;
|
|
91
|
+
top = Math.min(top, rect.top);
|
|
92
|
+
bottom = Math.max(bottom, rect.bottom);
|
|
93
|
+
}
|
|
94
|
+
if (!isFinite(top)) return 0;
|
|
95
|
+
return bottom - top;
|
|
96
|
+
})()`;
|
|
97
|
+
/**
|
|
98
|
+
* Create the default (production) {@link Renderer}: one headless Chromium
|
|
99
|
+
* instance (via lazy Playwright), reused across every `render()` call until
|
|
100
|
+
* `close()`. Returns `null` when Playwright cannot be loaded — the caller
|
|
101
|
+
* (`full-scan.ts`) degrades AC4/AC5 gracefully rather than failing the whole
|
|
102
|
+
* `validate` call over an environment gap (same posture as `refine.ts`'s AC7
|
|
103
|
+
* region crop).
|
|
104
|
+
*/
|
|
105
|
+
export async function createDefaultRenderer() {
|
|
106
|
+
const pw = await importPlaywright();
|
|
107
|
+
if (!pw)
|
|
108
|
+
return null;
|
|
109
|
+
let browser;
|
|
110
|
+
try {
|
|
111
|
+
// `--no-sandbox` so this runs in restricted CI/container environments —
|
|
112
|
+
// the same flag `refine.ts`'s `defaultRegionCropper` launches with.
|
|
113
|
+
browser = await pw.chromium.launch({ args: ["--no-sandbox"] });
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
// A Chromium binary that fails to LAUNCH (missing OS-level shared
|
|
117
|
+
// libraries, no downloaded browser, …) is the same "can't render" signal
|
|
118
|
+
// as Playwright not being installed at all — degrade the same way rather
|
|
119
|
+
// than letting a launch failure crash the whole `validate` call. Logged
|
|
120
|
+
// (not silent, PR #152 review) — same reasoning as `importPlaywright`'s
|
|
121
|
+
// catch above.
|
|
122
|
+
logStderr({
|
|
123
|
+
event: "validate.render.unavailable",
|
|
124
|
+
reason: "chromium-launch-failed",
|
|
125
|
+
error: String(error),
|
|
126
|
+
});
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
return {
|
|
130
|
+
async render(html, viewport) {
|
|
131
|
+
const page = await browser.newPage({ viewport });
|
|
132
|
+
try {
|
|
133
|
+
// `load`, not `networkidle`: an embedded-CSP preview
|
|
134
|
+
// (`default-src 'none'`) has no network to go idle on — see
|
|
135
|
+
// `refine.ts`'s identical reasoning for its own `setContent` call.
|
|
136
|
+
await page.setContent(html, { waitUntil: "load" });
|
|
137
|
+
const contentHeight = (await page.evaluate(MEASURE_CONTENT_HEIGHT_SCRIPT));
|
|
138
|
+
const buffer = await page.screenshot({ type: "png" });
|
|
139
|
+
const { PNG } = await import("pngjs");
|
|
140
|
+
const png = PNG.sync.read(buffer);
|
|
141
|
+
return {
|
|
142
|
+
contentHeight,
|
|
143
|
+
image: { data: new Uint8Array(png.data), width: png.width, height: png.height },
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
finally {
|
|
147
|
+
await page.close();
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
async close() {
|
|
151
|
+
await browser.close();
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
//# sourceMappingURL=render.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"render.js","sourceRoot":"","sources":["../../src/validate/render.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+CG;AACH,OAAO,EAAE,SAAS,EAAE,MAAM,8BAA8B,CAAC;AAqDzD;;;;;;cAMc;AACd,KAAK,UAAU,gBAAgB;IAC7B,MAAM,SAAS,GAAG,YAAY,CAAC;IAC/B,IAAI,CAAC;QACH,OAAO,CAAC,MAAM,MAAM,CAAC,SAAS,CAAC,CAAqB,CAAC;IACvD,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,0EAA0E;QAC1E,sEAAsE;QACtE,4DAA4D;QAC5D,wEAAwE;QACxE,SAAS,CAAC;YACR,KAAK,EAAE,6BAA6B;YACpC,MAAM,EAAE,0BAA0B;YAClC,KAAK,EAAE,MAAM,CAAC,KAAK,CAAC;SACrB,CAAC,CAAC;QACH,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,6BAA6B,GAAG;;;;;;;;;;;;;KAajC,CAAC;AAEN;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,qBAAqB;IACzC,MAAM,EAAE,GAAG,MAAM,gBAAgB,EAAE,CAAC;IACpC,IAAI,CAAC,EAAE;QAAE,OAAO,IAAI,CAAC;IAErB,IAAI,OAAkB,CAAC;IACvB,IAAI,CAAC;QACH,wEAAwE;QACxE,oEAAoE;QACpE,OAAO,GAAG,MAAM,EAAE,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,CAAC,cAAc,CAAC,EAAE,CAAC,CAAC;IACjE,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,kEAAkE;QAClE,yEAAyE;QACzE,yEAAyE;QACzE,wEAAwE;QACxE,wEAAwE;QACxE,eAAe;QACf,SAAS,CAAC;YACR,KAAK,EAAE,6BAA6B;YACpC,MAAM,EAAE,wBAAwB;YAChC,KAAK,EAAE,MAAM,CAAC,KAAK,CAAC;SACrB,CAAC,CAAC;QACH,OAAO,IAAI,CAAC;IACd,CAAC;IAED,OAAO;QACL,KAAK,CAAC,MAAM,CAAC,IAAI,EAAE,QAAQ;YACzB,MAAM,IAAI,GAAG,MAAM,OAAO,CAAC,OAAO,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC;YACjD,IAAI,CAAC;gBACH,qDAAqD;gBACrD,4DAA4D;gBAC5D,mEAAmE;gBACnE,MAAM,IAAI,CAAC,UAAU,CAAC,IAAI,EAAE,EAAE,SAAS,EAAE,MAAM,EAAE,CAAC,CAAC;gBACnD,MAAM,aAAa,GAAG,CAAC,MAAM,IAAI,CAAC,QAAQ,CAAC,6BAA6B,CAAC,CAAW,CAAC;gBACrF,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,UAAU,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;gBACtD,MAAM,EAAE,GAAG,EAAE,GAAG,MAAM,MAAM,CAAC,OAAO,CAAC,CAAC;gBACtC,MAAM,GAAG,GAAG,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;gBAClC,OAAO;oBACL,aAAa;oBACb,KAAK,EAAE,EAAE,IAAI,EAAE,IAAI,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE;iBAChF,CAAC;YACJ,CAAC;oBAAS,CAAC;gBACT,MAAM,IAAI,CAAC,KAAK,EAAE,CAAC;YACrB,CAAC;QACH,CAAC;QACD,KAAK,CAAC,KAAK;YACT,MAAM,OAAO,CAAC,KAAK,EAAE,CAAC;QACxB,CAAC;KACF,CAAC;AACJ,CAAC"}
|