@hegemonart/get-design-done 1.21.0 → 1.23.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 (39) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +184 -0
  4. package/hooks/_hook-emit.js +81 -0
  5. package/hooks/gdd-bash-guard.js +8 -0
  6. package/hooks/gdd-decision-injector.js +2 -0
  7. package/hooks/gdd-protected-paths.js +8 -0
  8. package/hooks/gdd-trajectory-capture.js +64 -0
  9. package/hooks/hooks.json +9 -0
  10. package/package.json +7 -2
  11. package/reference/output-contracts/planner-decision.schema.json +94 -0
  12. package/reference/output-contracts/verifier-decision.schema.json +66 -0
  13. package/scripts/cli/gdd-events.mjs +283 -0
  14. package/scripts/lib/audit-aggregator/index.cjs +219 -0
  15. package/scripts/lib/connection-probe/index.cjs +263 -0
  16. package/scripts/lib/design-solidify.mjs +265 -0
  17. package/scripts/lib/design-tokens/_js-harness.cjs +66 -0
  18. package/scripts/lib/design-tokens/css-vars.cjs +55 -0
  19. package/scripts/lib/design-tokens/figma.cjs +121 -0
  20. package/scripts/lib/design-tokens/index.cjs +100 -0
  21. package/scripts/lib/design-tokens/js-const.cjs +107 -0
  22. package/scripts/lib/design-tokens/tailwind.cjs +98 -0
  23. package/scripts/lib/domain-primitives/anti-patterns.cjs +66 -0
  24. package/scripts/lib/domain-primitives/nng.cjs +136 -0
  25. package/scripts/lib/domain-primitives/wcag.cjs +166 -0
  26. package/scripts/lib/event-chain.cjs +177 -0
  27. package/scripts/lib/event-stream/index.ts +20 -0
  28. package/scripts/lib/event-stream/reader.ts +139 -0
  29. package/scripts/lib/event-stream/types.ts +155 -1
  30. package/scripts/lib/event-stream/writer.ts +65 -8
  31. package/scripts/lib/parse-contract.cjs +168 -0
  32. package/scripts/lib/redact.cjs +122 -0
  33. package/scripts/lib/reference-resolver.cjs +184 -0
  34. package/scripts/lib/touches-analyzer/index.cjs +201 -0
  35. package/scripts/lib/touches-pattern-miner.cjs +195 -0
  36. package/scripts/lib/trajectory/index.cjs +126 -0
  37. package/scripts/lib/transports/ws.cjs +179 -0
  38. package/scripts/lib/visual-baseline/diff.cjs +137 -0
  39. package/scripts/lib/visual-baseline/index.cjs +139 -0
