@zhuan-ai/zhuanspec 2.2.4 → 2.4.8

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.
Files changed (103) hide show
  1. package/bin/zhuanspec-hook.js +3 -0
  2. package/dist/cli/hooks.d.ts +14 -0
  3. package/dist/cli/hooks.js +465 -0
  4. package/dist/cli/index.js +100 -0
  5. package/dist/commands/artifact-workflow.js +15 -34
  6. package/dist/commands/design.d.ts +42 -0
  7. package/dist/commands/design.js +337 -0
  8. package/dist/commands/progress.d.ts +32 -0
  9. package/dist/commands/progress.js +278 -0
  10. package/dist/commands/review.d.ts +32 -0
  11. package/dist/commands/review.js +472 -0
  12. package/dist/commands/validate.d.ts +14 -0
  13. package/dist/commands/validate.js +161 -10
  14. package/dist/core/archive.d.ts +1 -0
  15. package/dist/core/archive.js +45 -3
  16. package/dist/core/completions/command-registry.js +67 -0
  17. package/dist/core/configurators/slash/amazon-q.js +32 -2
  18. package/dist/core/configurators/slash/antigravity.js +8 -2
  19. package/dist/core/configurators/slash/auggie.js +16 -1
  20. package/dist/core/configurators/slash/base.js +1 -1
  21. package/dist/core/configurators/slash/claude.d.ts +4 -0
  22. package/dist/core/configurators/slash/claude.js +56 -1
  23. package/dist/core/configurators/slash/cline.js +8 -2
  24. package/dist/core/configurators/slash/codebuddy.js +22 -1
  25. package/dist/core/configurators/slash/codex.js +21 -0
  26. package/dist/core/configurators/slash/costrict.js +15 -0
  27. package/dist/core/configurators/slash/crush.js +22 -1
  28. package/dist/core/configurators/slash/cursor.js +22 -1
  29. package/dist/core/configurators/slash/factory.js +16 -1
  30. package/dist/core/configurators/slash/gemini.js +8 -2
  31. package/dist/core/configurators/slash/github-copilot.js +19 -1
  32. package/dist/core/configurators/slash/iflow.js +22 -1
  33. package/dist/core/configurators/slash/kilocode.js +4 -1
  34. package/dist/core/configurators/slash/opencode.js +27 -0
  35. package/dist/core/configurators/slash/qoder.d.ts +4 -0
  36. package/dist/core/configurators/slash/qoder.js +59 -1
  37. package/dist/core/configurators/slash/qwen.js +8 -2
  38. package/dist/core/configurators/slash/roocode.js +8 -2
  39. package/dist/core/configurators/slash/windsurf.js +8 -2
  40. package/dist/core/dashboard/metrics.d.ts +33 -0
  41. package/dist/core/dashboard/metrics.js +114 -0
  42. package/dist/core/hooks/collect-knowledge.d.ts +16 -0
  43. package/dist/core/hooks/collect-knowledge.js +203 -0
  44. package/dist/core/hooks/context-load-hook.d.ts +25 -0
  45. package/dist/core/hooks/context-load-hook.js +159 -0
  46. package/dist/core/hooks/deviation-check.d.ts +27 -0
  47. package/dist/core/hooks/deviation-check.js +403 -0
  48. package/dist/core/hooks/deviation-handler.d.ts +43 -0
  49. package/dist/core/hooks/deviation-handler.js +98 -0
  50. package/dist/core/hooks/init.d.ts +14 -0
  51. package/dist/core/hooks/init.js +244 -0
  52. package/dist/core/hooks/notify-milestone.d.ts +14 -0
  53. package/dist/core/hooks/notify-milestone.js +170 -0
  54. package/dist/core/hooks/post-apply.d.ts +29 -0
  55. package/dist/core/hooks/post-apply.js +173 -0
  56. package/dist/core/hooks/post-archive.d.ts +7 -0
  57. package/dist/core/hooks/post-archive.js +208 -0
  58. package/dist/core/hooks/pre-apply.d.ts +34 -0
  59. package/dist/core/hooks/pre-apply.js +139 -0
  60. package/dist/core/hooks/pre-archive.d.ts +7 -0
  61. package/dist/core/hooks/pre-archive.js +50 -0
  62. package/dist/core/hooks/record-progress.d.ts +49 -0
  63. package/dist/core/hooks/record-progress.js +494 -0
  64. package/dist/core/hooks/review-hooks.d.ts +89 -0
  65. package/dist/core/hooks/review-hooks.js +345 -0
  66. package/dist/core/hooks/review-orchestrator.d.ts +40 -0
  67. package/dist/core/hooks/review-orchestrator.js +146 -0
  68. package/dist/core/hooks/summarize.d.ts +15 -0
  69. package/dist/core/hooks/summarize.js +282 -0
  70. package/dist/core/hooks/user-input-hook.d.ts +25 -0
  71. package/dist/core/hooks/user-input-hook.js +179 -0
  72. package/dist/core/init.d.ts +8 -0
  73. package/dist/core/init.js +251 -23
  74. package/dist/core/parsers/requirement-blocks.js +13 -10
  75. package/dist/core/templates/agents-template.d.ts +1 -1
  76. package/dist/core/templates/agents-template.js +510 -243
  77. package/dist/core/templates/index.d.ts +1 -0
  78. package/dist/core/templates/index.js +1 -0
  79. package/dist/core/templates/skill-templates.js +46 -152
  80. package/dist/core/templates/slash-command-templates.d.ts +1 -1
  81. package/dist/core/templates/slash-command-templates.js +352 -20
  82. package/dist/core/templates/tasks-template.d.ts +7 -0
  83. package/dist/core/templates/tasks-template.js +130 -24
  84. package/dist/core/templates/tdd-tasks-template.d.ts +3 -0
  85. package/dist/core/templates/tdd-tasks-template.js +91 -38
  86. package/dist/core/templates/test-cases-template.d.ts +41 -0
  87. package/dist/core/templates/test-cases-template.js +128 -0
  88. package/dist/core/validation/strict-rules.d.ts +44 -5
  89. package/dist/core/validation/strict-rules.js +302 -8
  90. package/dist/core/validation/validator.js +52 -2
  91. package/dist/core/view.d.ts +1 -0
  92. package/dist/core/view.js +60 -2
  93. package/dist/mcp/index.d.ts +28 -0
  94. package/dist/mcp/index.js +31 -0
  95. package/dist/utils/file-system.d.ts +1 -0
  96. package/dist/utils/file-system.js +11 -0
  97. package/dist/utils/item-discovery.js +24 -2
  98. package/dist/utils/phase-utils.d.ts +36 -0
  99. package/dist/utils/phase-utils.js +117 -0
  100. package/package.json +22 -23
  101. package/schemas/spec-driven/schema.yaml +45 -31
  102. package/schemas/spec-driven/templates/spec.md +142 -5
  103. package/schemas/spec-driven/templates/tasks.md +73 -9
