@planu/cli 4.3.4 → 4.3.6

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 (55) hide show
  1. package/CHANGELOG.md +11 -1
  2. package/dist/config/license-plans.json +2 -0
  3. package/dist/engine/ai-cost-estimator/core.d.ts +1 -1
  4. package/dist/engine/ai-cost-estimator/core.js +2 -2
  5. package/dist/engine/ai-cost-estimator/spec-loader.d.ts +2 -2
  6. package/dist/engine/ai-cost-estimator/spec-loader.js +17 -46
  7. package/dist/engine/ai-cost-estimator/token-estimator.d.ts +1 -1
  8. package/dist/engine/ai-cost-estimator/token-estimator.js +1 -1
  9. package/dist/engine/code-graph-configurator.d.ts +1 -1
  10. package/dist/engine/code-graph-configurator.js +7 -0
  11. package/dist/engine/context-intelligence/compression-guards.d.ts +8 -0
  12. package/dist/engine/context-intelligence/compression-guards.js +74 -0
  13. package/dist/engine/context-intelligence/context-graph-provider.d.ts +9 -0
  14. package/dist/engine/context-intelligence/context-graph-provider.js +98 -0
  15. package/dist/engine/context-intelligence/eval-harness.d.ts +8 -0
  16. package/dist/engine/context-intelligence/eval-harness.js +45 -0
  17. package/dist/engine/context-intelligence/impact-map.d.ts +6 -0
  18. package/dist/engine/context-intelligence/impact-map.js +47 -0
  19. package/dist/engine/context-intelligence/index.d.ts +7 -0
  20. package/dist/engine/context-intelligence/index.js +6 -0
  21. package/dist/engine/context-intelligence/safe-context-compressor.d.ts +3 -0
  22. package/dist/engine/context-intelligence/safe-context-compressor.js +75 -0
  23. package/dist/engine/dashboard/data-loader.js +9 -11
  24. package/dist/engine/dashboard/templates-project.d.ts +1 -1
  25. package/dist/engine/dashboard/templates-project.js +6 -4
  26. package/dist/engine/docs-site-generator/index.js +2 -11
  27. package/dist/engine/drift-monitor.js +13 -11
  28. package/dist/engine/qa-gate.js +6 -1
  29. package/dist/engine/readiness-checker.js +3 -3
  30. package/dist/engine/spec-conflict-graph.d.ts +1 -1
  31. package/dist/engine/spec-conflict-graph.js +2 -3
  32. package/dist/engine/spec-format/read-technical-section.d.ts +1 -7
  33. package/dist/engine/spec-format/read-technical-section.js +4 -30
  34. package/dist/engine/spec-registry/scorer.d.ts +1 -1
  35. package/dist/engine/spec-registry/scorer.js +3 -4
  36. package/dist/engine/validator/extractors.js +4 -2
  37. package/dist/engine/validator.d.ts +1 -1
  38. package/dist/engine/validator.js +40 -2
  39. package/dist/tools/code-graph-handler.js +4 -0
  40. package/dist/tools/create-spec/post-creation.d.ts +2 -0
  41. package/dist/tools/create-spec/post-creation.js +5 -0
  42. package/dist/tools/create-spec.js +2 -2
  43. package/dist/tools/license-gate.js +6 -1
  44. package/dist/tools/output-integrity-guard.d.ts +11 -0
  45. package/dist/tools/output-integrity-guard.js +53 -0
  46. package/dist/tools/register-sdd-tools.d.ts +1 -1
  47. package/dist/tools/register-sdd-tools.js +2 -0
  48. package/dist/tools/safe-handler.js +11 -9
  49. package/dist/tools/tool-registry/group-infra.js +26 -0
  50. package/dist/tools/update-status/dod-gates.js +1 -1
  51. package/dist/types/code-graph-integration.d.ts +2 -2
  52. package/dist/types/context-intelligence.d.ts +61 -0
  53. package/dist/types/context-intelligence.js +2 -0
  54. package/dist/types/qa-gate.d.ts +1 -1
  55. package/package.json +9 -9
