@tekyzinc/gsd-t 3.24.10 → 3.25.11
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/CHANGELOG.md +43 -0
- package/README.md +7 -0
- package/bin/cli-preflight-checks/branch-guard.cjs +110 -0
- package/bin/cli-preflight-checks/contracts-stable.cjs +128 -0
- package/bin/cli-preflight-checks/deps-installed.cjs +89 -0
- package/bin/cli-preflight-checks/manifest-fresh.cjs +98 -0
- package/bin/cli-preflight-checks/ports-free.cjs +110 -0
- package/bin/cli-preflight-checks/working-tree-state.cjs +149 -0
- package/bin/cli-preflight.cjs +265 -0
- package/bin/gsd-t-context-brief-kinds/design-verify.cjs +139 -0
- package/bin/gsd-t-context-brief-kinds/execute.cjs +205 -0
- package/bin/gsd-t-context-brief-kinds/qa.cjs +130 -0
- package/bin/gsd-t-context-brief-kinds/red-team.cjs +131 -0
- package/bin/gsd-t-context-brief-kinds/scan.cjs +118 -0
- package/bin/gsd-t-context-brief-kinds/verify.cjs +157 -0
- package/bin/gsd-t-context-brief.cjs +395 -0
- package/bin/gsd-t-ratelimit-probe-worker.cjs +236 -0
- package/bin/gsd-t-ratelimit-probe.cjs +648 -0
- package/bin/gsd-t-verify-gate-judge.cjs +224 -0
- package/bin/gsd-t-verify-gate.cjs +612 -0
- package/bin/gsd-t.js +58 -2
- package/bin/m55-substrate-proof.cjs +134 -0
- package/bin/parallel-cli-tee.cjs +206 -0
- package/bin/parallel-cli.cjs +478 -0
- package/commands/gsd-t-execute.md +31 -0
- package/commands/gsd-t-help.md +21 -0
- package/commands/gsd-t-verify.md +38 -0
- package/docs/architecture.md +129 -0
- package/docs/diagrams/.gsd-t/.context-meter-state.json +10 -0
- package/docs/diagrams/.gsd-t/context-meter.log +9 -0
- package/docs/diagrams/.gsd-t/events/2026-05-08.jsonl +45 -0
- package/docs/diagrams/.gsd-t/events/2026-05-09.jsonl +1 -0
- package/docs/diagrams/.gsd-t/heartbeat-cd9e7f59-ba5b-406a-9ed6-16762f039e81.jsonl +48 -0
- package/docs/diagrams/01-top-level-map-d2.png +0 -0
- package/docs/diagrams/01-top-level-map.d2 +77 -0
- package/docs/diagrams/01-top-level-map.mmd +48 -0
- package/docs/diagrams/01-top-level-map.png +0 -0
- package/docs/diagrams/01-top-level-map.svg +126 -0
- package/docs/diagrams/02-milestone-lifecycle-d2.png +0 -0
- package/docs/diagrams/02-milestone-lifecycle.d2 +38 -0
- package/docs/diagrams/02-milestone-lifecycle.mmd +36 -0
- package/docs/diagrams/02-milestone-lifecycle.png +0 -0
- package/docs/diagrams/02-milestone-lifecycle.svg +114 -0
- package/docs/diagrams/03-wave-mode-d2.png +0 -0
- package/docs/diagrams/03-wave-mode.d2 +33 -0
- package/docs/diagrams/03-wave-mode.mmd +21 -0
- package/docs/diagrams/03-wave-mode.png +0 -0
- package/docs/diagrams/03-wave-mode.svg +113 -0
- package/docs/diagrams/04-design-to-code-d2.png +0 -0
- package/docs/diagrams/04-design-to-code.d2 +35 -0
- package/docs/diagrams/04-design-to-code.mmd +29 -0
- package/docs/diagrams/04-design-to-code.png +0 -0
- package/docs/diagrams/04-design-to-code.svg +115 -0
- package/docs/diagrams/05-backlog-d2.png +0 -0
- package/docs/diagrams/05-backlog.d2 +40 -0
- package/docs/diagrams/05-backlog.mmd +20 -0
- package/docs/diagrams/05-backlog.png +0 -0
- package/docs/diagrams/05-backlog.svg +113 -0
- package/docs/diagrams/06-automation-utilities-d2.png +0 -0
- package/docs/diagrams/06-automation-utilities.d2 +48 -0
- package/docs/diagrams/06-automation-utilities.mmd +47 -0
- package/docs/diagrams/06-automation-utilities.png +0 -0
- package/docs/diagrams/06-automation-utilities.svg +110 -0
- package/docs/diagrams/_theme.d2 +86 -0
- package/docs/requirements.md +31 -0
- package/docs/workflow-diagram.md +338 -0
- package/package.json +1 -1
- package/templates/CLAUDE-global.md +46 -0
- package/templates/prompts/design-verify-subagent.md +3 -0
- package/templates/prompts/qa-subagent.md +3 -0
- package/templates/prompts/red-team-subagent.md +3 -0
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* working-tree-state — git working tree clean or matches dirtyTreeWhitelist.
|
|
5
|
+
*
|
|
6
|
+
* Severity: warn.
|
|
7
|
+
*
|
|
8
|
+
* Reads `dirtyTreeWhitelist: string[]` from `.gsd-t/.unattended/config.json`.
|
|
9
|
+
* Each dirty path from `git status --porcelain` must match the whitelist for
|
|
10
|
+
* the check to pass. Whitelist patterns use simple glob:
|
|
11
|
+
* - `**` matches any sequence of characters (including `/`)
|
|
12
|
+
* - `*` matches any non-`/` segment
|
|
13
|
+
* - everything else is literal
|
|
14
|
+
*
|
|
15
|
+
* If the working tree is clean, the check passes with no whitelist consulted.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const fs = require('fs');
|
|
19
|
+
const path = require('path');
|
|
20
|
+
const { execSync } = require('child_process');
|
|
21
|
+
|
|
22
|
+
const ID = 'working-tree-state';
|
|
23
|
+
|
|
24
|
+
function _readWhitelist(projectDir) {
|
|
25
|
+
const cfgPath = path.join(projectDir, '.gsd-t', '.unattended', 'config.json');
|
|
26
|
+
if (!fs.existsSync(cfgPath)) return [];
|
|
27
|
+
let raw;
|
|
28
|
+
try { raw = fs.readFileSync(cfgPath, 'utf8'); } catch (_) { return []; }
|
|
29
|
+
let parsed;
|
|
30
|
+
try { parsed = JSON.parse(raw); } catch (_) { return []; }
|
|
31
|
+
const list = parsed && parsed.dirtyTreeWhitelist;
|
|
32
|
+
if (!Array.isArray(list)) return [];
|
|
33
|
+
return list.filter((s) => typeof s === 'string' && s.length > 0);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function _porcelain(projectDir) {
|
|
37
|
+
const stdout = execSync('git status --porcelain', {
|
|
38
|
+
cwd: projectDir,
|
|
39
|
+
encoding: 'utf8',
|
|
40
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
41
|
+
});
|
|
42
|
+
return String(stdout || '');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function _parseDirtyPaths(porcelain) {
|
|
46
|
+
// `git status --porcelain` output: "XY path" or "XY path1 -> path2" (rename).
|
|
47
|
+
// We take the (final) path; X/Y are status codes.
|
|
48
|
+
const out = [];
|
|
49
|
+
const lines = porcelain.split(/\r?\n/);
|
|
50
|
+
for (const line of lines) {
|
|
51
|
+
if (!line || line.length < 4) continue;
|
|
52
|
+
let rest = line.slice(3);
|
|
53
|
+
// Handle quoted paths from git (when path contains special chars).
|
|
54
|
+
if (rest.startsWith('"') && rest.endsWith('"')) {
|
|
55
|
+
rest = rest.slice(1, -1).replace(/\\"/g, '"').replace(/\\\\/g, '\\');
|
|
56
|
+
}
|
|
57
|
+
// Handle rename: "old -> new"
|
|
58
|
+
const renameIdx = rest.indexOf(' -> ');
|
|
59
|
+
if (renameIdx >= 0) rest = rest.slice(renameIdx + 4);
|
|
60
|
+
out.push(rest);
|
|
61
|
+
}
|
|
62
|
+
return out;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function _globToRegex(glob) {
|
|
66
|
+
// Escape regex specials except `*`, then expand `*` semantics.
|
|
67
|
+
// Use a placeholder dance so `**` becomes `.*` and `*` becomes `[^/]*`.
|
|
68
|
+
let s = '';
|
|
69
|
+
let i = 0;
|
|
70
|
+
while (i < glob.length) {
|
|
71
|
+
const ch = glob[i];
|
|
72
|
+
if (ch === '*' && glob[i + 1] === '*') {
|
|
73
|
+
s += '.*';
|
|
74
|
+
i += 2;
|
|
75
|
+
// Swallow optional trailing slash so `foo/**` matches `foo/anything`
|
|
76
|
+
// and also exactly `foo` is NOT matched (whitelisting expects the dir
|
|
77
|
+
// contents). Simpler semantic: just .* — caller can decide.
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (ch === '*') {
|
|
81
|
+
s += '[^/]*';
|
|
82
|
+
i++;
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (/[.+^${}()|[\]\\?]/.test(ch)) {
|
|
86
|
+
s += '\\' + ch;
|
|
87
|
+
i++;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
s += ch;
|
|
91
|
+
i++;
|
|
92
|
+
}
|
|
93
|
+
return new RegExp('^' + s + '$');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function _matchesAny(p, patterns) {
|
|
97
|
+
for (const pat of patterns) {
|
|
98
|
+
if (_globToRegex(pat).test(p)) return true;
|
|
99
|
+
}
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function run({ projectDir }) {
|
|
104
|
+
let porcelain;
|
|
105
|
+
try {
|
|
106
|
+
porcelain = _porcelain(projectDir);
|
|
107
|
+
} catch (err) {
|
|
108
|
+
return {
|
|
109
|
+
ok: false,
|
|
110
|
+
msg: 'git status failed: ' + (err && err.message || err),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
const dirty = _parseDirtyPaths(porcelain);
|
|
114
|
+
if (dirty.length === 0) {
|
|
115
|
+
return { ok: true, msg: 'working tree clean' };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const whitelist = _readWhitelist(projectDir);
|
|
119
|
+
const unmatched = dirty.filter((p) => !_matchesAny(p, whitelist));
|
|
120
|
+
|
|
121
|
+
if (unmatched.length === 0) {
|
|
122
|
+
return {
|
|
123
|
+
ok: true,
|
|
124
|
+
msg: 'working tree dirty (' + dirty.length + ' path(s)) but all whitelisted',
|
|
125
|
+
details: { dirty: dirty.length, whitelisted: dirty.length },
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
ok: false,
|
|
131
|
+
msg: unmatched.length + ' dirty path(s) outside whitelist',
|
|
132
|
+
details: {
|
|
133
|
+
dirty: dirty.length,
|
|
134
|
+
unmatched,
|
|
135
|
+
whitelist,
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
module.exports = {
|
|
141
|
+
id: ID,
|
|
142
|
+
severity: 'warn',
|
|
143
|
+
run,
|
|
144
|
+
// Test-only exports
|
|
145
|
+
_readWhitelist,
|
|
146
|
+
_parseDirtyPaths,
|
|
147
|
+
_globToRegex,
|
|
148
|
+
_matchesAny,
|
|
149
|
+
};
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* GSD-T CLI Preflight (M55 D1)
|
|
5
|
+
*
|
|
6
|
+
* Pluggable, deterministic, zero-dep state-precondition library + thin CLI.
|
|
7
|
+
*
|
|
8
|
+
* Pure inspector — no LLM spawn, no token spend, no side effects beyond reading
|
|
9
|
+
* the filesystem and running a small fixed list of read-only `git` / `lsof`
|
|
10
|
+
* commands inside individual checks.
|
|
11
|
+
*
|
|
12
|
+
* Contract: .gsd-t/contracts/cli-preflight-contract.md v1.0.0 STABLE.
|
|
13
|
+
*
|
|
14
|
+
* Hard rules (mirroring bin/parallelism-report.cjs):
|
|
15
|
+
* 1. Zero external runtime deps. Only Node built-ins.
|
|
16
|
+
* 2. Synchronous public API; never throws to the caller.
|
|
17
|
+
* 3. Per-check throws are caught, recorded, do not abort the run.
|
|
18
|
+
* 4. Deterministic output — sort `checks[]` by id, sort `notes[]`.
|
|
19
|
+
* 5. captureSpawn-exempt — see contract § captureSpawn Exemption.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const fs = require('fs');
|
|
23
|
+
const path = require('path');
|
|
24
|
+
|
|
25
|
+
const SCHEMA_VERSION = '1.0.0';
|
|
26
|
+
const CHECKS_DIR_NAME = 'cli-preflight-checks';
|
|
27
|
+
const VALID_SEVERITIES = new Set(['error', 'warn', 'info']);
|
|
28
|
+
|
|
29
|
+
// ── Public API ──────────────────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @param {object} [opts]
|
|
33
|
+
* @param {string} [opts.projectDir='.']
|
|
34
|
+
* @param {string[]} [opts.checks] restrict to these check ids (default = all built-ins)
|
|
35
|
+
* @param {string} [opts.mode='json'] informational; envelope is identical
|
|
36
|
+
* @returns {{ ok: boolean, schemaVersion: string, checks: object[], notes: string[] }}
|
|
37
|
+
*/
|
|
38
|
+
function runPreflight(opts) {
|
|
39
|
+
opts = opts || {};
|
|
40
|
+
const projectDir = opts.projectDir || '.';
|
|
41
|
+
const restrict = Array.isArray(opts.checks) ? new Set(opts.checks) : null;
|
|
42
|
+
const notes = [];
|
|
43
|
+
|
|
44
|
+
const registry = _loadRegistry(notes);
|
|
45
|
+
|
|
46
|
+
const selected = restrict
|
|
47
|
+
? registry.filter((c) => restrict.has(c.id))
|
|
48
|
+
: registry;
|
|
49
|
+
|
|
50
|
+
const checkResults = [];
|
|
51
|
+
for (const check of selected) {
|
|
52
|
+
const result = _runOneCheck(check, { projectDir }, notes);
|
|
53
|
+
checkResults.push(result);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Sort checks by id, ascending. Sort notes ascending.
|
|
57
|
+
checkResults.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
58
|
+
notes.sort();
|
|
59
|
+
|
|
60
|
+
const ok = !checkResults.some((c) => c.ok === false && c.severity === 'error');
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
schemaVersion: SCHEMA_VERSION,
|
|
64
|
+
ok,
|
|
65
|
+
checks: checkResults,
|
|
66
|
+
notes,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Render an envelope as a human-readable text summary.
|
|
72
|
+
* @param {object} envelope
|
|
73
|
+
* @returns {string}
|
|
74
|
+
*/
|
|
75
|
+
function renderText(envelope) {
|
|
76
|
+
const lines = [];
|
|
77
|
+
const status = envelope.ok ? 'OK' : 'FAIL';
|
|
78
|
+
lines.push('cli-preflight: ' + status + ' (schema v' + envelope.schemaVersion + ')');
|
|
79
|
+
lines.push('');
|
|
80
|
+
for (const c of envelope.checks) {
|
|
81
|
+
const icon = c.ok ? '✓' : (c.severity === 'error' ? '✗' : '!');
|
|
82
|
+
lines.push(' ' + icon + ' [' + c.severity.padEnd(5) + '] ' + c.id + ' — ' + c.msg);
|
|
83
|
+
}
|
|
84
|
+
if (envelope.notes && envelope.notes.length) {
|
|
85
|
+
lines.push('');
|
|
86
|
+
lines.push('Notes:');
|
|
87
|
+
for (const n of envelope.notes) lines.push(' - ' + n);
|
|
88
|
+
}
|
|
89
|
+
return lines.join('\n');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ── Internal helpers ────────────────────────────────────────────────────────
|
|
93
|
+
|
|
94
|
+
function _loadRegistry(notes) {
|
|
95
|
+
const dir = path.join(__dirname, CHECKS_DIR_NAME);
|
|
96
|
+
let entries;
|
|
97
|
+
try {
|
|
98
|
+
entries = fs.readdirSync(dir);
|
|
99
|
+
} catch (err) {
|
|
100
|
+
notes.push('registry: checks dir unreadable (' + (err && err.message || err) + ')');
|
|
101
|
+
return [];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const checks = [];
|
|
105
|
+
for (const filename of entries) {
|
|
106
|
+
if (!filename.endsWith('.cjs')) continue;
|
|
107
|
+
const full = path.join(dir, filename);
|
|
108
|
+
let mod;
|
|
109
|
+
try {
|
|
110
|
+
mod = require(full);
|
|
111
|
+
} catch (err) {
|
|
112
|
+
notes.push('registry: ' + filename + ' load failed (' + (err && err.message || err) + ')');
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (!_isValidCheckModule(mod, filename)) {
|
|
116
|
+
notes.push('registry: ' + filename + ' malformed');
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
checks.push(mod);
|
|
120
|
+
}
|
|
121
|
+
// Sort registry deterministically too — order of execution doesn't affect
|
|
122
|
+
// output (results are sorted again before return) but stable order makes
|
|
123
|
+
// notes ordering predictable.
|
|
124
|
+
checks.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
125
|
+
return checks;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function _isValidCheckModule(mod, filename) {
|
|
129
|
+
if (!mod || typeof mod !== 'object') return false;
|
|
130
|
+
if (typeof mod.id !== 'string' || !mod.id.length) return false;
|
|
131
|
+
if (!VALID_SEVERITIES.has(mod.severity)) return false;
|
|
132
|
+
if (typeof mod.run !== 'function') return false;
|
|
133
|
+
// Filename stem must match id, so a directory scan = a stable id namespace.
|
|
134
|
+
const stem = filename.replace(/\.cjs$/, '');
|
|
135
|
+
if (stem !== mod.id) return false;
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function _runOneCheck(check, ctx, notes) {
|
|
140
|
+
let raw;
|
|
141
|
+
try {
|
|
142
|
+
raw = check.run(ctx);
|
|
143
|
+
} catch (err) {
|
|
144
|
+
const msg = 'check threw: ' + (err && err.message || String(err));
|
|
145
|
+
notes.push(check.id + ': ' + msg);
|
|
146
|
+
return {
|
|
147
|
+
id: check.id,
|
|
148
|
+
ok: false,
|
|
149
|
+
severity: check.severity,
|
|
150
|
+
msg,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (!raw || typeof raw !== 'object' || typeof raw.ok !== 'boolean') {
|
|
155
|
+
const msg = 'check returned invalid shape';
|
|
156
|
+
notes.push(check.id + ': ' + msg);
|
|
157
|
+
return {
|
|
158
|
+
id: check.id,
|
|
159
|
+
ok: false,
|
|
160
|
+
severity: check.severity,
|
|
161
|
+
msg,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const out = {
|
|
166
|
+
id: check.id,
|
|
167
|
+
ok: raw.ok,
|
|
168
|
+
severity: check.severity,
|
|
169
|
+
msg: typeof raw.msg === 'string' ? raw.msg : '',
|
|
170
|
+
};
|
|
171
|
+
if (raw.details && typeof raw.details === 'object' && Object.keys(raw.details).length > 0) {
|
|
172
|
+
out.details = raw.details;
|
|
173
|
+
}
|
|
174
|
+
return out;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ── CLI ─────────────────────────────────────────────────────────────────────
|
|
178
|
+
|
|
179
|
+
function _parseArgv(argv) {
|
|
180
|
+
const out = { projectDir: '.', mode: 'json', skip: [] };
|
|
181
|
+
for (let i = 0; i < argv.length; i++) {
|
|
182
|
+
const a = argv[i];
|
|
183
|
+
if (a === '--project') {
|
|
184
|
+
out.projectDir = argv[++i] || '.';
|
|
185
|
+
} else if (a === '--json') {
|
|
186
|
+
out.mode = 'json';
|
|
187
|
+
} else if (a === '--text') {
|
|
188
|
+
out.mode = 'text';
|
|
189
|
+
} else if (a === '--skip') {
|
|
190
|
+
const list = argv[++i] || '';
|
|
191
|
+
out.skip = list.split(',').map((s) => s.trim()).filter(Boolean);
|
|
192
|
+
} else if (a === '--help' || a === '-h') {
|
|
193
|
+
out.help = true;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return out;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function _printHelp() {
|
|
200
|
+
const lines = [
|
|
201
|
+
'Usage: node bin/cli-preflight.cjs [options]',
|
|
202
|
+
'',
|
|
203
|
+
'Options:',
|
|
204
|
+
' --project DIR Project root (default: .)',
|
|
205
|
+
' --json Print JSON envelope (default)',
|
|
206
|
+
' --text Print human-readable summary',
|
|
207
|
+
' --skip id1,id2 Skip listed checks (each appends a note)',
|
|
208
|
+
' --help Show this help',
|
|
209
|
+
'',
|
|
210
|
+
'Exit codes:',
|
|
211
|
+
' 0 preflight ok',
|
|
212
|
+
' 4 preflight failed (>=1 error-severity check failed)',
|
|
213
|
+
];
|
|
214
|
+
process.stdout.write(lines.join('\n') + '\n');
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function _runCli(argv) {
|
|
218
|
+
const args = _parseArgv(argv);
|
|
219
|
+
if (args.help) {
|
|
220
|
+
_printHelp();
|
|
221
|
+
return 0;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Build initial registry to know what's available, then filter via --skip.
|
|
225
|
+
const noteSink = [];
|
|
226
|
+
const registry = _loadRegistry(noteSink);
|
|
227
|
+
const allIds = registry.map((c) => c.id);
|
|
228
|
+
const skipSet = new Set(args.skip);
|
|
229
|
+
const selected = allIds.filter((id) => !skipSet.has(id));
|
|
230
|
+
|
|
231
|
+
const envelope = runPreflight({
|
|
232
|
+
projectDir: args.projectDir,
|
|
233
|
+
checks: selected,
|
|
234
|
+
mode: args.mode,
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
// Append `--skip` notes deterministically.
|
|
238
|
+
for (const id of args.skip) {
|
|
239
|
+
envelope.notes.push('skipped: ' + id);
|
|
240
|
+
}
|
|
241
|
+
envelope.notes.sort();
|
|
242
|
+
|
|
243
|
+
if (args.mode === 'text') {
|
|
244
|
+
process.stdout.write(renderText(envelope) + '\n');
|
|
245
|
+
} else {
|
|
246
|
+
process.stdout.write(JSON.stringify(envelope, null, 2) + '\n');
|
|
247
|
+
}
|
|
248
|
+
return envelope.ok ? 0 : 4;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (require.main === module) {
|
|
252
|
+
const code = _runCli(process.argv.slice(2));
|
|
253
|
+
process.exit(code);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
module.exports = {
|
|
257
|
+
runPreflight,
|
|
258
|
+
renderText,
|
|
259
|
+
SCHEMA_VERSION,
|
|
260
|
+
// Exposed for unit tests only; not part of the public contract.
|
|
261
|
+
_loadRegistry,
|
|
262
|
+
_isValidCheckModule,
|
|
263
|
+
_runOneCheck,
|
|
264
|
+
_parseArgv,
|
|
265
|
+
};
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* design-verify kind collector — points the Design Verification subagent
|
|
5
|
+
* at its design contract path(s), extracts Figma URL(s), and surfaces
|
|
6
|
+
* any screenshot manifest.
|
|
7
|
+
*
|
|
8
|
+
* Fail-CLOSED: requires at least one of:
|
|
9
|
+
* - `.gsd-t/contracts/design-contract.md` (flat)
|
|
10
|
+
* - `.gsd-t/contracts/design/INDEX.md` (hierarchical)
|
|
11
|
+
*
|
|
12
|
+
* The library enforces fail-closed via the FAIL_CLOSED_KINDS rule:
|
|
13
|
+
* if NEITHER required source is present, generateBrief raises
|
|
14
|
+
* EREQUIRED_MISSING. To express the OR-of-required-sources, the
|
|
15
|
+
* collector itself emits the structured error during `collect`.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const fs = require('fs');
|
|
19
|
+
const path = require('path');
|
|
20
|
+
|
|
21
|
+
const NAME = 'design-verify';
|
|
22
|
+
const FLAT_CONTRACT = '.gsd-t/contracts/design-contract.md';
|
|
23
|
+
const HIER_INDEX = '.gsd-t/contracts/design/INDEX.md';
|
|
24
|
+
const SCREENSHOT_MANIFEST = '.gsd-t/screenshots/manifest.json';
|
|
25
|
+
|
|
26
|
+
function _readMaybe(file) {
|
|
27
|
+
try { return fs.readFileSync(file, 'utf8'); } catch (_) { return null; }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function _figmaUrls(text) {
|
|
31
|
+
if (!text) return [];
|
|
32
|
+
const re = /https?:\/\/(?:www\.)?figma\.com\/[A-Za-z0-9_./?#=&%-]+/g;
|
|
33
|
+
const out = [];
|
|
34
|
+
const seen = new Set();
|
|
35
|
+
let m;
|
|
36
|
+
while ((m = re.exec(text)) != null) {
|
|
37
|
+
const u = m[0].replace(/[).,;]+$/, '');
|
|
38
|
+
if (seen.has(u)) continue;
|
|
39
|
+
seen.add(u);
|
|
40
|
+
out.push(u);
|
|
41
|
+
}
|
|
42
|
+
return out;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function _designContractsList(projectDir, recordSource) {
|
|
46
|
+
const out = [];
|
|
47
|
+
// Flat: design-contract*.md at top of contracts/
|
|
48
|
+
const contractsDir = path.join(projectDir, '.gsd-t', 'contracts');
|
|
49
|
+
let entries;
|
|
50
|
+
try { entries = fs.readdirSync(contractsDir); } catch (_) { entries = []; }
|
|
51
|
+
for (const f of entries) {
|
|
52
|
+
if (/^design.*\.md$/i.test(f)) {
|
|
53
|
+
const rel = '.gsd-t/contracts/' + f;
|
|
54
|
+
out.push(rel);
|
|
55
|
+
recordSource(rel);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
// Hierarchical: .gsd-t/contracts/design/**/*.md
|
|
59
|
+
const designDir = path.join(contractsDir, 'design');
|
|
60
|
+
if (fs.existsSync(designDir)) {
|
|
61
|
+
const stack = [designDir];
|
|
62
|
+
while (stack.length) {
|
|
63
|
+
const cur = stack.pop();
|
|
64
|
+
let inner;
|
|
65
|
+
try { inner = fs.readdirSync(cur, { withFileTypes: true }); } catch (_) { continue; }
|
|
66
|
+
for (const ent of inner) {
|
|
67
|
+
const full = path.join(cur, ent.name);
|
|
68
|
+
if (ent.isDirectory()) {
|
|
69
|
+
stack.push(full);
|
|
70
|
+
} else if (ent.isFile() && full.endsWith('.md')) {
|
|
71
|
+
const rel = path.relative(projectDir, full);
|
|
72
|
+
out.push(rel);
|
|
73
|
+
recordSource(rel);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
out.sort();
|
|
79
|
+
return out;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function collect(ctx) {
|
|
83
|
+
const { projectDir, recordSource } = ctx;
|
|
84
|
+
|
|
85
|
+
// Required-source OR check (library only knows AND-of-required, so we
|
|
86
|
+
// express the OR rule here).
|
|
87
|
+
const flatPresent = fs.existsSync(path.join(projectDir, FLAT_CONTRACT));
|
|
88
|
+
const hierPresent = fs.existsSync(path.join(projectDir, HIER_INDEX));
|
|
89
|
+
if (!flatPresent && !hierPresent) {
|
|
90
|
+
const err = new Error('design-verify: no design contract found (' +
|
|
91
|
+
FLAT_CONTRACT + ' or ' + HIER_INDEX + ')');
|
|
92
|
+
err.code = 'EREQUIRED_MISSING';
|
|
93
|
+
err.missing = [FLAT_CONTRACT, HIER_INDEX];
|
|
94
|
+
throw err;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const designPaths = _designContractsList(projectDir, recordSource);
|
|
98
|
+
|
|
99
|
+
// Pull all Figma URLs across every located design contract.
|
|
100
|
+
const figmaUrls = [];
|
|
101
|
+
const seen = new Set();
|
|
102
|
+
for (const rel of designPaths) {
|
|
103
|
+
const text = _readMaybe(path.join(projectDir, rel));
|
|
104
|
+
if (!text) continue;
|
|
105
|
+
for (const u of _figmaUrls(text)) {
|
|
106
|
+
if (seen.has(u)) continue;
|
|
107
|
+
seen.add(u);
|
|
108
|
+
figmaUrls.push(u);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
figmaUrls.sort();
|
|
112
|
+
|
|
113
|
+
let screenshotManifest = null;
|
|
114
|
+
if (fs.existsSync(path.join(projectDir, SCREENSHOT_MANIFEST))) {
|
|
115
|
+
screenshotManifest = SCREENSHOT_MANIFEST;
|
|
116
|
+
recordSource(SCREENSHOT_MANIFEST);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
scope: { owned: [], notOwned: [], deliverables: [] },
|
|
121
|
+
constraints: [],
|
|
122
|
+
contracts: designPaths.map((p) => ({ path: p, status: 'UNKNOWN' })),
|
|
123
|
+
ancillary: {
|
|
124
|
+
designContractPaths: designPaths,
|
|
125
|
+
figmaUrls,
|
|
126
|
+
screenshotManifest,
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
module.exports = {
|
|
132
|
+
name: NAME,
|
|
133
|
+
// Library treats requiresSources as AND. design-verify uses the OR-of-two
|
|
134
|
+
// pattern instead, raising EREQUIRED_MISSING from inside collect().
|
|
135
|
+
requiresSources: [],
|
|
136
|
+
collect,
|
|
137
|
+
_figmaUrls,
|
|
138
|
+
_designContractsList,
|
|
139
|
+
};
|