@@ -1,17 +1,18 @@
1
1
  /**
2
2
  * Strict Validation Rules
3
3
  *
4
- * Implementation of three strict validation rules for `zhuanspec validate --strict`:
5
- * 1. pre-clarification-completed: Check Pre-Clarification Log section
6
- * 2. skill-tags-valid: Validate @skill tags against known skills
7
- * 3. task-ordering-by-wave: Verify task ordering follows wave structure
4
+ * Implementation of strict validation rules for `zhuanspec validate --strict`.
8
5
  */
9
6
  import { generateExecutionPlan } from '../task-graph/execution-planner.js';
10
7
  import { renderExecutionPlanXml } from '../task-graph/xml-renderer.js';
8
+ import { readdirSync, existsSync, readFileSync } from 'fs';
9
+ import { join } from 'path';
10
+ import { execSync } from 'child_process';
11
+ import path from 'path';
11
12
  // Patterns for detecting unresolved markers
12
13
  const UNRESOLVED_MARKERS = /\b(TODO|TBD|PENDING|\?)\b/i;
13
14
  const HTML_COMMENT_PATTERN = /<!--[\s\S]*?-->/g;
14
- const PRE_CLARIFICATION_SECTION_PATTERN = /^##\s+Pre-Clarification\s+Log\s*$/im;
15
+ const PRE_CLARIFICATION_SECTION_PATTERN = /^##\s+(Pre-Clarification\s+Log|澄清日志)\s*$/im;
15
16
  const VALID_COMPLETION_MARKERS = [
16
17
  /no\s+clarification\s+needed/i,
17
18
  /status:\s*completed/i,
@@ -102,7 +103,7 @@ export function checkSkillTagsValid(tasksContent, parsedTasks, knownSkillNames)
102
103
  // a. Check Skill Mapping section exists with table data
103
104
  const sectionMatch = tasksContent.match(SKILL_MAPPING_SECTION_PATTERN);
104
105
  if (!sectionMatch) {
105
- result.warnings.push('Skill Mapping section not found');
106
+ result.errors.push('Skill Mapping section not found');
106
107
  }
107
108
  else {
108
109
  // Extract section content (from header to next ## or end)
@@ -125,7 +126,7 @@ export function checkSkillTagsValid(tasksContent, parsedTasks, knownSkillNames)
125
126
  });
126
127
  // Need at least 2 rows (header + 1 data row)
127
128
  if (!dataRows || dataRows.length < 2) {
128
- result.warnings.push('Skill Mapping table has no data rows');
129
+ result.errors.push('Skill Mapping table has no data rows');
129
130
  }
130
131
  }
131
132
  // If no tasks, consider it valid
@@ -336,16 +337,309 @@ export function checkSubagentDirectives(parsedTasks) {
336
337
  result.passed = result.errors.length === 0;
337
338
  return result;
338
339
  }
