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.
@@ -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('[') ? m[2].split('_').length : parseInt(m[2], 10);
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 files = argv.slice(2);
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
- card = parseCardYaml(fs.readFileSync(file, 'utf8'));
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 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "baldart",
3
- "version": "4.92.0",
3
+ "version": "4.93.0",
4
4
  "description": "Claude Agent Framework - Reusable framework for coordinating AI agents and humans in software projects",
5
5
  "bin": {
6
6
  "baldart": "./bin/baldart.js"