@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,263 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* connection-probe/index.cjs — code-level connection liveness probe
|
|
3
|
+
* (Plan 22-08).
|
|
4
|
+
*
|
|
5
|
+
* Replaces today's per-connection ad-hoc probe bash snippets in
|
|
6
|
+
* `connections/` with one typed primitive. Used by Phase 21
|
|
7
|
+
* pipeline-runner and Phase 22 reflector.
|
|
8
|
+
*
|
|
9
|
+
* Contract:
|
|
10
|
+
* probe({
|
|
11
|
+
* name: 'figma' | 'pinterest' | … // free-form connection id
|
|
12
|
+
* cmd: async () => boolean | Truthy // probe action
|
|
13
|
+
* timeout: number ms // default 5000
|
|
14
|
+
* retries: number // default 3 attempts total
|
|
15
|
+
* fallback: async () => unknown // optional degraded path
|
|
16
|
+
* }) → {
|
|
17
|
+
* status: 'ok' | 'degraded' | 'down'
|
|
18
|
+
* latency_ms: number
|
|
19
|
+
* attempts: number
|
|
20
|
+
* fallback_used: boolean
|
|
21
|
+
* error?: string // last error message if any
|
|
22
|
+
* }
|
|
23
|
+
*
|
|
24
|
+
* State persistence:
|
|
25
|
+
* * `.design/telemetry/connection-state.json` records `{name → status}`
|
|
26
|
+
* across runs.
|
|
27
|
+
* * On every probe, if the new status differs from cached, emit a
|
|
28
|
+
* `connection.status_change` event via the event-stream bus and
|
|
29
|
+
* overwrite the cached value atomically (write to .tmp + rename).
|
|
30
|
+
*
|
|
31
|
+
* Backoff:
|
|
32
|
+
* * Uses `jittered-backoff.cjs` — `delayMs(attempt)` between retries.
|
|
33
|
+
*
|
|
34
|
+
* The probe `cmd` is awaited with a Promise.race against a timeout. On
|
|
35
|
+
* fulfilment with truthy → ok. On rejection or falsy → fail-this-attempt;
|
|
36
|
+
* retry until exhausted. After full-fail, if `fallback` is supplied,
|
|
37
|
+
* runs it and reports `degraded` + `fallback_used: true`.
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
'use strict';
|
|
41
|
+
|
|
42
|
+
const { writeFileSync, readFileSync, existsSync, mkdirSync, renameSync } = require('node:fs');
|
|
43
|
+
const { dirname, isAbsolute, resolve, join } = require('node:path');
|
|
44
|
+
|
|
45
|
+
const { delayMs } = require('../jittered-backoff.cjs');
|
|
46
|
+
|
|
47
|
+
const DEFAULT_STATE_PATH = '.design/telemetry/connection-state.json';
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Resolve the connection-state file path against a base dir.
|
|
51
|
+
* @param {{baseDir?: string, statePath?: string}} [opts]
|
|
52
|
+
*/
|
|
53
|
+
function statePathFor(opts = {}) {
|
|
54
|
+
const raw = opts.statePath ?? DEFAULT_STATE_PATH;
|
|
55
|
+
if (isAbsolute(raw)) return raw;
|
|
56
|
+
return resolve(opts.baseDir ?? process.cwd(), raw);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Load + return the cached state object (or `{}` if absent / corrupt).
|
|
61
|
+
* @param {string} path
|
|
62
|
+
* @returns {Record<string, string>}
|
|
63
|
+
*/
|
|
64
|
+
function loadState(path) {
|
|
65
|
+
if (!existsSync(path)) return {};
|
|
66
|
+
try {
|
|
67
|
+
return JSON.parse(readFileSync(path, 'utf8'));
|
|
68
|
+
} catch {
|
|
69
|
+
return {};
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Atomic state write: write to `.tmp` sibling, rename. Renames are
|
|
75
|
+
* atomic on POSIX and at least crash-safe on Windows for same-volume
|
|
76
|
+
* targets.
|
|
77
|
+
* @param {string} path
|
|
78
|
+
* @param {Record<string, string>} state
|
|
79
|
+
*/
|
|
80
|
+
function saveState(path, state) {
|
|
81
|
+
try {
|
|
82
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
83
|
+
const tmp = path + '.tmp';
|
|
84
|
+
writeFileSync(tmp, JSON.stringify(state, null, 2));
|
|
85
|
+
renameSync(tmp, path);
|
|
86
|
+
} catch (err) {
|
|
87
|
+
try {
|
|
88
|
+
process.stderr.write(
|
|
89
|
+
`[connection-probe] state write failed: ${err && err.message ? err.message : String(err)}\n`,
|
|
90
|
+
);
|
|
91
|
+
} catch {
|
|
92
|
+
/* swallow */
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Race `promise` against a timeout. Rejects with `TimeoutError` after
|
|
99
|
+
* `ms` if the promise hasn't settled.
|
|
100
|
+
*
|
|
101
|
+
* @template T
|
|
102
|
+
* @param {Promise<T>} promise
|
|
103
|
+
* @param {number} ms
|
|
104
|
+
* @returns {Promise<T>}
|
|
105
|
+
*/
|
|
106
|
+
function withTimeout(promise, ms) {
|
|
107
|
+
return new Promise((resolve, reject) => {
|
|
108
|
+
const timer = setTimeout(() => {
|
|
109
|
+
const err = new Error(`probe timed out after ${ms}ms`);
|
|
110
|
+
err.code = 'PROBE_TIMEOUT';
|
|
111
|
+
reject(err);
|
|
112
|
+
}, ms);
|
|
113
|
+
promise.then(
|
|
114
|
+
(v) => {
|
|
115
|
+
clearTimeout(timer);
|
|
116
|
+
resolve(v);
|
|
117
|
+
},
|
|
118
|
+
(e) => {
|
|
119
|
+
clearTimeout(timer);
|
|
120
|
+
reject(e);
|
|
121
|
+
},
|
|
122
|
+
);
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Run the probe with retries + optional fallback. Resolves to a
|
|
128
|
+
* structured outcome; never rejects.
|
|
129
|
+
*
|
|
130
|
+
* @param {{
|
|
131
|
+
* name: string,
|
|
132
|
+
* cmd: () => Promise<unknown>,
|
|
133
|
+
* timeout?: number,
|
|
134
|
+
* retries?: number,
|
|
135
|
+
* fallback?: () => Promise<unknown>,
|
|
136
|
+
* baseDir?: string,
|
|
137
|
+
* statePath?: string,
|
|
138
|
+
* emit?: (ev: unknown) => void,
|
|
139
|
+
* }} opts
|
|
140
|
+
* @returns {Promise<{
|
|
141
|
+
* status: 'ok' | 'degraded' | 'down',
|
|
142
|
+
* latency_ms: number,
|
|
143
|
+
* attempts: number,
|
|
144
|
+
* fallback_used: boolean,
|
|
145
|
+
* error?: string,
|
|
146
|
+
* }>}
|
|
147
|
+
*/
|
|
148
|
+
async function probe(opts) {
|
|
149
|
+
if (!opts || typeof opts.name !== 'string' || opts.name.length === 0) {
|
|
150
|
+
throw new TypeError('probe: name (string) required');
|
|
151
|
+
}
|
|
152
|
+
if (typeof opts.cmd !== 'function') {
|
|
153
|
+
throw new TypeError('probe: cmd (async fn) required');
|
|
154
|
+
}
|
|
155
|
+
const timeout = opts.timeout ?? 5000;
|
|
156
|
+
const retries = Math.max(1, opts.retries ?? 3);
|
|
157
|
+
const path = statePathFor(opts);
|
|
158
|
+
|
|
159
|
+
const start = Date.now();
|
|
160
|
+
let attempts = 0;
|
|
161
|
+
let lastError;
|
|
162
|
+
|
|
163
|
+
for (let i = 0; i < retries; i++) {
|
|
164
|
+
attempts += 1;
|
|
165
|
+
try {
|
|
166
|
+
const result = await withTimeout(Promise.resolve(opts.cmd()), timeout);
|
|
167
|
+
if (result) {
|
|
168
|
+
const outcome = {
|
|
169
|
+
status: /** @type {'ok'} */ ('ok'),
|
|
170
|
+
latency_ms: Date.now() - start,
|
|
171
|
+
attempts,
|
|
172
|
+
fallback_used: false,
|
|
173
|
+
};
|
|
174
|
+
await recordTransition(opts.name, outcome.status, path, opts.emit);
|
|
175
|
+
return outcome;
|
|
176
|
+
}
|
|
177
|
+
// falsy = soft-fail; retry
|
|
178
|
+
lastError = new Error('probe returned falsy');
|
|
179
|
+
} catch (err) {
|
|
180
|
+
lastError = err;
|
|
181
|
+
}
|
|
182
|
+
if (i < retries - 1) {
|
|
183
|
+
await sleep(delayMs(i));
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// All retries failed. Try fallback if supplied.
|
|
188
|
+
if (typeof opts.fallback === 'function') {
|
|
189
|
+
try {
|
|
190
|
+
await opts.fallback();
|
|
191
|
+
const outcome = {
|
|
192
|
+
status: /** @type {'degraded'} */ ('degraded'),
|
|
193
|
+
latency_ms: Date.now() - start,
|
|
194
|
+
attempts,
|
|
195
|
+
fallback_used: true,
|
|
196
|
+
error: lastError && lastError.message ? lastError.message : String(lastError),
|
|
197
|
+
};
|
|
198
|
+
await recordTransition(opts.name, outcome.status, path, opts.emit);
|
|
199
|
+
return outcome;
|
|
200
|
+
} catch {
|
|
201
|
+
/* fall through to down */
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const outcome = {
|
|
206
|
+
status: /** @type {'down'} */ ('down'),
|
|
207
|
+
latency_ms: Date.now() - start,
|
|
208
|
+
attempts,
|
|
209
|
+
fallback_used: false,
|
|
210
|
+
error: lastError && lastError.message ? lastError.message : String(lastError),
|
|
211
|
+
};
|
|
212
|
+
await recordTransition(opts.name, outcome.status, path, opts.emit);
|
|
213
|
+
return outcome;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** Sleep for `ms` milliseconds. */
|
|
217
|
+
function sleep(ms) {
|
|
218
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Compare against cached state. If status differs, emit a
|
|
223
|
+
* `connection.status_change` event (when an `emit` callback is supplied)
|
|
224
|
+
* and overwrite the cached value atomically.
|
|
225
|
+
*
|
|
226
|
+
* @param {string} name
|
|
227
|
+
* @param {string} status
|
|
228
|
+
* @param {string} statePath
|
|
229
|
+
* @param {undefined | ((ev: unknown) => void)} emit
|
|
230
|
+
*/
|
|
231
|
+
async function recordTransition(name, status, statePath, emit) {
|
|
232
|
+
const state = loadState(statePath);
|
|
233
|
+
const previous = state[name];
|
|
234
|
+
if (previous === status) return; // no transition
|
|
235
|
+
state[name] = status;
|
|
236
|
+
saveState(statePath, state);
|
|
237
|
+
if (typeof emit === 'function') {
|
|
238
|
+
try {
|
|
239
|
+
emit({
|
|
240
|
+
type: 'connection.status_change',
|
|
241
|
+
timestamp: new Date().toISOString(),
|
|
242
|
+
sessionId: process.env.GDD_SESSION_ID || 'unknown',
|
|
243
|
+
payload: { name, from: previous ?? 'unknown', to: status },
|
|
244
|
+
});
|
|
245
|
+
} catch (err) {
|
|
246
|
+
try {
|
|
247
|
+
process.stderr.write(
|
|
248
|
+
`[connection-probe] emit failed: ${err && err.message ? err.message : String(err)}\n`,
|
|
249
|
+
);
|
|
250
|
+
} catch {
|
|
251
|
+
/* swallow */
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
module.exports = {
|
|
258
|
+
probe,
|
|
259
|
+
statePathFor,
|
|
260
|
+
loadState,
|
|
261
|
+
saveState,
|
|
262
|
+
DEFAULT_STATE_PATH,
|
|
263
|
+
};
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* design-solidify.mjs — solidify-with-rollback gate (Plan 23-02).
|
|
3
|
+
*
|
|
4
|
+
* Code-level gate that runs the validation triplet for a task and, on
|
|
5
|
+
* any failure, rolls the working tree back via git stash (configurable)
|
|
6
|
+
* and appends a `solidify.rollback` event onto the Phase 22 causal chain.
|
|
7
|
+
*
|
|
8
|
+
* Replaces the prompt-encoded "stash if it broke" instruction in today's
|
|
9
|
+
* solidify agents with a typed, testable function.
|
|
10
|
+
*
|
|
11
|
+
* Why .mjs, not .ts:
|
|
12
|
+
* * Node 24 + Windows + .mjs dynamic-importing .ts triggers
|
|
13
|
+
* STATUS_STACK_BUFFER_OVERRUN (Phase 22 lesson). Keep this file as
|
|
14
|
+
* plain .mjs with a CJS test wrapper.
|
|
15
|
+
*
|
|
16
|
+
* Usage:
|
|
17
|
+
* import { solidify } from './design-solidify.mjs';
|
|
18
|
+
* const result = await solidify({ taskId: '23-02' });
|
|
19
|
+
* // result.outcome === 'pass' | 'fail'
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { spawnSync } from 'node:child_process';
|
|
23
|
+
import { existsSync } from 'node:fs';
|
|
24
|
+
import { dirname, resolve, join } from 'node:path';
|
|
25
|
+
import { createRequire } from 'node:module';
|
|
26
|
+
import { fileURLToPath } from 'node:url';
|
|
27
|
+
|
|
28
|
+
// Anchor on the .mjs file's own directory — NOT on the caller's cwd —
|
|
29
|
+
// because callers pass in arbitrary `cwd` values (test scaffolds,
|
|
30
|
+
// sub-repos) where event-chain.cjs is unreachable. Walk up from
|
|
31
|
+
// scripts/lib/ to repo root.
|
|
32
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
33
|
+
function _findRepoRoot(start) {
|
|
34
|
+
let dir = start;
|
|
35
|
+
for (let i = 0; i < 12; i++) {
|
|
36
|
+
if (existsSync(join(dir, 'package.json'))) return dir;
|
|
37
|
+
const parent = dirname(dir);
|
|
38
|
+
if (parent === dir) break;
|
|
39
|
+
dir = parent;
|
|
40
|
+
}
|
|
41
|
+
return start;
|
|
42
|
+
}
|
|
43
|
+
const PLUGIN_ROOT = _findRepoRoot(dirname(__filename));
|
|
44
|
+
const _nodeRequire = createRequire(join(PLUGIN_ROOT, 'package.json'));
|
|
45
|
+
const { appendChainEvent } = _nodeRequire(
|
|
46
|
+
resolve(PLUGIN_ROOT, 'scripts/lib/event-chain.cjs'),
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* @typedef {Object} SolidifyValidation
|
|
51
|
+
* @property {string} name
|
|
52
|
+
* @property {string} cmd
|
|
53
|
+
* @property {string[]} args
|
|
54
|
+
* @property {number} [timeout_ms]
|
|
55
|
+
*/
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* @typedef {Object} SolidifyOptions
|
|
59
|
+
* @property {string} taskId
|
|
60
|
+
* @property {SolidifyValidation[]} [validations]
|
|
61
|
+
* @property {'stash'|'hard'|'none'} [rollback]
|
|
62
|
+
* @property {string} [cwd]
|
|
63
|
+
* @property {string[]} [decisionRefs]
|
|
64
|
+
* @property {string} [parentEventId]
|
|
65
|
+
* @property {(ev: object) => void} [emit]
|
|
66
|
+
* @property {string} [chainPath]
|
|
67
|
+
*/
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* @typedef {Object} SolidifyStep
|
|
71
|
+
* @property {string} name
|
|
72
|
+
* @property {'pass'|'fail'} status
|
|
73
|
+
* @property {string} [stdout]
|
|
74
|
+
* @property {string} [stderr]
|
|
75
|
+
* @property {number|null} [code]
|
|
76
|
+
* @property {string|null} [signal]
|
|
77
|
+
*/
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* @typedef {Object} SolidifyResult
|
|
81
|
+
* @property {'pass'|'fail'} outcome
|
|
82
|
+
* @property {SolidifyStep[]} steps
|
|
83
|
+
* @property {'stash'|'hard'|'none'|'skipped'} [rolledBackVia]
|
|
84
|
+
* @property {string} eventId
|
|
85
|
+
* @property {string} [stashRef]
|
|
86
|
+
*/
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Build the default validation triplet for a task. Caller may override
|
|
90
|
+
* by supplying `opts.validations` directly.
|
|
91
|
+
*
|
|
92
|
+
* @param {string} taskId
|
|
93
|
+
* @returns {SolidifyValidation[]}
|
|
94
|
+
*/
|
|
95
|
+
function defaultValidations(taskId) {
|
|
96
|
+
return [
|
|
97
|
+
{ name: 'typecheck', cmd: 'npm', args: ['run', 'typecheck'], timeout_ms: 120_000 },
|
|
98
|
+
{ name: 'build', cmd: 'npm', args: ['run', 'build'], timeout_ms: 300_000 },
|
|
99
|
+
{
|
|
100
|
+
name: 'targeted-test',
|
|
101
|
+
cmd: 'npm',
|
|
102
|
+
args: ['test', '--', '--testPathPattern', String(taskId)],
|
|
103
|
+
timeout_ms: 120_000,
|
|
104
|
+
},
|
|
105
|
+
];
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Run one validation step via spawnSync. Always returns a step record;
|
|
110
|
+
* never throws.
|
|
111
|
+
*
|
|
112
|
+
* @param {SolidifyValidation} v
|
|
113
|
+
* @param {string} cwd
|
|
114
|
+
* @returns {SolidifyStep}
|
|
115
|
+
*/
|
|
116
|
+
function runStep(v, cwd) {
|
|
117
|
+
const r = spawnSync(v.cmd, v.args, {
|
|
118
|
+
cwd,
|
|
119
|
+
encoding: 'utf8',
|
|
120
|
+
shell: false,
|
|
121
|
+
timeout: v.timeout_ms ?? 120_000,
|
|
122
|
+
});
|
|
123
|
+
const status = r.status === 0 && !r.error ? 'pass' : 'fail';
|
|
124
|
+
return {
|
|
125
|
+
name: v.name,
|
|
126
|
+
status,
|
|
127
|
+
stdout: r.stdout,
|
|
128
|
+
stderr: r.stderr || (r.error ? String(r.error.message || r.error) : undefined),
|
|
129
|
+
code: r.status,
|
|
130
|
+
signal: r.signal,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Roll the tree back per the configured strategy. Never throws —
|
|
136
|
+
* returns the resolved verb (or 'skipped' if the strategy could not be
|
|
137
|
+
* applied) and an optional stash ref.
|
|
138
|
+
*
|
|
139
|
+
* @param {'stash'|'hard'|'none'} mode
|
|
140
|
+
* @param {string} cwd
|
|
141
|
+
* @param {string} taskId
|
|
142
|
+
* @returns {{via: 'stash'|'hard'|'none'|'skipped', stashRef?: string}}
|
|
143
|
+
*/
|
|
144
|
+
function rollback(mode, cwd, taskId) {
|
|
145
|
+
if (mode === 'none') return { via: 'none' };
|
|
146
|
+
if (!existsSync(join(cwd, '.git'))) return { via: 'skipped' };
|
|
147
|
+
const ts = new Date().toISOString();
|
|
148
|
+
|
|
149
|
+
if (mode === 'stash') {
|
|
150
|
+
const r = spawnSync(
|
|
151
|
+
'git',
|
|
152
|
+
['stash', 'push', '-u', '-m', `solidify-rollback:${taskId}:${ts}`],
|
|
153
|
+
{ cwd, encoding: 'utf8', shell: false, timeout: 30_000 },
|
|
154
|
+
);
|
|
155
|
+
if (r.status !== 0) return { via: 'skipped' };
|
|
156
|
+
const refRes = spawnSync('git', ['stash', 'list', '-1', '--format=%gd'], {
|
|
157
|
+
cwd,
|
|
158
|
+
encoding: 'utf8',
|
|
159
|
+
shell: false,
|
|
160
|
+
timeout: 10_000,
|
|
161
|
+
});
|
|
162
|
+
const stashRef = refRes.status === 0 ? refRes.stdout.trim() : undefined;
|
|
163
|
+
return { via: 'stash', stashRef };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (mode === 'hard') {
|
|
167
|
+
const r = spawnSync('git', ['reset', '--hard', 'HEAD'], {
|
|
168
|
+
cwd,
|
|
169
|
+
encoding: 'utf8',
|
|
170
|
+
shell: false,
|
|
171
|
+
timeout: 30_000,
|
|
172
|
+
});
|
|
173
|
+
if (r.status !== 0) return { via: 'skipped' };
|
|
174
|
+
return { via: 'hard' };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return { via: 'skipped' };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Run the solidify gate.
|
|
182
|
+
*
|
|
183
|
+
* @param {SolidifyOptions} opts
|
|
184
|
+
* @returns {Promise<SolidifyResult>}
|
|
185
|
+
*/
|
|
186
|
+
export async function solidify(opts) {
|
|
187
|
+
if (!opts || typeof opts.taskId !== 'string' || opts.taskId.length === 0) {
|
|
188
|
+
throw new TypeError('solidify: opts.taskId is required (non-empty string)');
|
|
189
|
+
}
|
|
190
|
+
const cwd = opts.cwd ? resolve(opts.cwd) : process.cwd();
|
|
191
|
+
const validations =
|
|
192
|
+
Array.isArray(opts.validations) && opts.validations.length > 0
|
|
193
|
+
? opts.validations
|
|
194
|
+
: defaultValidations(opts.taskId);
|
|
195
|
+
const mode = opts.rollback ?? 'stash';
|
|
196
|
+
|
|
197
|
+
/** @type {SolidifyStep[]} */
|
|
198
|
+
const steps = [];
|
|
199
|
+
/** @type {SolidifyStep|null} */
|
|
200
|
+
let failingStep = null;
|
|
201
|
+
for (const v of validations) {
|
|
202
|
+
const step = runStep(v, cwd);
|
|
203
|
+
steps.push(step);
|
|
204
|
+
if (step.status === 'fail') {
|
|
205
|
+
failingStep = step;
|
|
206
|
+
break;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
let rolledBackVia = 'none';
|
|
211
|
+
let stashRef;
|
|
212
|
+
if (failingStep && mode !== 'none') {
|
|
213
|
+
const r = rollback(mode, cwd, opts.taskId);
|
|
214
|
+
rolledBackVia = r.via;
|
|
215
|
+
stashRef = r.stashRef;
|
|
216
|
+
} else if (failingStep && mode === 'none') {
|
|
217
|
+
rolledBackVia = 'none';
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const chainEvent = {
|
|
221
|
+
parent_event_id: opts.parentEventId ?? null,
|
|
222
|
+
agent: 'design-solidify',
|
|
223
|
+
decision_refs: opts.decisionRefs ?? [],
|
|
224
|
+
outcome: failingStep ? 'rolled-back' : 'pass',
|
|
225
|
+
task_id: opts.taskId,
|
|
226
|
+
rolled_back_via: rolledBackVia,
|
|
227
|
+
steps: steps.map((s) => ({ name: s.name, status: s.status, code: s.code })),
|
|
228
|
+
};
|
|
229
|
+
if (failingStep) {
|
|
230
|
+
chainEvent.rollback_reason = `${failingStep.name} failed (code=${failingStep.code})`;
|
|
231
|
+
}
|
|
232
|
+
if (stashRef) chainEvent.stash_ref = stashRef;
|
|
233
|
+
if (opts.chainPath) chainEvent.path = opts.chainPath;
|
|
234
|
+
if (opts.cwd) chainEvent.baseDir = opts.cwd;
|
|
235
|
+
|
|
236
|
+
const eventId = appendChainEvent(chainEvent);
|
|
237
|
+
|
|
238
|
+
if (typeof opts.emit === 'function') {
|
|
239
|
+
try {
|
|
240
|
+
opts.emit({
|
|
241
|
+
type: 'solidify.rollback',
|
|
242
|
+
timestamp: new Date().toISOString(),
|
|
243
|
+
sessionId: process.env.GDD_SESSION_ID || 'unknown',
|
|
244
|
+
payload: {
|
|
245
|
+
task_id: opts.taskId,
|
|
246
|
+
outcome: chainEvent.outcome,
|
|
247
|
+
rolled_back_via: rolledBackVia,
|
|
248
|
+
failing_step: failingStep ? failingStep.name : null,
|
|
249
|
+
},
|
|
250
|
+
});
|
|
251
|
+
} catch {
|
|
252
|
+
/* swallow — emission must never bubble */
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** @type {SolidifyResult} */
|
|
257
|
+
const result = {
|
|
258
|
+
outcome: failingStep ? 'fail' : 'pass',
|
|
259
|
+
steps,
|
|
260
|
+
rolledBackVia,
|
|
261
|
+
eventId,
|
|
262
|
+
};
|
|
263
|
+
if (stashRef) result.stashRef = stashRef;
|
|
264
|
+
return result;
|
|
265
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* _js-harness.cjs — child process worker for js-const.cjs.
|
|
4
|
+
*
|
|
5
|
+
* Reads argv[2] as a file path, attempts to require() it (CJS first,
|
|
6
|
+
* then dynamic import for ESM), extracts tokens per recognised shapes,
|
|
7
|
+
* prints `{tokens: ..., error?: string}` JSON on stdout, exits 0.
|
|
8
|
+
*
|
|
9
|
+
* Never throws — errors are returned as JSON so the parent can render
|
|
10
|
+
* them as warnings.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
'use strict';
|
|
14
|
+
|
|
15
|
+
const target = process.argv[2];
|
|
16
|
+
|
|
17
|
+
function main() {
|
|
18
|
+
if (!target) {
|
|
19
|
+
return { tokens: {}, error: 'no-target' };
|
|
20
|
+
}
|
|
21
|
+
// Try CJS require first.
|
|
22
|
+
try {
|
|
23
|
+
// eslint-disable-next-line node/no-missing-require, global-require
|
|
24
|
+
const mod = require(target);
|
|
25
|
+
return { tokens: extract(mod) };
|
|
26
|
+
} catch (cjsErr) {
|
|
27
|
+
// Fall through to dynamic import.
|
|
28
|
+
}
|
|
29
|
+
// ESM dynamic import — async; wrap in IIFE.
|
|
30
|
+
return new Promise((resolve) => {
|
|
31
|
+
const url = require('node:url').pathToFileURL(target).href;
|
|
32
|
+
import(url).then(
|
|
33
|
+
(mod) => {
|
|
34
|
+
const candidate = mod.tokens ?? mod.default?.tokens ?? mod.default ?? mod;
|
|
35
|
+
resolve({ tokens: extract(candidate) });
|
|
36
|
+
},
|
|
37
|
+
(err) => resolve({ tokens: {}, error: `import-failed: ${err.message || String(err)}` }),
|
|
38
|
+
);
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function extract(mod) {
|
|
43
|
+
if (mod && typeof mod === 'object') {
|
|
44
|
+
if (mod.tokens && typeof mod.tokens === 'object') return mod.tokens;
|
|
45
|
+
if (mod.default && typeof mod.default === 'object' && mod.default.tokens) {
|
|
46
|
+
return mod.default.tokens;
|
|
47
|
+
}
|
|
48
|
+
return mod;
|
|
49
|
+
}
|
|
50
|
+
return {};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
Promise.resolve(main()).then(
|
|
54
|
+
(result) => {
|
|
55
|
+
try {
|
|
56
|
+
process.stdout.write(JSON.stringify(result));
|
|
57
|
+
} catch {
|
|
58
|
+
process.stdout.write(JSON.stringify({ tokens: {}, error: 'stringify-failed' }));
|
|
59
|
+
}
|
|
60
|
+
process.exit(0);
|
|
61
|
+
},
|
|
62
|
+
(err) => {
|
|
63
|
+
process.stdout.write(JSON.stringify({ tokens: {}, error: String(err) }));
|
|
64
|
+
process.exit(0);
|
|
65
|
+
},
|
|
66
|
+
);
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* design-tokens/css-vars.cjs — extract custom-property declarations
|
|
3
|
+
* from CSS / SCSS source (Plan 23-08).
|
|
4
|
+
*
|
|
5
|
+
* Parses any `--name: value;` declarations regardless of selector
|
|
6
|
+
* context. Last-write-wins on duplicate names. Block comments stripped.
|
|
7
|
+
* Strips the `--` prefix in the returned token map.
|
|
8
|
+
*
|
|
9
|
+
* Not supported (caller-warned via warnings):
|
|
10
|
+
* * SCSS `$var: value;` syntax — use the JS reader for SCSS exports
|
|
11
|
+
* * calc() / var() reference resolution — values returned verbatim
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
'use strict';
|
|
15
|
+
|
|
16
|
+
const fs = require('node:fs');
|
|
17
|
+
const path = require('node:path');
|
|
18
|
+
|
|
19
|
+
const CUSTOM_PROP_RE = /--([A-Za-z0-9][A-Za-z0-9_-]*)\s*:\s*([^;]+?)\s*(?=[;}])/g;
|
|
20
|
+
const BLOCK_COMMENT_RE = /\/\*[\s\S]*?\*\//g;
|
|
21
|
+
const SCSS_VAR_RE = /^\s*\$[A-Za-z][A-Za-z0-9_-]*\s*:/m;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Read a CSS/SCSS file and extract `--token: value;` declarations.
|
|
25
|
+
*
|
|
26
|
+
* @param {string} filePath
|
|
27
|
+
* @returns {{tokens: Record<string, string>, source: string, format: 'css-vars', warnings: string[]}}
|
|
28
|
+
*/
|
|
29
|
+
function readCssVars(filePath) {
|
|
30
|
+
const abs = path.resolve(filePath);
|
|
31
|
+
const raw = fs.readFileSync(abs, 'utf8');
|
|
32
|
+
const stripped = raw.replace(BLOCK_COMMENT_RE, '');
|
|
33
|
+
/** @type {string[]} */
|
|
34
|
+
const warnings = [];
|
|
35
|
+
if (SCSS_VAR_RE.test(stripped)) {
|
|
36
|
+
warnings.push('scss-vars-detected: $var: ... declarations are not parsed by css-vars.cjs');
|
|
37
|
+
}
|
|
38
|
+
/** @type {Record<string, string>} */
|
|
39
|
+
const tokens = {};
|
|
40
|
+
CUSTOM_PROP_RE.lastIndex = 0;
|
|
41
|
+
let m;
|
|
42
|
+
while ((m = CUSTOM_PROP_RE.exec(stripped)) !== null) {
|
|
43
|
+
const name = m[1];
|
|
44
|
+
const value = m[2].trim();
|
|
45
|
+
tokens[name] = value;
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
tokens,
|
|
49
|
+
source: abs,
|
|
50
|
+
format: 'css-vars',
|
|
51
|
+
warnings,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
module.exports = { readCssVars };
|