340
+ /**
341
+ * Rule 5: review-gate-defined
342
+ *
343
+ * Tasks must explicitly mention zhuanspec review gate.
344
+ */
345
+ export function checkReviewGateDefined(tasksContent) {
346
+ const result = {
347
+ ruleId: 'review-gate-defined',
348
+ ruleName: 'Review gate defined',
349
+ passed: false,
350
+ errors: [],
351
+ warnings: [],
352
+ };
353
+ const hasReviewGate = tasksContent.includes('zhuanspec review') ||
354
+ /review\s*gate/i.test(tasksContent) ||
355
+ /双轨审查/.test(tasksContent);
356
+ if (!hasReviewGate) {
357
+ result.errors.push('tasks.md must include a zhuanspec review gate before completion');
358
+ return result;
359
+ }
360
+ result.passed = true;
361
+ return result;
362
+ }
363
+ /**
364
+ * Rule 7: proposal-format-valid
365
+ *
366
+ * Validates that proposal.md contains required sections:
367
+ * - Why/变更原因
368
+ * - What Changes/变更内容
369
+ * - Impact/影响范围
370
+ */
371
+ export function checkProposalFormatValid(proposalContent) {
372
+ const result = {
373
+ ruleId: 'proposal-format-valid',
374
+ ruleName: 'Proposal format valid',
375
+ passed: false,
376
+ errors: [],
377
+ warnings: [],
378
+ };
379
+ const requiredSections = [
380
+ { pattern: /^##\s+(变更原因|Why)/im, name: 'Why/变更原因' },
381
+ { pattern: /^##\s+(变更内容|What\s+Changes)/im, name: 'What Changes/变更内容' },
382
+ { pattern: /^##\s+(影响范围|Impact)/im, name: 'Impact/影响范围' },
383
+ ];
384
+ for (const section of requiredSections) {
385
+ if (!section.pattern.test(proposalContent)) {
386
+ result.errors.push(`Proposal 缺少必需章节:${section.name}`);
387
+ }
388
+ }
389
+ result.passed = result.errors.length === 0;
390
+ return result;
391
+ }
392
+ /**
393
+ * Detect current workflow phase based on progress.json.
394
+ *
395
+ * Phase detection logic:
396
+ * 1. If progress.json doesn't exist -> Propose phase
397
+ * 2. If progress.json exists with phase field -> use that phase
398
+ * 3. If progress.json exists without phase field -> Apply phase (default)
399
+ */
400
+ export function detectPhase(changeDir) {
401
+ const progressPath = join(changeDir, 'metrics', 'progress.json');
402
+ // If no progress.json, we're in Propose phase
403
+ if (!existsSync(progressPath)) {
404
+ return 'propose';
405
+ }
406
+ try {
407
+ const progressContent = readFileSync(progressPath, 'utf-8');
408
+ const progress = JSON.parse(progressContent);
409
+ // If phase field exists, use it
410
+ if (progress.phase) {
411
+ return progress.phase;
412
+ }
413
+ // Default to Apply phase if progress.json exists but no phase field
414
+ return 'apply';
415
+ }
416
+ catch {
417
+ // If can't read/parse progress.json, assume Propose phase
418
+ return 'propose';
419
+ }
420
+ }
421
+ /**
422
+ * Rule 8: spec-code-consistent
423
+ *
424
+ * Validates that Requirements in delta specs have corresponding code changes.
425
+ * Uses weak signal matching: requirement name keywords should appear in touched files.
426
+ *
427
+ * SKIPPED in Propose phase (no code changes yet).
428
+ */
429
+ function checkSpecCodeConsistent(specsDir, repoRoot, phase) {
430
+ const result = {
431
+ ruleId: 'spec-code-consistent',
432
+ ruleName: 'Spec-Code consistency',
433
+ passed: false,
434
+ errors: [],
435
+ warnings: [],
436
+ };
437
+ // Skip this check in Propose phase
438
+ if (phase === 'propose') {
439
+ result.passed = true;
440
+ result.warnings.push('SKIPPED (Propose phase) - no code changes to validate');
441
+ return result;
442
+ }
443
+ // If specsDir doesn't exist, skip this check
444
+ if (!specsDir) {
445
+ result.passed = true;
446
+ return result;
447
+ }
448
+ // Collect requirements from delta specs
449
+ const requirements = [];
450
+ try {
451
+ const specDirs = readdirSync(specsDir, { withFileTypes: true });
452
+ for (const entry of specDirs) {
453
+ if (!entry.isDirectory())
454
+ continue;
455
+ const specFile = join(specsDir, entry.name, 'spec.md');
456
+ if (!existsSync(specFile))
457
+ continue;
458
+ const content = readFileSync(specFile, 'utf-8');
459
+ // Parse ADDED/MODIFIED Requirements (not REMOVED)
460
+ const addedMatches = content.matchAll(/^###\s+(Requirement|需求)[::]\s+(.+)$/gm);
461
+ const inAddedSection = content.includes('## ADDED Requirements') || content.includes('## 新增要求');
462
+ const inModifiedSection = content.includes('## MODIFIED Requirements') || content.includes('## 修改要求');
463
+ // Simple parsing: check if requirement is in ADDED or MODIFIED section
464
+ const lines = content.split('\n');
465
+ let currentSection = '';
466
+ for (const line of lines) {
467
+ if (line.match(/^##\s+(ADDED|Added|新增)/i))
468
+ currentSection = 'ADDED';
469
+ else if (line.match(/^##\s+(MODIFIED|Modified|修改)/i))
470
+ currentSection = 'MODIFIED';
471
+ else if (line.match(/^##\s+(REMOVED|Removed|删除)/i))
472
+ currentSection = 'REMOVED';
473
+ else if (line.match(/^##\s+(RENAMED|Renamed|重命名)/i))
474
+ currentSection = 'RENAMED';
475
+ else if (line.match(/^###\s+(Requirement|需求)[::]/i)) {
476
+ const reqName = line.replace(/^###\s+(Requirement|需求)[::]\s+/i, '').trim();
477
+ if (currentSection === 'ADDED' || currentSection === 'MODIFIED') {
478
+ requirements.push({ name: reqName, operation: currentSection });
479
+ }
480
+ }
481
+ }
482
+ }
483
+ }
484
+ catch {
485
+ // Directory read error, skip
486
+ result.passed = true;
487
+ return result;
488
+ }
489
+ // If no requirements found, consider passed
490
+ if (requirements.length === 0) {
491
+ result.passed = true;
492
+ return result;
493
+ }
494
+ // Get git diff files
495
+ let touchedFiles = [];
496
+ try {
497
+ const gitOutput = execSync('git diff --name-only', {
498
+ cwd: repoRoot,
499
+ encoding: 'utf-8',
500
+ stdio: ['pipe', 'pipe', 'ignore'],
501
+ });
502
+ touchedFiles = gitOutput.trim().split('\n').filter(Boolean);
503
+ }
504
+ catch {
505
+ // Git command failed, try git status for uncommitted changes
506
+ try {
507
+ const statusOutput = execSync('git status --porcelain', {
508
+ cwd: repoRoot,
509
+ encoding: 'utf-8',
510
+ stdio: ['pipe', 'pipe', 'ignore'],
511
+ });
512
+ touchedFiles = statusOutput.trim().split('\n')
513
+ .map(line => line.replace(/^\s*[A-Z]+\s+/, '').trim())
514
+ .filter(Boolean);
515
+ }
516
+ catch {
517
+ // No git info available, pass the check
518
+ result.passed = true;
519
+ return result;
520
+ }
521
+ }
522
+ // Check each requirement against touched files
523
+ for (const req of requirements) {
524
+ const normalized = req.name.toLowerCase();
525
+ const keywords = normalized.split(/\s+/).filter(w => w.length > 3);
526
+ const matched = touchedFiles.some(file => {
527
+ const basename = path.basename(file).toLowerCase();
528
+ const filePathLower = file.toLowerCase();
529
+ const filenameMatched = keywords.some((kw) => basename.includes(kw) || filePathLower.includes(kw));
530
+ if (filenameMatched) {
531
+ return true;
532
+ }
533
+ try {
534
+ const fullPath = path.join(repoRoot, file);
535
+ if (!existsSync(fullPath))
536
+ return false;
537
+ const content = readFileSync(fullPath, 'utf-8').toLowerCase();
538
+ return keywords.some((kw) => content.includes(kw));
539
+ }
540
+ catch {
541
+ return false;
542
+ }
543
+ });
544
+ if (!matched && touchedFiles.length > 0) {
545
+ result.errors.push(`Requirement "${req.name}" (${req.operation}) 未找到对应代码改动`);
546
+ }
547
+ }
548
+ result.passed = result.errors.length === 0;
549
+ return result;
550
+ }
551
+ /**
552
+ * Rule: test-case-coverage
553
+ * Validates that tasks.md has test case coverage section.
554
+ * Behavior differs based on hasTestCase mode:
555
+ * - hasTestCase=true (TDD or user provided cases): REQUIRED, blocking if missing
556
+ * - hasTestCase=false (normal spec-driven): SKIPPED, warning only
557
+ *
558
+ * Table format validation (when hasTestCase=true):
559
+ * - Must have four columns: Task, 测试 Case ID, 测试 Case 文件路径, 覆盖场景
560
+ * - Column headers must match expected format
561
+ */
562
+ export function checkTestCaseCoverage(tasksContent, hasTestCase = false) {
563
+ const result = {
564
+ ruleId: 'test-case-coverage',
565
+ ruleName: 'Test case coverage',
566
+ passed: false,
567
+ errors: [],
568
+ warnings: [],
569
+ };
570
+ // If no test case mode, skip with warning
571
+ if (!hasTestCase) {
572
+ result.passed = true;
573
+ result.warnings.push('SKIPPED (no test case mode) - test-case-coverage check only applies when test cases are provided');
574
+ return result;
575
+ }
576
+ // Check for Test Case Coverage section (required when hasTestCase=true)
577
+ const testCaseSectionPattern = /^##\s+Test\s+Case\s+Coverage/im;
578
+ if (!testCaseSectionPattern.test(tasksContent)) {
579
+ result.errors.push('Missing Test Case Coverage section - REQUIRED when test cases are provided (TDD mode or user-provided cases)');
580
+ return result;
581
+ }
582
+ // Check for @test-case tags in tasks
583
+ const testCaseTagPattern = /@test-case:TC-\d+/g;
584
+ const testCaseTags = tasksContent.match(testCaseTagPattern);
585
+ if (testCaseTags && testCaseTags.length > 0) {
586
+ result.passed = true;
587
+ }
588
+ else {
589
+ result.errors.push('No @test-case tags found - REQUIRED when test cases are provided. Link tasks to test cases using @test-case:TC-XXX');
590
+ }
591
+ // Validate Test Case Coverage table format (four columns required)
592
+ // Expected headers: Task, 测试 Case ID, 测试 Case 文件路径, 覆盖场景
593
+ const tablePattern = /^##\s+Test\s+Case\s+Coverage\s*\n\n(?:<!--[^>]*-->\s*\n)?\|.*\|.*\|.*\|.*\|/m;
594
+ const tableMatch = tasksContent.match(tablePattern);
595
+ if (tableMatch) {
596
+ // Extract header row and validate columns
597
+ const headerLine = tableMatch[0].split('\n').find(line => line.startsWith('|') && !line.includes('<!--'));
598
+ if (headerLine) {
599
+ const columns = headerLine.split('|').filter(col => col.trim() !== '');
600
+ if (columns.length < 4) {
601
+ result.warnings.push(`Test Case Coverage table should have 4 columns (Task, 测试 Case ID, 测试 Case 文件路径, 覆盖场景), found ${columns.length}`);
602
+ }
603
+ else {
604
+ // Check column names contain expected keywords
605
+ const headerText = headerLine.toLowerCase();
606
+ const hasTaskCol = headerText.includes('task');
607
+ const hasTestCaseIdCol = headerText.includes('测试 case id') || headerText.includes('test case') || headerText.includes('tc');
608
+ const hasPathCol = headerText.includes('文件路径') || headerText.includes('path') || headerText.includes('路径');
609
+ const hasScenarioCol = headerText.includes('覆盖场景') || headerText.includes('场景') || headerText.includes('scenario');
610
+ if (!hasTaskCol || !hasTestCaseIdCol || !hasPathCol || !hasScenarioCol) {
611
+ result.warnings.push('Test Case Coverage table headers should follow format: Task, 测试 Case ID, 测试 Case 文件路径, 覆盖场景');
612
+ }
613
+ }
614
+ }
615
+ }
616
+ else {
617
+ result.warnings.push('Test Case Coverage table format not detected - ensure table has 4 columns with proper headers');
618
+ }
619
+ return result;
620
+ }
339
621
  /**
340
622
  * Main entry: Run all strict validation rules
341
623
  */
342
- export async function runStrictValidation(tasksContent, parsedTasks, knownSkillNames) {
624
+ export async function runStrictValidation(tasksContent, parsedTasks, knownSkillNames, options) {
625
+ const hasTestCase = options?.hasTestCase ?? false;
343
626
  const checks = [
344
627
  checkPreClarification(tasksContent),
345
628
  checkSkillTagsValid(tasksContent, parsedTasks, knownSkillNames),
346
629
  checkTaskOrderingByWave(tasksContent, parsedTasks),
347
630
  checkSubagentDirectives(parsedTasks),
631
+ checkReviewGateDefined(tasksContent),
632
+ checkTestCaseCoverage(tasksContent, hasTestCase),
348
633
  ];
634
+ // Add new rules if parameters are provided
635
+ if (options?.proposalContent) {
636
+ checks.push(checkProposalFormatValid(options.proposalContent));
637
+ }
638
+ // Detect current phase for spec-code consistency check
639
+ if (options?.specsDir && options?.repoRoot) {
640
+ const phase = options?.changeDir ? detectPhase(options.changeDir) : 'apply';
641
+ checks.push(checkSpecCodeConsistent(options.specsDir, options.repoRoot, phase));
642
+ }
349
643
  const allPassed = checks.every(check => check.passed);
350
644
  return {
351
645
  checks,
@@ -288,14 +288,64 @@ export class Validator {
288
288
  });
289
289
  }
290
290
  // Run strict validation
291
- const strictResult = await runStrictValidation(tasksContent, parsedTasks, knownSkillNames);
291
+ // Read proposal.md for proposal-format-valid check
292
+ const proposalPath = path.join(changeDir, 'proposal.md');
293
+ let proposalContent;
294
+ try {
295
+ proposalContent = await fs.readFile(proposalPath, 'utf-8');
296
+ }
297
+ catch {
298
+ // proposal.md not found - skip proposal format validation
299
+ }
300
+ // Get specs directory and repo root for spec-code-consistent check
301
+ const repoRoot = this.findProjectRoot(changeDir);
302
+ // Detect hasTestCase mode:
303
+ // 1. Check for test-cases.md file
304
+ // 2. Check for @test-case tags in tasks.md
305
+ // 3. Check for TDD schema in metadata
306
+ let hasTestCase = false;
307
+ const testCasesPath = path.join(changeDir, 'test-cases.md');
308
+ if (existsSync(testCasesPath)) {
309
+ hasTestCase = true;
310
+ }
311
+ else if (tasksContent.includes('@test-case:')) {
312
+ hasTestCase = true;
313
+ }
314
+ else {
315
+ // Check metadata for schema type
316
+ const metadataPath = path.join(changeDir, '.zhuanspec.yaml');
317
+ try {
318
+ const metadataContent = await fs.readFile(metadataPath, 'utf-8');
319
+ if (metadataContent.includes('schema: tdd') || metadataContent.includes('schema:\n tdd')) {
320
+ hasTestCase = true;
321
+ }
322
+ }
323
+ catch {
324
+ // No metadata file - assume spec-driven (no test cases)
325
+ }
326
+ }
327
+ const strictResult = await runStrictValidation(tasksContent, parsedTasks, knownSkillNames, {
328
+ proposalContent,
329
+ specsDir,
330
+ repoRoot,
331
+ changeDir,
332
+ hasTestCase,
333
+ });
292
334
  strictChecks = strictResult.checks;
293
335
  // Merge strict errors into main issues
294
336
  for (const check of strictResult.checks) {
337
+ // Determine path based on rule ID
338
+ let issuePath = 'tasks.md';
339
+ if (check.ruleId === 'proposal-format-valid') {
340
+ issuePath = 'proposal.md';
341
+ }
342
+ else if (check.ruleId === 'spec-code-consistent') {
343
+ issuePath = 'specs/';
344
+ }
295
345
  for (const error of check.errors) {
296
346
  issues.push({
297
347
  level: 'ERROR',
298
- path: 'tasks.md',
348
+ path: issuePath,
299
349
  message: `[${check.ruleName}] ${error}`,
300
350
  });
301
351
  }
@@ -3,6 +3,7 @@ export declare class ViewCommand {
3
3
  private getChangesData;
4
4
  private getSpecsData;
5
5
  private displaySummary;
6
+ private displayRichMetrics;
6
7
  private createProgressBar;
7
8
  }
8
9
  //# sourceMappingURL=view.d.ts.map
package/dist/core/view.js CHANGED
@@ -3,6 +3,7 @@ import * as path from 'path';
3
3
  import chalk from 'chalk';
4
4
  import { getTaskProgressForChange } from '../utils/task-progress.js';
5
5
  import { MarkdownParser } from './parsers/markdown-parser.js';
6
+ import { loadChangeMetrics } from './dashboard/metrics.js';
6
7
  export class ViewCommand {
7
8
  async execute(targetPath = '.') {
8
9
  const zhuanspecDir = path.join(targetPath, 'zhuanspec');
@@ -15,8 +16,9 @@ export class ViewCommand {
15
16
  // Get changes and specs data
16
17
  const changesData = await this.getChangesData(zhuanspecDir);
17
18
  const specsData = await this.getSpecsData(zhuanspecDir);
19
+ const activeMetrics = await Promise.all(changesData.active.map((change) => loadChangeMetrics(zhuanspecDir, change.name)));
18
20
  // Display summary metrics
19
- this.displaySummary(changesData, specsData);
21
+ this.displaySummary(changesData, specsData, activeMetrics);
20
22
  // Display draft changes
21
23
  if (changesData.draft.length > 0) {
22
24
  console.log(chalk.bold.gray('\nDraft Changes'));
@@ -36,6 +38,7 @@ export class ViewCommand {
36
38
  : 0;
37
39
  console.log(` ${chalk.yellow('◉')} ${chalk.bold(change.name.padEnd(30))} ${progressBar} ${chalk.dim(`${percentage}%`)}`);
38
40
  });
41
+ this.displayRichMetrics(activeMetrics);
39
42
  }
40
43
  // Display completed changes
41
44
  if (changesData.completed.length > 0) {
@@ -127,7 +130,7 @@ export class ViewCommand {
127
130
  }
128
131
  return specs;
129
132
  }
130
- displaySummary(changesData, specsData) {
133
+ displaySummary(changesData, specsData, activeMetrics) {
131
134
  const totalChanges = changesData.draft.length + changesData.active.length + changesData.completed.length;
132
135
  const totalSpecs = specsData.length;
133
136
  const totalRequirements = specsData.reduce((sum, spec) => sum + spec.requirementCount, 0);
@@ -153,6 +156,61 @@ export class ViewCommand {
153
156
  const overallProgress = Math.round((completedTasks / totalTasks) * 100);
154
157
  console.log(` ${chalk.magenta('●')} Task Progress: ${chalk.bold(`${completedTasks}/${totalTasks}`)} (${overallProgress}% complete)`);
155
158
  }
159
+ const totalTokenUsage = activeMetrics.reduce((sum, item) => sum + item.tokenUsage, 0);
160
+ const totalContextLoad = activeMetrics.reduce((sum, item) => sum + item.contextLoad, 0);
161
+ if (activeMetrics.length > 0) {
162
+ console.log(` ${chalk.white('●')} Runtime Metrics: ${chalk.bold(`token=${totalTokenUsage}`)}, ${chalk.bold(`context=${totalContextLoad}`)}`);
163
+ }
164
+ }
165
+ displayRichMetrics(metricsList) {
166
+ const total = metricsList.length;
167
+ if (total === 0)
168
+ return;
169
+ console.log(chalk.bold.magenta('\nFlow Overview'));
170
+ console.log('─'.repeat(60));
171
+ for (const item of metricsList) {
172
+ const flowText = `${item.phase} -> ${item.currentNode}`;
173
+ const waveText = item.currentWave > 0 ? `Wave ${item.currentWave}` : 'Wave -';
174
+ const taskText = item.currentTask || item.nextTask || '-';
175
+ console.log(` ${chalk.magenta('▸')} ${chalk.bold(item.changeId)} ${chalk.dim(flowText)} ${chalk.dim(waveText)} task:${chalk.white(taskText)}`);
176
+ }
177
+ const totalToolCalls = metricsList.reduce((sum, m) => sum + m.toolCalls, 0);
178
+ const reviewCritical = metricsList.reduce((sum, m) => sum + m.review.critical, 0);
179
+ const reviewImportant = metricsList.reduce((sum, m) => sum + m.review.important, 0);
180
+ const reviewMinor = metricsList.reduce((sum, m) => sum + m.review.minor, 0);
181
+ const testPassed = metricsList.reduce((sum, m) => sum + m.tests.passed, 0);
182
+ const testFailed = metricsList.reduce((sum, m) => sum + m.tests.failed, 0);
183
+ const coverageValues = metricsList
184
+ .map((m) => m.tests.coverage)
185
+ .filter((v) => typeof v === 'number');
186
+ const avgCoverage = coverageValues.length > 0
187
+ ? Math.round(coverageValues.reduce((sum, v) => sum + v, 0) / coverageValues.length)
188
+ : null;
189
+ console.log(chalk.bold.magenta('\nEfficiency Metrics'));
190
+ console.log('─'.repeat(60));
191
+ console.log(` ${chalk.magenta('▸')} Tool Calls: ${chalk.bold(totalToolCalls)}`);
192
+ const skillMap = {};
193
+ for (const metric of metricsList) {
194
+ for (const [name, count] of Object.entries(metric.skillCalls)) {
195
+ skillMap[name] = (skillMap[name] ?? 0) + count;
196
+ }
197
+ }
198
+ const topSkills = Object.entries(skillMap)
199
+ .sort((a, b) => b[1] - a[1])
200
+ .slice(0, 5)
201
+ .map(([name, count]) => `${name}:${count}`)
202
+ .join(', ');
203
+ console.log(` ${chalk.magenta('▸')} Skill Calls: ${topSkills || 'N/A'}`);
204
+ console.log(chalk.bold.magenta('\nQuality Metrics'));
205
+ console.log('─'.repeat(60));
206
+ console.log(` ${chalk.magenta('▸')} Review: C=${reviewCritical} I=${reviewImportant} M=${reviewMinor}`);
207
+ console.log(` ${chalk.magenta('▸')} Tests: passed=${testPassed} failed=${testFailed} coverage=${avgCoverage !== null ? `${avgCoverage}%` : 'N/A'}`);
208
+ const totalDuration = metricsList.reduce((sum, m) => {
209
+ return sum + m.durations.proposeMs + m.durations.applyMs + m.durations.reviewMs + m.durations.archiveMs;
210
+ }, 0);
211
+ console.log(chalk.bold.magenta('\nTimeline'));
212
+ console.log('─'.repeat(60));
213
+ console.log(` ${chalk.magenta('▸')} Total Duration: ${Math.round(totalDuration / 1000)}s`);
156
214
  }
157
215
  createProgressBar(completed, total, width = 20) {
158
216
  if (total === 0)
@@ -0,0 +1,28 @@
1
+ /**
2
+ * MCP Client Wrapper
3
+ *
4
+ * This module provides a wrapper for MCP tools that may be available
5
+ * in the runtime environment. If the MCP server is not configured,
6
+ * these functions will gracefully return empty results.
7
+ */
8
+ interface SonarQueryParams {
9
+ branchName?: string;
10
+ staticType?: string;
11
+ }
12
+ interface SonarIssue {
13
+ type: string;
14
+ severity: string;
15
+ message: string;
16
+ file: string;
17
+ line?: number;
18
+ }
19
+ /**
20
+ * Query Sonar issues for a branch
21
+ *
22
+ * Note: This function uses the MCP tool mcp__sonar__get_Sonar_Query
23
+ * which must be configured in the environment. If not available,
24
+ * it returns an empty array.
25
+ */
26
+ export declare function mcp__sonar__get_Sonar_Query(params: SonarQueryParams): Promise<SonarIssue[]>;
27
+ export type { SonarQueryParams, SonarIssue };
28
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,31 @@
1
+ /**
2
+ * MCP Client Wrapper
3
+ *
4
+ * This module provides a wrapper for MCP tools that may be available
5
+ * in the runtime environment. If the MCP server is not configured,
6
+ * these functions will gracefully return empty results.
7
+ */
8
+ /**
9
+ * Query Sonar issues for a branch
10
+ *
11
+ * Note: This function uses the MCP tool mcp__sonar__get_Sonar_Query
12
+ * which must be configured in the environment. If not available,
13
+ * it returns an empty array.
14
+ */
15
+ export async function mcp__sonar__get_Sonar_Query(params) {
16
+ // In a real implementation, this would call the MCP tool
17
+ // For now, we provide a placeholder that returns empty results
18
+ // when the MCP is not configured
19
+ try {
20
+ // Dynamic import to handle cases where MCP tools are not available
21
+ // The actual MCP tool mcp__sonar__get_Sonar_Query is provided by the runtime
22
+ // This wrapper serves as a TypeScript-compatible interface
23
+ // Return empty array as fallback
24
+ // The actual implementation would be handled by the MCP runtime
25
+ return [];
26
+ }
27
+ catch {
28
+ return [];
29
+ }
30
+ }
31
+ //# sourceMappingURL=index.js.map
@@ -13,6 +13,7 @@ export declare class FileSystemUtils {
13
13
  static directoryExists(dirPath: string): Promise<boolean>;
14
14
  static writeFile(filePath: string, content: string): Promise<void>;
15
15
  static readFile(filePath: string): Promise<string>;
16
+ static deleteFile(filePath: string): Promise<void>;
16
17
  static updateFileWithMarkers(filePath: string, content: string, startMarker: string, endMarker: string): Promise<void>;
17
18
  static ensureWritePermissions(dirPath: string): Promise<boolean>;
18
19
  }
@@ -125,6 +125,17 @@ export class FileSystemUtils {
125
125
  static async readFile(filePath) {
126
126
  return await fs.readFile(filePath, 'utf-8');
127
127
  }
128
+ static async deleteFile(filePath) {
129
+ try {
130
+ await fs.unlink(filePath);
131
+ }
132
+ catch (error) {
133
+ if (error.code !== 'ENOENT') {
134
+ throw error;
135
+ }
136
+ // File doesn't exist, nothing to delete
137
+ }
138
+ }
128
139
  static async updateFileWithMarkers(filePath, content, startMarker, endMarker) {
129
140
  let existingContent = '';
130
141
  if (await this.fileExists(filePath)) {
@@ -1,5 +1,6 @@
1
1
  import { promises as fs } from 'fs';
2
2
  import path from 'path';
3
+ import { FileSystemUtils } from './file-system.js';
3
4
  export async function getActiveChangeIds(root = process.cwd()) {
4
5
  const changesPath = path.join(root, 'zhuanspec', 'changes');
5
6
  try {
@@ -8,13 +9,34 @@ export async function getActiveChangeIds(root = process.cwd()) {
8
9
  for (const entry of entries) {
9
10
  if (!entry.isDirectory() || entry.name.startsWith('.') || entry.name === 'archive')
10
11
  continue;
11
- const proposalPath = path.join(changesPath, entry.name, 'proposal.md');
12
+ const changeDir = path.join(changesPath, entry.name);
13
+ const proposalPath = path.join(changeDir, 'proposal.md');
12
14
  try {
15
+ // Check for proposal.md (normal propose/apply/review change)
13
16
  await fs.access(proposalPath);
14
17
  result.push(entry.name);
15
18
  }
16
19
  catch {
17
- // skip directories without proposal.md
20
+ // No proposal.md - check if it's a techDesign change
21
+ const techDesignMarker = path.join(changeDir, '.tech-design');
22
+ const techDesignMarkerExists = await FileSystemUtils.fileExists(techDesignMarker);
23
+ // Also check progress.json phase
24
+ const progressPath = path.join(changeDir, 'metrics', 'progress.json');
25
+ let progressPhaseIsTechDesign = false;
26
+ if (await FileSystemUtils.fileExists(progressPath)) {
27
+ try {
28
+ const progressContent = await FileSystemUtils.readFile(progressPath);
29
+ const progress = JSON.parse(progressContent);
30
+ progressPhaseIsTechDesign = progress.phase === 'techDesign';
31
+ }
32
+ catch {
33
+ // Ignore parse errors
34
+ }
35
+ }
36
+ // If techDesign markers exist, add to active changes
37
+ if (techDesignMarkerExists || progressPhaseIsTechDesign) {
38
+ result.push(entry.name);
39
+ }
18
40
  }
19
41
  }
20
42
  return result.sort();