cabloy 5.1.194 → 5.1.196
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/.github/workflows/agent-governance.yml +1 -0
- package/CHANGELOG.md +22 -0
- package/README.md +13 -24
- package/package.json +2 -1
- package/repo-agent-governance/managed-assets.json +138 -33
- package/repo-agent-governance/scripts/pack-check.mjs +17 -0
- package/repo-agent-governance/skills/cabloy-backend-scaffold/references/follow-up-checklist.md +1 -0
- package/repo-agent-governance/skills/cabloy-domain-planning/SKILL.md +5 -3
- package/repo-agent-governance/skills/cabloy-spec-execution/SKILL.md +13 -8
- package/repo-agent-governance/skills/cabloy-spec-execution/evals/evals.json +108 -12
- package/repo-agent-governance/skills/cabloy-spec-execution/evals/files/scenarios.json +84 -0
- package/repo-agent-governance/skills/cabloy-spec-execution/evals/protocol.md +9 -0
- package/repo-agent-governance/skills/cabloy-spec-execution/references/execution-protocol.md +21 -5
- package/repo-agent-governance/skills/cabloy-spec-execution/references/status-and-evidence.md +5 -3
- package/repo-agent-governance/skills/cabloy-spec-generation/SKILL.md +78 -168
- package/repo-agent-governance/skills/cabloy-spec-generation/evals/evals.json +165 -17
- package/repo-agent-governance/skills/cabloy-spec-generation/evals/files/scenarios.json +114 -0
- package/repo-agent-governance/skills/cabloy-spec-generation/evals/protocol.md +44 -0
- package/repo-agent-governance/skills/cabloy-spec-generation/references/canonical-spec-input.md +145 -0
- package/repo-agent-governance/skills/cabloy-spec-generation/references/repo-aware-discovery.md +55 -57
- package/repo-agent-governance/skills/cabloy-spec-generation/references/repo-specs-document-set.md +6 -4
- package/repo-agent-governance/skills/cabloy-spec-generation/references/traceability-and-status-rules.md +12 -8
- package/repo-agent-governance/tests/governance.test.mjs +10 -1
- package/repo-agent-governance/tests/spec-audit.test.mjs +255 -0
- package/repo-agent-governance/tools/spec-audit/audit.mjs +427 -0
- package/repo-agent-governance/tools/spec-charts/generate-implementation-charts.mjs +97 -408
- package/repo-agent-governance/tools/spec-charts/generate-implementation-charts.test.mjs +462 -1
- package/repo-agent-governance/tools/spec-charts/spec-parser.mjs +561 -0
- package/repo-docs/.vitepress/config.mjs +54 -29
- package/repo-docs/ai/playbook-spec-execution.md +10 -4
- package/repo-docs/ai/playbook-spec-generation.md +40 -3
- package/repo-docs/ai/skills.md +3 -3
- package/repo-docs/backend/controller-aop-guide.md +10 -0
- package/repo-docs/backend/field-indexes.md +25 -0
- package/repo-docs/blogs/cabloy-fullstack-resource-addressing/index.md +1 -1
- package/repo-docs/fullstack/contract-loop-playbook.md +1 -1
- package/repo-docs/fullstack/development-history.md +24 -0
- package/repo-docs/fullstack/introduction.md +25 -156
- package/repo-docs/fullstack/quickstart.md +1 -1
- package/repo-docs/fullstack/ssr-entry-modes.md +52 -0
- package/repo-docs/fullstack/ssr-site-and-flavor-setup.md +3 -1
- package/repo-docs/fullstack/tutorial-5-backend-contract-sharing.md +13 -7
- package/repo-docs/fullstack/tutorial-6-one-contract-four-uses.md +11 -15
- package/repo-docs/fullstack/tutorials-overview.md +2 -2
- package/repo-docs/index.md +23 -45
- package/repo-docs/public/cabloy.png +0 -0
- package/repo-docs/public/cabloy.svg +3 -0
- package/repo-docs/public/favicon.svg +3 -0
- package/repo-docs/reference/repo-scripts.md +6 -1
- package/repo-e2e/specs/home-user-account.spec.ts +311 -207
- package/scripts/bootstrapAgentGovernance.mjs +1 -0
- package/scripts/upgrade.ts +1 -0
- package/vona/packages-cli/cli/package.json +1 -1
- package/vona/packages-cli/cli-set-api/cli/templates/tools/crudBasic/snippets/2-meta.index.ts +4 -10
- package/vona/packages-cli/cli-set-api/cli/templates/tools/crudStart/snippets/2-meta.index.ts +4 -10
- package/vona/packages-cli/cli-set-api/package.json +8 -2
- package/vona/packages-cli/cli-set-api/src/index.ts +1 -0
- package/vona/packages-cli/cli-set-api/src/lib/bean/cli.tools.masterDetail.ts +9 -12
- package/vona/packages-cli/cli-set-api/src/lib/mergeMetaIndex.ts +121 -0
- package/vona/packages-cli/cli-set-api/test/indexSnippets.test.ts +41 -0
- package/vona/packages-cli/cli-set-api/test/mergeMetaIndex.test.ts +78 -0
- package/vona/packages-vona/vona/package.json +1 -1
- package/vona/pnpm-lock.yaml +6 -6
- package/vona/src/suite/a-commerce/modules/commerce-catalog/src/service/sku.ts +25 -4
- package/vona/src/suite/a-commerce/modules/commerce-catalog/test/skuUniqueness.test.ts +161 -0
- package/vona/src/suite/a-commerce/modules/commerce-payment/src/bean/meta.index.ts +21 -20
- package/vona/src/suite/a-commerce/modules/commerce-payment/test/paymentIndexes.test.ts +165 -0
- package/vona/src/suite/a-commerce/modules/commerce-promotion/src/bean/meta.index.ts +16 -13
- package/vona/src/suite/a-commerce/modules/commerce-promotion/test/promotionIndexes.test.ts +82 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/bean/meta.index.ts +23 -21
- package/vona/src/suite/a-commerce/modules/commerce-trade/test/tradeIndexes.test.ts +86 -0
- package/vona/src/suite/a-home/modules/home-user/src/.metadata/index.ts +379 -376
- package/vona/src/suite/a-home/modules/home-user/src/controller/passportTest.ts +60 -0
- package/vona/src/suite/a-home/modules/home-user/test/passportTest.test.ts +164 -1
- package/vona/src/suite-vendor/a-cabloy/modules/a-rbac/package.json +1 -1
- package/vona/src/suite-vendor/a-cabloy/package.json +2 -2
- package/vona/src/suite-vendor/a-pay/modules/a-pay/package.json +1 -1
- package/vona/src/suite-vendor/a-pay/modules/a-pay/src/bean/meta.index.ts +35 -26
- package/vona/src/suite-vendor/a-pay/modules/pay-mock/package.json +1 -1
- package/vona/src/suite-vendor/a-pay/modules/pay-paypal/package.json +1 -1
- package/vona/src/suite-vendor/a-pay/modules/pay-stripe/package.json +1 -1
- package/vona/src/suite-vendor/a-pay/package.json +5 -5
- package/vona/src/suite-vendor/a-vona/modules/a-orm/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-orm/src/service/transactionFiber_.ts +6 -2
- package/vona/src/suite-vendor/a-vona/modules/a-orm/src/service/transaction_.ts +4 -1
- package/vona/src/suite-vendor/a-vona/modules/a-ormutils/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-ormutils/src/lib/columns.ts +3 -1
- package/vona/src/suite-vendor/a-vona/modules/a-permission/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/package.json +1 -1
- /package/repo-docs/{.vitepress/public → public}/CNAME +0 -0
|
@@ -10,7 +10,10 @@ import {
|
|
|
10
10
|
parseAtpIds,
|
|
11
11
|
parseProgress,
|
|
12
12
|
parseWbs,
|
|
13
|
+
renderGantt,
|
|
14
|
+
renderBurndown,
|
|
13
15
|
} from './generate-implementation-charts.mjs';
|
|
16
|
+
import { markdownLines, findIdentifiers, parseDefinitions } from './spec-parser.mjs';
|
|
14
17
|
|
|
15
18
|
const files = {
|
|
16
19
|
'README.md': '# Demo Internal Planning\n\nEnglish planning records.\n',
|
|
@@ -160,7 +163,7 @@ test('selects the earliest dependency-ready WBS item', async t => {
|
|
|
160
163
|
await generateCharts(directory);
|
|
161
164
|
const gantt = await readFile(resolve(directory, 'implementation-gantt.svg'), 'utf8');
|
|
162
165
|
assert.match(gantt, /WBS-30-01: Start the independent follow-up/);
|
|
163
|
-
assert.match(gantt, /
|
|
166
|
+
assert.match(gantt, /Dependency-ready WBS candidate/);
|
|
164
167
|
});
|
|
165
168
|
|
|
166
169
|
test('renders a phase description directly beneath its phase label', async t => {
|
|
@@ -269,6 +272,464 @@ test('renders a completion marker only when no WBS items remain', async t => {
|
|
|
269
272
|
assert.match(burndown, /M904\.0 650\.0 L908\.0 654\.0 L916\.0 645\.0/);
|
|
270
273
|
});
|
|
271
274
|
|
|
275
|
+
test('shared Markdown lines ignore fences and comments while retaining source locations', () => {
|
|
276
|
+
const markdown = [
|
|
277
|
+
'# Real',
|
|
278
|
+
'<!-- ## Hidden',
|
|
279
|
+
'- **PRD-HIDDEN-01**: comment -->',
|
|
280
|
+
'```markdown',
|
|
281
|
+
'### ATP-HIDDEN-01: example',
|
|
282
|
+
'````',
|
|
283
|
+
'~~~text',
|
|
284
|
+
'#### WBS-HIDDEN-01: example',
|
|
285
|
+
'~~~',
|
|
286
|
+
'- **PRD-REAL-01**: actual requirement <!-- ATP-HIDDEN-02 -->',
|
|
287
|
+
].join('\n');
|
|
288
|
+
const lines = markdownLines(markdown);
|
|
289
|
+
assert.deepEqual(
|
|
290
|
+
lines.filter(record => record.heading).map(record => record.heading.text),
|
|
291
|
+
['Real'],
|
|
292
|
+
);
|
|
293
|
+
const definitions = parseDefinitions(markdown, 'PRD-', { file: 'prd.md' });
|
|
294
|
+
assert.deepEqual([...definitions.keys()], ['PRD-REAL-01']);
|
|
295
|
+
assert.equal(definitions.get('PRD-REAL-01').line, 10);
|
|
296
|
+
assert.equal(definitions.get('PRD-REAL-01').file, 'prd.md');
|
|
297
|
+
assert.doesNotMatch(lines.at(-1).text, /ATP-HIDDEN/);
|
|
298
|
+
assert.deepEqual(findIdentifiers('`ATP-REAL-01`, `ATP-REAL-*`, ATP-BAD-:', 'ATP-'), [
|
|
299
|
+
'ATP-REAL-01',
|
|
300
|
+
]);
|
|
301
|
+
assert.deepEqual(
|
|
302
|
+
[...parseDefinitions('- **PRD-<!-- hidden -->FAKE-01**: not a formal token', 'PRD-')],
|
|
303
|
+
[],
|
|
304
|
+
);
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
test('formal requirement definitions support bold bullets and ID-leading headings only', () => {
|
|
308
|
+
const definitions = parseDefinitions(
|
|
309
|
+
[
|
|
310
|
+
'## Requirements',
|
|
311
|
+
'- **PRD-REAL-01**: requirement; trace SRS-REAL-01.',
|
|
312
|
+
'### PRD-REAL-02: another requirement',
|
|
313
|
+
'Mention PRD-REF-01 in prose.',
|
|
314
|
+
'| PRD-REF-02 | coverage |',
|
|
315
|
+
'### Coverage for PRD-REF-03',
|
|
316
|
+
].join('\n'),
|
|
317
|
+
'PRD-',
|
|
318
|
+
);
|
|
319
|
+
assert.deepEqual([...definitions.keys()], ['PRD-REAL-01', 'PRD-REAL-02']);
|
|
320
|
+
assert.throws(
|
|
321
|
+
() => parseDefinitions('- **SRS-DUP-01**: a\n### SRS-DUP-01: b', 'SRS-'),
|
|
322
|
+
/Duplicate SRS-DUP-01/,
|
|
323
|
+
);
|
|
324
|
+
assert.throws(() => parseDefinitions('- **PRD-A-01, PRD-A-02**: both', 'PRD-'), /Multiple PRD-/);
|
|
325
|
+
assert.throws(() => parseDefinitions('### SRS-A-01 and SRS-A-02: both', 'SRS-'), /Multiple SRS-/);
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
test('definition bodies stop before the next definition or a sibling section', () => {
|
|
329
|
+
const definitions = parseDefinitions(
|
|
330
|
+
[
|
|
331
|
+
'- **PRD-BODY-01**: first',
|
|
332
|
+
'Traceability: SRS-BODY-01.',
|
|
333
|
+
'- **PRD-BODY-02**: second',
|
|
334
|
+
'Traceability: SRS-BODY-02.',
|
|
335
|
+
'## Separate section',
|
|
336
|
+
'Traceability: SRS-NOT-OWNED-01.',
|
|
337
|
+
'### PRD-BODY-03: heading definition',
|
|
338
|
+
'#### Supporting details',
|
|
339
|
+
'Traceability: SRS-BODY-03.',
|
|
340
|
+
'### Unrelated sibling',
|
|
341
|
+
'Traceability: SRS-NOT-OWNED-02.',
|
|
342
|
+
].join('\n'),
|
|
343
|
+
'PRD-',
|
|
344
|
+
);
|
|
345
|
+
assert.match(definitions.get('PRD-BODY-01').body, /SRS-BODY-01/);
|
|
346
|
+
assert.doesNotMatch(definitions.get('PRD-BODY-01').body, /SRS-BODY-02/);
|
|
347
|
+
assert.equal(definitions.get('PRD-BODY-01').endLine, 2);
|
|
348
|
+
assert.doesNotMatch(definitions.get('PRD-BODY-02').body, /NOT-OWNED/);
|
|
349
|
+
assert.match(definitions.get('PRD-BODY-03').body, /SRS-BODY-03/);
|
|
350
|
+
assert.doesNotMatch(definitions.get('PRD-BODY-03').body, /NOT-OWNED/);
|
|
351
|
+
const wbs = parseWbs(
|
|
352
|
+
'### Phase 10: phase\n#### WBS-BODY-01: first\nTraceability: ATP-BODY-01.\n#### WBS-BODY-02: second\nTraceability: ATP-BODY-02.\n## Global\nTraceability: ATP-NOT-OWNED-01.',
|
|
353
|
+
);
|
|
354
|
+
assert.match(wbs.tasks[0].body, /ATP-BODY-01/);
|
|
355
|
+
assert.doesNotMatch(wbs.tasks[0].body, /ATP-BODY-02/);
|
|
356
|
+
assert.equal(wbs.tasks[0].endLine, 3);
|
|
357
|
+
assert.doesNotMatch(wbs.tasks[1].body, /NOT-OWNED/);
|
|
358
|
+
const mixed = parseDefinitions(
|
|
359
|
+
'- **PRD-MIXED-01**: first\nTraceability: SRS-MIXED-01\n- **SRS-MIXED-01**: distinct definition\nTraceability: WBS-NOT-OWNED-01',
|
|
360
|
+
'PRD-',
|
|
361
|
+
);
|
|
362
|
+
assert.doesNotMatch(mixed.get('PRD-MIXED-01').body, /WBS-NOT-OWNED/);
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
test('leading definitions preserve plain, code, and bold delimiters with long titles', () => {
|
|
366
|
+
for (const label of [
|
|
367
|
+
'PRD-FORMAT-01',
|
|
368
|
+
'`PRD-FORMAT-01`',
|
|
369
|
+
'**PRD-FORMAT-01**',
|
|
370
|
+
'**`PRD-FORMAT-01`**',
|
|
371
|
+
]) {
|
|
372
|
+
const definitions = parseDefinitions(`### ${label}: ${'A'.repeat(10000)}`, 'PRD-');
|
|
373
|
+
assert.deepEqual([...definitions.keys()], ['PRD-FORMAT-01']);
|
|
374
|
+
}
|
|
375
|
+
assert.equal(parseDefinitions('### **PRD-FORMAT-01: unclosed bold', 'PRD-').size, 0);
|
|
376
|
+
assert.equal(parseDefinitions('### `PRD-FORMAT-01: unclosed code', 'PRD-').size, 0);
|
|
377
|
+
const wbs = parseWbs(
|
|
378
|
+
'### Phase 10: Trimmed phase \n#### WBS-FORMAT-01: task\nDependencies: none.',
|
|
379
|
+
);
|
|
380
|
+
assert.equal(wbs.phases[0].title, 'Trimmed phase');
|
|
381
|
+
assert.equal(wbs.tasks[0].dependency, 'none.');
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
test('same-line multiple bold definitions or explicit heading definitions are rejected', () => {
|
|
385
|
+
assert.throws(
|
|
386
|
+
() => parseDefinitions('- **PRD-A-01**: first; **PRD-A-02**: second', 'PRD-'),
|
|
387
|
+
/Multiple PRD-/,
|
|
388
|
+
);
|
|
389
|
+
assert.throws(() => parseAtpIds('### ATP-A-01: first; ATP-A-02: second'), /Multiple ATP-/);
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
test('ATP definitions reject duplicate catalogue rows and ambiguous multi-ID cells', () => {
|
|
393
|
+
assert.throws(
|
|
394
|
+
() => parseAtpIds(`${files['test-plan.md']}| \`ATP-DEMO-01\` | duplicate |\n`),
|
|
395
|
+
/Duplicate ATP-DEMO-01/,
|
|
396
|
+
);
|
|
397
|
+
assert.throws(
|
|
398
|
+
() =>
|
|
399
|
+
parseAtpIds(files['test-plan.md'].replace('`ATP-DEMO-01`', '`ATP-DEMO-01`, `ATP-DEMO-02`')),
|
|
400
|
+
/Multiple ATP IDs/,
|
|
401
|
+
);
|
|
402
|
+
assert.throws(
|
|
403
|
+
() => parseAtpIds(`${files['test-plan.md']}\n### ATP-DEMO-01: duplicate heading`),
|
|
404
|
+
/Duplicate ATP-DEMO-01/,
|
|
405
|
+
);
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
test('ATP catalogue role headers define scenarios but coverage and evidence tables do not', () => {
|
|
409
|
+
const markdown = [
|
|
410
|
+
'## Coverage',
|
|
411
|
+
'| ATP ID | Evidence |',
|
|
412
|
+
'| --- | --- |',
|
|
413
|
+
'| `ATP-REF-01` | reference only |',
|
|
414
|
+
'## Acceptance Scenarios',
|
|
415
|
+
'- **ATP-BULLET-01**: explicit scenario',
|
|
416
|
+
'### ATP-HEADING-01: Explicit scenario',
|
|
417
|
+
'Setup: fixture',
|
|
418
|
+
'## Evidence',
|
|
419
|
+
'- **ATP-REF-02**: evidence reference',
|
|
420
|
+
'### CI evidence for ATP-REF-03',
|
|
421
|
+
'| ATP ID | Scenario |',
|
|
422
|
+
'| --- | --- |',
|
|
423
|
+
'| ATP-REF-04 | rollout reference only |',
|
|
424
|
+
'## Acceptance Scenario Catalogue',
|
|
425
|
+
'| Primary traceability | Scenario and minimum proof | ATP ID |',
|
|
426
|
+
'| --- | --- | --- |',
|
|
427
|
+
'| WBS-REAL-01 | legacy definition | `ATP-CATALOGUE-01` |',
|
|
428
|
+
].join('\n');
|
|
429
|
+
assert.deepEqual(
|
|
430
|
+
[...parseAtpIds(markdown)],
|
|
431
|
+
['ATP-BULLET-01', 'ATP-HEADING-01', 'ATP-CATALOGUE-01'],
|
|
432
|
+
);
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
test('existing business ATP catalogues remain compatible with strict definitions', async () => {
|
|
436
|
+
const root = resolve(import.meta.dirname, '../../..');
|
|
437
|
+
const commerce = parseAtpIds(
|
|
438
|
+
await readFile(resolve(root, 'repo-specs/a-commerce/test-plan.md'), 'utf8'),
|
|
439
|
+
);
|
|
440
|
+
const account = parseAtpIds(
|
|
441
|
+
await readFile(resolve(root, 'repo-specs/home-user/test-plan.md'), 'utf8'),
|
|
442
|
+
);
|
|
443
|
+
assert.ok(commerce.has('ATP-SPC-04'));
|
|
444
|
+
assert.ok(commerce.has('ATP-SPC-05'));
|
|
445
|
+
assert.ok(account.has('ATP-HUA-ACC-01'));
|
|
446
|
+
assert.ok(account.has('ATP-HUA-RST-03'));
|
|
447
|
+
assert.ok(commerce.size > 10);
|
|
448
|
+
assert.ok(account.size > 10);
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
test('progress rejects duplicate rows and multiple IDs in one row with source locations', () => {
|
|
452
|
+
assert.throws(
|
|
453
|
+
() =>
|
|
454
|
+
parseProgress(`${files['progress.md']}| \`WBS-10-01\` | verified |\n`, {
|
|
455
|
+
file: 'progress.md',
|
|
456
|
+
}),
|
|
457
|
+
/Duplicate WBS-10-01.*progress.md:/,
|
|
458
|
+
);
|
|
459
|
+
assert.throws(
|
|
460
|
+
() => parseProgress(files['progress.md'].replace('`WBS-10-01`', '`WBS-10-01`, `WBS-20-01`')),
|
|
461
|
+
/Multiple WBS IDs/,
|
|
462
|
+
);
|
|
463
|
+
const progress = parseProgress(files['progress.md'], { file: 'progress.md' });
|
|
464
|
+
assert.deepEqual(progress.locations.get('WBS-10-01'), { file: 'progress.md', line: 7 });
|
|
465
|
+
assert.deepEqual(progress.reviewedLocation, { file: 'progress.md', line: 3 });
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
test('WBS rejects duplicate and multi-ID headings rather than merging tasks', () => {
|
|
469
|
+
assert.throws(
|
|
470
|
+
() => parseWbs(`${files['pdp-wbs.md']}\n#### WBS-20-01: duplicate`),
|
|
471
|
+
/Duplicate WBS-20-01/,
|
|
472
|
+
);
|
|
473
|
+
assert.throws(
|
|
474
|
+
() => parseWbs('### Phase 10: phase\n#### WBS-A-01 and WBS-A-02: both'),
|
|
475
|
+
/Multiple WBS IDs/,
|
|
476
|
+
);
|
|
477
|
+
assert.throws(
|
|
478
|
+
() => parseWbs('### Phase 10: phase\n#### WBS-A-01: first; WBS-A-02: second'),
|
|
479
|
+
/Multiple WBS IDs/,
|
|
480
|
+
);
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
test('phase defaults are only read before the first task and task sections stop before global matrices', () => {
|
|
484
|
+
const parsed = parseWbs(
|
|
485
|
+
[
|
|
486
|
+
'### Phase 10: phase',
|
|
487
|
+
'#### WBS-A-01: first',
|
|
488
|
+
'Dependency: none.',
|
|
489
|
+
'#### WBS-A-02: second',
|
|
490
|
+
'Dependency: WBS-A-01.',
|
|
491
|
+
'#### WBS-A-03: third',
|
|
492
|
+
'## Traceability Matrix',
|
|
493
|
+
'| WBS-A-03 | ATP-NOT-OWNED-01 |',
|
|
494
|
+
'Dependencies: WBS-UNKNOWN-01.',
|
|
495
|
+
].join('\n'),
|
|
496
|
+
);
|
|
497
|
+
assert.equal(parsed.phases[0].dependency, 'none');
|
|
498
|
+
assert.deepEqual(
|
|
499
|
+
parsed.tasks.map(task => task.dependencyIds),
|
|
500
|
+
[[], ['WBS-A-01'], []],
|
|
501
|
+
);
|
|
502
|
+
assert.deepEqual(parsed.tasks[2].atps, []);
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
function dependencyFixture(dependency, extra = '') {
|
|
506
|
+
return [
|
|
507
|
+
'### Phase 10: first',
|
|
508
|
+
'Dependency: none.',
|
|
509
|
+
'#### WBS-10-01: first',
|
|
510
|
+
'#### WBS-10-02: second',
|
|
511
|
+
'### Phase 20: middle',
|
|
512
|
+
'Dependencies: none.',
|
|
513
|
+
'#### WBS-20-01: middle',
|
|
514
|
+
'### Phase 30: last',
|
|
515
|
+
'Dependencies: none.',
|
|
516
|
+
'#### WBS-30-01: last',
|
|
517
|
+
'### Phase 40: consumer',
|
|
518
|
+
`Dependencies: ${dependency}`,
|
|
519
|
+
'#### WBS-40-01: consumer',
|
|
520
|
+
extra,
|
|
521
|
+
].join('\n');
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
test('dependency expansion includes exact IDs, all wildcard matches, and range middle tasks', () => {
|
|
525
|
+
const exact = parseWbs(dependencyFixture('`WBS-10-01`, `WBS-30-01`.'));
|
|
526
|
+
assert.deepEqual(exact.tasks.at(-1).dependencyIds, ['WBS-10-01', 'WBS-30-01']);
|
|
527
|
+
const wildcard = parseWbs(dependencyFixture('`WBS-10-*`.'));
|
|
528
|
+
assert.deepEqual(wildcard.tasks.at(-1).dependencyIds, ['WBS-10-01', 'WBS-10-02']);
|
|
529
|
+
const range = parseWbs(dependencyFixture('`WBS-10-*` through `WBS-30-*`.'));
|
|
530
|
+
assert.deepEqual(range.tasks.at(-1).dependencyIds, [
|
|
531
|
+
'WBS-10-01',
|
|
532
|
+
'WBS-10-02',
|
|
533
|
+
'WBS-20-01',
|
|
534
|
+
'WBS-30-01',
|
|
535
|
+
]);
|
|
536
|
+
assert.equal(range.expandedEdges.get('WBS-40-01'), range.tasks.at(-1).dependencyIds);
|
|
537
|
+
const dash = parseWbs(dependencyFixture('`WBS-10-02`–`WBS-30-01`.'));
|
|
538
|
+
assert.deepEqual(dash.tasks.at(-1).dependencyIds, ['WBS-10-02', 'WBS-20-01', 'WBS-30-01']);
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
test('dependency parsing rejects unknown, malformed, reversed, and gate-only dependencies', () => {
|
|
542
|
+
for (const dependency of [
|
|
543
|
+
'WBS-UNKNOWN-01.',
|
|
544
|
+
'WBS-UNKNOWN-*.',
|
|
545
|
+
'WBS-10-01-:',
|
|
546
|
+
'WBS-10-01**',
|
|
547
|
+
'WBS-10-01 to',
|
|
548
|
+
'WBS-10-01 through',
|
|
549
|
+
'WBS-10-01 through WBS-<phase>-01',
|
|
550
|
+
'WBS-30-01 through WBS-10-01',
|
|
551
|
+
'approved scope only',
|
|
552
|
+
'',
|
|
553
|
+
]) {
|
|
554
|
+
assert.throws(
|
|
555
|
+
() => parseWbs(dependencyFixture(dependency)),
|
|
556
|
+
/unknown|unparseable/i,
|
|
557
|
+
dependency,
|
|
558
|
+
);
|
|
559
|
+
}
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
test('self dependencies and cycles are rejected after wildcard and range expansion', () => {
|
|
563
|
+
assert.throws(() => parseWbs(dependencyFixture('WBS-40-*')), /self dependency/);
|
|
564
|
+
assert.throws(
|
|
565
|
+
() => parseWbs(dependencyFixture('WBS-10-01 through WBS-40-01')),
|
|
566
|
+
/self dependency/,
|
|
567
|
+
);
|
|
568
|
+
assert.throws(
|
|
569
|
+
() =>
|
|
570
|
+
parseWbs(
|
|
571
|
+
dependencyFixture('WBS-20-01 through WBS-30-01').replace(
|
|
572
|
+
'#### WBS-20-01: middle',
|
|
573
|
+
'#### WBS-20-01: middle\nDependency: WBS-40-01.',
|
|
574
|
+
),
|
|
575
|
+
),
|
|
576
|
+
/dependency cycle/,
|
|
577
|
+
);
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
test('descriptive dependency gates survive parsing without becoming execution approval', () => {
|
|
581
|
+
const parsed = parseWbs(
|
|
582
|
+
dependencyFixture(
|
|
583
|
+
'`WBS-10-01` and retained CI evidence. Each source slice additionally requires review.',
|
|
584
|
+
),
|
|
585
|
+
);
|
|
586
|
+
assert.deepEqual(parsed.tasks.at(-1).dependencyIds, ['WBS-10-01']);
|
|
587
|
+
assert.match(parsed.tasks.at(-1).dependencyGate, /retained CI evidence.*requires review/);
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
test('candidate selection uses the expanded range graph including unverified middle tasks', () => {
|
|
591
|
+
const wbs = dependencyFixture('WBS-10-01 through WBS-30-01.');
|
|
592
|
+
const progress = [
|
|
593
|
+
'| WBS ID | Status |',
|
|
594
|
+
'| --- | --- |',
|
|
595
|
+
'| WBS-10-01 | verified |',
|
|
596
|
+
'| WBS-10-02 | verified |',
|
|
597
|
+
'| WBS-20-01 | blocked |',
|
|
598
|
+
'| WBS-30-01 | verified |',
|
|
599
|
+
'| WBS-40-01 | not-started |',
|
|
600
|
+
].join('\n');
|
|
601
|
+
const model = createChartModel({ readme: files['README.md'], wbs, progress, testPlan: '' });
|
|
602
|
+
const gantt = renderGantt(model, 'Demo');
|
|
603
|
+
assert.match(gantt, /No dependency-ready WBS candidate is recorded/);
|
|
604
|
+
assert.doesNotMatch(gantt, /class="caption" x="96" y="195">WBS-40-01/);
|
|
605
|
+
const ready = createChartModel({
|
|
606
|
+
readme: files['README.md'],
|
|
607
|
+
wbs,
|
|
608
|
+
progress: progress.replace('blocked', 'verified'),
|
|
609
|
+
testPlan: '',
|
|
610
|
+
});
|
|
611
|
+
assert.match(renderGantt(ready, 'Demo'), /class="caption" x="96" y="195">WBS-40-01: consumer/);
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
test('fenced examples and comments are ignored in WBS, ATP, and progress authority', () => {
|
|
615
|
+
const examples =
|
|
616
|
+
'\n```markdown\n### Phase 90: Fake\n#### WBS-FAKE-01: example\nDependency: WBS-NOT-DEFINED-01\n### ATP-FAKE-01: example\n| WBS ID | Status |\n| --- | --- |\n| WBS-FAKE-01 | verified |\n```\n<!-- ### ATP-COMMENT-01: hidden -->\n';
|
|
617
|
+
assert.equal(parseWbs(files['pdp-wbs.md'] + examples).tasks.length, 3);
|
|
618
|
+
assert.equal(parseAtpIds(files['test-plan.md'] + examples).size, 1);
|
|
619
|
+
assert.equal(parseProgress(files['progress.md'] + examples).rows.size, 3);
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
test('explicit WBS Status must agree with progress, without treating text as verified evidence', () => {
|
|
623
|
+
const create = wbs =>
|
|
624
|
+
createChartModel({
|
|
625
|
+
readme: files['README.md'],
|
|
626
|
+
wbs,
|
|
627
|
+
progress: files['progress.md'],
|
|
628
|
+
testPlan: files['test-plan.md'],
|
|
629
|
+
});
|
|
630
|
+
assert.throws(
|
|
631
|
+
() =>
|
|
632
|
+
create(
|
|
633
|
+
files['pdp-wbs.md'].replace(
|
|
634
|
+
'#### WBS-10-01: Freeze the baseline',
|
|
635
|
+
'#### WBS-10-01: Freeze the baseline\nStatus: verified.',
|
|
636
|
+
),
|
|
637
|
+
),
|
|
638
|
+
/explicit.*Status verified.*not-started/,
|
|
639
|
+
);
|
|
640
|
+
const model = create(
|
|
641
|
+
files['pdp-wbs.md'].replace(
|
|
642
|
+
'#### WBS-10-01: Freeze the baseline',
|
|
643
|
+
'#### WBS-10-01: Freeze the baseline\nStatus: not-started.',
|
|
644
|
+
),
|
|
645
|
+
);
|
|
646
|
+
assert.equal(model.tasks[0].recordedStatus, 'not-started');
|
|
647
|
+
assert.equal(model.verified, 0);
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
test('Chinese descriptions, tooltips, aria labels, and fallback copy contain no generated English', () => {
|
|
651
|
+
const model = createChartModel({
|
|
652
|
+
readme: '# 中文示例内部规划\n\n完整的中文规划记录。',
|
|
653
|
+
wbs: '### Phase 10: 初始阶段\nDependency: none.\n#### WBS-ZH-01: 示例工作',
|
|
654
|
+
progress: '| WBS ID | Status |\n| --- | --- |\n| WBS-ZH-01 | blocked |',
|
|
655
|
+
testPlan: '',
|
|
656
|
+
});
|
|
657
|
+
const gantt = renderGantt(model, '中文示例');
|
|
658
|
+
const burndown = renderBurndown(model, '中文示例');
|
|
659
|
+
for (const svg of [gantt, burndown]) {
|
|
660
|
+
assert.match(svg, /最后审查日期: 未记录/);
|
|
661
|
+
assert.doesNotMatch(
|
|
662
|
+
svg,
|
|
663
|
+
/formal WBS tasks|Current derived|active WBS items|remaining|not recorded|dependencies:| of |Approved|executable|verified/,
|
|
664
|
+
);
|
|
665
|
+
}
|
|
666
|
+
assert.match(gantt, /aria-label="WBS-ZH-01: 示例工作; 依赖: 无; 受阻"/);
|
|
667
|
+
assert.match(gantt, /<title>WBS-ZH-01: 示例工作; 依赖: 无; 受阻<\/title>/);
|
|
668
|
+
assert.match(gantt, /没有已记录的依赖就绪 WBS 候选项/);
|
|
669
|
+
assert.match(burndown, /aria-label="阶段 10:0 \/ 1 项已核验"/);
|
|
670
|
+
assert.match(gantt, /依赖就绪不代表实施批准/);
|
|
671
|
+
assert.match(burndown, /已记录 WBS 范围/);
|
|
672
|
+
});
|
|
673
|
+
|
|
674
|
+
test('burndown grows for eight phases and keeps phase rows clear of the footer', () => {
|
|
675
|
+
const wbs = Array.from(
|
|
676
|
+
{ length: 8 },
|
|
677
|
+
(_, index) =>
|
|
678
|
+
`### Phase ${index + 1}: 阶段说明\nDependency: none.\n#### WBS-P-${index + 1}: 工作项`,
|
|
679
|
+
).join('\n');
|
|
680
|
+
const progressRows = Array.from(
|
|
681
|
+
{ length: 8 },
|
|
682
|
+
(_, index) => `| WBS-P-${index + 1} | not-started |`,
|
|
683
|
+
).join('\n');
|
|
684
|
+
const progress = `| WBS ID | Status |\n| --- | --- |\n${progressRows}`;
|
|
685
|
+
const model = createChartModel({
|
|
686
|
+
readme: '# 中文规划\n完整中文记录。',
|
|
687
|
+
wbs,
|
|
688
|
+
progress,
|
|
689
|
+
testPlan: '',
|
|
690
|
+
});
|
|
691
|
+
const svg = renderBurndown(model, '示例');
|
|
692
|
+
const phaseY = Array.from(svg.matchAll(/<text class="task" x="96" y="(\d+)">阶段/g), match =>
|
|
693
|
+
Number(match[1]),
|
|
694
|
+
);
|
|
695
|
+
assert.equal(phaseY.length, 8);
|
|
696
|
+
const footerY = Number(svg.match(/<line class="grid" x1="72" y1="(\d+)" x2="1500"/)[1]);
|
|
697
|
+
const height = Number(svg.match(/width="1600" height="(\d+)"/)[1]);
|
|
698
|
+
assert.ok(footerY > Math.max(...phaseY) + 30);
|
|
699
|
+
assert.ok(height > footerY + 48);
|
|
700
|
+
assert.ok(phaseY[0] > 800 + 20);
|
|
701
|
+
});
|
|
702
|
+
|
|
703
|
+
test('long status labels and burndown explanations wrap within their existing columns', () => {
|
|
704
|
+
const model = createChartModel({
|
|
705
|
+
readme: files['README.md'],
|
|
706
|
+
wbs: files['pdp-wbs.md'],
|
|
707
|
+
progress: files['progress.md'].replace('`in-progress`', '`implementation-complete`'),
|
|
708
|
+
testPlan: files['test-plan.md'],
|
|
709
|
+
});
|
|
710
|
+
const gantt = renderGantt(model, 'Demo');
|
|
711
|
+
assert.match(
|
|
712
|
+
gantt,
|
|
713
|
+
/<text class="small" x="917" y="\d+"><tspan x="917" dy="0">[^<]*<\/tspan><tspan x="917" dy="12">[^<]*<\/tspan>/,
|
|
714
|
+
);
|
|
715
|
+
assert.doesNotMatch(gantt, /<text class="small" x="917" y="\d+">Implementation complete/);
|
|
716
|
+
const burndown = renderBurndown(model, 'Demo');
|
|
717
|
+
assert.match(
|
|
718
|
+
burndown,
|
|
719
|
+
/<text class="caption" x="1070" y="\d+"><tspan x="1070" dy="0">Relative positions/,
|
|
720
|
+
);
|
|
721
|
+
const explanationLines = Array.from(
|
|
722
|
+
burndown.matchAll(/<tspan x="1070" dy="(?:0|18)">([^<]*)<\/tspan>/g),
|
|
723
|
+
match => match[1],
|
|
724
|
+
);
|
|
725
|
+
assert.ok(explanationLines.length >= 2);
|
|
726
|
+
assert.doesNotMatch(
|
|
727
|
+
burndown,
|
|
728
|
+
/<text class="caption" x="1070" y="\d+">Relative positions and overlap/,
|
|
729
|
+
);
|
|
730
|
+
assert.ok(explanationLines.every(line => line.length < 95));
|
|
731
|
+
});
|
|
732
|
+
|
|
272
733
|
test('writes deterministic XML-escaped artifacts and detects stale output', async t => {
|
|
273
734
|
const directory = await fixture();
|
|
274
735
|
t.after(() => removeFixture(directory));
|