baldart 4.92.0 → 4.93.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/CHANGELOG.md +25 -0
- package/VERSION +1 -1
- package/framework/.claude/agents/plan-auditor.md +9 -8
- package/framework/.claude/agents/prd-card-writer.md +8 -0
- package/framework/.claude/skills/e2e-review/CHANGELOG.md +10 -0
- package/framework/.claude/skills/e2e-review/SKILL.md +11 -4
- package/framework/.claude/skills/new2/CHANGELOG.md +16 -0
- package/framework/.claude/skills/new2/SKILL.md +32 -11
- package/framework/.claude/skills/prd/CHANGELOG.md +13 -0
- package/framework/.claude/skills/prd/SKILL.md +28 -7
- package/framework/.claude/skills/prd/references/audit-phase.md +33 -130
- package/framework/.claude/skills/prd/references/audit-teammate-prompt.md +144 -0
- package/framework/.claude/skills/prd/references/discovery-phase.md +28 -11
- package/framework/.claude/skills/prd/references/validation-phase.md +53 -57
- package/framework/.claude/skills/ui-implement/CHANGELOG.md +5 -0
- package/framework/.claude/skills/ui-implement/SKILL.md +7 -2
- package/framework/.claude/workflows/new2.js +49 -16
- package/framework/scripts/stamp-holistic-audit.js +143 -0
- package/framework/scripts/structural-compare.mjs +7 -2
- package/framework/scripts/validate-card-baseline.js +51 -5
- package/package.json +1 -1
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* stamp-holistic-audit.js — deterministic writer of the `metadata.holistic_audit`
|
|
4
|
+
* provenance block on backlog cards.
|
|
5
|
+
*
|
|
6
|
+
* Why this exists (/prd gate-hygiene wave): the per-card stamp write in
|
|
7
|
+
* audit-phase.md Step 6.9.4 and its backstop in validation-phase.md Step 7 item 5b
|
|
8
|
+
* were BOTH model-driven prose — and both failed together on a real run (a whole
|
|
9
|
+
* card family shipped with zero audit trail, so `/new` re-spawned the plan-auditor
|
|
10
|
+
* on every card). A prose gate degrades under context pressure; this script makes
|
|
11
|
+
* the stamp a single deterministic operation both call sites EXECUTE.
|
|
12
|
+
*
|
|
13
|
+
* Semantics are defined in audit-phase.md § "Holistic-audit provenance stamp" —
|
|
14
|
+
* this script only mechanizes the write:
|
|
15
|
+
* - idempotent: a card already carrying `holistic_audit` with a non-empty
|
|
16
|
+
* `audited_commit` is left untouched;
|
|
17
|
+
* - additive: never overwrites other `metadata` keys;
|
|
18
|
+
* - EPIC cards are skipped (they carry `planning_session` only — card-schema.md);
|
|
19
|
+
* - `--commit ""` is written EXPLICITLY (never omitted) so `/new` treats it as
|
|
20
|
+
* drift and runs its own plan-auditor (the fail-safe contract).
|
|
21
|
+
*
|
|
22
|
+
* Usage:
|
|
23
|
+
* node framework/scripts/stamp-holistic-audit.js \
|
|
24
|
+
* --at "2026-07-02T10:00:00Z" --commit "<sha-or-empty>" \
|
|
25
|
+
* --set "FEAT-0070-01,FEAT-0070-02" <card1.yml> [<card2.yml> ...]
|
|
26
|
+
*
|
|
27
|
+
* Exit 0 = every non-epic card carries the stamp afterwards; exit 1 = a write or
|
|
28
|
+
* parse failed (per-file errors printed). Zero-dep, node-core only.
|
|
29
|
+
*/
|
|
30
|
+
'use strict';
|
|
31
|
+
|
|
32
|
+
const fs = require('fs');
|
|
33
|
+
const path = require('path');
|
|
34
|
+
const { parseCardYaml, detectProfile } = require(path.join(__dirname, 'validate-card-baseline.js'));
|
|
35
|
+
|
|
36
|
+
function argValue(args, flag) {
|
|
37
|
+
const i = args.indexOf(flag);
|
|
38
|
+
return i > -1 && i + 1 < args.length ? args[i + 1] : null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** True when the card text already carries holistic_audit with a non-empty audited_commit. */
|
|
42
|
+
function alreadyStamped(text) {
|
|
43
|
+
const m = String(text).match(/^\s+holistic_audit:\s*$[\s\S]*?^\s+audited_commit:\s*"([^"]+)"/m);
|
|
44
|
+
return !!(m && m[1].trim() !== '');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Remove a pre-existing holistic_audit block (e.g. one with an empty commit) so the rewrite is clean. */
|
|
48
|
+
function stripExistingBlock(text) {
|
|
49
|
+
const lines = String(text).split('\n');
|
|
50
|
+
const start = lines.findIndex((l) => /^\s+holistic_audit:\s*$/.test(l));
|
|
51
|
+
if (start === -1) return text;
|
|
52
|
+
const indent = lines[start].length - lines[start].trimStart().length;
|
|
53
|
+
let end = start + 1;
|
|
54
|
+
while (end < lines.length) {
|
|
55
|
+
const l = lines[end];
|
|
56
|
+
if (l.trim() === '') { end++; continue; }
|
|
57
|
+
const li = l.length - l.trimStart().length;
|
|
58
|
+
if (li <= indent) break;
|
|
59
|
+
end++;
|
|
60
|
+
}
|
|
61
|
+
return lines.slice(0, start).concat(lines.slice(end)).join('\n');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function buildBlock(at, commit, set, childIndent) {
|
|
65
|
+
const pad = ' '.repeat(childIndent);
|
|
66
|
+
const inner = ' '.repeat(childIndent + 2);
|
|
67
|
+
const ids = set.map((s) => `"${s}"`).join(', ');
|
|
68
|
+
return [
|
|
69
|
+
`${pad}holistic_audit:`,
|
|
70
|
+
`${inner}audited_at: "${at}"`,
|
|
71
|
+
`${inner}audited_commit: "${commit}"`,
|
|
72
|
+
`${inner}audited_set: [${ids}]`,
|
|
73
|
+
].join('\n');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function stampFile(file, at, commit, set) {
|
|
77
|
+
let text = fs.readFileSync(file, 'utf8');
|
|
78
|
+
let card;
|
|
79
|
+
try { card = parseCardYaml(text); } catch (e) { return { file, status: 'error', detail: `YAML parse error — ${e.message}` }; }
|
|
80
|
+
if (!card || typeof card !== 'object') return { file, status: 'error', detail: 'not a YAML mapping' };
|
|
81
|
+
if (detectProfile(card, path.basename(file)) === 'EPIC') return { file, status: 'epic-skipped' };
|
|
82
|
+
if (alreadyStamped(text)) return { file, status: 'already-stamped' };
|
|
83
|
+
|
|
84
|
+
text = stripExistingBlock(text);
|
|
85
|
+
const lines = text.split('\n');
|
|
86
|
+
const metaIdx = lines.findIndex((l) => /^metadata:\s*$/.test(l));
|
|
87
|
+
if (metaIdx > -1) {
|
|
88
|
+
// child indent = indent of the first existing metadata child, else 2 spaces
|
|
89
|
+
let childIndent = 2;
|
|
90
|
+
for (let j = metaIdx + 1; j < lines.length; j++) {
|
|
91
|
+
const l = lines[j];
|
|
92
|
+
if (l.trim() === '') continue;
|
|
93
|
+
const li = l.length - l.trimStart().length;
|
|
94
|
+
if (li > 0) childIndent = li;
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
lines.splice(metaIdx + 1, 0, buildBlock(at, commit, set, childIndent));
|
|
98
|
+
text = lines.join('\n');
|
|
99
|
+
} else {
|
|
100
|
+
if (!text.endsWith('\n')) text += '\n';
|
|
101
|
+
text += `\nmetadata:\n${buildBlock(at, commit, set, 2)}\n`;
|
|
102
|
+
}
|
|
103
|
+
fs.writeFileSync(file, text);
|
|
104
|
+
// verify the write landed (the 5b "re-read one card" contract, made universal)
|
|
105
|
+
if (!alreadyStamped(fs.readFileSync(file, 'utf8')) && commit !== '') {
|
|
106
|
+
return { file, status: 'error', detail: 'post-write verification failed — stamp not found' };
|
|
107
|
+
}
|
|
108
|
+
return { file, status: 'stamped' };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function main(argv) {
|
|
112
|
+
const args = argv.slice(2);
|
|
113
|
+
const at = argValue(args, '--at');
|
|
114
|
+
const commit = argValue(args, '--commit'); // may legitimately be "" — must still be PASSED
|
|
115
|
+
const setRaw = argValue(args, '--set');
|
|
116
|
+
const files = [];
|
|
117
|
+
for (let i = 0; i < args.length; i++) {
|
|
118
|
+
if (args[i] === '--at' || args[i] === '--commit' || args[i] === '--set') { i++; continue; }
|
|
119
|
+
files.push(args[i]);
|
|
120
|
+
}
|
|
121
|
+
if (!at || commit === null || setRaw === null || !files.length) {
|
|
122
|
+
process.stderr.write('Usage: stamp-holistic-audit.js --at <iso8601> --commit <sha-or-empty> --set "ID1,ID2" <card.yml> [...]\n');
|
|
123
|
+
return 2;
|
|
124
|
+
}
|
|
125
|
+
const set = setRaw.split(',').map((s) => s.trim()).filter(Boolean).sort();
|
|
126
|
+
let failed = 0, stamped = 0, already = 0, epics = 0;
|
|
127
|
+
for (const f of files) {
|
|
128
|
+
let r;
|
|
129
|
+
try { r = stampFile(f, at, commit, set); } catch (e) { r = { file: f, status: 'error', detail: e.message }; }
|
|
130
|
+
if (r.status === 'error') { failed++; process.stdout.write(`✖ ${f}: ${r.detail}\n`); }
|
|
131
|
+
else if (r.status === 'stamped') { stamped++; process.stdout.write(`✓ ${f}: stamped\n`); }
|
|
132
|
+
else if (r.status === 'already-stamped') { already++; process.stdout.write(`· ${f}: already stamped\n`); }
|
|
133
|
+
else { epics++; process.stdout.write(`· ${f}: epic — skipped\n`); }
|
|
134
|
+
}
|
|
135
|
+
process.stdout.write(`holistic_audit provenance: ${stamped + already}/${files.length - epics} non-epic cards stamped (${stamped} written here, ${already} already present, ${epics} epic skipped)\n`);
|
|
136
|
+
return failed ? 1 : 0;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (require.main === module) {
|
|
140
|
+
process.exit(main(process.argv));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
module.exports = { stampFile, alreadyStamped, stripExistingBlock };
|
|
@@ -111,9 +111,14 @@ function signature(texts) {
|
|
|
111
111
|
sig.grid_cols_responsive_max = Math.max(sig.grid_cols_responsive_max, countTracks(m[1]));
|
|
112
112
|
}
|
|
113
113
|
}
|
|
114
|
-
// Tailwind: grid-cols-N (+ responsive variants), incl. arbitrary grid-cols-[a_b]
|
|
114
|
+
// Tailwind: grid-cols-N (+ responsive variants), incl. arbitrary grid-cols-[a_b].
|
|
115
|
+
// Arbitrary values use `_` as the space stand-in and may contain functions —
|
|
116
|
+
// `grid-cols-[repeat(2,_minmax(0,1fr))]` is 2 tracks, not split('_').length=3:
|
|
117
|
+
// normalize `_`→space and reuse the parens-aware countTracks().
|
|
115
118
|
for (const m of t.matchAll(/(?:^|[\s"'`])(?:(sm|md|lg|xl|2xl):)?grid-cols-(\d+|\[[^\]]+\])/g)) {
|
|
116
|
-
const cols = m[2].startsWith('[')
|
|
119
|
+
const cols = m[2].startsWith('[')
|
|
120
|
+
? countTracks(m[2].slice(1, -1).replace(/_/g, ' '))
|
|
121
|
+
: parseInt(m[2], 10);
|
|
117
122
|
sig.grid_cols_max = Math.max(sig.grid_cols_max, cols);
|
|
118
123
|
if (m[1]) {
|
|
119
124
|
sig.has_responsive_prefixes = true;
|
|
@@ -19,8 +19,18 @@
|
|
|
19
19
|
*
|
|
20
20
|
* Usage:
|
|
21
21
|
* node framework/scripts/validate-card-baseline.js <card.yml> [<card2.yml> ...]
|
|
22
|
+
* node framework/scripts/validate-card-baseline.js --prd <card.yml> [...]
|
|
22
23
|
* Exit 0 = all valid; exit 1 = at least one card has errors (printed per-field).
|
|
23
24
|
*
|
|
25
|
+
* --prd (since the /prd gate-hygiene wave): adds the deterministic subset of the
|
|
26
|
+
* /prd validation-phase Step 6 item 0 checks, so the orchestrator EXECUTES them
|
|
27
|
+
* instead of reciting them (prose gates fail silently under context pressure):
|
|
28
|
+
* - conditional `requirements` (PO1): child/standalone with non-empty
|
|
29
|
+
* acceptance_criteria + scope MUST have non-empty requirements;
|
|
30
|
+
* - epic `review_profile` must be `skip`;
|
|
31
|
+
* - `[NEEDS CLARIFICATION: …]` ambiguity markers in the raw YAML are BLOCKERS —
|
|
32
|
+
* a card ships only when every planning ambiguity has been resolved.
|
|
33
|
+
*
|
|
24
34
|
* Module API (for CI self-tests in scripts/check-card-baseline.js):
|
|
25
35
|
* const { validateCard, detectProfile, loadSchema, loadEnums } = require(...)
|
|
26
36
|
*/
|
|
@@ -349,21 +359,56 @@ function validateCard(card, { matrix, enums, filename = '' } = {}) {
|
|
|
349
359
|
return { profile, errors };
|
|
350
360
|
}
|
|
351
361
|
|
|
362
|
+
// --- /prd deterministic gate additions (--prd) -------------------------------
|
|
363
|
+
|
|
364
|
+
/** Scan raw YAML text for unresolved planning-ambiguity markers. */
|
|
365
|
+
function scanAmbiguityMarkers(text) {
|
|
366
|
+
const out = [];
|
|
367
|
+
const lines = String(text).split('\n');
|
|
368
|
+
for (let i = 0; i < lines.length; i++) {
|
|
369
|
+
if (lines[i].includes('[NEEDS CLARIFICATION')) {
|
|
370
|
+
out.push(`line ${i + 1}: ${lines[i].trim().slice(0, 120)}`);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
return out;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* The deterministic subset of /prd validation-phase Step 6 item 0 (see file header).
|
|
378
|
+
* Returns string[] errors. `text` is the raw YAML (marker scan), `card` the parsed map.
|
|
379
|
+
*/
|
|
380
|
+
function prdGateChecks(card, text, profile) {
|
|
381
|
+
const errors = [];
|
|
382
|
+
if (profile !== 'EPIC' && nonEmpty(card.acceptance_criteria) && nonEmpty(card.scope) && !nonEmpty(card.requirements)) {
|
|
383
|
+
errors.push("has acceptance_criteria + scope but no 'requirements' — emit requirements (faithful restatement of AC + scope) before commit (PO1)");
|
|
384
|
+
}
|
|
385
|
+
if (profile === 'EPIC' && nonEmpty(card.review_profile) && String(card.review_profile) !== 'skip') {
|
|
386
|
+
errors.push(`epic 'review_profile' must be 'skip' (got '${card.review_profile}') — trackers have no code work`);
|
|
387
|
+
}
|
|
388
|
+
for (const m of scanAmbiguityMarkers(text)) {
|
|
389
|
+
errors.push(`unresolved ambiguity marker — resolve with the user before commit: ${m}`);
|
|
390
|
+
}
|
|
391
|
+
return errors;
|
|
392
|
+
}
|
|
393
|
+
|
|
352
394
|
// --- CLI --------------------------------------------------------------------
|
|
353
395
|
|
|
354
396
|
function main(argv) {
|
|
355
|
-
const
|
|
397
|
+
const args = argv.slice(2);
|
|
398
|
+
const prdMode = args.includes('--prd');
|
|
399
|
+
const files = args.filter((a) => a !== '--prd');
|
|
356
400
|
if (!files.length) {
|
|
357
|
-
process.stderr.write('Usage: validate-card-baseline.js <card.yml> [<card2.yml> ...]\n');
|
|
401
|
+
process.stderr.write('Usage: validate-card-baseline.js [--prd] <card.yml> [<card2.yml> ...]\n');
|
|
358
402
|
return 2;
|
|
359
403
|
}
|
|
360
404
|
const matrix = loadSchema();
|
|
361
405
|
const enums = loadEnums();
|
|
362
406
|
let failed = 0;
|
|
363
407
|
for (const file of files) {
|
|
364
|
-
let card;
|
|
408
|
+
let text, card;
|
|
365
409
|
try {
|
|
366
|
-
|
|
410
|
+
text = fs.readFileSync(file, 'utf8');
|
|
411
|
+
card = parseCardYaml(text);
|
|
367
412
|
} catch (e) {
|
|
368
413
|
process.stdout.write(`✖ ${file}: YAML parse error — ${e.message}\n`);
|
|
369
414
|
failed++;
|
|
@@ -375,6 +420,7 @@ function main(argv) {
|
|
|
375
420
|
continue;
|
|
376
421
|
}
|
|
377
422
|
const { profile, errors } = validateCard(card, { matrix, enums, filename: path.basename(file) });
|
|
423
|
+
if (prdMode) errors.push(...prdGateChecks(card, text, profile));
|
|
378
424
|
if (errors.length) {
|
|
379
425
|
failed++;
|
|
380
426
|
process.stdout.write(`✖ ${file} [${profile}] — ${errors.length} issue(s):\n`);
|
|
@@ -390,4 +436,4 @@ if (require.main === module) {
|
|
|
390
436
|
process.exit(main(process.argv));
|
|
391
437
|
}
|
|
392
438
|
|
|
393
|
-
module.exports = { validateCard, detectProfile, loadSchema, loadEnums, nonEmpty, parseCardYaml };
|
|
439
|
+
module.exports = { validateCard, detectProfile, loadSchema, loadEnums, nonEmpty, parseCardYaml, prdGateChecks, scanAmbiguityMarkers };
|