cabloy 5.1.200 → 5.1.202
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/.cabloy-version +1 -1
- package/CHANGELOG.md +21 -0
- package/README.md +6 -0
- package/package.json +1 -1
- package/repo-agent-governance/managed-assets.json +21 -21
- package/repo-agent-governance/skills/cabloy-spec-execution/SKILL.md +3 -2
- package/repo-agent-governance/skills/cabloy-spec-execution/references/execution-protocol.md +1 -1
- package/repo-agent-governance/skills/cabloy-spec-execution/references/status-and-evidence.md +11 -10
- package/repo-agent-governance/skills/cabloy-spec-generation/SKILL.md +1 -1
- package/repo-agent-governance/skills/cabloy-spec-generation/references/canonical-spec-input.md +6 -2
- package/repo-agent-governance/skills/cabloy-spec-generation/references/repo-specs-document-set.md +16 -13
- package/repo-agent-governance/skills/cabloy-spec-generation/references/traceability-and-status-rules.md +19 -12
- package/repo-agent-governance/tests/spec-audit.test.mjs +493 -0
- package/repo-agent-governance/tools/spec-audit/audit.mjs +189 -5
- package/repo-agent-governance/tools/spec-charts/generate-implementation-charts.mjs +26 -7
- package/repo-agent-governance/tools/spec-charts/generate-implementation-charts.test.mjs +82 -0
- package/repo-agent-governance/tools/spec-charts/spec-parser.mjs +40 -1
- package/repo-docs/ai/ai-spec-driven-development.md +1 -0
- package/repo-docs/ai/playbook-spec-execution.md +3 -2
- package/repo-docs/ai/playbook-spec-generation.md +1 -1
- package/repo-docs/assets/img/cabloy-admin-ssr-cover-en.png +0 -0
- package/repo-docs/index.md +6 -0
|
@@ -4,6 +4,8 @@ import { fileURLToPath } from 'node:url';
|
|
|
4
4
|
|
|
5
5
|
import {
|
|
6
6
|
findIdentifiers,
|
|
7
|
+
isPlanningCompleteEligible,
|
|
8
|
+
isPrerequisiteSatisfied,
|
|
7
9
|
markdownLines,
|
|
8
10
|
parseDefinitions,
|
|
9
11
|
parseProgress,
|
|
@@ -71,6 +73,46 @@ function addMapping(groups, edges, definitions) {
|
|
|
71
73
|
}
|
|
72
74
|
}
|
|
73
75
|
|
|
76
|
+
function traceabilityException(definition, report, annotatedLines) {
|
|
77
|
+
const fields = markdownLines(definition.body ?? '').flatMap(({ text, line }) =>
|
|
78
|
+
Array.from(text.matchAll(/\bTraceability\s+exception\b/gi), match => ({
|
|
79
|
+
text,
|
|
80
|
+
line,
|
|
81
|
+
start: match.index,
|
|
82
|
+
})),
|
|
83
|
+
);
|
|
84
|
+
if (!fields.length) return null;
|
|
85
|
+
for (const field of fields) {
|
|
86
|
+
const line = definition.line + field.line - (definition.kind === 'WBS' ? 0 : 1);
|
|
87
|
+
annotatedLines.add(`${definition.file}:${line}`);
|
|
88
|
+
}
|
|
89
|
+
const field = fields[0];
|
|
90
|
+
const value =
|
|
91
|
+
definition.kind === 'bullet' && field.line === 1
|
|
92
|
+
? field.text.slice(field.start).trim()
|
|
93
|
+
: field.text.trim();
|
|
94
|
+
const match = value.match(/^Traceability exception:\s*(technical-only|authority-only)\s+—\s+/i);
|
|
95
|
+
const exception = match?.[1].toLowerCase();
|
|
96
|
+
const rationale = match && value.slice(match[0].length);
|
|
97
|
+
const validOwner =
|
|
98
|
+
(exception === 'technical-only' &&
|
|
99
|
+
definition.id.startsWith('SRS-') &&
|
|
100
|
+
definition.kind === 'bullet' &&
|
|
101
|
+
field.line === 1) ||
|
|
102
|
+
(exception === 'authority-only' && definition.id.startsWith('WBS-'));
|
|
103
|
+
if (fields.length !== 1 || !validOwner || !/[\p{L}\p{N}]/u.test(rationale ?? '')) {
|
|
104
|
+
report(
|
|
105
|
+
'traceability-exception',
|
|
106
|
+
definition.file,
|
|
107
|
+
definition.line + field.line - (definition.kind === 'WBS' ? 0 : 1),
|
|
108
|
+
'Invalid Traceability exception: use one declaration-local technical-only SRS or authority-only WBS field with a rationale.',
|
|
109
|
+
definition.id,
|
|
110
|
+
);
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
return exception;
|
|
114
|
+
}
|
|
115
|
+
|
|
74
116
|
function collectMappings(records, definitions) {
|
|
75
117
|
const edges = new Map(Array.from(definitions.keys(), id => [id, new Set()]));
|
|
76
118
|
for (const definition of definitions.values()) {
|
|
@@ -120,6 +162,116 @@ function collectMappings(records, definitions) {
|
|
|
120
162
|
return edges;
|
|
121
163
|
}
|
|
122
164
|
|
|
165
|
+
function acceptedDecision(markdown) {
|
|
166
|
+
if (!markdown) return false;
|
|
167
|
+
const lines = markdownLines(markdown);
|
|
168
|
+
const statuses = [];
|
|
169
|
+
let section = null;
|
|
170
|
+
for (const [index, { text, heading }] of lines.entries()) {
|
|
171
|
+
if (heading?.level === 2) section = heading.text;
|
|
172
|
+
const field = text.match(/^\s*-\s*\*\*Status:\*\*/i);
|
|
173
|
+
if (field && (section === null || /^Status$/i.test(section)))
|
|
174
|
+
statuses.push(text.slice(field[0].length).trim());
|
|
175
|
+
if (!heading || !/^Status$/i.test(heading.text)) continue;
|
|
176
|
+
const value = lines.slice(index + 1).find(record => {
|
|
177
|
+
if (record.heading && record.heading.level <= heading.level) return true;
|
|
178
|
+
return record.text.trim();
|
|
179
|
+
});
|
|
180
|
+
if (value && !(value.heading && value.heading.level <= heading.level))
|
|
181
|
+
statuses.push(value.text.trim());
|
|
182
|
+
}
|
|
183
|
+
return statuses.length > 0 && statuses.every(status => /^Accepted\.?$/i.test(status));
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function planningBaselineGates(records, wbs, edges, exceptions, report) {
|
|
187
|
+
const gates = new Set();
|
|
188
|
+
const field = /^\s*(?:-\s*)?(?:\*\*)?Traceability mode(?:\*\*)?\s*:/i;
|
|
189
|
+
const declarations = new Map();
|
|
190
|
+
for (const [file, markdown] of records) {
|
|
191
|
+
for (const { text, line } of markdownLines(markdown)) {
|
|
192
|
+
if (!field.test(text)) continue;
|
|
193
|
+
const task =
|
|
194
|
+
file === 'pdp-wbs.md'
|
|
195
|
+
? wbs?.tasks.find(candidate => candidate.line < line && candidate.endLine >= line)
|
|
196
|
+
: null;
|
|
197
|
+
if (!task) {
|
|
198
|
+
report(
|
|
199
|
+
'traceability-mode',
|
|
200
|
+
file,
|
|
201
|
+
line,
|
|
202
|
+
'Traceability mode must be declared inside a WBS task.',
|
|
203
|
+
);
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
if (!declarations.has(task.id)) declarations.set(task.id, []);
|
|
207
|
+
declarations.get(task.id).push({ text, line });
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
for (const task of wbs?.tasks ?? []) {
|
|
211
|
+
const declared = declarations.get(task.id);
|
|
212
|
+
if (!declared) continue;
|
|
213
|
+
const [{ text, line }] = declared;
|
|
214
|
+
const mode = text.replace(field, '').trim().replace(/`/g, '');
|
|
215
|
+
const body = markdownLines(task.body).map(record => record.text);
|
|
216
|
+
const deliveryAction =
|
|
217
|
+
/(?:^|[,;]\s*|\b(?:and|then|to)\s+)(?:implement|deploy|migrate|release|regenerate|develop|ship|code|build\b(?!\s+risks?\b)|create|run\s+(?:an?\s+)?(?:migrations?|tests?|ATP-|scenarios?)|execute\s+(?:an?\s+)?(?:ATP-|tests?|scenarios?))\b/i;
|
|
218
|
+
const mixedScope =
|
|
219
|
+
deliveryAction.test(task.title) ||
|
|
220
|
+
body.some(value => {
|
|
221
|
+
const bullet = value.match(/^\s*[-*]\s+(.*)/);
|
|
222
|
+
return bullet && deliveryAction.test(bullet[1].replace(/^\*\*[^*]+\*\*\s*/, ''));
|
|
223
|
+
});
|
|
224
|
+
const hasItems = label =>
|
|
225
|
+
body.some((value, index) => {
|
|
226
|
+
if (value.trim().toLowerCase() !== `${label}:`) return false;
|
|
227
|
+
const next = body.slice(index + 1).find(line => line.trim());
|
|
228
|
+
return Boolean(next?.trim().match(/^[-*] \S/));
|
|
229
|
+
});
|
|
230
|
+
const authorities = body.flatMap(value => {
|
|
231
|
+
const match = value.match(/^\s*(?:-\s*)?(?:\*\*)?Review authority(?:\*\*)?\s*:/i);
|
|
232
|
+
return match ? [value.replace(match[0], '').trim()] : [];
|
|
233
|
+
});
|
|
234
|
+
const authority = authorities[0]?.match(
|
|
235
|
+
/^\[([^\]]+)\]\((\.\/decisions\/[^/()]+\.md)\)(?:\s*\([^\n]*\))?\s*\.?$/,
|
|
236
|
+
);
|
|
237
|
+
const adr = authority?.[2].slice(2);
|
|
238
|
+
const decision = adr && records.get(adr);
|
|
239
|
+
const mappedIncoming = [...edges].some(
|
|
240
|
+
([id, targets]) => id.startsWith('SRS-') && targets.has(task.id),
|
|
241
|
+
);
|
|
242
|
+
const valid =
|
|
243
|
+
declared.length === 1 &&
|
|
244
|
+
mode === 'planning-baseline-review' &&
|
|
245
|
+
task.phase.number === '10' &&
|
|
246
|
+
/\b(?:review|freeze)\b/i.test(task.title) &&
|
|
247
|
+
/\bbaseline\b/i.test(task.title) &&
|
|
248
|
+
hasItems('tasks') &&
|
|
249
|
+
hasItems('acceptance checks') &&
|
|
250
|
+
!mixedScope &&
|
|
251
|
+
!findIdentifiers(task.title, 'SRS-').length &&
|
|
252
|
+
!findIdentifiers(task.title, 'ATP-').length &&
|
|
253
|
+
!findIdentifiers(task.body, 'SRS-').length &&
|
|
254
|
+
!findIdentifiers(task.body, 'ATP-').length &&
|
|
255
|
+
!mappedIncoming &&
|
|
256
|
+
!edges.get(task.id)?.size &&
|
|
257
|
+
!exceptions.has(task.id) &&
|
|
258
|
+
authorities.length === 1 &&
|
|
259
|
+
acceptedDecision(decision);
|
|
260
|
+
if (!valid) {
|
|
261
|
+
report(
|
|
262
|
+
'traceability-mode',
|
|
263
|
+
'pdp-wbs.md',
|
|
264
|
+
line,
|
|
265
|
+
'Planning-baseline review requires one valid mode, Phase 10 baseline review, substantive review-only Tasks and Acceptance checks, no individual SRS/ATP mapping or other traceability exception, and one accepted local ADR review authority.',
|
|
266
|
+
task.id,
|
|
267
|
+
);
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
gates.add(task.id);
|
|
271
|
+
}
|
|
272
|
+
return gates;
|
|
273
|
+
}
|
|
274
|
+
|
|
123
275
|
function headingIds(markdown) {
|
|
124
276
|
const slugs = new Set();
|
|
125
277
|
const counts = new Map();
|
|
@@ -271,8 +423,22 @@ export async function auditPlanning(
|
|
|
271
423
|
report('declaration', file, 1, error.message);
|
|
272
424
|
}
|
|
273
425
|
}
|
|
426
|
+
const exceptions = new Map();
|
|
427
|
+
const annotatedLines = new Set();
|
|
428
|
+
for (const definition of definitions.values()) {
|
|
429
|
+
const exception = traceabilityException(definition, report, annotatedLines);
|
|
430
|
+
if (exception) exceptions.set(definition.id, exception);
|
|
431
|
+
}
|
|
274
432
|
for (const [file, markdown] of records) {
|
|
275
433
|
for (const { text, line } of markdownLines(markdown)) {
|
|
434
|
+
if (/\bTraceability\s+exception\b/i.test(text) && !annotatedLines.has(`${file}:${line}`)) {
|
|
435
|
+
report(
|
|
436
|
+
'traceability-exception',
|
|
437
|
+
file,
|
|
438
|
+
line,
|
|
439
|
+
'Traceability exception must be in its own formal declaration.',
|
|
440
|
+
);
|
|
441
|
+
}
|
|
276
442
|
for (const prefix of OWNERS.keys()) {
|
|
277
443
|
for (const id of findIdentifiers(referenceText(text), prefix)) {
|
|
278
444
|
if (!definitions.has(id)) {
|
|
@@ -308,10 +474,23 @@ export async function auditPlanning(
|
|
|
308
474
|
task.id,
|
|
309
475
|
);
|
|
310
476
|
}
|
|
311
|
-
if (
|
|
477
|
+
if (rows.get(task.id) === 'planning-complete' && !isPlanningCompleteEligible(task)) {
|
|
478
|
+
report(
|
|
479
|
+
'completion-mode',
|
|
480
|
+
'progress.md',
|
|
481
|
+
locations?.get(task.id)?.line ?? 1,
|
|
482
|
+
'Planning-complete requires a declaration-local Completion mode: planning-only field.',
|
|
483
|
+
task.id,
|
|
484
|
+
);
|
|
485
|
+
}
|
|
486
|
+
if (
|
|
487
|
+
['planning-complete', 'implementation-complete', 'verified'].includes(rows.get(task.id))
|
|
488
|
+
) {
|
|
312
489
|
for (const dependency of task.dependencyIds) {
|
|
490
|
+
const predecessor = wbs.tasks.find(item => item.id === dependency);
|
|
313
491
|
if (
|
|
314
|
-
|
|
492
|
+
rows.has(dependency) &&
|
|
493
|
+
!isPrerequisiteSatisfied(predecessor, rows.get(dependency))
|
|
315
494
|
) {
|
|
316
495
|
report(
|
|
317
496
|
'dependency-state',
|
|
@@ -344,10 +523,11 @@ export async function auditPlanning(
|
|
|
344
523
|
}
|
|
345
524
|
if (!lightweight) {
|
|
346
525
|
const edges = collectMappings(records, definitions);
|
|
526
|
+
const gates = planningBaselineGates(records, wbs, edges, exceptions, report);
|
|
347
527
|
const incoming = new Set([...edges.values()].flatMap(values => [...values]));
|
|
348
528
|
for (const [id, definition] of definitions) {
|
|
349
|
-
if (definition.status === 'deferred' || definition.deferred) continue;
|
|
350
|
-
if (!id.startsWith('PRD-') && !incoming.has(id)) {
|
|
529
|
+
if (definition.status === 'deferred' || definition.deferred || gates.has(id)) continue;
|
|
530
|
+
if (!id.startsWith('PRD-') && !incoming.has(id) && exceptions.get(id) !== 'technical-only') {
|
|
351
531
|
report(
|
|
352
532
|
'traceability-incoming',
|
|
353
533
|
definition.file,
|
|
@@ -356,7 +536,11 @@ export async function auditPlanning(
|
|
|
356
536
|
id,
|
|
357
537
|
);
|
|
358
538
|
}
|
|
359
|
-
if (
|
|
539
|
+
if (
|
|
540
|
+
!id.startsWith('ATP-') &&
|
|
541
|
+
!edges.get(id).size &&
|
|
542
|
+
exceptions.get(id) !== 'authority-only'
|
|
543
|
+
) {
|
|
360
544
|
report(
|
|
361
545
|
'traceability-outgoing',
|
|
362
546
|
definition.file,
|
|
@@ -2,12 +2,20 @@
|
|
|
2
2
|
import { readFile, writeFile } from 'node:fs/promises';
|
|
3
3
|
import { basename, resolve } from 'node:path';
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
isPlanningCompleteEligible,
|
|
7
|
+
isPrerequisiteSatisfied,
|
|
8
|
+
markdownLines,
|
|
9
|
+
parseAtpIds,
|
|
10
|
+
parseProgress,
|
|
11
|
+
parseWbs,
|
|
12
|
+
} from './spec-parser.mjs';
|
|
6
13
|
export { parseAtpIds, parseProgress, parseWbs } from './spec-parser.mjs';
|
|
7
14
|
|
|
8
15
|
const STATUS_COLORS = {
|
|
9
16
|
'not-started': 'var(--blue)',
|
|
10
17
|
'in-progress': 'var(--orange)',
|
|
18
|
+
'planning-complete': 'var(--teal)',
|
|
11
19
|
'implementation-complete': 'var(--violet)',
|
|
12
20
|
'verified': 'var(--aqua)',
|
|
13
21
|
'blocked': 'var(--red)',
|
|
@@ -47,6 +55,7 @@ const COPY = {
|
|
|
47
55
|
statuses: {
|
|
48
56
|
'not-started': 'Not started',
|
|
49
57
|
'in-progress': 'In progress',
|
|
58
|
+
'planning-complete': 'Planning complete',
|
|
50
59
|
'implementation-complete': 'Implementation complete',
|
|
51
60
|
'verified': 'Verified',
|
|
52
61
|
'blocked': 'Blocked',
|
|
@@ -104,6 +113,7 @@ const COPY = {
|
|
|
104
113
|
statuses: {
|
|
105
114
|
'not-started': '未开始',
|
|
106
115
|
'in-progress': '进行中',
|
|
116
|
+
'planning-complete': '规划完成',
|
|
107
117
|
'implementation-complete': '实施完成',
|
|
108
118
|
'verified': '已核验',
|
|
109
119
|
'blocked': '受阻',
|
|
@@ -248,6 +258,11 @@ export function createChartModel({ readme, wbs, progress, testPlan }) {
|
|
|
248
258
|
}
|
|
249
259
|
task.recordedStatus = task.status;
|
|
250
260
|
task.status = progressStatus;
|
|
261
|
+
if (task.status === 'planning-complete' && !isPlanningCompleteEligible(task)) {
|
|
262
|
+
throw new Error(
|
|
263
|
+
`${task.id} has planning-complete status without Completion mode: planning-only.`,
|
|
264
|
+
);
|
|
265
|
+
}
|
|
251
266
|
if (task.deferred && task.status !== 'deferred') {
|
|
252
267
|
throw new Error(
|
|
253
268
|
`${task.id} is marked deferred in pdp-wbs.md but has progress status ${task.status}.`,
|
|
@@ -274,10 +289,10 @@ export function createChartModel({ readme, wbs, progress, testPlan }) {
|
|
|
274
289
|
};
|
|
275
290
|
}
|
|
276
291
|
|
|
277
|
-
function style() {
|
|
292
|
+
function style(planningComplete) {
|
|
278
293
|
return `<style>
|
|
279
|
-
:root { --surface:#fcfcfb;--page:#f9f9f7;--ink:#0b0b0b;--secondary:#52514e;--muted:#898781;--grid:#e1e0d9;--axis:#c3c2b7;--blue:#2a78d6;--orange:#eb6834
|
|
280
|
-
@media (prefers-color-scheme:dark) { :root { --surface:#1a1a19;--page:#0d0d0d;--ink:#fff;--secondary:#c3c2b7;--muted:#a09f99;--grid:#2c2c2a;--axis:#383835;--blue:#3987e5;--orange:#d95926
|
|
294
|
+
:root { --surface:#fcfcfb;--page:#f9f9f7;--ink:#0b0b0b;--secondary:#52514e;--muted:#898781;--grid:#e1e0d9;--axis:#c3c2b7;--blue:#2a78d6;--orange:#eb6834;${planningComplete ? '--teal:#087f83;' : ''}--aqua:#1baf7a;--violet:#4a3aa7;--red:#e34948;--yellow:#eda100;--blue-wash:#eaf3fd;--aqua-wash:#e7f7f0; }
|
|
295
|
+
@media (prefers-color-scheme:dark) { :root { --surface:#1a1a19;--page:#0d0d0d;--ink:#fff;--secondary:#c3c2b7;--muted:#a09f99;--grid:#2c2c2a;--axis:#383835;--blue:#3987e5;--orange:#d95926;${planningComplete ? '--teal:#29aeb0;' : ''}--aqua:#199e70;--violet:#9085e9;--red:#e66767;--yellow:#c98500;--blue-wash:#172435;--aqua-wash:#122b23; } }
|
|
281
296
|
.canvas{fill:var(--page)}.card{fill:var(--surface);stroke:var(--grid);stroke-width:1}.title{fill:var(--ink);font-family:Arial,Helvetica,sans-serif;font-size:27px;font-weight:700}.subtitle{fill:var(--secondary);font-family:Arial,Helvetica,sans-serif;font-size:14px}.heading{fill:var(--ink);font-family:Arial,Helvetica,sans-serif;font-size:13px;font-weight:700}.phase{fill:var(--ink);font-family:Arial,Helvetica,sans-serif;font-size:10px;font-weight:700}.task{fill:var(--ink);font-family:Arial,Helvetica,sans-serif;font-size:10px}.small{fill:var(--muted);font-family:Arial,Helvetica,sans-serif;font-size:9px}.caption{fill:var(--secondary);font-family:Arial,Helvetica,sans-serif;font-size:11px}.num{fill:var(--ink);font-family:Arial,Helvetica,sans-serif;font-size:11px;font-weight:700;font-variant-numeric:tabular-nums}.grid{stroke:var(--grid);stroke-width:1;shape-rendering:crispEdges}.axis{stroke:var(--axis);stroke-width:1;shape-rendering:crispEdges}.row:focus{outline:2px solid var(--ink);outline-offset:2px}
|
|
282
297
|
</style>`;
|
|
283
298
|
}
|
|
@@ -302,13 +317,13 @@ function renderChartTextLines(className, x, y, lines, lineHeight) {
|
|
|
302
317
|
return `<text class="${className}" x="${x}" y="${y}">${lines.map((line, lineIndex) => `<tspan x="${x}" dy="${lineIndex ? lineHeight : 0}">${escapeXml(line)}</tspan>`).join('')}</text>`;
|
|
303
318
|
}
|
|
304
319
|
|
|
305
|
-
function svgDocument(title, desc, body, height, metadata) {
|
|
320
|
+
function svgDocument(title, desc, body, height, metadata, planningComplete = false) {
|
|
306
321
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
307
322
|
<svg xmlns="http://www.w3.org/2000/svg" width="1600" height="${height}" viewBox="0 0 1600 ${height}" role="img" aria-labelledby="chart-title chart-desc">
|
|
308
323
|
<title id="chart-title">${escapeXml(title)}</title>
|
|
309
324
|
<desc id="chart-desc">${escapeXml(desc)}</desc>
|
|
310
325
|
<metadata>${escapeXml(metadata)}</metadata>
|
|
311
|
-
${style()}
|
|
326
|
+
${style(planningComplete)}
|
|
312
327
|
${body}
|
|
313
328
|
</svg>
|
|
314
329
|
`;
|
|
@@ -392,9 +407,12 @@ export function renderGantt(model, suiteName) {
|
|
|
392
407
|
},
|
|
393
408
|
)
|
|
394
409
|
.join('\n');
|
|
395
|
-
const completed = new Set(
|
|
410
|
+
const completed = new Set(
|
|
411
|
+
tasks.filter(task => isPrerequisiteSatisfied(task, task.status)).map(task => task.id),
|
|
412
|
+
);
|
|
396
413
|
const nextTask = tasks.find(
|
|
397
414
|
task =>
|
|
415
|
+
task.status !== 'planning-complete' &&
|
|
398
416
|
task.status !== 'verified' &&
|
|
399
417
|
task.status !== 'deferred' &&
|
|
400
418
|
task.status !== 'waived' &&
|
|
@@ -418,6 +436,7 @@ export function renderGantt(model, suiteName) {
|
|
|
418
436
|
body,
|
|
419
437
|
height,
|
|
420
438
|
t.metadata,
|
|
439
|
+
tasks.some(task => task.status === 'planning-complete'),
|
|
421
440
|
);
|
|
422
441
|
}
|
|
423
442
|
|
|
@@ -152,6 +152,88 @@ test('creates a mixed-status model with deferred scope separated', () => {
|
|
|
152
152
|
assert.equal(model.activeTasks.length, 2);
|
|
153
153
|
assert.equal(model.deferredTasks.length, 1);
|
|
154
154
|
assert.equal(model.verified, 0);
|
|
155
|
+
assert.doesNotMatch(renderGantt(model, 'Demo'), /--teal:/);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test('planning completion requires an explicit task mode and preserves verified-only metrics', () => {
|
|
159
|
+
const wbs = files['pdp-wbs.md'].replace(
|
|
160
|
+
'#### WBS-10-01: Freeze the baseline',
|
|
161
|
+
'#### WBS-10-01: Freeze the baseline\n\nCompletion mode: planning-only.',
|
|
162
|
+
);
|
|
163
|
+
const progress = files['progress.md'].replace('`not-started`', '`planning-complete`');
|
|
164
|
+
assert.throws(
|
|
165
|
+
() =>
|
|
166
|
+
createChartModel({
|
|
167
|
+
readme: files['README.md'],
|
|
168
|
+
wbs: files['pdp-wbs.md'],
|
|
169
|
+
progress,
|
|
170
|
+
testPlan: files['test-plan.md'],
|
|
171
|
+
}),
|
|
172
|
+
/planning-complete status without Completion mode/,
|
|
173
|
+
);
|
|
174
|
+
const model = createChartModel({
|
|
175
|
+
readme: files['README.md'],
|
|
176
|
+
wbs,
|
|
177
|
+
progress,
|
|
178
|
+
testPlan: files['test-plan.md'],
|
|
179
|
+
});
|
|
180
|
+
assert.equal(model.tasks[0].completionMode, 'planning-only');
|
|
181
|
+
assert.equal(model.verified, 0);
|
|
182
|
+
const gantt = renderGantt(model, 'Demo');
|
|
183
|
+
assert.match(gantt, /Planning complete/);
|
|
184
|
+
assert.match(gantt, /--teal:#087f83/);
|
|
185
|
+
assert.doesNotMatch(renderBurndown(model, 'Demo'), /--teal:/);
|
|
186
|
+
assert.match(gantt, /class="caption" x="96" y="195">WBS-20-01: Deliver/);
|
|
187
|
+
assert.doesNotMatch(gantt, /class="caption" x="96" y="195">WBS-10-01/);
|
|
188
|
+
assert.match(renderBurndown(model, 'Demo'), />2 remaining · 0 verified</);
|
|
189
|
+
const blocked = createChartModel({
|
|
190
|
+
readme: files['README.md'],
|
|
191
|
+
wbs,
|
|
192
|
+
progress: progress.replace('`in-progress`', '`blocked`'),
|
|
193
|
+
testPlan: files['test-plan.md'],
|
|
194
|
+
});
|
|
195
|
+
assert.match(renderGantt(blocked, 'Demo'), /No dependency-ready WBS candidate is recorded/);
|
|
196
|
+
assert.equal(blocked.verified, 0);
|
|
197
|
+
const chinese = createChartModel({
|
|
198
|
+
readme: '# 中文规划\n\n完整中文记录。',
|
|
199
|
+
wbs,
|
|
200
|
+
progress,
|
|
201
|
+
testPlan: files['test-plan.md'],
|
|
202
|
+
});
|
|
203
|
+
assert.match(renderGantt(chinese, '示例'), /规划完成/);
|
|
204
|
+
assert.match(renderBurndown(chinese, '示例'), /2 剩余 · 0 已核验/);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test('completion mode must be unique, supported and task-local', () => {
|
|
208
|
+
const wbs = files['pdp-wbs.md'];
|
|
209
|
+
for (const value of ['unknown.', '', 'planning-only.\nCompletion mode: planning-only.']) {
|
|
210
|
+
assert.throws(
|
|
211
|
+
() => parseWbs(wbs.replace('Tasks:', `Completion mode: ${value}\n\nTasks:`)),
|
|
212
|
+
/Completion mode/,
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
assert.throws(
|
|
216
|
+
() =>
|
|
217
|
+
parseWbs(
|
|
218
|
+
wbs.replace('Dependencies: none.', 'Dependencies: none.\nCompletion mode: planning-only.'),
|
|
219
|
+
),
|
|
220
|
+
/Completion mode must belong to a formal WBS task/,
|
|
221
|
+
);
|
|
222
|
+
assert.throws(
|
|
223
|
+
() => parseWbs(`Completion mode: planning-only.\n${wbs}`),
|
|
224
|
+
/Completion mode must belong to a formal WBS task/,
|
|
225
|
+
);
|
|
226
|
+
const progress = files['progress.md'].replace('`not-started`', '`planning-complete`');
|
|
227
|
+
assert.throws(
|
|
228
|
+
() =>
|
|
229
|
+
createChartModel({
|
|
230
|
+
readme: files['README.md'],
|
|
231
|
+
wbs: wbs.replace('Tasks:', 'Status: in-progress.\nCompletion mode: planning-only.\nTasks:'),
|
|
232
|
+
progress,
|
|
233
|
+
testPlan: files['test-plan.md'],
|
|
234
|
+
}),
|
|
235
|
+
/explicit.*Status in-progress.*planning-complete/,
|
|
236
|
+
);
|
|
155
237
|
});
|
|
156
238
|
|
|
157
239
|
test('selects the earliest dependency-ready WBS item', async t => {
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
export const STATUS_ORDER = [
|
|
3
3
|
'not-started',
|
|
4
4
|
'in-progress',
|
|
5
|
+
'planning-complete',
|
|
5
6
|
'implementation-complete',
|
|
6
7
|
'verified',
|
|
7
8
|
'blocked',
|
|
@@ -306,6 +307,16 @@ function normalizeStatus(value) {
|
|
|
306
307
|
return normalized;
|
|
307
308
|
}
|
|
308
309
|
|
|
310
|
+
export function isPlanningCompleteEligible(task) {
|
|
311
|
+
return task.completionMode === 'planning-only';
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
export function isPrerequisiteSatisfied(task, status) {
|
|
315
|
+
return (
|
|
316
|
+
status === 'verified' || (status === 'planning-complete' && isPlanningCompleteEligible(task))
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
|
|
309
320
|
function reviewDateFromLine(line) {
|
|
310
321
|
return (
|
|
311
322
|
line.match(/(?:last reviewed|最后审查日期)\s*(?:[::]\s*)?(\d{4}-\d{2}-\d{2})/i)?.[1] ?? null
|
|
@@ -360,7 +371,7 @@ export function parseProgress(markdown, { file } = {}) {
|
|
|
360
371
|
}
|
|
361
372
|
|
|
362
373
|
function labeledValue(record, labels) {
|
|
363
|
-
const match = record.text.trim().match(/^([A-Z]+)\s*:(.*)$/i);
|
|
374
|
+
const match = record.text.trim().match(/^([A-Z]+(?:\s+[A-Z]+)*)\s*:(.*)$/i);
|
|
364
375
|
return match && labels.includes(match[1].toLowerCase()) ? match[2].trim() : null;
|
|
365
376
|
}
|
|
366
377
|
|
|
@@ -502,6 +513,11 @@ export function parseWbs(markdown, { file } = {}) {
|
|
|
502
513
|
task = null;
|
|
503
514
|
inPhase = false;
|
|
504
515
|
}
|
|
516
|
+
if (!task && labeledValue(record, ['completion mode']) !== null) {
|
|
517
|
+
throw new Error(
|
|
518
|
+
`Completion mode must belong to a formal WBS task at ${atLocation(record, file)}.`,
|
|
519
|
+
);
|
|
520
|
+
}
|
|
505
521
|
if (!inPhase) continue;
|
|
506
522
|
if (task) task.body.push(record);
|
|
507
523
|
else if (!phase.tasks.length) phase.preamble.push(record);
|
|
@@ -519,6 +535,29 @@ export function parseWbs(markdown, { file } = {}) {
|
|
|
519
535
|
? { line: dependency.line, ...(file ? { file } : {}) }
|
|
520
536
|
: undefined;
|
|
521
537
|
task.phase = { number: phase.number, title: phase.title };
|
|
538
|
+
const modeRecords = task.body.filter(
|
|
539
|
+
record => labeledValue(record, ['completion mode']) !== null,
|
|
540
|
+
);
|
|
541
|
+
if (modeRecords.length > 1) {
|
|
542
|
+
throw new Error(
|
|
543
|
+
`Multiple ${task.id} Completion mode fields at ${atLocation(modeRecords[1], file)}.`,
|
|
544
|
+
);
|
|
545
|
+
}
|
|
546
|
+
const mode = modeRecords.length
|
|
547
|
+
? labeledValue(modeRecords[0], ['completion mode'])
|
|
548
|
+
.replace(/`/g, '')
|
|
549
|
+
.replace(/\.$/, '')
|
|
550
|
+
.toLowerCase()
|
|
551
|
+
: undefined;
|
|
552
|
+
if (mode && mode !== 'planning-only') {
|
|
553
|
+
throw new Error(
|
|
554
|
+
`Unsupported ${task.id} Completion mode at ${atLocation(modeRecords[0], file)}.`,
|
|
555
|
+
);
|
|
556
|
+
}
|
|
557
|
+
if (modeRecords.length && !mode) {
|
|
558
|
+
throw new Error(`Empty ${task.id} Completion mode at ${atLocation(modeRecords[0], file)}.`);
|
|
559
|
+
}
|
|
560
|
+
task.completionMode = mode;
|
|
522
561
|
const statusRecords = task.body.filter(record => labeledValue(record, ['status']) !== null);
|
|
523
562
|
if (statusRecords.length > 1) {
|
|
524
563
|
throw new Error(
|
|
@@ -53,6 +53,7 @@ For the detailed authority table, identifiers, and chart boundary, see [Generate
|
|
|
53
53
|
|
|
54
54
|
**Evidence-gated delivery** means delivery status stays precise about what is known and what has been proven.
|
|
55
55
|
|
|
56
|
+
- `planning-complete` means an explicitly `planning-only` WBS task's documentary checks have a named-reviewer, revision-scoped closure disposition and retained planning proof. It satisfies that task's prerequisite edge, not implementation, ATP, successor approval, or release.
|
|
56
57
|
- `implementation-complete` means the source work is complete, while required ATP or release proof remains.
|
|
57
58
|
- `verified` means all applicable WBS checks and ATPs have durable, linked, redacted observed evidence.
|
|
58
59
|
|
|
@@ -109,11 +109,12 @@ Use the narrowest approved check first, then follow the linked ATP procedures an
|
|
|
109
109
|
|
|
110
110
|
Use status precisely:
|
|
111
111
|
|
|
112
|
-
- `in-progress` — approved
|
|
112
|
+
- `in-progress` — approved work or verification has actually begun
|
|
113
|
+
- `planning-complete` — only an opted-in documentary/design WBS task with a named-reviewer, revision-scoped closure disposition and linked planning proof; no source or ATP closure
|
|
113
114
|
- `implementation-complete` — source work is complete, but required ATP or release proof remains
|
|
114
115
|
- `verified` — all applicable WBS checks and ATPs have durable, linked, redacted observed evidence
|
|
115
116
|
|
|
116
|
-
A successful build, generation command, hook, manual walkthrough, screenshot, or unrelated test run is not by itself `verified` unless the authoritative test plan defines it as sufficient retained proof. Evidence is revision- and authority-scoped; mark old proof superseded or requiring rerun when relevant source or authority changes.
|
|
117
|
+
`planning-complete` satisfies a predecessor dependency edge but does not unblock a task still recorded `blocked` or grant an execution dossier. Charts count only `verified` toward verified/remaining metrics. A successful build, generation command, hook, manual walkthrough, screenshot, or unrelated test run is not by itself `verified` unless the authoritative test plan defines it as sufficient retained proof. Evidence is revision- and authority-scoped; mark old proof superseded or requiring rerun when relevant source or authority changes.
|
|
117
118
|
|
|
118
119
|
## Refresh derived charts last
|
|
119
120
|
|
|
@@ -115,7 +115,7 @@ PRD → SRS → WBS → ATP → Evidence
|
|
|
115
115
|
|
|
116
116
|
A product or technical change belongs in its PRD, SRS, or accepted ADR before its WBS, acceptance, progress, evidence, and chart implications are updated. A progress entry or chart cannot introduce a requirement, resolve a contract conflict, or accept an ADR.
|
|
117
117
|
|
|
118
|
-
Planning records and derived charts do not establish `implementation-complete` or `verified`. `verified` requires the applicable acceptance procedure and retained, redacted observed evidence. A generated plan, planned command, scaffold, screenshot, or unrelated check is not automatically sufficient proof.
|
|
118
|
+
A WBS task may declare `Completion mode: planning-only.` only for documentary/design-only checks. It can reach `planning-complete` after an explicit named-reviewer, revision-scoped closure disposition and linked planning proof; this satisfies its dependency edge without executing source, passing future ATPs, unblocking a recorded `blocked` successor, or counting as `verified` in charts. Planning records and derived charts do not establish `implementation-complete` or `verified`. `verified` requires the applicable acceptance procedure and retained, redacted observed evidence. A generated plan, planned command, scaffold, screenshot, or unrelated check is not automatically sufficient proof.
|
|
119
119
|
|
|
120
120
|
## Check planning without claiming implementation
|
|
121
121
|
|
|
Binary file
|
package/repo-docs/index.md
CHANGED
|
@@ -30,6 +30,12 @@ features:
|
|
|
30
30
|
details: Organize capabilities for SSR, SPA, Web, and Admin applications with shared conventions.
|
|
31
31
|
---
|
|
32
32
|
|
|
33
|
+
## Demonstrations(Videos)
|
|
34
|
+
|
|
35
|
+
### 1. Can an Admin Site Use SSR? CabloyJS in Three Practical Demos (Duration: 1:16)
|
|
36
|
+
|
|
37
|
+
[](https://youtu.be/786IQhRdr1I)
|
|
38
|
+
|
|
33
39
|
## Choose a reading path
|
|
34
40
|
|
|
35
41
|
### Start a project
|