@@ -3,7 +3,7 @@ import type { DashboardProjectSummary, DashboardUsageStats, ProjectHealthMetrics
3
3
  export declare function renderHomePage(projects: DashboardProjectSummary[], usage: DashboardUsageStats): string;
4
4
  /** Render the project detail page — spec cards + kanban board + health panel */
5
5
  export declare function renderProjectPage(project: DashboardProjectSummary, metrics: ProjectHealthMetrics, usage: DashboardUsageStats): string;
6
- /** Render the spec detail page — HU, FICHA, PROGRESS tabs */
6
+ /** Render the spec detail page — canonical spec.md sections */
7
7
  export declare function renderSpecPage(projectId: string, projectName: string, specId: string, hu: string | null, ficha: string | null, progress: string | null): string;
8
8
  /** Render a 404 not found page */
9
9
  export declare function render404Page(path: string): string;
@@ -226,15 +226,17 @@ export function renderProjectPage(project, metrics, usage) {
226
226
  `;
227
227
  return renderLayout(project.name, body, getKanbanStyles(), getKanbanScript());
228
228
  }
229
- /** Render the spec detail page — HU, FICHA, PROGRESS tabs */
229
+ /** Render the spec detail page — canonical spec.md sections */
230
230
  export function renderSpecPage(projectId, projectName, specId, hu, ficha, progress) {
231
- const huHtml = hu !== null ? markdownToHtml(hu) : '<p style="color:var(--text-muted)">HU.md no encontrado</p>';
231
+ const huHtml = hu !== null
232
+ ? markdownToHtml(hu)
233
+ : '<p style="color:var(--text-muted)">spec.md no encontrado</p>';
232
234
  const fichaHtml = ficha !== null
233
235
  ? markdownToHtml(ficha)
234
- : '<p style="color:var(--text-muted)">FICHA-TECNICA.md no encontrado</p>';
236
+ : '<p style="color:var(--text-muted)">## Technical no encontrado en spec.md</p>';
235
237
  const progressHtml = progress !== null
236
238
  ? markdownToHtml(progress)
237
- : '<p style="color:var(--text-muted)">PROGRESS.md no encontrado</p>';
239
+ : '<p style="color:var(--text-muted)">## Progress no encontrado en spec.md</p>';
238
240
  const body = `
239
241
  <div class="breadcrumb">
240
242
  <a href="/">Inicio</a> &rsaquo;
@@ -7,6 +7,7 @@ import { renderSpecPage, renderSpecIndex } from './spec-renderer.js';
7
7
  import { renderToolsCatalog } from './tools-renderer.js';
8
8
  import { renderDiagramsPage } from './diagrams-renderer.js';
9
9
  import { stripFrontmatter } from '../frontmatter-parser.js';
10
+ import { extractSectionBody } from '../spec-format/read-technical-section.js';
10
11
  export async function generateDocsSite(config) {
11
12
  const { projectPath, outputDir, title = 'Planu Documentation' } = config;
12
13
  const [specs, metrics] = await Promise.all([
@@ -33,7 +34,6 @@ export async function generateDocsSite(config) {
33
34
  });
34
35
  for (const spec of specs) {
35
36
  let specContent = '';
36
- let techContent = '';
37
37
  try {
38
38
  const raw = await readFile(spec.specPath, 'utf-8');
39
39
  specContent = stripFrontmatter(raw);
@@ -41,16 +41,7 @@ export async function generateDocsSite(config) {
41
41
  catch {
42
42
  /* missing */
43
43
  }
44
- try {
45
- const techPath = spec.specPath
46
- .replace(/spec\.md$/, 'technical.md')
47
- .replace(/HU\.md$/, 'FICHA-TECNICA.md');
48
- const rawTech = await readFile(techPath, 'utf-8');
49
- techContent = stripFrontmatter(rawTech);
50
- }
51
- catch {
52
- /* missing */
53
- }
44
+ const techContent = extractSectionBody(specContent, 'Technical');
54
45
  pages.push({
55
46
  path: `specs/${spec.slug}.html`,
56
47
  title: spec.title,
@@ -3,6 +3,7 @@ import fs from 'node:fs';
3
3
  import { readFile, stat } from 'node:fs/promises';
4
4
  import path from 'node:path';
5
5
  import { fastScanProjectMetadata, isNativeActive, fastDetectDriftParallel, } from './core-bridge.js';
6
+ import { readSpecTechnicalSection } from './spec-format/read-technical-section.js';
6
7
  // ---------------------------------------------------------------------------
7
8
  // Config builder
8
9
  // ---------------------------------------------------------------------------
@@ -107,14 +108,14 @@ const TEMPORAL_DRIFT_DAYS = 30;
107
108
  * Note: score represents HEALTH (100 = no drift). Callers may invert if needed.
108
109
  */
109
110
  export async function computeDriftScore(projectPath, specId, spec, technicalPath, metadataMap) {
110
- const resolvedTechnical = resolveTechnicalPath(projectPath, specId, technicalPath);
111
+ const resolvedSpec = resolveSpecPath(projectPath, specId, technicalPath);
111
112
  let content = '';
112
- if (resolvedTechnical !== null) {
113
+ if (resolvedSpec !== null) {
113
114
  try {
114
- content = await readFile(resolvedTechnical, 'utf8');
115
+ content = await readSpecTechnicalSection({ specPath: resolvedSpec });
115
116
  }
116
117
  catch {
117
- // cannot read technical.md — treat all layers as fully healthy
118
+ // cannot read spec.md technical section — treat all layers as fully healthy
118
119
  }
119
120
  }
120
121
  const filePaths = extractFilePaths(content, projectPath);
@@ -311,13 +312,14 @@ async function computeAcCoverageScore(criteria, filePaths) {
311
312
  // ---------------------------------------------------------------------------
312
313
  // Internal helpers
313
314
  // ---------------------------------------------------------------------------
314
- /** Resolve the path to a spec's technical.md file. */
315
- function resolveTechnicalPath(projectPath, specId, technicalPath) {
316
- /* v8 ignore next 3 */
315
+ /** Resolve the path to a canonical spec.md file. */
316
+ function resolveSpecPath(projectPath, specId, technicalPath) {
317
+ /* v8 ignore next 5 */
317
318
  if (technicalPath !== undefined && fs.existsSync(technicalPath)) {
318
- return technicalPath;
319
+ const specPathFromLegacyHint = path.join(path.dirname(technicalPath), 'spec.md');
320
+ return fs.existsSync(specPathFromLegacyHint) ? specPathFromLegacyHint : null;
319
321
  }
320
- // Convention: planu/specs/<specId>-<slug>/technical.md
322
+ // Convention: planu/specs/<specId>-<slug>/spec.md
321
323
  const specsDir = path.join(projectPath, 'planu', 'specs');
322
324
  if (!fs.existsSync(specsDir)) {
323
325
  return null;
@@ -334,10 +336,10 @@ function resolveTechnicalPath(projectPath, specId, technicalPath) {
334
336
  if (match === undefined) {
335
337
  return null;
336
338
  }
337
- const candidate = path.join(specsDir, match, 'technical.md');
339
+ const candidate = path.join(specsDir, match, 'spec.md');
338
340
  return fs.existsSync(candidate) ? candidate : null;
339
341
  }
340
- /** Extract implementation file paths from technical.md content. */
342
+ /** Extract implementation file paths from a spec's inline ## Technical content. */
341
343
  function extractFilePaths(content, projectPath) {
342
344
  const FILE_PATTERN = /\b([\w./-]+\.(?:ts|tsx|js|jsx|py|go|rs|java|rb|cs))\b/g;
343
345
  const found = new Set();
@@ -26,7 +26,12 @@ export function runQaGate(spec, projectPath) {
26
26
  const coverageThreshold = extractCoverageThreshold(spec);
27
27
  const checks = [];
28
28
  checks.push(runCheck('typecheck', 'pnpm', ['typecheck'], projectPath));
29
- checks.push(runCheck('test-coverage', 'pnpm', ['test:coverage'], projectPath));
29
+ if (coverageThreshold === null) {
30
+ checks.push(runCheck('test', 'pnpm', ['test'], projectPath));
31
+ }
32
+ else {
33
+ checks.push(runCheck('test-coverage', 'pnpm', ['test:coverage'], projectPath));
34
+ }
30
35
  const passed = checks.every((c) => c.passed);
31
36
  return Promise.resolve({
32
37
  specId: spec.id,
@@ -155,7 +155,7 @@ function scoreFilesIdentified(spec, fichaContent) {
155
155
  }
156
156
  else {
157
157
  points = 0;
158
- warnings.push('No FICHA-TECNICA.md found — files to create/modify are not identified. Run create_spec_tech first.');
158
+ warnings.push('No inline ## Technical section found in spec.md — files to create/modify are not identified.');
159
159
  }
160
160
  return { points, blockers, warnings };
161
161
  }
@@ -180,7 +180,7 @@ function buildRecommendations(score, huPoints, criteriaPoints, filesPoints, deps
180
180
  recs.push('Add more acceptance criteria (aim for 5+ testable criteria)');
181
181
  }
182
182
  if (filesPoints < 20) {
183
- recs.push('Generate FICHA-TECNICA.md with create_spec_tech to identify key files');
183
+ recs.push('Add an inline ## Technical section to spec.md with key files');
184
184
  }
185
185
  if (depsPoints < 30) {
186
186
  recs.push('Verify all spec dependencies are in "done" status');
@@ -360,7 +360,7 @@ export function checkReadinessInternal(body) {
360
360
  // Vague criteria detection (re-uses existing logic)
361
361
  const criteriaResult = scoreCriteria(criteriaLines.length > 0 ? criteriaLines : [], vagueWords);
362
362
  warnings.push(...criteriaResult.warnings);
363
- // Technical files presence (proxy for technical.md — check src/ paths in body)
363
+ // Technical files presence (proxy for inline ## Technical — check src/ paths in body)
364
364
  const hasTechnicalFiles = /\bsrc\/[^\s]+\.[jt]s\b/.test(bodyContent);
365
365
  const technicalScore = hasTechnicalFiles ? 30 : 0;
366
366
  if (!hasTechnicalFiles && criteriaCount > 0) {
@@ -2,7 +2,7 @@ import type { SpecConflictSafety, SpecConflictCheckResult, ConflictGraphEntry, S
2
2
  export type { SpecConflictSafety, SpecConflictCheckResult, ConflictGraphEntry, SpecConflictGraph };
3
3
  /**
4
4
  * SPEC-657: Update the conflict graph entry for a single spec.
5
- * Called whenever a spec's technical.md changes.
5
+ * Called whenever a spec's inline ## Technical section changes.
6
6
  */
7
7
  export declare function updateConflictGraphEntry(projectPath: string, specId: string): Promise<void>;
8
8
  /**
@@ -39,8 +39,7 @@ async function readTechnicalFiles(projectPath, specId) {
39
39
  return [];
40
40
  }
41
41
  const specPath = join(specsDir, folder, 'spec.md');
42
- const technicalPath = join(specsDir, folder, 'technical.md');
43
- const content = await readSpecTechnicalSection({ specPath, technicalPath });
42
+ const content = await readSpecTechnicalSection({ specPath });
44
43
  if (content.length === 0) {
45
44
  return [];
46
45
  }
@@ -62,7 +61,7 @@ async function saveGraph(projectPath, graph) {
62
61
  }
63
62
  /**
64
63
  * SPEC-657: Update the conflict graph entry for a single spec.
65
- * Called whenever a spec's technical.md changes.
64
+ * Called whenever a spec's inline ## Technical section changes.
66
65
  */
67
66
  export async function updateConflictGraphEntry(projectPath, specId) {
68
67
  const files = await readTechnicalFiles(projectPath, specId);
@@ -2,14 +2,8 @@ import type { Spec } from '../../types/index.js';
2
2
  /**
3
3
  * Read the `## Technical` section body from a spec's unified `spec.md`.
4
4
  * Returns "" when the spec file is unreadable or the section is missing.
5
- *
6
- * The legacy `spec.technicalPath` is consulted as a transitional fallback:
7
- * if the spec.md does not yet contain a `## Technical` section but a
8
- * standalone `technical.md` still exists on disk (pre-migration data),
9
- * we read that file instead and strip its YAML frontmatter so callers
10
- * receive the body content uniformly.
11
5
  */
12
- export declare function readSpecTechnicalSection(spec: Pick<Spec, 'specPath' | 'technicalPath'>): Promise<string>;
6
+ export declare function readSpecTechnicalSection(spec: Pick<Spec, 'specPath'>): Promise<string>;
13
7
  /**
14
8
  * Extract the body of a top-level (`## `) section from a markdown document.
15
9
  * Returns the content AFTER the heading line, up to the next top-level
@@ -1,35 +1,16 @@
1
1
  // engine/spec-format/read-technical-section.ts — SPEC-1010 PR-B
2
2
  //
3
3
  // SSR back-migration (SPEC-752, v2.4.0) folded `technical.md` into the
4
- // unified `spec.md` as a `## Technical` section. The reader call sites
5
- // that previously read the standalone `technical.md` file silently
6
- // degrade to "" today because the file never gets written. This helper
7
- // is the single source of truth for "give me the technical body for
8
- // this spec": it extracts the `## Technical` section from `spec.md`
9
- // and falls back to the legacy `technical.md` only when the unified
10
- // section is empty (transitional safety net for specs not yet auto-
11
- // migrated).
4
+ // unified `spec.md` as a `## Technical` section. This helper is the
5
+ // single source of truth for "give me the technical body for this spec":
6
+ // it extracts only the canonical inline `## Technical` section.
12
7
  import { readFile } from 'node:fs/promises';
13
8
  /**
14
9
  * Read the `## Technical` section body from a spec's unified `spec.md`.
15
10
  * Returns "" when the spec file is unreadable or the section is missing.
16
- *
17
- * The legacy `spec.technicalPath` is consulted as a transitional fallback:
18
- * if the spec.md does not yet contain a `## Technical` section but a
19
- * standalone `technical.md` still exists on disk (pre-migration data),
20
- * we read that file instead and strip its YAML frontmatter so callers
21
- * receive the body content uniformly.
22
11
  */
23
12
  export async function readSpecTechnicalSection(spec) {
24
- const fromUnified = await extractFromUnified(spec.specPath);
25
- if (fromUnified.length > 0) {
26
- return fromUnified;
27
- }
28
- if (!spec.technicalPath) {
29
- return '';
30
- }
31
- const legacy = await readFile(spec.technicalPath, 'utf-8').catch(() => '');
32
- return stripFrontmatter(legacy).trim();
13
+ return extractFromUnified(spec.specPath);
33
14
  }
34
15
  async function extractFromUnified(specPath) {
35
16
  const content = await readFile(specPath, 'utf-8').catch(() => '');
@@ -85,13 +66,6 @@ function maskFencedBlocks(body) {
85
66
  return block.replace(/[^\n]/g, ' ');
86
67
  });
87
68
  }
88
- /**
89
- * Strip a leading YAML frontmatter block (between `---` fences at the top
90
- * of the document). No-op if frontmatter is absent.
91
- */
92
- function stripFrontmatter(content) {
93
- return content.replace(/^---\n[\s\S]*?\n---\n?/, '');
94
- }
95
69
  function escapeRegex(input) {
96
70
  return input.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
97
71
  }
@@ -1,7 +1,7 @@
1
1
  import type { ScoreBreakdown } from '../../types/index.js';
2
2
  /**
3
3
  * Calculate a completeness score (0-100) for a spec directory.
4
- * Reads spec.md and technical.md, evaluates 6 quality categories.
4
+ * Reads canonical spec.md, evaluates 6 quality categories.
5
5
  */
6
6
  export declare function calculateCompletenessScore(specDir: string): Promise<{
7
7
  score: number;
@@ -81,7 +81,7 @@ function scoreKeyFiles(specContent, technicalContent) {
81
81
  /* v8 ignore next -- files header present but no file-like entries */
82
82
  return 0;
83
83
  }
84
- /** Score technical.md based on existence and length. */
84
+ /** Score inline technical content based on existence and length. */
85
85
  function scoreTechnicalCompleteness(technicalContent) {
86
86
  if (!technicalContent) {
87
87
  return 0;
@@ -126,14 +126,13 @@ function scoreFrontmatter(specContent) {
126
126
  // ---------------------------------------------------------------------------
127
127
  /**
128
128
  * Calculate a completeness score (0-100) for a spec directory.
129
- * Reads spec.md and technical.md, evaluates 6 quality categories.
129
+ * Reads canonical spec.md, evaluates 6 quality categories.
130
130
  */
131
131
  export async function calculateCompletenessScore(specDir) {
132
132
  const specPath = join(specDir, 'spec.md');
133
- const technicalPath = join(specDir, 'technical.md');
134
133
  const [specContent, technicalContent] = await Promise.all([
135
134
  readFileOrEmpty(specPath),
136
- readSpecTechnicalSection({ specPath, technicalPath }),
135
+ readSpecTechnicalSection({ specPath }),
137
136
  ]);
138
137
  const breakdown = {
139
138
  acceptanceCriteria: scoreAcceptanceCriteria(specContent),
@@ -17,8 +17,10 @@ export async function extractCriteria(spec) {
17
17
  const items = extractListItems(acSection);
18
18
  criteria.push(...items);
19
19
  }
20
- // Also check for "Given/When/Then" patterns
21
- const gwtMatches = huContent.matchAll(/(?:Given|Dado|When|Cuando|Then|Entonces)\s+(.+)/gi);
20
+ // Also check for explicit "Given/When/Then" patterns at line start.
21
+ // Keep this anchored so narrative text containing "when" is not treated
22
+ // as an acceptance criterion.
23
+ const gwtMatches = huContent.matchAll(/^\s*(?:[-*+]\s+|\d+[.)]\s+)?(?:Given|Dado|When|Cuando|Then|Entonces)\s+(.+)$/gim);
22
24
  for (const match of gwtMatches) {
23
25
  /* v8 ignore next 3 -- defensive regex capture group guard */
24
26
  if (match[1]) {
@@ -7,7 +7,7 @@ export { buildHolisticReportFromFlatScore } from './validator/holistic-report.js
7
7
  export type { HolisticValidateReport } from '../types/analysis.js';
8
8
  /**
9
9
  * Validate a spec against the actual codebase.
10
- * Reads the spec files (HU.md, FICHA-TECNICA.md), extracts criteria,
10
+ * Reads the canonical spec.md, extracts criteria,
11
11
  * then checks the filesystem/code for compliance.
12
12
  *
13
13
  * SPEC-730: The returned ValidateResult is enriched with `holisticReport`
@@ -6,6 +6,7 @@ import { extractCriteria, extractVerifyBlocks } from './validator/extractors.js'
6
6
  import { scanCodeForSpec, checkCriterion, classifyDriftSeverity, quickQualityCheck, } from './validator/analyzer.js';
7
7
  import { resolveApplicableDimensions } from './validator/scope-resolver.js';
8
8
  import { buildHolisticReportFromFlatScore } from './validator/holistic-report.js';
9
+ import { readEvidenceArtifacts } from './evidence-gates/artifact-reader.js';
9
10
  // Re-export the full public API so external callers never need to know
10
11
  // about the sub-module layout.
11
12
  export { generateDoR, generateDoD } from './validator/dor-dod.js';
@@ -13,9 +14,43 @@ export { generateChecklist } from './validator/checklist.js';
13
14
  // SPEC-730: export scope-resolver and holistic-report for consumers
14
15
  export { resolveApplicableDimensions, normaliseScore } from './validator/scope-resolver.js';
15
16
  export { buildHolisticReportFromFlatScore } from './validator/holistic-report.js';
17
+ function normalizeCriterionForEvidence(value) {
18
+ return value
19
+ .replace(/^-\s*\[[ xX]\]\s*/i, '')
20
+ .replace(/\s+/g, ' ')
21
+ .trim()
22
+ .toLowerCase();
23
+ }
24
+ function rowHasValidationEvidence(row) {
25
+ const hasEvidence = [
26
+ row.testEvidence?.length,
27
+ row.contractEvidence?.length,
28
+ row.manualEvidence?.trim(),
29
+ row.validationEvidence?.trim(),
30
+ row.reviewerEvidence?.trim(),
31
+ ].some((value) => Boolean(value));
32
+ return row.changedFiles.length > 0 && hasEvidence;
33
+ }
34
+ async function readTraceabilityEvidence(spec, projectPath) {
35
+ try {
36
+ const artifacts = await readEvidenceArtifacts({
37
+ spec,
38
+ projectId: spec.projectId,
39
+ specId: spec.id,
40
+ projectPath,
41
+ });
42
+ const rows = artifacts.traceabilityMatrix?.rows ?? [];
43
+ return new Map(rows
44
+ .filter(rowHasValidationEvidence)
45
+ .map((row) => [normalizeCriterionForEvidence(row.acceptanceCriterion), row]));
46
+ }
47
+ catch {
48
+ return new Map();
49
+ }
50
+ }
16
51
  /**
17
52
  * Validate a spec against the actual codebase.
18
- * Reads the spec files (HU.md, FICHA-TECNICA.md), extracts criteria,
53
+ * Reads the canonical spec.md, extracts criteria,
19
54
  * then checks the filesystem/code for compliance.
20
55
  *
21
56
  * SPEC-730: The returned ValidateResult is enriched with `holisticReport`
@@ -45,6 +80,7 @@ export async function validateSpec(spec, projectPath) {
45
80
  };
46
81
  }
47
82
  const criteria = await extractCriteria(spec);
83
+ const traceabilityEvidence = await readTraceabilityEvidence(spec, projectPath);
48
84
  const codeState = await scanCodeForSpec(spec, projectPath);
49
85
  // Extract verify blocks from spec file (forgiving — empty map if file unreadable)
50
86
  let verifyBlockMap = new Map();
@@ -61,7 +97,9 @@ export async function validateSpec(spec, projectPath) {
61
97
  const qualityIssues = [];
62
98
  for (const criterion of criteria) {
63
99
  const verifyBlock = verifyBlockMap.get(criterion);
64
- const found = await checkCriterion(criterion, projectPath, codeState, verifyBlock);
100
+ const evidenceRow = traceabilityEvidence.get(normalizeCriterionForEvidence(criterion));
101
+ const found = evidenceRow !== undefined ||
102
+ (await checkCriterion(criterion, projectPath, codeState, verifyBlock));
65
103
  if (found) {
66
104
  matches.push(criterion);
67
105
  }
@@ -43,6 +43,10 @@ export async function handleConfigureCodeGraph(input) {
43
43
  JSON.stringify({ [providerKey]: entry }, null, 2),
44
44
  '```',
45
45
  '',
46
+ providerKey === 'codegraph'
47
+ ? 'Initialize the project index with `npx -y @colbymchenry/codegraph init -i` from the project root before relying on graph queries.'
48
+ : 'Initialize the provider index using the provider documentation before relying on graph queries.',
49
+ '',
46
50
  'Restart Claude Code (or your MCP client) to activate the new server.',
47
51
  ];
48
52
  return { content: [{ type: 'text', text: lines.join('\n') }] };
@@ -11,6 +11,8 @@ export declare function checkContradictions(projectId: string, specId: string, d
11
11
  export declare function fireSpecCreatedHook(projectId: string, spec: Spec, projectPath: string): void;
12
12
  /** Generates post-creation suggestions based on project state and spec description. Best-effort: never throws. */
13
13
  export declare function generatePostCreationSuggestions(projectPath: string, description: string, knowledge?: ProjectKnowledge): Promise<PostCreationSuggestion[]>;
14
+ /** Formats a structured post-creation suggestion for human-readable next steps. */
15
+ export declare function formatPostCreationSuggestion(suggestion: PostCreationSuggestion): string;
14
16
  /**
15
17
  * SPEC-781: Run heavy analysis (contradiction detection, complexity advice) in a
16
18
  * fire-and-forget fashion after the spec has already been persisted synchronously.
@@ -162,6 +162,11 @@ export async function generatePostCreationSuggestions(projectPath, description,
162
162
  });
163
163
  return suggestions;
164
164
  }
165
+ /** Formats a structured post-creation suggestion for human-readable next steps. */
166
+ export function formatPostCreationSuggestion(suggestion) {
167
+ const templateSuffix = suggestion.templateCategory !== undefined ? ` (${suggestion.templateCategory})` : '';
168
+ return `${suggestion.tool}${templateSuffix}: ${suggestion.reason}`;
169
+ }
165
170
  /**
166
171
  * SPEC-781: Run heavy analysis (contradiction detection, complexity advice) in a
167
172
  * fire-and-forget fashion after the spec has already been persisted synchronously.
@@ -10,7 +10,7 @@ import { estimateSpec } from '../engine/estimator.js';
10
10
  import { checkSpecReadiness } from '../engine/readiness-checker.js';
11
11
  import { buildSpecContext, buildSplitResult } from './create-spec/spec-builder.js';
12
12
  import { validateConstitution } from './create-spec/constitution-validator.js';
13
- import { setupGitBranch, checkContradictions, fireSpecCreatedHook, generatePostCreationSuggestions, runAutopilotAsync, getAsyncAnalysisPath, } from './create-spec/post-creation.js';
13
+ import { setupGitBranch, checkContradictions, fireSpecCreatedHook, generatePostCreationSuggestions, formatPostCreationSuggestion, runAutopilotAsync, getAsyncAnalysisPath, } from './create-spec/post-creation.js';
14
14
  import { notifyStoreChange } from '../engine/doc-generator/portal/regen-hook.js';
15
15
  import { compactObj } from '../engine/compact-obj.js';
16
16
  import { buildCreateSpecSummary } from '../engine/human-summary.js';
@@ -894,7 +894,7 @@ export async function handleCreateSpec(inputParams, server) {
894
894
  }
895
895
  // Append auto-pipeline output (SPEC-445)
896
896
  lines.push(...formatPipelineLines(pipelineResult));
897
- const allNextSteps = result.nextSteps ?? [];
897
+ const allNextSteps = (result.nextSteps ?? []).map((step) => (typeof step === 'string' ? step : formatPostCreationSuggestion(step)));
898
898
  const markdownText = allNextSteps.length > 0
899
899
  ? addNextSteps(formatSuccess(ti('tools.create_spec.success', { id: spec.id, title: spec.title }), lines.join('\n')), allNextSteps)
900
900
  : formatSuccess(ti('tools.create_spec.success', { id: spec.id, title: spec.title }), lines.join('\n'));
@@ -88,7 +88,12 @@ export function getTierSync() {
88
88
  // ---------------------------------------------------------------------------
89
89
  const sessionTierCache = new Map();
90
90
  const SESSION_TIER_TTL_MS = 10 * 60 * 1000; // 10 min — longer since online validation is expensive
91
- const RATE_LIMIT_EXEMPT_TOOLS = new Set(['planu_status', 'update_status_batch']);
91
+ const RATE_LIMIT_EXEMPT_TOOLS = new Set([
92
+ 'planu_status',
93
+ 'update_status_batch',
94
+ 'configure_code_graph',
95
+ 'code_graph_status',
96
+ ]);
92
97
  export function shouldBypassDailyRateLimit(toolName) {
93
98
  return RATE_LIMIT_EXEMPT_TOOLS.has(toolName);
94
99
  }
@@ -0,0 +1,11 @@
1
+ import type { ToolResult } from '../types/index.js';
2
+ type MalformedOutputCode = 'OBJECT_OBJECT' | 'OBJECT_PROMISE';
3
+ interface MalformedOutputIssue {
4
+ code: MalformedOutputCode;
5
+ pattern: string;
6
+ contentIndex: number;
7
+ }
8
+ export declare function findMalformedToolOutput(result: ToolResult): MalformedOutputIssue | null;
9
+ export declare function guardToolResultOutput(toolName: string | undefined, result: ToolResult): ToolResult;
10
+ export {};
11
+ //# sourceMappingURL=output-integrity-guard.d.ts.map
@@ -0,0 +1,53 @@
1
+ const MALFORMED_TEXT_PATTERNS = [
2
+ { code: 'OBJECT_OBJECT', pattern: /\[object Object\]/, label: '[object Object]' },
3
+ { code: 'OBJECT_PROMISE', pattern: /\[object Promise\]/, label: '[object Promise]' },
4
+ ];
5
+ export function findMalformedToolOutput(result) {
6
+ for (let index = 0; index < result.content.length; index++) {
7
+ const item = result.content[index];
8
+ const text = item?.text ?? '';
9
+ for (const rule of MALFORMED_TEXT_PATTERNS) {
10
+ if (rule.pattern.test(text)) {
11
+ return {
12
+ code: rule.code,
13
+ pattern: rule.label,
14
+ contentIndex: index,
15
+ };
16
+ }
17
+ }
18
+ }
19
+ return null;
20
+ }
21
+ export function guardToolResultOutput(toolName, result) {
22
+ const issue = findMalformedToolOutput(result);
23
+ if (issue === null) {
24
+ return result;
25
+ }
26
+ const name = toolName ?? 'unknown';
27
+ const structuredKeys = Object.keys(result.structuredContent ?? {});
28
+ return {
29
+ content: [
30
+ {
31
+ type: 'text',
32
+ text: [
33
+ '❌ **Output integrity guard blocked malformed response**',
34
+ '',
35
+ `Tool \`${name}\` produced non-human-readable text (${issue.code}).`,
36
+ 'The operation may have completed before the response was blocked.',
37
+ 'Run `planu_status` or retry the tool after the formatting bug is fixed.',
38
+ ].join('\n'),
39
+ },
40
+ ],
41
+ isError: true,
42
+ structuredContent: {
43
+ error: 'OUTPUT_INTEGRITY_GUARD',
44
+ toolName: name,
45
+ pattern: issue.code,
46
+ contentIndex: issue.contentIndex,
47
+ operationMayHaveCompleted: true,
48
+ originalStructuredContentKeys: structuredKeys,
49
+ fixHint: 'A Planu tool attempted to render a structured object as human text. Format the object explicitly before returning content[].text.',
50
+ },
51
+ };
52
+ }
53
+ //# sourceMappingURL=output-integrity-guard.js.map
@@ -1,4 +1,4 @@
1
1
  import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
- export declare const OFFICIAL_SDD_TOOL_NAMES: readonly ["planu_status", "facilitate", "init_project", "clarify_requirements", "create_spec", "challenge_spec", "check_readiness", "update_status", "update_status_batch", "package_handoff", "validate", "reconcile_spec", "create_rule", "create_skill", "skill_search"];
2
+ export declare const OFFICIAL_SDD_TOOL_NAMES: readonly ["planu_status", "facilitate", "init_project", "clarify_requirements", "create_spec", "challenge_spec", "check_readiness", "update_status", "update_status_batch", "package_handoff", "validate", "reconcile_spec", "create_rule", "create_skill", "skill_search", "configure_code_graph", "code_graph_status"];
3
3
  export declare function registerSddTools(server: McpServer): void;
4
4
  //# sourceMappingURL=register-sdd-tools.d.ts.map
@@ -28,6 +28,8 @@ export const OFFICIAL_SDD_TOOL_NAMES = [
28
28
  'create_rule',
29
29
  'create_skill',
30
30
  'skill_search',
31
+ 'configure_code_graph',
32
+ 'code_graph_status',
31
33
  ];
32
34
  const OFFICIAL_SDD_TOOL_SET = new Set(OFFICIAL_SDD_TOOL_NAMES);
33
35
  const noop = () => undefined;
@@ -17,6 +17,7 @@ import { consumeUpdateBanner } from '../engine/update-notifier.js';
17
17
  import { getOrCreateInstallationId } from '../engine/telemetry/telemetry-store.js';
18
18
  import { compressToolOutput } from './output-compressor.js';
19
19
  import { recordToolTokens as recordLlmTokens } from '../engine/llm-runtime/index.js';
20
+ import { guardToolResultOutput } from './output-integrity-guard.js';
20
21
  // SPEC-902: Structured error contract
21
22
  import { wrapError, isStructuredError } from '../engine/errors/error-wrapper.js';
22
23
  import { getErrorRecoveryRegistry } from '../engine/errors/registry-loader.js';
@@ -279,26 +280,27 @@ function safeWithTelemetry(toolName, handler) {
279
280
  content: [...withDrift.content, { type: 'text', text: updateBanner }],
280
281
  }
281
282
  : withDrift;
283
+ const guardedResult = guardToolResultOutput(toolName, finalResult);
282
284
  // Report validation errors (isError:true returned by handler logic, not exceptions)
283
- if (toolName !== undefined && finalResult.isError === true) {
284
- const firstContent = finalResult.content[0];
285
+ if (toolName !== undefined && guardedResult.isError === true) {
286
+ const firstContent = guardedResult.content[0];
285
287
  const errText = firstContent?.type === 'text' ? firstContent.text : 'unknown error';
286
288
  reportToolValidationError(toolName, errText);
287
289
  }
288
290
  // Record token usage for successful calls (fire-and-forget, only when projectPath available)
289
- if (toolName !== undefined && projectPath !== undefined && finalResult.isError !== true) {
290
- const outputText = extractOutputText(finalResult);
291
+ if (toolName !== undefined && projectPath !== undefined && guardedResult.isError !== true) {
292
+ const outputText = extractOutputText(guardedResult);
291
293
  recordToolTokens(toolName, projectPath, args, outputText, SESSION_ID);
292
294
  }
293
295
  // SPEC-460: Track real token estimates in LLM runtime (session-scoped, measured)
294
296
  if (toolName !== undefined) {
295
297
  const inputText = JSON.stringify(args);
296
- const outputText = extractOutputText(finalResult);
298
+ const outputText = extractOutputText(guardedResult);
297
299
  recordLlmTokens(toolName, inputText, outputText);
298
300
  }
299
301
  // Emit tool_used telemetry for successful calls (fire-and-forget)
300
302
  /* v8 ignore start */
301
- if (toolName !== undefined && finalResult.isError !== true && isErrorReportingEnabled()) {
303
+ if (toolName !== undefined && guardedResult.isError !== true && isErrorReportingEnabled()) {
302
304
  getOrCreateInstallationId()
303
305
  .then((installationId) => {
304
306
  sendTelemetryEvent({
@@ -331,7 +333,7 @@ function safeWithTelemetry(toolName, handler) {
331
333
  const partial = buildAuditEntry({
332
334
  toolName,
333
335
  args,
334
- outputType: finalResult.isError === true ? 'error' : 'success',
336
+ outputType: guardedResult.isError === true ? 'error' : 'success',
335
337
  durationMs: Date.now() - startTime,
336
338
  prevHash,
337
339
  });
@@ -344,9 +346,9 @@ function safeWithTelemetry(toolName, handler) {
344
346
  /* v8 ignore stop */
345
347
  // SPEC-563: Auto session checkpoint — fire-and-forget, non-blocking
346
348
  if (toolName !== undefined) {
347
- maybeWriteCheckpoint(toolName, finalResult.isError !== true, projectPath);
349
+ maybeWriteCheckpoint(toolName, guardedResult.isError !== true, projectPath);
348
350
  }
349
- return finalResult;
351
+ return guardedResult;
350
352
  }
351
353
  catch (error) {
352
354
  const message = error instanceof Error ? error.message : String(error);