yadflow 2.17.0 → 2.18.1
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 +3 -3
- package/README.md +60 -638
- package/bin/yad.mjs +24 -0
- package/cli/docs.mjs +6 -5
- package/cli/doctor.mjs +31 -1
- package/cli/epic-state.mjs +152 -0
- package/cli/manifest.mjs +6 -0
- package/cli/thread.mjs +174 -0
- package/package.json +5 -4
- package/skills/sdlc/config.yaml +46 -1
- package/skills/sdlc/install.sh +1 -1
- package/skills/sdlc/module-help.csv +4 -0
- package/skills/yad-change/SKILL.md +174 -0
- package/skills/yad-change/references/triage.md +102 -0
- package/skills/yad-checks/SKILL.md +13 -1
- package/skills/yad-checks/references/check-gates.md +27 -0
- package/skills/yad-checks/templates/checks/epic-open.sh +98 -0
- package/skills/yad-checks/templates/checks/lineage-check.sh +97 -0
- package/skills/yad-checks/templates/checks/reconcile-debt-check.sh +105 -0
- package/skills/yad-checks/templates/github/yad-checks.yml +25 -0
- package/skills/yad-checks/templates/gitlab/yad-checks.gitlab-ci.yml +20 -0
- package/skills/yad-defects/SKILL.md +79 -0
- package/skills/yad-docs-overview/SKILL.md +1 -1
- package/skills/yad-docs-overview/references/pipeline-model.md +22 -3
- package/skills/yad-epic/references/state-schema.md +106 -0
- package/skills/yad-reconcile/SKILL.md +75 -0
- package/skills/yad-timeline/SKILL.md +78 -0
package/bin/yad.mjs
CHANGED
|
@@ -15,6 +15,7 @@ import { runDocs } from '../cli/docs.mjs';
|
|
|
15
15
|
import { runDoctor } from '../cli/doctor.mjs';
|
|
16
16
|
import { runNext } from '../cli/next.mjs';
|
|
17
17
|
import { syncStatuses } from '../cli/artifact-status.mjs';
|
|
18
|
+
import { runThread, runReconcile } from '../cli/thread.mjs';
|
|
18
19
|
|
|
19
20
|
const HELP = `${c.bold('yad')} — setup, review-gate & build helpers for the SDLC Workflow module ${c.dim('v' + VERSION)}
|
|
20
21
|
|
|
@@ -63,6 +64,12 @@ ${c.bold('Build helpers')}
|
|
|
63
64
|
yad repo list Show connected repos (fresh / stale)
|
|
64
65
|
yad repo refresh [name] Re-pack a stale repo (a human decision)
|
|
65
66
|
|
|
67
|
+
${c.bold('Feature threads (post-lock change management)')}
|
|
68
|
+
yad thread List every feature thread (genesis → changes → defects)
|
|
69
|
+
yad thread <epic> [--json] Show one thread: its epics, the resolved current truth, open debt
|
|
70
|
+
yad reconcile [check|refresh|wire] Flag orphan drift + open hotfix debt across threads (advisory,
|
|
71
|
+
never a gate — the gates block at merge)
|
|
72
|
+
|
|
66
73
|
${c.bold('Interactive docs (generated sites)')}
|
|
67
74
|
yad docs list Show the docs target + per-site freshness
|
|
68
75
|
yad docs build [--epic <id>|--overview] npm-build a generated doc site
|
|
@@ -216,6 +223,23 @@ async function main() {
|
|
|
216
223
|
await runDocs(o.dir, { action: action || 'list', epic: o.epic, overview: o.overview, sync, today });
|
|
217
224
|
break;
|
|
218
225
|
}
|
|
226
|
+
case 'thread': {
|
|
227
|
+
const [, epic] = o._;
|
|
228
|
+
if (epic && !isValidEpicId(epic)) { log(c.red(`invalid epic id: ${epic} (expected EP-<slug>, [a-z0-9-] only)`)); process.exitCode = 1; break; }
|
|
229
|
+
await runThread(o.dir, { epic, json: o.json });
|
|
230
|
+
break;
|
|
231
|
+
}
|
|
232
|
+
case 'reconcile': {
|
|
233
|
+
const [, action] = o._;
|
|
234
|
+
const thread = o.epic || o.thread || null;
|
|
235
|
+
if (thread && !isValidEpicId(thread)) { log(c.red(`invalid epic id: ${thread} (expected EP-<slug>, [a-z0-9-] only)`)); process.exitCode = 1; break; }
|
|
236
|
+
const act = action || (o.wire ? 'wire' : o.refresh ? 'refresh' : 'check');
|
|
237
|
+
if (!['check', 'refresh', 'wire'].includes(act)) {
|
|
238
|
+
log(c.red(`unknown reconcile action: ${act} (check|refresh|wire)`)); process.exitCode = 1; break;
|
|
239
|
+
}
|
|
240
|
+
await runReconcile(o.dir, { action: act, thread });
|
|
241
|
+
break;
|
|
242
|
+
}
|
|
219
243
|
default:
|
|
220
244
|
log(c.red(`unknown command: ${cmd}`));
|
|
221
245
|
log(HELP);
|
package/cli/docs.mjs
CHANGED
|
@@ -121,6 +121,7 @@ export function docsStale(manifest, { artifactHash, repoHeads = {}, templateVers
|
|
|
121
121
|
const BUILD_PUBLIC = [
|
|
122
122
|
'mkdir -p public',
|
|
123
123
|
'if [ -d docs/sdlc-site ]; then (cd docs/sdlc-site && npm ci && npm run build) && mkdir -p public/app && cp -r docs/sdlc-site/dist/. public/app/ && cp docs/sdlc-site/public/report.html public/index.html && cp docs/sdlc-site/public/report.html public/report.html; fi',
|
|
124
|
+
'if [ -d docs/tutorial-site ]; then (cd docs/tutorial-site && npm ci && npm run build) && mkdir -p public/tutorial && cp -r docs/tutorial-site/dist/. public/tutorial/; fi',
|
|
124
125
|
'for d in epics/*/docs-site; do [ -d "$d" ] || continue; id=$(basename "$(dirname "$d")"); (cd "$d" && npm ci && npm run build) && mkdir -p "public/epics/$id" && cp -r "$d/dist/." "public/epics/$id/"; done',
|
|
125
126
|
];
|
|
126
127
|
export function pagesWorkflow(platform) {
|
|
@@ -160,19 +161,19 @@ jobs:
|
|
|
160
161
|
name: github-pages
|
|
161
162
|
url: \${{ steps.deployment.outputs.page_url }}
|
|
162
163
|
steps:
|
|
163
|
-
- uses: actions/checkout@
|
|
164
|
-
- uses: actions/setup-node@
|
|
164
|
+
- uses: actions/checkout@v7
|
|
165
|
+
- uses: actions/setup-node@v6
|
|
165
166
|
with:
|
|
166
167
|
node-version: 20
|
|
167
168
|
- name: Build the overview + per-epic sites into ./public
|
|
168
169
|
run: |
|
|
169
170
|
${BUILD_PUBLIC.map((l) => ` ${l}`).join('\n')}
|
|
170
|
-
- uses: actions/configure-pages@
|
|
171
|
-
- uses: actions/upload-pages-artifact@
|
|
171
|
+
- uses: actions/configure-pages@v6
|
|
172
|
+
- uses: actions/upload-pages-artifact@v5
|
|
172
173
|
with:
|
|
173
174
|
path: public
|
|
174
175
|
- id: deployment
|
|
175
|
-
uses: actions/deploy-pages@
|
|
176
|
+
uses: actions/deploy-pages@v5
|
|
176
177
|
`;
|
|
177
178
|
}
|
|
178
179
|
export function pagesWorkflowPath(platform) {
|
package/cli/doctor.mjs
CHANGED
|
@@ -6,7 +6,8 @@ import path from 'node:path';
|
|
|
6
6
|
import fs from 'node:fs';
|
|
7
7
|
import { c, log, ok, info, warn, fail, hand, run, has, exists, readJSON, readJSONStrict } from './lib.mjs';
|
|
8
8
|
import { VERSION, PROJECT_FILES, DESIGN_TOOLS, TESTING_TOOLS, LEARNING_TOOLS } from './manifest.mjs';
|
|
9
|
-
import { loadLedger, epicRoot } from './epic-state.mjs';
|
|
9
|
+
import { loadLedger, epicRoot, isValidEpicId, epicLineage, resolveThread } from './epic-state.mjs';
|
|
10
|
+
import { loadDebt } from './thread.mjs';
|
|
10
11
|
import { gitHead } from './setup.mjs';
|
|
11
12
|
import { cliFor, validateLogin, hostFromGitUrl } from './platform.mjs';
|
|
12
13
|
|
|
@@ -264,11 +265,40 @@ export function epicChecks(checks, root) {
|
|
|
264
265
|
}
|
|
265
266
|
}
|
|
266
267
|
|
|
268
|
+
// Phase 6 — feature-thread integrity. A change-epic must thread to a real parent and its denormalized
|
|
269
|
+
// `thread` cache must equal the computed root; an open hotfix reconcile-debt is a warn (the next change
|
|
270
|
+
// on that thread is blocked at the gate until it is paid). Pure reporting, like the other sections.
|
|
271
|
+
export function threadChecks(checks, root) {
|
|
272
|
+
const epicsDir = path.join(root, 'epics');
|
|
273
|
+
if (!exists(epicsDir)) return;
|
|
274
|
+
for (const e of fs.readdirSync(epicsDir).sort()) {
|
|
275
|
+
if (!fs.statSync(path.join(epicsDir, e)).isDirectory() || !isValidEpicId(e)) continue;
|
|
276
|
+
if (!exists(path.join(epicsDir, e, 'epic.md'))) continue;
|
|
277
|
+
const lin = epicLineage(root, e);
|
|
278
|
+
if (lin.kind === 'feature' && !lin.parent) continue; // genesis with no lineage — nothing to check
|
|
279
|
+
const { broken } = resolveThread(root, e);
|
|
280
|
+
if (broken) {
|
|
281
|
+
check(checks, `thread:${e}`, 'threads', 'fail', `${e}: ${broken}`,
|
|
282
|
+
'a change-epic must thread to a real parent; fix `parent:`/`thread:` in epic.md frontmatter');
|
|
283
|
+
} else {
|
|
284
|
+
check(checks, `thread:${e}`, 'threads', 'ok', `${e}: ${lin.kind} threaded to ${lin.thread || lin.parent}`);
|
|
285
|
+
}
|
|
286
|
+
for (const d of loadDebt(root, e)) {
|
|
287
|
+
if (d.status === 'open') {
|
|
288
|
+
check(checks, `thread:${e}:debt`, 'threads', 'warn',
|
|
289
|
+
`${e}: open reconcile debt (${d.reason || 'hotfix shipped first'})`,
|
|
290
|
+
'pay it — update the artifacts + add a regression test; the next change on this thread is blocked until then');
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
267
296
|
export async function runDoctor(root, { json = false } = {}) {
|
|
268
297
|
const checks = [];
|
|
269
298
|
envChecks(checks);
|
|
270
299
|
projectChecks(checks, root);
|
|
271
300
|
epicChecks(checks, root);
|
|
301
|
+
threadChecks(checks, root);
|
|
272
302
|
|
|
273
303
|
const failed = checks.filter((x) => x.status === 'fail');
|
|
274
304
|
const warned = checks.filter((x) => x.status === 'warn');
|
package/cli/epic-state.mjs
CHANGED
|
@@ -190,6 +190,21 @@ export function gatePredicate({
|
|
|
190
190
|
merged = true,
|
|
191
191
|
solo = false,
|
|
192
192
|
}) {
|
|
193
|
+
// Phase 6: an INHERITED step (a change-epic carrying a parent artifact by reference) is satisfied
|
|
194
|
+
// without re-review — its approval lives upstream in the thread, recorded as an `inherited` provenance
|
|
195
|
+
// entry. It is pre-marked `done` in state.json, so the gate is normally never invoked on it; this
|
|
196
|
+
// short-circuit makes a direct call safe and surfaces a corrupted boundHash (a referenced artifact
|
|
197
|
+
// cannot change under the child, so a mismatch is corruption — re-thread, do not silently pass).
|
|
198
|
+
if (step?.inherited) {
|
|
199
|
+
const drift = step.boundHash && currentHash && step.boundHash !== currentHash;
|
|
200
|
+
return {
|
|
201
|
+
approvalsSatisfied: true, threadsResolved: true, merged: true, staleDropped: 0,
|
|
202
|
+
passed: !drift,
|
|
203
|
+
missing: drift ? [`inherited artifact drifted from ${step.inheritedFrom || 'parent'} — re-thread`] : [],
|
|
204
|
+
rule: 'inherited',
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
193
208
|
const forStep = approvals.filter((a) => a.step === step.id && a.status === 'approved');
|
|
194
209
|
// Revoke-on-change: an approval bound to a stale content hash no longer counts.
|
|
195
210
|
const stale = forStep.filter((a) => a.artifactHash && currentHash && a.artifactHash !== currentHash);
|
|
@@ -372,4 +387,141 @@ export function nextAction(ledger, { epic } = {}) {
|
|
|
372
387
|
why: pr ? `review PR #${pr.number} is open — sync its state to advance` : `${step.id} is open — create the review PR/MR` };
|
|
373
388
|
}
|
|
374
389
|
|
|
390
|
+
// ---- Phase 6: feature threads (lineage frontmatter on epic.md) -----------------------------------
|
|
391
|
+
|
|
392
|
+
// Minimal frontmatter reader (key: value, and `inherits: [a, b]` arrays). Mirrors gate.mjs's reader so
|
|
393
|
+
// the thread helpers and the gate agree on the same parse; shared here as the lineage source.
|
|
394
|
+
export function readFrontmatter(file) {
|
|
395
|
+
if (!fs.existsSync(file)) return {};
|
|
396
|
+
const m = fs.readFileSync(file, 'utf8').match(/^---\n([\s\S]*?)\n---/);
|
|
397
|
+
if (!m) return {};
|
|
398
|
+
const out = {};
|
|
399
|
+
for (const line of m[1].split('\n')) {
|
|
400
|
+
const kv = line.match(/^(\w[\w-]*):\s*(.*)$/);
|
|
401
|
+
if (!kv) continue;
|
|
402
|
+
const [, k, v] = kv;
|
|
403
|
+
out[k] = /^\[.*\]$/.test(v) ? v.slice(1, -1).split(',').map((s) => s.trim()).filter(Boolean) : v.trim();
|
|
404
|
+
}
|
|
405
|
+
return out;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const asList = (v) => (Array.isArray(v) ? v : v ? [v] : []);
|
|
409
|
+
|
|
410
|
+
// The lineage of an epic from epic.md frontmatter. `kind` defaults to `feature` (genesis) when absent,
|
|
411
|
+
// so an un-migrated genesis epic behaves as the thread root. Greenfield/missing-safe.
|
|
412
|
+
export function epicLineage(root, epic) {
|
|
413
|
+
const fm = readFrontmatter(path.join(epicRoot(root, epic), 'epic.md'));
|
|
414
|
+
return {
|
|
415
|
+
kind: fm.kind || 'feature',
|
|
416
|
+
parent: fm.parent || null,
|
|
417
|
+
thread: fm.thread || null,
|
|
418
|
+
inherits: asList(fm.inherits),
|
|
419
|
+
supersedes: asList(fm.supersedes),
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// Walk `parent` to the thread root. Cycle- and missing-safe. Returns the genesis-first `chain`, the
|
|
424
|
+
// computed `rootId`, and a `broken` reason (missing parent dir, a cycle, or a denormalized `thread`
|
|
425
|
+
// cache that disagrees with the computed root) — the signal yad doctor / yad next --check report.
|
|
426
|
+
export function resolveThread(root, epicId) {
|
|
427
|
+
const chain = [];
|
|
428
|
+
const seen = new Set();
|
|
429
|
+
let cur = epicId;
|
|
430
|
+
let broken = null;
|
|
431
|
+
while (cur) {
|
|
432
|
+
if (seen.has(cur)) { broken = `cycle at ${cur}`; break; }
|
|
433
|
+
seen.add(cur);
|
|
434
|
+
if (!fs.existsSync(epicRoot(root, cur))) {
|
|
435
|
+
broken = cur === epicId ? `missing epic ${cur}` : `missing parent epic ${cur}`;
|
|
436
|
+
break;
|
|
437
|
+
}
|
|
438
|
+
chain.unshift(cur); // genesis ends up first
|
|
439
|
+
const { parent } = epicLineage(root, cur);
|
|
440
|
+
if (!parent) break; // reached genesis
|
|
441
|
+
cur = parent;
|
|
442
|
+
}
|
|
443
|
+
const rootId = chain[0] || epicId;
|
|
444
|
+
const tip = epicLineage(root, epicId);
|
|
445
|
+
// A non-genesis epic (has a parent) MUST carry a `thread:` cache that equals the computed root.
|
|
446
|
+
// A missing cache is corruption too — without it the bash gates' parent-walk is the only safety net,
|
|
447
|
+
// and a tool reading the field would mis-scope the thread.
|
|
448
|
+
if (!broken && tip.parent && !tip.thread) {
|
|
449
|
+
broken = `missing thread cache on ${epicId} (kind:${tip.kind}, parent:${tip.parent}) — should be '${rootId}'`;
|
|
450
|
+
}
|
|
451
|
+
if (!broken && tip.thread && tip.thread !== rootId) {
|
|
452
|
+
broken = `thread cache '${tip.thread}' != computed root '${rootId}'`;
|
|
453
|
+
}
|
|
454
|
+
return { rootId, chain, broken };
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// Every epic that belongs to a thread (resolved root == this thread's root), ordered genesis-first by
|
|
458
|
+
// chain depth. Derived by scanning epics/ — no duplicated thread registry. Used by yad-timeline/yad-defects.
|
|
459
|
+
export function threadEpics(root, threadOrEpicId) {
|
|
460
|
+
const { rootId } = resolveThread(root, threadOrEpicId);
|
|
461
|
+
const dir = path.join(root, 'epics');
|
|
462
|
+
if (!fs.existsSync(dir)) return [rootId];
|
|
463
|
+
const depth = new Map();
|
|
464
|
+
const members = [];
|
|
465
|
+
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
466
|
+
if (!e.isDirectory() || !isValidEpicId(e.name) || !fs.existsSync(path.join(dir, e.name, 'epic.md'))) continue;
|
|
467
|
+
const rt = resolveThread(root, e.name); // one walk per member (not per comparison)
|
|
468
|
+
if (rt.rootId !== rootId) continue;
|
|
469
|
+
members.push(e.name);
|
|
470
|
+
depth.set(e.name, rt.chain.length);
|
|
471
|
+
}
|
|
472
|
+
// Genesis-first by depth, then a STABLE, machine-independent tie-break (id order) so the resolver is
|
|
473
|
+
// deterministic across filesystems even when two epics sit at the same depth (a branch).
|
|
474
|
+
return members.sort((a, b) => (depth.get(a) - depth.get(b)) || a.localeCompare(b));
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// Compose the CURRENT authoritative source per artifact base across a thread: the LATEST epic in the
|
|
478
|
+
// chain that actually RE-AUTHORED it (did NOT list it in `inherits`). Genesis owns everything; a later
|
|
479
|
+
// change-epic shadows only what it re-authored. Returns { <base>: <owning epic id> } — the source-of-
|
|
480
|
+
// truth map AI/humans read for the next change (rendered by yad-timeline as thread-resolved.md).
|
|
481
|
+
export const THREAD_ARTIFACT_BASES = ['epic', 'architecture', 'contract', 'ui-design', 'stories', 'test-cases'];
|
|
482
|
+
// REPLACE bases — a re-author supersedes the prior version wholesale, so the LATEST re-author owns it
|
|
483
|
+
// (a contract-surface change re-locks and replaces; a re-authored architecture supersedes the old one).
|
|
484
|
+
const REPLACE_BASES = ['epic', 'architecture', 'contract', 'ui-design'];
|
|
485
|
+
// ADDITIVE bases — each re-authoring epic CONTRIBUTES (stories add files; a change adds its test-cases
|
|
486
|
+
// file), so the current truth is the UNION of contributors, never a single owner. Collapsing these to
|
|
487
|
+
// one epic would drop the parent's inherited stories/cases.
|
|
488
|
+
const ADDITIVE_BASES = ['stories', 'test-cases'];
|
|
489
|
+
|
|
490
|
+
// The owning epic per artifact base across a thread. REPLACE bases resolve to a single epic id (the
|
|
491
|
+
// latest re-author); ADDITIVE bases resolve to the ordered LIST of every epic that re-authored them
|
|
492
|
+
// (genesis-first) — use resolveCurrentStories for story-id-level ownership of the composed set.
|
|
493
|
+
export function resolveCurrentArtifacts(root, threadOrEpicId) {
|
|
494
|
+
const members = threadEpics(root, threadOrEpicId); // genesis-first
|
|
495
|
+
const out = {};
|
|
496
|
+
for (const b of REPLACE_BASES) out[b] = null;
|
|
497
|
+
for (const b of ADDITIVE_BASES) out[b] = [];
|
|
498
|
+
for (const id of members) {
|
|
499
|
+
const { inherits } = epicLineage(root, id);
|
|
500
|
+
for (const b of REPLACE_BASES) if (!inherits.includes(b)) out[b] = id;
|
|
501
|
+
for (const b of ADDITIVE_BASES) if (!inherits.includes(b)) out[b].push(id);
|
|
502
|
+
}
|
|
503
|
+
return out;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// Compose the current STORY SET at story-id granularity across the thread: each re-authoring epic's
|
|
507
|
+
// stories/ files are overlaid (a later same-id supersedes; a `supersedes:` entry retires a parent
|
|
508
|
+
// story). Returns { <story-id>: <owning epic id> } — the real current truth for stories, because a
|
|
509
|
+
// change-epic re-authors only the stories it changes and inherits the rest by reference. Without this,
|
|
510
|
+
// a defect-fix that adds one regression story would appear to drop every unchanged parent story.
|
|
511
|
+
export function resolveCurrentStories(root, threadOrEpicId) {
|
|
512
|
+
const members = threadEpics(root, threadOrEpicId); // genesis-first
|
|
513
|
+
const owner = {};
|
|
514
|
+
for (const id of members) {
|
|
515
|
+
const lin = epicLineage(root, id);
|
|
516
|
+
for (const sid of lin.supersedes) delete owner[sid]; // explicitly retired parent stories
|
|
517
|
+
if (lin.inherits.includes('stories')) continue; // inherited wholesale -> contributes nothing new
|
|
518
|
+
const sdir = path.join(epicRoot(root, id), 'stories');
|
|
519
|
+
if (!fs.existsSync(sdir)) continue;
|
|
520
|
+
for (const f of fs.readdirSync(sdir).filter((x) => /\.md$/.test(x))) {
|
|
521
|
+
owner[f.replace(/\.md$/, '')] = id; // contribute / override same-id
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
return owner;
|
|
525
|
+
}
|
|
526
|
+
|
|
375
527
|
export { writeJSON };
|
package/cli/manifest.mjs
CHANGED
|
@@ -157,6 +157,9 @@ export const epicFiles = (epicRoot) => ({
|
|
|
157
157
|
comments: `${epicRoot}/.sdlc/comments.json`,
|
|
158
158
|
hubPrs: `${epicRoot}/.sdlc/hub-prs.json`,
|
|
159
159
|
contractLock: `${epicRoot}/.sdlc/contract-lock.json`,
|
|
160
|
+
buildLog: `${epicRoot}/.sdlc/build-log.json`,
|
|
161
|
+
change: `${epicRoot}/.sdlc/change.json`, // Phase 6 — change/defect intake + triage
|
|
162
|
+
reconcileDebt: `${epicRoot}/.sdlc/reconcile-debt.json`, // Phase 6 — hotfix ship-first debt
|
|
160
163
|
});
|
|
161
164
|
|
|
162
165
|
// Per-repo wiring: src is relative to PKG_ROOT, dest relative to the repo root.
|
|
@@ -166,6 +169,9 @@ export const REPO_WIRING = {
|
|
|
166
169
|
{ src: 'skills/yad-checks/templates/checks/spec-link.sh', dest: 'checks/spec-link.sh', exec: true },
|
|
167
170
|
{ src: 'skills/yad-checks/templates/checks/contract-check.sh', dest: 'checks/contract-check.sh', exec: true },
|
|
168
171
|
{ src: 'skills/yad-checks/templates/checks/build-test-lint.sh', dest: 'checks/build-test-lint.sh', exec: true },
|
|
172
|
+
{ src: 'skills/yad-checks/templates/checks/lineage-check.sh', dest: 'checks/lineage-check.sh', exec: true },
|
|
173
|
+
{ src: 'skills/yad-checks/templates/checks/epic-open.sh', dest: 'checks/epic-open.sh', exec: true },
|
|
174
|
+
{ src: 'skills/yad-checks/templates/checks/reconcile-debt-check.sh', dest: 'checks/reconcile-debt-check.sh', exec: true },
|
|
169
175
|
{ src: 'skills/yad-checks/templates/checks/verified-commits.sh', dest: 'checks/verified-commits.sh', exec: true },
|
|
170
176
|
{ src: 'skills/yad-checks/templates/checks/commit-message.sh', dest: 'checks/commit-message.sh', exec: true },
|
|
171
177
|
{ src: 'skills/yad-pr-template/templates/checks/risk-route.sh', dest: 'checks/risk-route.sh', exec: true },
|
package/cli/thread.mjs
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
// `yad thread` (read-only: print a feature thread + its resolved current artifacts) and
|
|
2
|
+
// `yad reconcile` (Phase 6 change-reconciler: flag orphan drift + open hotfix debt, never a gate —
|
|
3
|
+
// mirrors `yad docs sync`). The hard merge BLOCK is the CI gates (lineage-check / reconcile-debt);
|
|
4
|
+
// this only DISCOVERS, exactly as yad-docs-sync flags and the build gates block. Node built-ins only.
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import { c, log, ok, info, warn, hand, readJSON, exists } from './lib.mjs';
|
|
8
|
+
import {
|
|
9
|
+
epicRoot, isValidEpicId, epicLineage, readFrontmatter,
|
|
10
|
+
resolveThread, threadEpics, resolveCurrentArtifacts, resolveCurrentStories, THREAD_ARTIFACT_BASES,
|
|
11
|
+
} from './epic-state.mjs';
|
|
12
|
+
|
|
13
|
+
// ---- file readers (all derived; no DB) -----------------------------------------------------------
|
|
14
|
+
|
|
15
|
+
export const loadChange = (root, epic) => readJSON(path.join(epicRoot(root, epic), '.sdlc', 'change.json'), null);
|
|
16
|
+
export const loadDebt = (root, epic) => {
|
|
17
|
+
const v = readJSON(path.join(epicRoot(root, epic), '.sdlc', 'reconcile-debt.json'), []);
|
|
18
|
+
return Array.isArray(v) ? v : [];
|
|
19
|
+
};
|
|
20
|
+
export const loadBuildLog = (root, epic) => readJSON(path.join(epicRoot(root, epic), '.sdlc', 'build-log.json'), null);
|
|
21
|
+
|
|
22
|
+
// An epic is SEALED once every authored story is `shipped` (config.yaml change.seal_on). A sealed epic
|
|
23
|
+
// refuses new behaviour (epic-open.sh) — a further change must open a new threaded change-epic, which is
|
|
24
|
+
// what keeps the front artifacts from going stale. An epic with no stories is NOT sealed (nothing built).
|
|
25
|
+
export function sealedEpic(root, epic) {
|
|
26
|
+
const dir = path.join(epicRoot(root, epic), 'stories');
|
|
27
|
+
if (!exists(dir)) return false;
|
|
28
|
+
const stories = fs.readdirSync(dir, { withFileTypes: true })
|
|
29
|
+
.filter((e) => e.isFile() && /\.md$/.test(e.name)).map((e) => e.name);
|
|
30
|
+
if (!stories.length) return false;
|
|
31
|
+
return stories.every((f) => readFrontmatter(path.join(dir, f)).status === 'shipped');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// The OPEN hotfix debt across a thread (every epic that shares the root). An open entry blocks the next
|
|
35
|
+
// normal change on the thread until paid (reconcile-debt-check.sh enforces; this reports).
|
|
36
|
+
export function openDebtOnThread(root, threadOrEpic) {
|
|
37
|
+
const out = [];
|
|
38
|
+
for (const id of threadEpics(root, threadOrEpic)) {
|
|
39
|
+
for (const d of loadDebt(root, id)) if (d.status === 'open') out.push({ ...d, epicId: d.epicId || id });
|
|
40
|
+
}
|
|
41
|
+
return out;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ---- thread summary (what `yad thread` + yad-status render) --------------------------------------
|
|
45
|
+
|
|
46
|
+
export function threadSummary(root, threadOrEpic) {
|
|
47
|
+
const { rootId, broken } = resolveThread(root, threadOrEpic);
|
|
48
|
+
const members = threadEpics(root, rootId);
|
|
49
|
+
const nodes = members.map((id) => {
|
|
50
|
+
const lin = epicLineage(root, id);
|
|
51
|
+
const state = readJSON(path.join(epicRoot(root, id), '.sdlc', 'state.json'), null);
|
|
52
|
+
const change = loadChange(root, id);
|
|
53
|
+
return {
|
|
54
|
+
id, kind: lin.kind, parent: lin.parent, inherits: lin.inherits,
|
|
55
|
+
currentStep: state?.currentStep || 'unseeded',
|
|
56
|
+
sealed: sealedEpic(root, id),
|
|
57
|
+
depth: change?.depth || null,
|
|
58
|
+
defect: change?.defect || null,
|
|
59
|
+
brokenThread: resolveThread(root, id).broken || null,
|
|
60
|
+
};
|
|
61
|
+
});
|
|
62
|
+
return {
|
|
63
|
+
thread: rootId,
|
|
64
|
+
broken,
|
|
65
|
+
nodes,
|
|
66
|
+
resolved: resolveCurrentArtifacts(root, rootId),
|
|
67
|
+
resolvedStories: resolveCurrentStories(root, rootId),
|
|
68
|
+
openDebt: openDebtOnThread(root, rootId),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const KIND_TAG = { feature: c.green('feature'), change: c.cyan('change'), defect: c.yellow('defect'), hotfix: c.red('hotfix') };
|
|
73
|
+
|
|
74
|
+
export async function runThread(root, { epic, json = false } = {}) {
|
|
75
|
+
if (!epic) {
|
|
76
|
+
// List every distinct thread root in the project.
|
|
77
|
+
const dir = path.join(root, 'epics');
|
|
78
|
+
if (!exists(dir)) { log(c.red('no epics/ directory')); process.exitCode = 1; return; }
|
|
79
|
+
const roots = new Set();
|
|
80
|
+
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
81
|
+
if (e.isDirectory() && isValidEpicId(e.name) && exists(path.join(dir, e.name, 'epic.md'))) {
|
|
82
|
+
roots.add(resolveThread(root, e.name).rootId);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
log(c.bold('\nFeature threads'));
|
|
86
|
+
for (const r of [...roots].sort()) {
|
|
87
|
+
const s = threadSummary(root, r);
|
|
88
|
+
const debt = s.openDebt.length ? c.red(` ⚠ ${s.openDebt.length} open reconcile-debt`) : '';
|
|
89
|
+
log(` ${c.bold(r)} ${c.dim(`${s.nodes.length} epic(s)`)}${debt}`);
|
|
90
|
+
}
|
|
91
|
+
log(c.dim('\n yad thread <epic> show one thread in full'));
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (!isValidEpicId(epic)) { log(c.red(`invalid epic id: ${epic}`)); process.exitCode = 1; return; }
|
|
95
|
+
const s = threadSummary(root, epic);
|
|
96
|
+
if (json) { log(JSON.stringify(s, null, 2)); return; }
|
|
97
|
+
|
|
98
|
+
log(c.bold(`\nThread ${s.thread}`) + c.dim(' (genesis → tip)'));
|
|
99
|
+
if (s.broken) log(c.red(` ✗ broken lineage: ${s.broken}`));
|
|
100
|
+
for (const n of s.nodes) {
|
|
101
|
+
const tag = KIND_TAG[n.kind] || n.kind;
|
|
102
|
+
const seal = n.sealed ? c.dim(' [sealed]') : '';
|
|
103
|
+
const dep = n.depth ? c.dim(` ${n.depth}`) : '';
|
|
104
|
+
log(` • ${c.bold(n.id)} ${tag}${dep} ${c.dim('@ ' + n.currentStep)}${seal}`);
|
|
105
|
+
if (n.parent) log(c.dim(` parent: ${n.parent} inherits: [${n.inherits.join(', ') || '—'}]`));
|
|
106
|
+
if (n.defect) log(c.dim(` defect: ${n.defect.severity || '?'} · escaped@${n.defect.escape_stage || '?'} · ${n.defect.root_cause || ''}`));
|
|
107
|
+
if (n.brokenThread) log(c.red(` ✗ ${n.brokenThread}`));
|
|
108
|
+
}
|
|
109
|
+
log(c.bold('\n Current truth') + c.dim(' (authoritative source per artifact)'));
|
|
110
|
+
for (const base of THREAD_ARTIFACT_BASES) {
|
|
111
|
+
const v = s.resolved[base];
|
|
112
|
+
const disp = Array.isArray(v) ? (v.length ? v.join(' + ') : c.dim('(none)')) : (v || c.dim('(none)'));
|
|
113
|
+
log(` ${base.padEnd(12)} ${c.dim('←')} ${disp}`);
|
|
114
|
+
}
|
|
115
|
+
const sids = Object.keys(s.resolvedStories).sort();
|
|
116
|
+
if (sids.length) {
|
|
117
|
+
log(c.dim(` composed stories (${sids.length}): ` + sids.map((id) => `${id}←${s.resolvedStories[id]}`).join(', ')));
|
|
118
|
+
}
|
|
119
|
+
if (s.openDebt.length) {
|
|
120
|
+
log(c.red('\n ⚠ Open reconcile debt (blocks the next change until paid):'));
|
|
121
|
+
for (const d of s.openDebt) log(` ${d.epicId}: ${d.reason || ''} — requires [${(d.requires || []).join(', ')}]`);
|
|
122
|
+
} else {
|
|
123
|
+
log(c.green('\n ✓ no open reconcile debt'));
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ---- the change-reconciler (yad reconcile — advisory, never a gate) ------------------------------
|
|
128
|
+
|
|
129
|
+
// Distinct thread roots in the project.
|
|
130
|
+
function threadRoots(root) {
|
|
131
|
+
const dir = path.join(root, 'epics');
|
|
132
|
+
if (!exists(dir)) return [];
|
|
133
|
+
const roots = new Set();
|
|
134
|
+
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
135
|
+
if (e.isDirectory() && isValidEpicId(e.name) && exists(path.join(dir, e.name, 'epic.md'))) {
|
|
136
|
+
roots.add(resolveThread(root, e.name).rootId);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return [...roots].sort();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export async function runReconcile(root, { action = 'check', thread = null } = {}) {
|
|
143
|
+
const roots = thread ? [resolveThread(root, thread).rootId] : threadRoots(root);
|
|
144
|
+
if (!roots.length) { info('no feature threads found (no epics with epic.md yet)'); return; }
|
|
145
|
+
|
|
146
|
+
log(c.bold(`\nChange reconcile ${c.dim(action)}`));
|
|
147
|
+
let flags = 0;
|
|
148
|
+
for (const r of roots) {
|
|
149
|
+
const s = threadSummary(root, r);
|
|
150
|
+
const issues = [];
|
|
151
|
+
if (s.broken) issues.push(`broken lineage: ${s.broken}`);
|
|
152
|
+
for (const n of s.nodes) if (n.brokenThread) issues.push(`${n.id}: ${n.brokenThread}`);
|
|
153
|
+
for (const d of s.openDebt) issues.push(`open reconcile debt on ${d.epicId} — next change blocked until paid`);
|
|
154
|
+
if (!issues.length) { ok(`${r} — clean`); continue; }
|
|
155
|
+
flags += issues.length;
|
|
156
|
+
warn(`${r}`);
|
|
157
|
+
for (const i of issues) hand(i);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (action === 'refresh') {
|
|
161
|
+
log('');
|
|
162
|
+
info('refresh is advisory: open a reconcile change-epic with `yad-change` (kind: change) threaded to');
|
|
163
|
+
info('the affected feature, then pay any open debt (update artifacts + add a regression test).');
|
|
164
|
+
}
|
|
165
|
+
if (action === 'wire') {
|
|
166
|
+
log('');
|
|
167
|
+
info('wire installs an advisory CI job that runs `yad reconcile --check` on push (no block) — the');
|
|
168
|
+
info('hard enforcement is the lineage-check / epic-open / reconcile-debt gates in yad-checks.');
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
log('');
|
|
172
|
+
if (flags) { warn(`${flags} item(s) need attention — reconcile is advisory; the gates block at merge`); }
|
|
173
|
+
else { ok('all threads reconciled — no drift, no open debt'); }
|
|
174
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "yadflow",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"description": "Yadflow — the gated, team, multi-repo SDLC: author → review → build with a PR-driven review gate and a zero-dependency `yad` CLI (setup, gate, commit, open-pr, ship, repo). A BMAD module +
|
|
3
|
+
"version": "2.18.1",
|
|
4
|
+
"description": "Yadflow — the gated, team, multi-repo SDLC: author → review → build with a PR-driven review gate and a zero-dependency `yad` CLI (setup, gate, commit, open-pr, ship, repo, thread, reconcile). A BMAD module + 35 yad-* skills.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"author": "AbdelRahman Nasr",
|
|
7
7
|
"license": "MIT",
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
"cli/",
|
|
22
22
|
"!cli/test.mjs",
|
|
23
23
|
"!cli/test-checks.mjs",
|
|
24
|
+
"!cli/test-threads.mjs",
|
|
24
25
|
"skills/",
|
|
25
26
|
"README.md",
|
|
26
27
|
"LICENSE",
|
|
@@ -36,9 +37,9 @@
|
|
|
36
37
|
"scripts": {
|
|
37
38
|
"yad": "node bin/yad.mjs",
|
|
38
39
|
"lint": "eslint cli bin",
|
|
39
|
-
"test": "node --test cli/test.mjs cli/test-checks.mjs",
|
|
40
|
+
"test": "node --test cli/test.mjs cli/test-checks.mjs cli/test-threads.mjs",
|
|
40
41
|
"test:e2e": "bash test/e2e/run.sh",
|
|
41
|
-
"coverage": "node --test --experimental-test-coverage --test-coverage-exclude='cli/test*.mjs' --test-coverage-lines=70 --test-coverage-branches=70 cli/test.mjs cli/test-checks.mjs",
|
|
42
|
+
"coverage": "node --test --experimental-test-coverage --test-coverage-exclude='cli/test*.mjs' --test-coverage-lines=70 --test-coverage-branches=70 cli/test.mjs cli/test-checks.mjs cli/test-threads.mjs",
|
|
42
43
|
"diagrams": "npx -y @mermaid-js/mermaid-cli -i docs/diagrams/sdlc-overview.mmd -o docs/diagrams/sdlc-overview.svg -b transparent && npx -y @mermaid-js/mermaid-cli -i docs/diagrams/review-loop.mmd -o docs/diagrams/review-loop.svg -b transparent",
|
|
43
44
|
"prepublishOnly": "npm test"
|
|
44
45
|
},
|
package/skills/sdlc/config.yaml
CHANGED
|
@@ -96,7 +96,11 @@ build:
|
|
|
96
96
|
- { id: coderabbit, name: "CodeRabbit", email: "noreply@coderabbit.ai" }
|
|
97
97
|
- { id: none, name: "(no AI assistance)", email: "" }
|
|
98
98
|
# Step C (yad-checks) — the CI gates that must pass before merge. CI-agnostic bash in checks/.
|
|
99
|
-
gates:
|
|
99
|
+
# Phase 6 adds three thread-aware gates: lineage-check (every change links a real threaded epic),
|
|
100
|
+
# epic-open (a SEALED epic — all stories shipped — refuses new behaviour, forcing a change-epic so the
|
|
101
|
+
# front artifacts can never go stale), and reconcile-debt (a hotfix's ship-first debt blocks the next
|
|
102
|
+
# change on its thread until paid). See the `change:` block below.
|
|
103
|
+
gates: [spec-link, contract-check, build-test-lint, lineage-check, epic-open, reconcile-debt]
|
|
100
104
|
# Pattern gates — commit subject + PR/MR title + PR/MR template-usage. Profile-aware (code|hub): code
|
|
101
105
|
# repos validate the Conventional-Commits / task-PR conventions; the product hub validates its
|
|
102
106
|
# artifact-review conventions (review/EP-<slug>/<artifact>, the hub PR template). verified-commits is
|
|
@@ -256,3 +260,44 @@ automation:
|
|
|
256
260
|
# Kill switch (phase-4-build-plan.md §Safety): true => every step forced to human_approve
|
|
257
261
|
# system-wide, no per-step edits. One line, instantly reversible. Toggle via `yad-run action: kill`.
|
|
258
262
|
kill_switch: false
|
|
263
|
+
|
|
264
|
+
# Phase 6 (post-lock change management) — FEATURE THREADS. After the contract locks and code ships, a
|
|
265
|
+
# change must not MUTATE a locked artifact (that destroys the audit trail and the lock). Instead every
|
|
266
|
+
# change request becomes a NEW epic, threaded to its parent: a feature is a thread of linked epics
|
|
267
|
+
# (genesis -> change -> defect -> ...). A change-epic INHERITS unchanged front artifacts from its parent
|
|
268
|
+
# BY REFERENCE and only RE-AUTHORS what it changes — so artifacts are never stale, only superseded; the
|
|
269
|
+
# feature's current truth is the head of the thread, and the chain IS the evolution timeline. See
|
|
270
|
+
# docs/phase-6-build-plan.md and skills/yad-change. yad-defects/yad-timeline render it; yad-reconcile
|
|
271
|
+
# (read-only, like yad-docs-sync) flags orphan/drift; the three gates above enforce it.
|
|
272
|
+
change:
|
|
273
|
+
# The kind of an epic (epic.md frontmatter `kind:`). `feature` is the genesis (default when absent).
|
|
274
|
+
kinds: [feature, change, defect, hotfix]
|
|
275
|
+
# Triage DEPTH (yad-change auto-proposes, human confirms) -> which front states are re-authored vs
|
|
276
|
+
# inherited. defect-fix re-authors stories+test-cases only; contract-surface re-authors architecture
|
|
277
|
+
# and RE-LOCKS (a new hash routes architecture-review through the contract escalation, as today).
|
|
278
|
+
depths: [defect-fix, behavioral-no-surface, contract-surface, new-capability]
|
|
279
|
+
thread_id: genesis_epic_id # the thread id = the genesis epic's id (never renamed -> stablest anchor)
|
|
280
|
+
# Lineage frontmatter added to epic.md (mirrors the design:/testing: enrichment blocks — the locked
|
|
281
|
+
# state.json step shape is untouched). `thread` is a DERIVED cache (authoritative = walk `parent` to
|
|
282
|
+
# the root); a mismatch is detectable corruption (yad doctor threadChecks). defect/hotfix also carry
|
|
283
|
+
# origin/severity/escape_stage/root_cause for the quality report (yad-defects).
|
|
284
|
+
lineage_frontmatter: [kind, thread, parent, inherits, supersedes, origin, severity, escape_stage, root_cause]
|
|
285
|
+
artifact_bases: [epic, architecture, contract, ui-design, stories, test-cases] # what `inherits` may list
|
|
286
|
+
severity: [sev1, sev2, sev3, sev4] # defect/hotfix severity
|
|
287
|
+
# Per-change-epic ledgers (siblings of approvals.json; the locked state.json shape is untouched).
|
|
288
|
+
ledgers:
|
|
289
|
+
change: "{project-root}/epics/EP-<slug>/.sdlc/change.json" # intake + triage (one per change/defect/hotfix epic)
|
|
290
|
+
debt: "{project-root}/epics/EP-<slug>/.sdlc/reconcile-debt.json" # append-only hotfix ship-first debt
|
|
291
|
+
# An inherited artifact is taken by REFERENCE: the change-epic's contract-lock.json is a POINTER-LOCK
|
|
292
|
+
# carrying the parent's hash verbatim (+ inheritedFrom/ref), so contract-check.sh passes UNCHANGED and
|
|
293
|
+
# the surface physically cannot drift (there is no contract.md in the child to edit). Omitting
|
|
294
|
+
# `architecture` from `inherits` is what triggers a real re-lock + the architecture gate.
|
|
295
|
+
inherit_by: reference
|
|
296
|
+
# An epic is SEALED once every story is `shipped` (build.story_build_states). epic-open.sh refuses new
|
|
297
|
+
# behaviour on a sealed epic -> the change must land in a new threaded change-epic (the front half is
|
|
298
|
+
# forced to stay current; staleness is unshippable).
|
|
299
|
+
seal_on: all-stories-shipped
|
|
300
|
+
hotfix:
|
|
301
|
+
ship_first: true # a hotfix may run the build half BEFORE its front gates approve
|
|
302
|
+
debt_blocks_next_change: true # but opens reconcile-debt.json; the next change on the thread is blocked until paid
|
|
303
|
+
debt_requires: [artifacts-updated, regression-test] # evidence that clears the debt
|
package/skills/sdlc/install.sh
CHANGED
|
@@ -11,7 +11,7 @@ set -euo pipefail
|
|
|
11
11
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
12
12
|
cd "$ROOT"
|
|
13
13
|
|
|
14
|
-
SKILLS=(yad-discovery yad-analysis yad-epic yad-architecture yad-ui yad-stories yad-test-cases yad-connect-repos yad-sync-repos yad-connect-design yad-connect-testing yad-connect-learning yad-connect-docs yad-docs yad-docs-overview yad-docs-sync yad-learn yad-spec yad-implement yad-checks yad-pr-template yad-review-comments yad-hub-bridge yad-commit yad-open-pr yad-ship yad-engineer-review yad-backfill yad-run yad-review-gate yad-status)
|
|
14
|
+
SKILLS=(yad-discovery yad-analysis yad-epic yad-architecture yad-ui yad-stories yad-test-cases yad-connect-repos yad-sync-repos yad-connect-design yad-connect-testing yad-connect-learning yad-connect-docs yad-docs yad-docs-overview yad-docs-sync yad-learn yad-spec yad-implement yad-checks yad-pr-template yad-review-comments yad-hub-bridge yad-commit yad-open-pr yad-ship yad-engineer-review yad-backfill yad-run yad-review-gate yad-status yad-change yad-timeline yad-defects yad-reconcile)
|
|
15
15
|
|
|
16
16
|
echo "Installing sdlc module from $ROOT/skills ..."
|
|
17
17
|
|
|
@@ -30,3 +30,7 @@ SDLC Workflow,yad-connect-docs,Connect Docs Target,DX,"Setup/maintenance: connec
|
|
|
30
30
|
SDLC Workflow,yad-docs,Author Docs Site,DS,"Generate the per-epic interactive documentation SPA (animated flow canvas + role-based stakeholder doc pages) from the epic's approved artifacts (epic, architecture, locked contract, ui-design, stories, code-context, test-cases), themed by the connected design system, into epics/EP-<slug>/docs-site/. Copies the vendored React/Vite/Tailwind shell verbatim and generates src/data/*.ts deterministically; drives the yad docs build/deploy CLI to publish to Pages (or build-only). An OUTPUT ENRICHMENT — never a gate; never mutates state.json, approvals, or the contract lock.",,{epic: EP-<slug>} {action: generate|refresh|deploy} {login_gate: true|false},,yad-review-gate,,false,epics/EP-<slug>/docs-site/,docs-site/ docs-build.json
|
|
31
31
|
SDLC Workflow,yad-docs-overview,Docs Overview Site,DO,"Generate the project SDLC-overview interactive site (docs/sdlc-site/) — every stage from setup to ship modeled as flow paths, system components, and stakeholder roles — reusing the same vendored shell, themed with yadflow's brand palette. Reads config.yaml + module-help.csv + the overview diagram as the pipeline source. Folds the hand-maintained docs/index.html report into the site as report.html (linked from the nav). Not a gate — a project-level enrichment that regenerates whenever the skill set / pipeline changes.",,{action: generate|deploy},,,,false,docs/sdlc-site/,sdlc-site/ .docs-build.json
|
|
32
32
|
SDLC Workflow,yad-docs-sync,Docs Sync,DY,"Maintenance/CI: keep the generated doc sites fresh. Detect staleness (a content hash of the approved artifacts + the connected repos' HEAD shas + the doc-shell version vs each site's build manifest), report which sites drifted and why, regenerate + redeploy the stale ones, and wire a CI job that rebuilds on push (carrying [skip ci] + a concurrency group to prevent deploy loops). Generalizes the rule that feature work must hand-update docs/index.html + diagrams + skill counts. Refresh is always a human/CI decision; never a gate.",,{action: check|refresh|wire} {epic: EP-<slug>},,,,false,epics/EP-<slug>/.sdlc/,docs-build.json yad-docs.yml
|
|
33
|
+
SDLC Workflow,yad-change,Change/Defect Intake,CH,"Phase 6 post-lock change management: the INTAKE + TRIAGE step of a feature thread. Classifies the change DEPTH (defect-fix / behavioral-no-surface / contract-surface / new-capability), seeds a NEW EP-<slug> change-epic threaded to its parent (lineage frontmatter kind/parent/thread/inherits/supersedes + a state.json whose inherited steps are pre-marked done and only the changed steps run; a pointer-lock contract-lock.json when architecture is inherited), and records the intake in change.json (escape_stage + root_cause for defects). For hotfixes it records the ship-first exception and opens reconcile-debt.json. Never auto-advances — hands off to the normal authoring skills + yad-review-gate.",,{parent: EP-<slug>} {title: one-line} {kind: change|defect|hotfix} {origin: production|staging|qa|review} {severity: sev1..sev4} {description: text} {affected: artifacts},1-front,,yad-review-gate,false,epics/EP-<slug>/,epic.md state.json change.json reconcile-debt.json contract-lock.json
|
|
34
|
+
SDLC Workflow,yad-timeline,Feature Timeline,TL,"Render a feature THREAD (its linked epics, genesis->changes->defects) as an evolution view (the vendored React/Vite/Tailwind shell HTML + a TIMELINE.md summary) AND resolve the inheritance chain into the authoritative current artifact set (thread-resolved.md: the winning source per artifact + the resolved contract-lock hash) — the composed source-of-truth AI/humans read for the next change. Reads frontmatter lineage + each change.json + build-log.json. An OUTPUT ENRICHMENT — never a gate; never mutates state.",,{thread: EP-<genesis>} {action: generate|deploy},,,,false,epics/EP-<genesis>/,timeline-site/ thread-resolved.md TIMELINE.md
|
|
35
|
+
SDLC Workflow,yad-defects,Quality-Gap Report,DF,"Generate a per-epic AND per-thread defect/bug report (same vendored shell + DEFECTS.md). Walks the thread for every kind:defect change-epic + each change.json defect block + shipped regressions in build-log.json, aggregates by escape_stage (the SDLC gate that should have caught it) and root_cause, and visualizes WHERE quality gaps systematically come from (e.g. % of thread defects that escaped at the test-cases gate) so the team can harden the originating stage. An OUTPUT ENRICHMENT — never a gate; never mutates state.",,{epic: EP-<slug> | thread: EP-<genesis>} {action: generate|deploy},,,,false,epics/EP-<slug>/,defects-site/ DEFECTS.md
|
|
36
|
+
SDLC Workflow,yad-reconcile,Change Reconciler,RE,"Maintenance/CI (mirrors yad-docs-sync — never a gate): detect post-lock DRIFT/ORPHANS — shipped code or a repo HEAD advance (the repos.json syncedHead-vs-current-HEAD rule) with NO owning change-epic in any thread — plus open hotfix reconcile debt, and report which thread drifted and why. refresh scaffolds a reconcile change-epic stub (hands to yad-change) — never silent; wire commits advisory CI ([skip ci] + concurrency, like yad-docs-sync). The actual merge BLOCK is the lineage-check / reconcile-debt gates; this only discovers.",,{action: check|refresh|wire} {thread: EP-<genesis>},,,,false,epics/EP-<genesis>/.sdlc/,(report) reconcile-debt.json yad-reconcile.yml
|