@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.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +184 -0
- package/hooks/_hook-emit.js +81 -0
- package/hooks/gdd-bash-guard.js +8 -0
- package/hooks/gdd-decision-injector.js +2 -0
- package/hooks/gdd-protected-paths.js +8 -0
- package/hooks/gdd-trajectory-capture.js +64 -0
- package/hooks/hooks.json +9 -0
- package/package.json +7 -2
- package/reference/output-contracts/planner-decision.schema.json +94 -0
- package/reference/output-contracts/verifier-decision.schema.json +66 -0
- package/scripts/cli/gdd-events.mjs +283 -0
- package/scripts/lib/audit-aggregator/index.cjs +219 -0
- package/scripts/lib/connection-probe/index.cjs +263 -0
- package/scripts/lib/design-solidify.mjs +265 -0
- package/scripts/lib/design-tokens/_js-harness.cjs +66 -0
- package/scripts/lib/design-tokens/css-vars.cjs +55 -0
- package/scripts/lib/design-tokens/figma.cjs +121 -0
- package/scripts/lib/design-tokens/index.cjs +100 -0
- package/scripts/lib/design-tokens/js-const.cjs +107 -0
- package/scripts/lib/design-tokens/tailwind.cjs +98 -0
- package/scripts/lib/domain-primitives/anti-patterns.cjs +66 -0
- package/scripts/lib/domain-primitives/nng.cjs +136 -0
- package/scripts/lib/domain-primitives/wcag.cjs +166 -0
- package/scripts/lib/event-chain.cjs +177 -0
- package/scripts/lib/event-stream/index.ts +20 -0
- package/scripts/lib/event-stream/reader.ts +139 -0
- package/scripts/lib/event-stream/types.ts +155 -1
- package/scripts/lib/event-stream/writer.ts +65 -8
- package/scripts/lib/parse-contract.cjs +168 -0
- package/scripts/lib/redact.cjs +122 -0
- package/scripts/lib/reference-resolver.cjs +184 -0
- package/scripts/lib/touches-analyzer/index.cjs +201 -0
- package/scripts/lib/touches-pattern-miner.cjs +195 -0
- package/scripts/lib/trajectory/index.cjs +126 -0
- package/scripts/lib/transports/ws.cjs +179 -0
- package/scripts/lib/visual-baseline/diff.cjs +137 -0
- 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
|
+
};
|