@tekyzinc/gsd-t 4.7.10 → 4.8.10
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 +30 -0
- package/README.md +1 -1
- package/bin/gsd-t-archive-domains.cjs +136 -0
- package/bin/gsd-t-divergence-grammar.cjs +213 -0
- package/bin/gsd-t-guard-map-derive.cjs +210 -0
- package/bin/gsd-t-guard-map.cjs +279 -0
- package/bin/gsd-t-milestone-state.cjs +236 -0
- package/bin/gsd-t-rule-consume.cjs +141 -0
- package/bin/gsd-t-traceability-gate.cjs +321 -24
- package/bin/gsd-t.js +27 -0
- package/commands/gsd-t-complete-milestone.md +15 -2
- package/commands/gsd-t-doc-ripple.md +1 -1
- package/commands/gsd-t-milestone.md +33 -1
- package/package.json +1 -1
- package/templates/CLAUDE-global.md +2 -0
- package/templates/PseudoCode-spec.md +194 -0
- package/templates/prompts/keep-or-supersede-subagent.md +61 -0
- package/templates/prompts/qa-subagent.md +10 -0
- package/templates/prompts/red-team-subagent.md +12 -0
- package/templates/workflows/gsd-t-phase.workflow.js +22 -2
- package/templates/workflows/gsd-t-verify.workflow.js +101 -1
|
@@ -33,10 +33,21 @@
|
|
|
33
33
|
*
|
|
34
34
|
* Input: --milestone Mxx --project-dir PATH (reads .gsd-t/domains/* /tasks.md).
|
|
35
35
|
* OR --tasks <file> to check a single tasks.md (used by tests).
|
|
36
|
+
* OR --domains a,b,c to scope to EXACTLY those domain dirs (M87 D2-T4).
|
|
36
37
|
* Output: JSON envelope { ok, exitCode, milestone, tasks:[...], violations:[...] }.
|
|
37
38
|
* Exit: 0 all tasks traceable · 4 ≥1 violation (blocks execute) · 64 bad input.
|
|
38
39
|
*
|
|
39
40
|
* Hard rules: zero deps, never throws, pure/read-only.
|
|
41
|
+
*
|
|
42
|
+
* ── M87 D2 EXTENSION (section-citation coverage; pseudocode-source-of-truth §3) ──
|
|
43
|
+
* In ADDITION to the M83 AC→(path+test) binding, when a PseudoCode-[Title].md doc
|
|
44
|
+
* is in scope (present in the pseudocode dir AND cited by ≥1 task), every one of
|
|
45
|
+
* its `##` sections (§3.1: level-2 headings OUTSIDE Appendix code fences) must be
|
|
46
|
+
* cited by ≥1 task via a `**PseudoCode-Section**: <Title>#<anchor>` field. A
|
|
47
|
+
* section with zero citing tasks is an uncovered structural gap; a cited anchor
|
|
48
|
+
* that resolves to NO `##`-heading slug (§3.2 GitHub-style, slug-as-slug never
|
|
49
|
+
* substring) is an unresolvable-citation FAILURE. The M83 behavior + exit codes
|
|
50
|
+
* are preserved untouched.
|
|
40
51
|
*/
|
|
41
52
|
|
|
42
53
|
const fs = require("node:fs");
|
|
@@ -160,6 +171,90 @@ function _acBulletText(lines) {
|
|
|
160
171
|
return out.join("\n");
|
|
161
172
|
}
|
|
162
173
|
|
|
174
|
+
// ─── M87 D2: section-citation grammar (pseudocode-source-of-truth §3) ─────
|
|
175
|
+
|
|
176
|
+
// `**PseudoCode-Section**: <Title>#<anchor>` — matched emphasis-agnostically on
|
|
177
|
+
// the BARED line, then split path-as-path into Title + anchor segments. The
|
|
178
|
+
// raw line is bared via _bare (emphasis stripped) so the colon-inside/outside
|
|
179
|
+
// bold forms both match, exactly like the M83 field scan.
|
|
180
|
+
const PSEUDOCODE_SECTION_FIELD_RE = /^\s*[-*]?\s*pseudocode-section\s*:/i;
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Parse the `**PseudoCode-Section**: <Title>#<anchor>` citation from a task's
|
|
184
|
+
* lines. Returns { title, anchor } structured segments, or null if absent.
|
|
185
|
+
* Path-as-path: the value is split on the FIRST `#`; everything left of it is
|
|
186
|
+
* the doc Title, everything right is the anchor slug. Never substring-matched
|
|
187
|
+
* against doc text.
|
|
188
|
+
*
|
|
189
|
+
* Title normalization: the corpus cites with the full filename stem
|
|
190
|
+
* (`PseudoCode-PayPal#…`) while the doc loader keys by the bare subject
|
|
191
|
+
* (`PayPal`, the stem minus the `PseudoCode-` prefix). A leading `PseudoCode-`
|
|
192
|
+
* in the title segment is therefore stripped so both forms resolve to one key.
|
|
193
|
+
*/
|
|
194
|
+
function _normalizeDocTitle(t) {
|
|
195
|
+
const s = String(t == null ? "" : t).trim();
|
|
196
|
+
const m = s.match(/^PseudoCode-(.+)$/);
|
|
197
|
+
return m ? m[1] : s;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function parseSectionCitation(lines) {
|
|
201
|
+
for (const ln of lines) {
|
|
202
|
+
const bare = _bare(ln);
|
|
203
|
+
if (!PSEUDOCODE_SECTION_FIELD_RE.test(bare)) continue;
|
|
204
|
+
const idx = bare.indexOf(":");
|
|
205
|
+
if (idx < 0) continue;
|
|
206
|
+
const val = bare.slice(idx + 1).trim();
|
|
207
|
+
const hash = val.indexOf("#");
|
|
208
|
+
if (hash < 0) return { title: _normalizeDocTitle(val), anchor: "", raw: val };
|
|
209
|
+
return { title: _normalizeDocTitle(val.slice(0, hash)), anchor: val.slice(hash + 1).trim(), raw: val };
|
|
210
|
+
}
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* GitHub-style heading→slug (contract §3.2, deterministic/pure):
|
|
216
|
+
* lowercase → drop every char that is NOT ascii-alnum / space / hyphen →
|
|
217
|
+
* spaces to hyphens → collapse hyphen runs. slug-as-slug, never substring.
|
|
218
|
+
*/
|
|
219
|
+
function slugifyHeading(text) {
|
|
220
|
+
return String(text == null ? "" : text)
|
|
221
|
+
.trim()
|
|
222
|
+
.toLowerCase()
|
|
223
|
+
.replace(/[^a-z0-9 -]/g, "")
|
|
224
|
+
.replace(/ /g, "-")
|
|
225
|
+
.replace(/-+/g, "-");
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Enumerate the citable `##` sections of a PseudoCode doc (contract §3.1):
|
|
230
|
+
* every level-2 heading OUTSIDE fenced code blocks (so the `# 0.` banner lines
|
|
231
|
+
* inside the Appendix raw-pseudocode fences are EXCLUDED). Returns an ordered
|
|
232
|
+
* list of { title, slug }. Pure/read-only over the doc text.
|
|
233
|
+
*/
|
|
234
|
+
function enumerateSections(md) {
|
|
235
|
+
const lines = (md || "").split(/\r?\n/);
|
|
236
|
+
const out = [];
|
|
237
|
+
let inFence = false;
|
|
238
|
+
for (const line of lines) {
|
|
239
|
+
// Fence delimiters: ``` or ~~~ (3+), optionally indented, with an info string.
|
|
240
|
+
if (/^\s*(```+|~~~+)/.test(line)) { inFence = !inFence; continue; }
|
|
241
|
+
if (inFence) continue;
|
|
242
|
+
const m = line.match(/^##\s+(.*\S.*)$/);
|
|
243
|
+
if (m) out.push({ title: m[1].trim(), slug: slugifyHeading(m[1].trim()) });
|
|
244
|
+
}
|
|
245
|
+
return out;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Derive the doc <Title> from a PseudoCode-[Title].md filename (basename minus
|
|
250
|
+
* the `PseudoCode-` prefix and `.md` suffix). Path-as-path.
|
|
251
|
+
*/
|
|
252
|
+
function docTitleFromFilename(filename) {
|
|
253
|
+
const base = path.basename(String(filename || ""));
|
|
254
|
+
const m = base.match(/^PseudoCode-(.+)\.md$/);
|
|
255
|
+
return m ? m[1] : null;
|
|
256
|
+
}
|
|
257
|
+
|
|
163
258
|
/**
|
|
164
259
|
* A task is "behavioral" (subject to the gate) if it declares acceptance
|
|
165
260
|
* criteria — i.e. it promises an observable behavior. Pure-scaffolding tasks
|
|
@@ -167,9 +262,12 @@ function _acBulletText(lines) {
|
|
|
167
262
|
*/
|
|
168
263
|
function assessTask(task) {
|
|
169
264
|
const lines = task.lines;
|
|
265
|
+
// M87 D2: every task may carry a section citation, AC-bearing or not — capture
|
|
266
|
+
// it regardless so non-behavioral coverage tasks still count toward citations.
|
|
267
|
+
const sectionCitation = parseSectionCitation(lines);
|
|
170
268
|
const hasAc = hasMultiField(lines, AC_FIELD_RE);
|
|
171
269
|
if (!hasAc) {
|
|
172
|
-
return { title: task.title, behavioral: false, violations: [] };
|
|
270
|
+
return { title: task.title, behavioral: false, violations: [], sectionCitation };
|
|
173
271
|
}
|
|
174
272
|
|
|
175
273
|
// Underscore-preserving values for path/runner scans (Red Team recheck HIGH).
|
|
@@ -218,42 +316,203 @@ function assessTask(task) {
|
|
|
218
316
|
behavioral: true,
|
|
219
317
|
isHeadline,
|
|
220
318
|
hasFiles, hasTest, hasImplPath,
|
|
319
|
+
sectionCitation,
|
|
221
320
|
violations,
|
|
222
321
|
};
|
|
223
322
|
}
|
|
224
323
|
|
|
225
324
|
// ─── driver ──────────────────────────────────────────────────────────────
|
|
226
325
|
|
|
227
|
-
|
|
326
|
+
/**
|
|
327
|
+
* Resolve the set of tasks.md files to gate.
|
|
328
|
+
*
|
|
329
|
+
* Returns { files: [...] } on success, or { error: { reason, ... } } when an
|
|
330
|
+
* explicit scope cannot be honored (M87 D2-T4 — never silently fall back to all
|
|
331
|
+
* historical domains).
|
|
332
|
+
*
|
|
333
|
+
* Scoping precedence:
|
|
334
|
+
* 1. `domains` (explicit list) → EXACTLY those dirs; each must exist + carry a
|
|
335
|
+
* tasks.md, else a structured error (a named-but-missing domain is never a
|
|
336
|
+
* silent drop).
|
|
337
|
+
* 2. `milestone` prefix-match → domains whose name starts with the mNN prefix
|
|
338
|
+
* (M83 path, preserved for prefix-named milestones).
|
|
339
|
+
* 3. `milestone` set but prefix-match yields zero AND no `domains` →
|
|
340
|
+
* structured `milestone-scope-unresolved` error (exit 64) instructing
|
|
341
|
+
* `--domains`, REPLACING the M83 fall-back-to-all.
|
|
342
|
+
* 4. No milestone, no domains → all domains (single-milestone-repo default).
|
|
343
|
+
*/
|
|
344
|
+
/**
|
|
345
|
+
* Containment guard (per feedback_destructive_path_ops_containment): a child path
|
|
346
|
+
* is valid only if it resolves strictly INSIDE root (root + separator prefix) —
|
|
347
|
+
* never outside, never equal to root. Refuses `../`-escapes, absolute paths, and
|
|
348
|
+
* any name resolving to a sibling/ancestor. Returns the resolved path or null.
|
|
349
|
+
*/
|
|
350
|
+
function containedChild(root, name) {
|
|
351
|
+
const resolvedRoot = path.resolve(root);
|
|
352
|
+
const candidate = path.resolve(resolvedRoot, name);
|
|
353
|
+
if (candidate === resolvedRoot) return null; // equal-to-root is not a child
|
|
354
|
+
if (!candidate.startsWith(resolvedRoot + path.sep)) return null; // outside-root
|
|
355
|
+
return candidate;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function listTasksFiles(projectDir, milestone, domains = null) {
|
|
228
359
|
const domainsDir = path.join(projectDir, ".gsd-t", "domains");
|
|
360
|
+
|
|
361
|
+
// (1) Explicit domain list — scope to EXACTLY those, error on any miss.
|
|
362
|
+
if (domains && domains.length) {
|
|
363
|
+
const files = [];
|
|
364
|
+
const missing = [];
|
|
365
|
+
const escaped = [];
|
|
366
|
+
for (const name of domains) {
|
|
367
|
+
// Containment: a domain name must resolve strictly inside the domains dir.
|
|
368
|
+
// A `../`-escape / absolute path / sibling reference is REFUSED, never
|
|
369
|
+
// silently gated against an out-of-tree file (read-only today, but the
|
|
370
|
+
// gate becomes attacker-reachable the moment --domains is doc-derived).
|
|
371
|
+
const domainDir = containedChild(domainsDir, name);
|
|
372
|
+
if (!domainDir) { escaped.push(name); continue; }
|
|
373
|
+
const tasksPath = path.join(domainDir, "tasks.md");
|
|
374
|
+
let stat = null;
|
|
375
|
+
try { stat = fs.statSync(domainDir); } catch {/* missing */}
|
|
376
|
+
if (!stat || !stat.isDirectory() || !fs.existsSync(tasksPath)) {
|
|
377
|
+
missing.push(name);
|
|
378
|
+
continue;
|
|
379
|
+
}
|
|
380
|
+
files.push({ domain: name, tasksPath });
|
|
381
|
+
}
|
|
382
|
+
if (escaped.length) {
|
|
383
|
+
return { error: { reason: "domain-path-escape", escapedDomains: escaped, requestedDomains: domains } };
|
|
384
|
+
}
|
|
385
|
+
if (missing.length) {
|
|
386
|
+
return { error: { reason: "domain-not-found", missingDomains: missing, requestedDomains: domains } };
|
|
387
|
+
}
|
|
388
|
+
return { files };
|
|
389
|
+
}
|
|
390
|
+
|
|
229
391
|
let entries = [];
|
|
230
392
|
try {
|
|
231
393
|
entries = fs.readdirSync(domainsDir, { withFileTypes: true });
|
|
232
394
|
} catch {
|
|
233
|
-
return [];
|
|
395
|
+
return { files: [] };
|
|
234
396
|
}
|
|
235
|
-
const
|
|
236
|
-
const mPrefix = milestone ? milestone.toLowerCase() : null;
|
|
397
|
+
const all = [];
|
|
237
398
|
for (const e of entries) {
|
|
238
399
|
if (!e.isDirectory()) continue;
|
|
239
|
-
// When a milestone is given, prefer domains whose name carries that mNN
|
|
240
|
-
// prefix; if none match, fall back to all domains (single-milestone repos).
|
|
241
400
|
const tasksPath = path.join(domainsDir, e.name, "tasks.md");
|
|
242
|
-
if (fs.existsSync(tasksPath))
|
|
401
|
+
if (fs.existsSync(tasksPath)) all.push({ domain: e.name, tasksPath });
|
|
243
402
|
}
|
|
403
|
+
|
|
404
|
+
const mPrefix = milestone ? milestone.toLowerCase() : null;
|
|
244
405
|
if (mPrefix) {
|
|
245
|
-
const matched =
|
|
246
|
-
if (matched.length) return matched;
|
|
406
|
+
const matched = all.filter((d) => d.domain.toLowerCase().startsWith(mPrefix));
|
|
407
|
+
if (matched.length) return { files: matched };
|
|
408
|
+
// Zero prefix-match + no explicit --domains → DO NOT fall back to all.
|
|
409
|
+
return { error: { reason: "milestone-scope-unresolved", milestone } };
|
|
247
410
|
}
|
|
248
|
-
return
|
|
411
|
+
return { files: all };
|
|
249
412
|
}
|
|
250
413
|
|
|
251
|
-
|
|
414
|
+
// ─── M87 D2: section-coverage over the in-scope PseudoCode docs ───────────
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Enumerate the PseudoCode-*.md docs available for coverage. Default location is
|
|
418
|
+
* `.gsd-t/pseudocode/` (contract §7), overridable via pseudocodeDir (the D2 test
|
|
419
|
+
* points it at D1's byte-verbatim fixtures). Returns a Map<Title, {path, md,
|
|
420
|
+
* sections}>; a missing dir yields an empty Map (logged skip, never a throw).
|
|
421
|
+
*/
|
|
422
|
+
function loadPseudocodeDocs(pseudocodeDir) {
|
|
423
|
+
const docs = new Map();
|
|
424
|
+
let entries = [];
|
|
425
|
+
try { entries = fs.readdirSync(pseudocodeDir, { withFileTypes: true }); }
|
|
426
|
+
catch { return docs; }
|
|
427
|
+
for (const e of entries) {
|
|
428
|
+
if (!e.isFile()) continue;
|
|
429
|
+
const title = docTitleFromFilename(e.name);
|
|
430
|
+
if (!title) continue;
|
|
431
|
+
const full = path.join(pseudocodeDir, e.name);
|
|
432
|
+
let md;
|
|
433
|
+
try { md = fs.readFileSync(full, "utf8"); } catch { continue; }
|
|
434
|
+
docs.set(title, { path: full, md, sections: enumerateSections(md) });
|
|
435
|
+
}
|
|
436
|
+
return docs;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Run section-citation coverage over the in-scope PseudoCode docs.
|
|
441
|
+
*
|
|
442
|
+
* A doc is "in scope" when it exists in the pseudocode dir AND is cited by ≥1
|
|
443
|
+
* task. For each in-scope doc: (a) every cited anchor MUST resolve to a real
|
|
444
|
+
* `##`-heading slug (else unresolvable-citation FAILURE); (b) every `##` section
|
|
445
|
+
* MUST be cited by ≥1 task (else uncovered-section gap). All slug-as-slug, never
|
|
446
|
+
* substring.
|
|
447
|
+
*
|
|
448
|
+
* @returns {{ violations, docs }}
|
|
449
|
+
*/
|
|
450
|
+
function assessSectionCoverage(taskResults, pseudocodeDocs) {
|
|
451
|
+
const violations = [];
|
|
452
|
+
const docReports = [];
|
|
453
|
+
|
|
454
|
+
// citationsByTitle: Map<Title, Array<{ anchor, domain, task }>>
|
|
455
|
+
const citationsByTitle = new Map();
|
|
456
|
+
for (const r of taskResults) {
|
|
457
|
+
const c = r.sectionCitation;
|
|
458
|
+
if (!c || !c.title) continue;
|
|
459
|
+
if (!citationsByTitle.has(c.title)) citationsByTitle.set(c.title, []);
|
|
460
|
+
citationsByTitle.get(c.title).push({ anchor: c.anchor, domain: r.domain, task: r.title });
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// A doc is in scope when present in the dir AND cited by ≥1 task.
|
|
464
|
+
for (const [title, citations] of citationsByTitle) {
|
|
465
|
+
const doc = pseudocodeDocs.get(title);
|
|
466
|
+
if (!doc) continue; // cited but no doc available → out of section-coverage scope
|
|
467
|
+
const slugSet = new Set(doc.sections.map((s) => s.slug));
|
|
468
|
+
const citedSlugs = new Set();
|
|
469
|
+
|
|
470
|
+
// (a) Unresolvable-citation FAILURE: a cited anchor matching no `##` slug.
|
|
471
|
+
for (const cit of citations) {
|
|
472
|
+
if (cit.anchor && slugSet.has(cit.anchor)) {
|
|
473
|
+
citedSlugs.add(cit.anchor);
|
|
474
|
+
} else {
|
|
475
|
+
violations.push({
|
|
476
|
+
domain: cit.domain, task: cit.task,
|
|
477
|
+
kind: "unresolvable-section-citation",
|
|
478
|
+
detail: `PseudoCode-Section cites "${title}#${cit.anchor}" but no \`##\` heading in PseudoCode-${title}.md slugifies to that anchor (§3.2 slug-as-slug).`,
|
|
479
|
+
doc: title, anchor: cit.anchor,
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// (b) Uncovered-section gap: a `##` section with zero citing tasks.
|
|
485
|
+
for (const s of doc.sections) {
|
|
486
|
+
if (!citedSlugs.has(s.slug)) {
|
|
487
|
+
violations.push({
|
|
488
|
+
domain: null, task: null,
|
|
489
|
+
kind: "uncovered-section",
|
|
490
|
+
detail: `PseudoCode-${title}.md section "${s.title}" (#${s.slug}) has zero citing tasks — structural coverage gap (§3.1).`,
|
|
491
|
+
doc: title, anchor: s.slug, section: s.title,
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
docReports.push({
|
|
497
|
+
doc: title,
|
|
498
|
+
sectionsTotal: doc.sections.length,
|
|
499
|
+
sectionsCovered: citedSlugs.size,
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
return { violations, docs: docReports };
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function runGate({ projectDir = process.cwd(), milestone = null, tasksFile = null, domains = null, pseudocodeDir = null } = {}) {
|
|
252
507
|
let files;
|
|
253
508
|
if (tasksFile) {
|
|
254
509
|
files = [{ domain: path.basename(path.dirname(tasksFile)), tasksPath: tasksFile }];
|
|
255
510
|
} else {
|
|
256
|
-
|
|
511
|
+
const resolved = listTasksFiles(projectDir, milestone, domains);
|
|
512
|
+
if (resolved.error) {
|
|
513
|
+
return { ok: false, exitCode: 64, milestone, ...resolved.error, tasks: [], violations: [] };
|
|
514
|
+
}
|
|
515
|
+
files = resolved.files;
|
|
257
516
|
}
|
|
258
517
|
if (!files.length) {
|
|
259
518
|
return { ok: false, exitCode: 64, milestone, reason: "no-tasks-files", tasks: [], violations: [] };
|
|
@@ -276,6 +535,26 @@ function runGate({ projectDir = process.cwd(), milestone = null, tasksFile = nul
|
|
|
276
535
|
}
|
|
277
536
|
}
|
|
278
537
|
|
|
538
|
+
// M87 D2: section-citation coverage over the in-scope PseudoCode docs.
|
|
539
|
+
// Containment: an explicit --pseudocode-dir must resolve INSIDE the project
|
|
540
|
+
// tree (the D2 test legitimately points it at test/fixtures/, also inside the
|
|
541
|
+
// project; an out-of-tree `../../evildocs` is refused). The default
|
|
542
|
+
// `.gsd-t/pseudocode/` is always in-tree. A refused dir → empty doc set
|
|
543
|
+
// (logged skip, never an out-of-tree read), same fail-closed shape as a missing dir.
|
|
544
|
+
let resolvedPcDir = pseudocodeDir || path.join(projectDir, ".gsd-t", "pseudocode");
|
|
545
|
+
if (pseudocodeDir) {
|
|
546
|
+
const resolvedProject = path.resolve(projectDir);
|
|
547
|
+
const candidate = path.resolve(resolvedProject, pseudocodeDir);
|
|
548
|
+
if (candidate !== resolvedProject && !candidate.startsWith(resolvedProject + path.sep)) {
|
|
549
|
+
resolvedPcDir = null; // out-of-tree → no docs loaded (fail-closed)
|
|
550
|
+
} else {
|
|
551
|
+
resolvedPcDir = candidate;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
const pseudocodeDocs = resolvedPcDir ? loadPseudocodeDocs(resolvedPcDir) : new Map();
|
|
555
|
+
const coverage = assessSectionCoverage(taskResults, pseudocodeDocs);
|
|
556
|
+
for (const v of coverage.violations) violations.push(v);
|
|
557
|
+
|
|
279
558
|
const ok = violations.length === 0;
|
|
280
559
|
return {
|
|
281
560
|
ok,
|
|
@@ -285,6 +564,7 @@ function runGate({ projectDir = process.cwd(), milestone = null, tasksFile = nul
|
|
|
285
564
|
tasksTotal: taskResults.length,
|
|
286
565
|
behavioral: behavioralCount,
|
|
287
566
|
violations: violations.length,
|
|
567
|
+
sectionCoverageDocs: coverage.docs,
|
|
288
568
|
},
|
|
289
569
|
tasks: taskResults,
|
|
290
570
|
violations,
|
|
@@ -295,30 +575,42 @@ function runGate({ projectDir = process.cwd(), milestone = null, tasksFile = nul
|
|
|
295
575
|
// ─── CLI ─────────────────────────────────────────────────────────────────
|
|
296
576
|
|
|
297
577
|
function parseArgs(argv) {
|
|
298
|
-
const o = { projectDir: process.cwd(), milestone: null, tasksFile: null, help: false };
|
|
578
|
+
const o = { projectDir: process.cwd(), milestone: null, tasksFile: null, domains: null, pseudocodeDir: null, help: false };
|
|
299
579
|
for (let i = 0; i < argv.length; i++) {
|
|
300
580
|
const a = argv[i];
|
|
301
581
|
if (a === "--help" || a === "-h") o.help = true;
|
|
302
582
|
else if (a === "--project-dir") o.projectDir = argv[++i];
|
|
303
583
|
else if (a === "--milestone") o.milestone = argv[++i];
|
|
304
584
|
else if (a === "--tasks") o.tasksFile = argv[++i];
|
|
585
|
+
else if (a === "--domains") {
|
|
586
|
+
o.domains = String(argv[++i] || "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
587
|
+
}
|
|
588
|
+
else if (a === "--pseudocode-dir") o.pseudocodeDir = argv[++i];
|
|
305
589
|
else if (a === "--json") {/* default */}
|
|
306
590
|
}
|
|
307
591
|
return o;
|
|
308
592
|
}
|
|
309
593
|
|
|
310
|
-
const HELP = `Usage: gsd-t traceability-gate [--milestone Mxx] [--project-dir PATH] [--tasks FILE]
|
|
594
|
+
const HELP = `Usage: gsd-t traceability-gate [--milestone Mxx] [--project-dir PATH] [--tasks FILE] [--domains a,b,c] [--pseudocode-dir PATH]
|
|
311
595
|
|
|
312
|
-
Plan-phase acceptance-traceability gate (M83)
|
|
313
|
-
the milestone's .gsd-t/domains/* /tasks.md binds
|
|
314
|
-
implementing **Files** path AND a named test.
|
|
315
|
-
real implementation path and a test.
|
|
596
|
+
Plan-phase acceptance-traceability gate (M83) + section-citation coverage (M87 D2).
|
|
597
|
+
Asserts every behavioral task in the milestone's .gsd-t/domains/* /tasks.md binds
|
|
598
|
+
its acceptance criteria to an implementing **Files** path AND a named test.
|
|
599
|
+
Headline tasks must have BOTH a real implementation path and a test. Additionally,
|
|
600
|
+
when a PseudoCode-[Title].md doc is in scope, every \`##\` section must be cited by
|
|
601
|
+
≥1 task (\`**PseudoCode-Section**: <Title>#<anchor>\`) and every citation must
|
|
602
|
+
resolve to a real heading slug. Blocks execute on any violation.
|
|
316
603
|
|
|
317
|
-
--milestone Mxx
|
|
318
|
-
|
|
319
|
-
--
|
|
604
|
+
--milestone Mxx Scope by mNN-prefix domains (M83). Zero match + no --domains
|
|
605
|
+
→ exit 64 milestone-scope-unresolved (never fall-back-to-all).
|
|
606
|
+
--domains a,b,c Scope to EXACTLY these domain dirs (each must exist + carry a
|
|
607
|
+
tasks.md; a missing named domain is an error, not a drop).
|
|
608
|
+
--project-dir P Project root (default: cwd).
|
|
609
|
+
--tasks FILE Check a single tasks.md (overrides domain discovery).
|
|
610
|
+
--pseudocode-dir P PseudoCode-*.md dir for section coverage (default
|
|
611
|
+
.gsd-t/pseudocode).
|
|
320
612
|
|
|
321
|
-
Exit: 0 all traceable · 4 ≥1 violation · 64
|
|
613
|
+
Exit: 0 all traceable · 4 ≥1 violation · 64 bad input / unresolved scope.`;
|
|
322
614
|
|
|
323
615
|
function main() {
|
|
324
616
|
const o = parseArgs(process.argv.slice(2));
|
|
@@ -335,4 +627,9 @@ function main() {
|
|
|
335
627
|
|
|
336
628
|
if (require.main === module) main();
|
|
337
629
|
|
|
338
|
-
module.exports = {
|
|
630
|
+
module.exports = {
|
|
631
|
+
runGate, parseTasks, assessTask, listTasksFiles,
|
|
632
|
+
parseSectionCitation, slugifyHeading, enumerateSections, docTitleFromFilename,
|
|
633
|
+
loadPseudocodeDocs, assessSectionCoverage,
|
|
634
|
+
_internal: { fieldValue, TEST_PATH_RE },
|
|
635
|
+
};
|
package/bin/gsd-t.js
CHANGED
|
@@ -1268,6 +1268,8 @@ const GLOBAL_BIN_TOOLS = [
|
|
|
1268
1268
|
"gsd-t-architectural-trigger.cjs",
|
|
1269
1269
|
// M90 D2 — Loop ledger (non-convergence detection + halt directive; §3).
|
|
1270
1270
|
"gsd-t-loop-ledger.cjs",
|
|
1271
|
+
// Backlog #40 — deterministic archive+sweep of a completed milestone's domain dirs.
|
|
1272
|
+
"gsd-t-archive-domains.cjs",
|
|
1271
1273
|
];
|
|
1272
1274
|
|
|
1273
1275
|
function installGlobalBinTools() {
|
|
@@ -2579,6 +2581,17 @@ const PROJECT_BIN_TOOLS = [
|
|
|
2579
2581
|
// M90 D2 — Loop ledger (non-convergence detection + halt directive; §3).
|
|
2580
2582
|
// Propagated so project-local runCli helpers (gsd-t-debug.workflow.js) can invoke it.
|
|
2581
2583
|
"gsd-t-loop-ledger.cjs",
|
|
2584
|
+
// Backlog #40 — deterministic archive+sweep of a completed milestone's domain dirs
|
|
2585
|
+
// (complete-milestone Step 7). Propagated so complete-milestone can invoke it project-local.
|
|
2586
|
+
"gsd-t-archive-domains.cjs",
|
|
2587
|
+
// M91 (merged M87+M88) — PseudoCode source-of-truth deterministic gates, propagated so
|
|
2588
|
+
// project-local runCli helpers (gsd-t-verify / gsd-t-milestone workflows) resolve them
|
|
2589
|
+
// without the global binary. guard-map = the [RULE] guard-bridge verify gate (D1);
|
|
2590
|
+
// guard-map-derive = build→map derivation seam (G2); milestone-state = sign-off /
|
|
2591
|
+
// isDefined predicate (G1); rule-consume = triad rule-set consumer (G3);
|
|
2592
|
+
// divergence-grammar = §4 parse/format round-trip (G4).
|
|
2593
|
+
"gsd-t-guard-map.cjs", "gsd-t-guard-map-derive.cjs", "gsd-t-milestone-state.cjs",
|
|
2594
|
+
"gsd-t-rule-consume.cjs", "gsd-t-divergence-grammar.cjs",
|
|
2582
2595
|
];
|
|
2583
2596
|
|
|
2584
2597
|
// Files that older versions of this installer copied into project bin/ but
|
|
@@ -4754,6 +4767,20 @@ if (require.main === module) {
|
|
|
4754
4767
|
});
|
|
4755
4768
|
process.exit(res.status == null ? 1 : res.status);
|
|
4756
4769
|
}
|
|
4770
|
+
case "archive-domains": {
|
|
4771
|
+
// Backlog #40 — `gsd-t archive-domains --domains a,b --archive <dir>` deterministic
|
|
4772
|
+
// archive+sweep of a completed milestone's domain dirs (complete-milestone Step 7).
|
|
4773
|
+
const { spawnSync } = require("child_process");
|
|
4774
|
+
const js = path.join(__dirname, "gsd-t-archive-domains.cjs");
|
|
4775
|
+
if (!require("node:fs").existsSync(js)) {
|
|
4776
|
+
error(`gsd-t-archive-domains.cjs not found at ${js} — install or build backlog #40 first`);
|
|
4777
|
+
process.exit(1);
|
|
4778
|
+
}
|
|
4779
|
+
const res = spawnSync(process.execPath, [js, ...args.slice(1)], {
|
|
4780
|
+
stdio: "inherit",
|
|
4781
|
+
});
|
|
4782
|
+
process.exit(res.status == null ? 1 : res.status);
|
|
4783
|
+
}
|
|
4757
4784
|
case "metrics":
|
|
4758
4785
|
doMetrics(args.slice(1));
|
|
4759
4786
|
break;
|
|
@@ -401,8 +401,21 @@ node scripts/gsd-t-watch-state.js advance --agent-id "$GSD_T_AGENT_ID" --parent-
|
|
|
401
401
|
|
|
402
402
|
Reset `.gsd-t/` for next milestone:
|
|
403
403
|
|
|
404
|
-
1. Archive
|
|
405
|
-
|
|
404
|
+
1. **Archive + sweep the completed milestone's domains — DETERMINISTICALLY (backlog #40, NOT prose).**
|
|
405
|
+
This step was prose-only for ~30 milestones and got skipped, accumulating 77 stale domain dirs
|
|
406
|
+
that polluted the file-disjointness oracle. Run the helper with the EXPLICIT set of THIS
|
|
407
|
+
milestone's domain dirs (the partition's domain set — NOT a blanket wipe; a later still-active
|
|
408
|
+
milestone may legitimately have live domains):
|
|
409
|
+
```bash
|
|
410
|
+
gsd-t archive-domains --domains "{comma-separated domain dir names for THIS milestone}" \
|
|
411
|
+
--archive ".gsd-t/milestones/{name}-{date}" --projectDir .
|
|
412
|
+
```
|
|
413
|
+
(prefer the project-local `bin/gsd-t-archive-domains.cjs` when present). It (a) copies each named
|
|
414
|
+
domain → `<archive>/domains/<name>/`, then (b) removes it from `.gsd-t/domains/`. Idempotent
|
|
415
|
+
(re-running is a no-op), containment-guarded (refuses any name with path separators / dot-segments
|
|
416
|
+
or resolving outside `.gsd-t/domains/`), and fail-closed (a bad name aborts the whole batch — no
|
|
417
|
+
partial sweep). Domains belonging to other (still-active) milestones are left untouched.
|
|
418
|
+
2. (Step 1 already cleared this milestone's domains from `.gsd-t/domains/` — do NOT blanket-wipe the dir.)
|
|
406
419
|
3. Archive current reports → milestone folder
|
|
407
420
|
4. Clear `.gsd-t/impact-report.md`, `.gsd-t/test-coverage.md`
|
|
408
421
|
5. Update `.gsd-t/progress.md`:
|
|
@@ -8,7 +8,7 @@ You are the doc-ripple agent. You identify and update all downstream documents a
|
|
|
8
8
|
preflight → brief (kind=doc-ripple) → doc-ripple agent (opus, with phase protocol)
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
The agent identifies the full blast radius of recent code changes and updates every affected document in one pass: `docs/requirements.md`, `docs/architecture.md`, `docs/workflows.md`, `.gsd-t/contracts/`, owning `scope.md`, `README.md`, and (for this repo) the 4 command-reference files when a command interface changed.
|
|
11
|
+
The agent identifies the full blast radius of recent code changes and updates every affected document in one pass: `docs/requirements.md`, `docs/architecture.md`, `docs/workflows.md`, `.gsd-t/contracts/`, `PseudoCode-[Title].md` (the intention-first behavior map — when the implemented behavior diverges from its pseudocode, ripple the change or write a `⚠ Divergence` flag per the source-of-truth contract §4), owning `scope.md`, `README.md`, and (for this repo) the 4 command-reference files when a command interface changed.
|
|
12
12
|
|
|
13
13
|
## Step 1: Determine the change set
|
|
14
14
|
|
|
@@ -6,10 +6,40 @@ You are the lead agent. Define a new milestone by invoking the generic upper-sta
|
|
|
6
6
|
|
|
7
7
|
```
|
|
8
8
|
preflight → brief (kind=milestone) → milestone agent (opus, with phase protocol)
|
|
9
|
+
→ TWO-ALTITUDE intention-first authoring flow (default-ON):
|
|
10
|
+
1. HIGH-LEVEL APPROACH altitude (what/why/when, actors, one-breath summary)
|
|
11
|
+
→ user signs off the APPROACH
|
|
12
|
+
2. DETAILED altitude — author `PseudoCode-[Title].md` at exemplar granularity
|
|
9
13
|
```
|
|
10
14
|
|
|
11
15
|
The agent defines the milestone — origin, goal, falsifiable success criteria — and appends it to `.gsd-t/progress.md`. Partition is deferred (the Next Up successor). Effort/scope is expressed in GSD-T-native units (domain count, wave count, spawn count) — never developer-hours.
|
|
12
16
|
|
|
17
|
+
## Two-Altitude Intention-First Flow (default-ON, M87)
|
|
18
|
+
|
|
19
|
+
A milestone is authored at **two altitudes, in order** — the high-level approach is signed off BEFORE the detailed decomposition is written. This is the intention-first source-of-truth flow (contract `.gsd-t/contracts/pseudocode-source-of-truth-contract.md` §1). It is a **prose flow** — the command DESCRIBES the sign-off checkpoint below; it does NOT bind a machine-checkable "DEFINED only after sign-off" predicate (that deterministic `isDefined(milestone)` gate is M88).
|
|
20
|
+
|
|
21
|
+
### Altitude 1 — High-Level Approach (signed off FIRST)
|
|
22
|
+
|
|
23
|
+
Before any field-level detail, the milestone agent emits a **high-level approach pseudocode** covering:
|
|
24
|
+
|
|
25
|
+
- **What / Why / When** — the directive the milestone serves, in the user's own intention (never agent reasoning).
|
|
26
|
+
- **Actors** — the parties/components/realms the approach touches.
|
|
27
|
+
- **One-breath summary** — "one call in one breath": the whole approach stated as a single coherent thought.
|
|
28
|
+
|
|
29
|
+
The agent then **presents this approach to the user for SIGN-OFF**. The user signs off the APPROACH before the detailed doc is written. This sign-off checkpoint is the gate between the two altitudes — the detailed `PseudoCode-[Title].md` is only authored AFTER the approach is approved.
|
|
30
|
+
|
|
31
|
+
### Altitude 2 — Detailed `PseudoCode-[Title].md`
|
|
32
|
+
|
|
33
|
+
Only after the approach is signed off, the agent authors the detailed `PseudoCode-[Title].md` at exemplar granularity (the five section elements of contract §1: Intention, Mechanism, one-breath summary table, Guard map, Divergence flags, Appendix). `[Title]` is the SUBJECT the doc represents (e.g. `PseudoCode-PayPal.md`), never a milestone id; a milestone may produce several. Per-milestone docs live at `.gsd-t/pseudocode/PseudoCode-[Title].md` (contract §7).
|
|
34
|
+
|
|
35
|
+
### Default-ON; skip is a LOGGED decision
|
|
36
|
+
|
|
37
|
+
The two-altitude flow is **default-ON**. Skipping the detailed-doc altitude is a **LOGGED decision** in `.gsd-t/progress.md` (Decision Log entry naming WHY it was skipped) — **never a silent default-off** (`feedback_no_silent_degradation`). A skip that is not logged is a process failure.
|
|
38
|
+
|
|
39
|
+
### Keep-or-supersede on inherited shipped code
|
|
40
|
+
|
|
41
|
+
When the milestone inherits a model from shipped code, the agent runs the **keep-or-supersede** protocol (`templates/prompts/keep-or-supersede-subagent.md`): per inherited model, ASK keep or supersede. Each **supersede** WRITES a `⚠ Divergence` flag into the doc (contract §4 grammar shape). Keep = no flag. The deterministic divergence-grammar round-trip is M88; the ASK + flag-writing is the M87 prose protocol.
|
|
42
|
+
|
|
13
43
|
## Step 1: Load context
|
|
14
44
|
|
|
15
45
|
Read `.gsd-t/progress.md` (current version + completed milestones), `docs/requirements.md`, and `docs/architecture.md` so the new milestone is framed against existing state.
|
|
@@ -75,7 +105,9 @@ The Workflow returns `{ status, artifacts, summary, decisions }` (plus `competit
|
|
|
75
105
|
|
|
76
106
|
## Document Ripple
|
|
77
107
|
|
|
78
|
-
The milestone agent appends the milestone definition + a Decision Log entry to `.gsd-t/progress.md
|
|
108
|
+
The milestone agent appends the milestone definition + a Decision Log entry to `.gsd-t/progress.md`, and:
|
|
109
|
+
|
|
110
|
+
- **`PseudoCode-[Title].md`** (at `.gsd-t/pseudocode/PseudoCode-[Title].md`) — the detailed intention-first source-of-truth doc authored at Altitude 2 (one per coherent subject; a milestone may produce several). Written only after the Altitude-1 approach is signed off. A skip is a LOGGED Decision Log entry, never a silent omission.
|
|
79
111
|
|
|
80
112
|
## Next Up
|
|
81
113
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tekyzinc/gsd-t",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.8.10",
|
|
4
4
|
"description": "GSD-T: Contract-Driven Development for Claude Code — 54 slash commands with headless-by-default workflow spawning, unattended supervisor relay with event stream, graph-powered code analysis, real-time agent dashboard, task telemetry, doc-ripple enforcement, backlog management, impact analysis, test sync, milestone archival, and PRD generation",
|
|
5
5
|
"author": "Tekyz, Inc.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -66,6 +66,7 @@ These documents MUST be maintained and referenced throughout development:
|
|
|
66
66
|
| **README** | `README.md` | Project overview, setup, features |
|
|
67
67
|
| **Progress** | `.gsd-t/progress.md` | Current milestone/phase state + version |
|
|
68
68
|
| **Contracts** | `.gsd-t/contracts/` | Interfaces between domains |
|
|
69
|
+
| **PseudoCode** | `.gsd-t/pseudocode/PseudoCode-[Title].md` | Intention-first behavior map — the milestone source-of-truth (authored before the build; rippled when code/contract/schema changes) |
|
|
69
70
|
| **Tech Debt** | `.gsd-t/techdebt.md` | Debt register from scans |
|
|
70
71
|
|
|
71
72
|
## The "No Re-Research" Rule
|
|
@@ -278,6 +279,7 @@ NEVER commit without this checklist. For each trigger that fired, do the action
|
|
|
278
279
|
- **UI component interface changed** → `.gsd-t/contracts/component-contract.md`.
|
|
279
280
|
- **New files/dirs** → owning domain's `scope.md`.
|
|
280
281
|
- **Requirement implemented/changed** → `docs/requirements.md`.
|
|
282
|
+
- **Behavior/intention changed vs. the signed-off pseudocode** → update the owning `PseudoCode-[Title].md` (the milestone source-of-truth) in the same pass; a supersede writes a `⚠ Divergence` flag.
|
|
281
283
|
- **Component or data-flow changed** → `docs/architecture.md`.
|
|
282
284
|
- **ANY document/script/code file modified** → timestamped `.gsd-t/progress.md` Decision Log entry (`- YYYY-MM-DD HH:MM: {what} — {result}`); covers every workflow command AND manual edits. Architectural decision → include rationale in that entry.
|
|
283
285
|
- **Tech debt found/fixed** → `.gsd-t/techdebt.md`.
|