@@ -0,0 +1,137 @@
1
+ /**
2
+ * visual-baseline/diff.cjs — pixel-diff primitive (Plan 23-07).
3
+ *
4
+ * Compares two PNG buffers. With `pngjs` installed (probeOptional),
5
+ * decodes both and counts pixels whose R/G/B/A channels differ beyond
6
+ * the tolerance. Without `pngjs`, falls back to bytewise equality
7
+ * (SHA-256 hash compare) — ratio is `equal ? 0 : 1`.
8
+ *
9
+ * Dimension mismatch in pixel mode → ratio=1, drifted=true,
10
+ * reason='dimension-mismatch'. Never throws on shape mismatch.
11
+ */
12
+
13
+ 'use strict';
14
+
15
+ const { createHash } = require('node:crypto');
16
+ const { probeOptional } = require('../probe-optional.cjs');
17
+
18
+ const _pngjs = probeOptional('pngjs');
19
+
20
+ const DEFAULT_THRESHOLD = 0.005;
21
+ const DEFAULT_TOLERANCE = 4;
22
+
23
+ /**
24
+ * @typedef {Object} DiffResult
25
+ * @property {boolean} drifted
26
+ * @property {number} ratio
27
+ * @property {number} diffPixels
28
+ * @property {number} totalPixels
29
+ * @property {'pixel'|'bytewise'} mode
30
+ * @property {string} [reason]
31
+ */
32
+
33
+ function bytewiseDiff(a, b, threshold) {
34
+ const ha = createHash('sha256').update(a).digest('hex');
35
+ const hb = createHash('sha256').update(b).digest('hex');
36
+ const equal = ha === hb;
37
+ const ratio = equal ? 0 : 1;
38
+ return {
39
+ drifted: ratio > threshold,
40
+ ratio,
41
+ diffPixels: equal ? 0 : 1,
42
+ totalPixels: 1,
43
+ mode: 'bytewise',
44
+ reason: 'pngjs-not-available',
45
+ };
46
+ }
47
+
48
+ function pixelDiff(a, b, threshold, tolerance) {
49
+ const { PNG } = _pngjs;
50
+ let pa;
51
+ let pb;
52
+ try {
53
+ pa = PNG.sync.read(a);
54
+ } catch (err) {
55
+ return {
56
+ drifted: true,
57
+ ratio: 1,
58
+ diffPixels: 0,
59
+ totalPixels: 0,
60
+ mode: 'pixel',
61
+ reason: `decode-a-failed: ${err && err.message ? err.message : String(err)}`,
62
+ };
63
+ }
64
+ try {
65
+ pb = PNG.sync.read(b);
66
+ } catch (err) {
67
+ return {
68
+ drifted: true,
69
+ ratio: 1,
70
+ diffPixels: 0,
71
+ totalPixels: 0,
72
+ mode: 'pixel',
73
+ reason: `decode-b-failed: ${err && err.message ? err.message : String(err)}`,
74
+ };
75
+ }
76
+ if (pa.width !== pb.width || pa.height !== pb.height) {
77
+ return {
78
+ drifted: true,
79
+ ratio: 1,
80
+ diffPixels: 0,
81
+ totalPixels: 0,
82
+ mode: 'pixel',
83
+ reason: 'dimension-mismatch',
84
+ };
85
+ }
86
+ const total = pa.width * pa.height;
87
+ const A = pa.data;
88
+ const B = pb.data;
89
+ let diffPx = 0;
90
+ for (let i = 0; i < A.length; i += 4) {
91
+ const dr = Math.abs(A[i] - B[i]);
92
+ const dg = Math.abs(A[i + 1] - B[i + 1]);
93
+ const db = Math.abs(A[i + 2] - B[i + 2]);
94
+ const da = Math.abs(A[i + 3] - B[i + 3]);
95
+ if (dr > tolerance || dg > tolerance || db > tolerance || da > tolerance) {
96
+ diffPx += 1;
97
+ }
98
+ }
99
+ const ratio = total > 0 ? diffPx / total : 0;
100
+ return {
101
+ drifted: ratio > threshold,
102
+ ratio,
103
+ diffPixels: diffPx,
104
+ totalPixels: total,
105
+ mode: 'pixel',
106
+ };
107
+ }
108
+
109
+ /**
110
+ * Compare two PNG buffers.
111
+ *
112
+ * @param {Buffer} a
113
+ * @param {Buffer} b
114
+ * @param {{threshold?: number, tolerance?: number}} [opts]
115
+ * @returns {DiffResult}
116
+ */
117
+ function diff(a, b, opts = {}) {
118
+ if (!Buffer.isBuffer(a) || !Buffer.isBuffer(b)) {
119
+ throw new TypeError('visual-baseline/diff: both inputs must be Buffers');
120
+ }
121
+ const threshold = typeof opts.threshold === 'number' ? opts.threshold : DEFAULT_THRESHOLD;
122
+ const tolerance = typeof opts.tolerance === 'number' ? opts.tolerance : DEFAULT_TOLERANCE;
123
+ if (!_pngjs) return bytewiseDiff(a, b, threshold);
124
+ return pixelDiff(a, b, threshold, tolerance);
125
+ }
126
+
127
+ /** Test-only: report whether pngjs is available. */
128
+ function pngjsAvailable() {
129
+ return _pngjs !== null && _pngjs !== undefined;
130
+ }
131
+
132
+ module.exports = {
133
+ diff,
134
+ pngjsAvailable,
135
+ DEFAULT_THRESHOLD,
136
+ DEFAULT_TOLERANCE,
137
+ };
@@ -0,0 +1,139 @@
1
+ /**
2
+ * visual-baseline/index.cjs — baseline manager for PNG drift checks
3
+ * (Plan 23-07).
4
+ *
5
+ * Reads/writes `.design/baselines/<key>.png`. Compare delegates to
6
+ * `./diff.cjs#diff`. Atomic write via `.tmp` sibling + rename.
7
+ *
8
+ * Defers Playwright/Preview MCP screenshot capture orchestration to a
9
+ * later phase — this module only handles "given a PNG buffer, compare it
10
+ * / save it as the baseline".
11
+ */
12
+
13
+ 'use strict';
14
+
15
+ const fs = require('node:fs');
16
+ const path = require('node:path');
17
+
18
+ const { diff, DEFAULT_THRESHOLD, DEFAULT_TOLERANCE } = require('./diff.cjs');
19
+
20
+ const DEFAULT_BASELINE_DIR = '.design/baselines';
21
+ const SAFE_KEY_RE = /^[a-z0-9][a-z0-9._-]{0,127}$/i;
22
+
23
+ /**
24
+ * @typedef {Object} CompareOutcome
25
+ * @property {boolean} drifted
26
+ * @property {number} ratio
27
+ * @property {boolean} baselineExists
28
+ * @property {string} baselinePath
29
+ * @property {'pixel'|'bytewise'|'absent'} mode
30
+ * @property {number} [diffPixels]
31
+ * @property {number} [totalPixels]
32
+ * @property {string} [reason]
33
+ */
34
+
35
+ /**
36
+ * Validate a baseline key. Rejects path separators, '..', and unsafe
37
+ * characters that could traverse outside the baseline dir.
38
+ *
39
+ * @param {string} key
40
+ * @returns {string}
41
+ */
42
+ function validateKey(key) {
43
+ if (typeof key !== 'string' || key.length === 0) {
44
+ throw new TypeError('visual-baseline: key must be a non-empty string');
45
+ }
46
+ if (key.includes('..') || key.includes('/') || key.includes('\\')) {
47
+ throw new RangeError(
48
+ `visual-baseline: key "${key}" contains illegal characters (/, \\, ..)`,
49
+ );
50
+ }
51
+ if (!SAFE_KEY_RE.test(key)) {
52
+ throw new RangeError(
53
+ `visual-baseline: key "${key}" must match /^[a-z0-9][a-z0-9._-]{0,127}$/i`,
54
+ );
55
+ }
56
+ return key;
57
+ }
58
+
59
+ /**
60
+ * Resolve baseline file path.
61
+ *
62
+ * @param {string} key
63
+ * @param {{cwd?: string, baselineDir?: string}} [opts]
64
+ * @returns {string}
65
+ */
66
+ function baselinePathFor(key, opts = {}) {
67
+ validateKey(key);
68
+ const cwd = opts.cwd ?? process.cwd();
69
+ const dir = opts.baselineDir ?? DEFAULT_BASELINE_DIR;
70
+ const root = path.isAbsolute(dir) ? dir : path.join(cwd, dir);
71
+ return path.join(root, `${key}.png`);
72
+ }
73
+
74
+ /**
75
+ * Compare a PNG buffer to the on-disk baseline.
76
+ *
77
+ * @param {string} key
78
+ * @param {Buffer} pngBuffer
79
+ * @param {{cwd?: string, threshold?: number, tolerance?: number, baselineDir?: string}} [opts]
80
+ * @returns {CompareOutcome}
81
+ */
82
+ function compareToBaseline(key, pngBuffer, opts = {}) {
83
+ if (!Buffer.isBuffer(pngBuffer)) {
84
+ throw new TypeError('visual-baseline: pngBuffer must be a Buffer');
85
+ }
86
+ const baselinePath = baselinePathFor(key, opts);
87
+ if (!fs.existsSync(baselinePath)) {
88
+ return {
89
+ drifted: true,
90
+ ratio: NaN,
91
+ baselineExists: false,
92
+ baselinePath,
93
+ mode: 'absent',
94
+ reason: 'baseline-not-found',
95
+ };
96
+ }
97
+ const a = fs.readFileSync(baselinePath);
98
+ const r = diff(a, pngBuffer, opts);
99
+ return {
100
+ drifted: r.drifted,
101
+ ratio: r.ratio,
102
+ baselineExists: true,
103
+ baselinePath,
104
+ mode: r.mode,
105
+ diffPixels: r.diffPixels,
106
+ totalPixels: r.totalPixels,
107
+ reason: r.reason,
108
+ };
109
+ }
110
+
111
+ /**
112
+ * Persist a PNG buffer as the baseline. Atomic write (.tmp + rename).
113
+ *
114
+ * @param {string} key
115
+ * @param {Buffer} pngBuffer
116
+ * @param {{cwd?: string, baselineDir?: string}} [opts]
117
+ * @returns {string} absolute path written
118
+ */
119
+ function applyBaseline(key, pngBuffer, opts = {}) {
120
+ if (!Buffer.isBuffer(pngBuffer)) {
121
+ throw new TypeError('visual-baseline: pngBuffer must be a Buffer');
122
+ }
123
+ const out = baselinePathFor(key, opts);
124
+ fs.mkdirSync(path.dirname(out), { recursive: true });
125
+ const tmp = out + '.tmp';
126
+ fs.writeFileSync(tmp, pngBuffer);
127
+ fs.renameSync(tmp, out);
128
+ return out;
129
+ }
130
+
131
+ module.exports = {
132
+ compareToBaseline,
133
+ applyBaseline,
134
+ baselinePathFor,
135
+ validateKey,
136
+ DEFAULT_BASELINE_DIR,
137
+ DEFAULT_THRESHOLD,
138
+ DEFAULT_TOLERANCE,
139
+ };