@planu/cli 4.6.1 → 4.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/config/project-knowledge-graph.json +65 -0
  3. package/dist/engine/project-graph/builder.d.ts +7 -0
  4. package/dist/engine/project-graph/builder.js +92 -0
  5. package/dist/engine/project-graph/cache.d.ts +26 -0
  6. package/dist/engine/project-graph/cache.js +160 -0
  7. package/dist/engine/project-graph/extractors/git-extractor.d.ts +7 -0
  8. package/dist/engine/project-graph/extractors/git-extractor.js +89 -0
  9. package/dist/engine/project-graph/extractors/handoff-extractor.d.ts +3 -0
  10. package/dist/engine/project-graph/extractors/handoff-extractor.js +55 -0
  11. package/dist/engine/project-graph/extractors/spec-extractor.d.ts +3 -0
  12. package/dist/engine/project-graph/extractors/spec-extractor.js +189 -0
  13. package/dist/engine/project-graph/extractors/validation-extractor.d.ts +3 -0
  14. package/dist/engine/project-graph/extractors/validation-extractor.js +36 -0
  15. package/dist/engine/project-graph/index.d.ts +4 -0
  16. package/dist/engine/project-graph/index.js +4 -0
  17. package/dist/engine/project-graph/query.d.ts +9 -0
  18. package/dist/engine/project-graph/query.js +161 -0
  19. package/dist/tools/create-spec/post-creation.js +13 -1
  20. package/dist/tools/package-handoff.js +31 -2
  21. package/dist/tools/schemas/index.d.ts +1 -0
  22. package/dist/tools/schemas/index.js +1 -0
  23. package/dist/tools/schemas/project-graph.d.ts +18 -0
  24. package/dist/tools/schemas/project-graph.js +8 -0
  25. package/dist/tools/schemas/token-intelligence.d.ts +1 -0
  26. package/dist/tools/schemas/token-intelligence.js +3 -2
  27. package/dist/tools/status-handler.js +9 -2
  28. package/dist/tools/token-intelligence-handler.js +28 -1
  29. package/dist/tools/validate.js +75 -30
  30. package/dist/types/index.d.ts +1 -0
  31. package/dist/types/index.js +1 -0
  32. package/dist/types/project-knowledge-graph.d.ts +139 -0
  33. package/dist/types/project-knowledge-graph.js +2 -0
  34. package/package.json +12 -11
  35. package/planu-native.json +1 -1
  36. package/planu-plugin.json +1 -1
@@ -2,6 +2,7 @@ import { getEntries, getAggregation } from '../storage/token-ledger-store.js';
2
2
  import { hashProjectPath } from '../storage/base-store.js';
3
3
  import { buildTokenWasteReport, detectTokenWasteLoops, formatTokenWasteReport, loadTokenWastePolicy, recommendRelevantTools, } from '../engine/token-optimizer/index.js';
4
4
  import { join } from 'node:path';
5
+ import { formatProjectGraphContext, queryProjectGraphSlice, } from '../engine/project-graph/index.js';
5
6
  // ---------------------------------------------------------------------------
6
7
  // Constants
7
8
  // ---------------------------------------------------------------------------
@@ -390,6 +391,26 @@ async function renderAutopilot(entries, agg, period, projectPath) {
390
391
  '',
391
392
  ].join('\n');
392
393
  }
394
+ async function renderGraphView(projectPath, specId) {
395
+ const slice = await queryProjectGraphSlice({ projectPath, specId }).catch(() => null);
396
+ if (slice === null) {
397
+ return [
398
+ '# Token Intelligence Dashboard — Graph View',
399
+ '',
400
+ 'Project graph is unavailable; existing token intelligence views still work.',
401
+ '',
402
+ ].join('\n');
403
+ }
404
+ return [
405
+ '# Token Intelligence Dashboard — Graph View',
406
+ '',
407
+ `Graph freshness: ${slice.freshness.stale ? slice.freshness.reason : 'fresh'}`,
408
+ `Compact slice: ${String(slice.nodes.length)} nodes / ${String(slice.edges.length)} edges`,
409
+ `Estimated raw context avoided: ${String(slice.tokenSavings.estimatedRawContextAvoided)} graph items`,
410
+ '',
411
+ await formatProjectGraphContext(slice),
412
+ ].join('\n');
413
+ }
393
414
  // ---------------------------------------------------------------------------
394
415
  // Empty state
395
416
  // ---------------------------------------------------------------------------
@@ -429,7 +450,10 @@ export async function handleTokenIntelligence(input) {
429
450
  getEntries(projectHash, dataDir, filter),
430
451
  getAggregation(projectHash, dataDir, filter),
431
452
  ]);
432
- if (entries.length === 0 && view !== 'reconciliation' && view !== 'autopilot') {
453
+ if (entries.length === 0 &&
454
+ view !== 'reconciliation' &&
455
+ view !== 'autopilot' &&
456
+ view !== 'graph') {
433
457
  return { content: [{ type: 'text', text: renderEmpty(view, period) }] };
434
458
  }
435
459
  let text;
@@ -449,6 +473,9 @@ export async function handleTokenIntelligence(input) {
449
473
  case 'autopilot':
450
474
  text = await renderAutopilot(entries, agg, period, projectPath);
451
475
  break;
476
+ case 'graph':
477
+ text = await renderGraphView(projectPath, specId);
478
+ break;
452
479
  default:
453
480
  text = renderSummary(agg, entries, period, groupBy);
454
481
  break;
@@ -17,8 +17,67 @@ import { validateScopeCompliance } from '../engine/scope-boundaries/index.js';
17
17
  import { compareWithBaseline } from '../storage/convention-baseline.js';
18
18
  import { writeImplementationReviewReport } from '../engine/validator/validation-report-writer.js';
19
19
  import { evaluateNewCodeGate } from '../engine/ai-assurance/index.js';
20
+ import { queryProjectGraphSlice } from '../engine/project-graph/index.js';
20
21
  // Re-export for external use (SPEC-018)
21
22
  export { validateContractCompliance };
23
+ function graphCoverageGaps(slice) {
24
+ if (slice === null) {
25
+ return [];
26
+ }
27
+ const criteria = slice.nodes.filter((node) => node.type === 'criterion');
28
+ return criteria
29
+ .filter((criterion) => {
30
+ const outgoing = slice.edges.filter((edge) => edge.from === criterion.id);
31
+ return !outgoing.some((edge) => edge.type === 'implements' || edge.type === 'tests');
32
+ })
33
+ .map((criterion) => criterion.label)
34
+ .slice(0, 10);
35
+ }
36
+ async function buildGraphCoverageReport(args) {
37
+ const graphSlice = await queryProjectGraphSlice(args).catch(() => null);
38
+ return {
39
+ freshness: graphSlice?.freshness,
40
+ gaps: graphCoverageGaps(graphSlice),
41
+ compactNodes: graphSlice?.nodes.length ?? 0,
42
+ compactEdges: graphSlice?.edges.length ?? 0,
43
+ };
44
+ }
45
+ function formatGraphCoverageText(report) {
46
+ return report.gaps.length > 0
47
+ ? `\nGRAPH ${String(report.gaps.length)} graph-backed coverage gap(s)`
48
+ : '';
49
+ }
50
+ async function validateStrictLayoutOrError(args) {
51
+ try {
52
+ const { validateStrictPlanuLayout } = await import('../engine/spec-migrator/index.js');
53
+ const layout = await validateStrictPlanuLayout(args.projectPath, {
54
+ specPath: args.specPath,
55
+ includeRoot: false,
56
+ });
57
+ if (layout.ok) {
58
+ return null;
59
+ }
60
+ return {
61
+ content: [
62
+ {
63
+ type: 'text',
64
+ text: `Strict Planu layout validation failed for ${args.specId}.\n\n` +
65
+ `Non-canonical paths:\n${layout.offenders.map((p) => `- ${p}`).join('\n')}\n\n` +
66
+ `Canonical contract:\n${layout.contract}`,
67
+ },
68
+ ],
69
+ isError: true,
70
+ structuredContent: {
71
+ error: 'strict_planu_layout_violation',
72
+ offenders: layout.offenders,
73
+ contract: layout.contract,
74
+ },
75
+ };
76
+ }
77
+ catch {
78
+ return null;
79
+ }
80
+ }
22
81
  /** Build the fallback interactiveQuestion for validation failures. */
23
82
  function buildValidateFailureFallback(failedCount) {
24
83
  return {
@@ -72,36 +131,17 @@ export async function handleValidate(args, server) {
72
131
  }
73
132
  const projectPath = knowledge.projectPath;
74
133
  // SPEC-1017: fail closed when planu/ contains non-canonical artifacts.
75
- try {
76
- const { validateStrictPlanuLayout } = await import('../engine/spec-migrator/index.js');
77
- const layout = await validateStrictPlanuLayout(projectPath, {
78
- specPath: spec.specPath,
79
- includeRoot: false,
80
- });
81
- if (!layout.ok) {
82
- return {
83
- content: [
84
- {
85
- type: 'text',
86
- text: `Strict Planu layout validation failed for ${specId}.\n\n` +
87
- `Non-canonical paths:\n${layout.offenders.map((p) => `- ${p}`).join('\n')}\n\n` +
88
- `Canonical contract:\n${layout.contract}`,
89
- },
90
- ],
91
- isError: true,
92
- structuredContent: {
93
- error: 'strict_planu_layout_violation',
94
- offenders: layout.offenders,
95
- contract: layout.contract,
96
- },
97
- };
98
- }
99
- }
100
- catch {
101
- /* best-effort — implementation validation still runs if layout check is unavailable */
134
+ const strictLayoutError = await validateStrictLayoutOrError({
135
+ projectPath,
136
+ specId,
137
+ specPath: spec.specPath,
138
+ });
139
+ if (strictLayoutError !== null) {
140
+ return strictLayoutError;
102
141
  }
103
142
  // 3. Run validation engine
104
143
  const result = await validateSpec(spec, projectPath);
144
+ const graphCoverage = await buildGraphCoverageReport({ projectId, projectPath, specId });
105
145
  // 4. Generate DoR and DoD checklists
106
146
  const dor = generateDoR(spec);
107
147
  const dod = await generateDoD(spec, result, undefined, projectPath);
@@ -222,6 +262,7 @@ export async function handleValidate(args, server) {
222
262
  lintCheck,
223
263
  assuranceGates,
224
264
  validationReport,
265
+ graphCoverage,
225
266
  };
226
267
  // SPEC-612: Scope boundary validation — warn if impl files match outOfScope items
227
268
  if (spec.outOfScope !== undefined && spec.outOfScope.length > 0) {
@@ -256,6 +297,9 @@ export async function handleValidate(args, server) {
256
297
  if (result.missing.length > 0) {
257
298
  suggestions.push(`${result.missing.length} criteria still missing. Run detect_drift to identify gaps.`);
258
299
  }
300
+ if (graphCoverage.gaps.length > 0) {
301
+ suggestions.push(`${graphCoverage.gaps.length} graph-backed coverage gap(s) found without broad repository scan.`);
302
+ }
259
303
  if (conventionViolations.length > 0) {
260
304
  suggestions.push(`${conventionViolations.length} convention violation(s) detected. Fix before marking as done.`);
261
305
  }
@@ -327,6 +371,7 @@ export async function handleValidate(args, server) {
327
371
  commitReminder,
328
372
  planuReminder,
329
373
  });
374
+ const graphText = formatGraphCoverageText(graphCoverage);
330
375
  // SPEC-512: Compact structuredContent — essential fields only at top level
331
376
  const compactSummary = {
332
377
  specId,
@@ -346,7 +391,7 @@ export async function handleValidate(args, server) {
346
391
  return {
347
392
  phaseEvent: createBasicPhaseEvent('validate', 'completed', 100, 'validating'),
348
393
  content: [
349
- { type: 'text', text: compactText },
394
+ { type: 'text', text: compactText + graphText },
350
395
  {
351
396
  type: 'text',
352
397
  text: '⚡ INTERACTIVE — call AskUserQuestion now with the interactiveQuestions from structuredContent',
@@ -364,7 +409,7 @@ export async function handleValidate(args, server) {
364
409
  : 're-implement';
365
410
  return {
366
411
  phaseEvent: createBasicPhaseEvent('validate', 'completed', 100, 'validating'),
367
- content: [{ type: 'text', text: compactText }],
412
+ content: [{ type: 'text', text: compactText + graphText }],
368
413
  structuredContent: {
369
414
  ...outputWithSuggestions,
370
415
  summary: compactSummary,
@@ -374,7 +419,7 @@ export async function handleValidate(args, server) {
374
419
  }
375
420
  return {
376
421
  phaseEvent: createBasicPhaseEvent('validate', 'completed', 100, 'validating'),
377
- content: [{ type: 'text', text: compactText }],
422
+ content: [{ type: 'text', text: compactText + graphText }],
378
423
  structuredContent: {
379
424
  ...outputWithSuggestions,
380
425
  summary: compactSummary,
@@ -220,6 +220,7 @@ export * from './sentry.js';
220
220
  export * from './supabase.js';
221
221
  export * from './skill-bootstrap.js';
222
222
  export * from './technology-selection.js';
223
+ export * from './project-knowledge-graph.js';
223
224
  export * from './compliance-gate.js';
224
225
  export * from './schema-parity.js';
225
226
  export * from './auto-update.js';
@@ -217,6 +217,7 @@ export * from './sentry.js';
217
217
  export * from './supabase.js';
218
218
  export * from './skill-bootstrap.js';
219
219
  export * from './technology-selection.js';
220
+ export * from './project-knowledge-graph.js';
220
221
  export * from './compliance-gate.js';
221
222
  export * from './schema-parity.js';
222
223
  export * from './auto-update.js';
@@ -0,0 +1,139 @@
1
+ export type ProjectGraphNodeType = string;
2
+ export type ProjectGraphEdgeType = string;
3
+ export type ProjectGraphConfidence = 'high' | 'medium' | 'low';
4
+ export type ProjectGraphEdgeClassification = 'extracted' | 'inferred' | 'ambiguous';
5
+ export interface ProjectGraphPolicy {
6
+ version: 1;
7
+ graphVersion: string;
8
+ enabled: boolean;
9
+ artifactPaths: {
10
+ directory: string;
11
+ graph: string;
12
+ sourceHashes: string;
13
+ };
14
+ nodeTypes: string[];
15
+ edgeTypes: string[];
16
+ classifications: ProjectGraphEdgeClassification[];
17
+ confidenceLabels: ProjectGraphConfidence[];
18
+ freshness: {
19
+ staleAfterMinutes: number;
20
+ };
21
+ query: {
22
+ maxNodes: number;
23
+ maxEdges: number;
24
+ maxListItems: number;
25
+ };
26
+ redaction: {
27
+ maxSnippetChars: number;
28
+ redactPatterns: string[];
29
+ };
30
+ toolNamePatterns: string[];
31
+ }
32
+ export interface ProjectGraphEvidencePointer {
33
+ path: string;
34
+ selector?: string;
35
+ hash?: string;
36
+ }
37
+ export interface ProjectGraphNode {
38
+ id: string;
39
+ type: ProjectGraphNodeType;
40
+ label: string;
41
+ source: string;
42
+ evidence: ProjectGraphEvidencePointer[];
43
+ metadata?: Record<string, string | number | boolean | string[] | null>;
44
+ updatedAt: string;
45
+ }
46
+ export interface ProjectGraphEdge {
47
+ id: string;
48
+ from: string;
49
+ to: string;
50
+ type: ProjectGraphEdgeType;
51
+ source: string;
52
+ confidence: ProjectGraphConfidence;
53
+ evidence: ProjectGraphEvidencePointer;
54
+ classification: ProjectGraphEdgeClassification;
55
+ updatedAt: string;
56
+ }
57
+ export interface ProjectKnowledgeGraph {
58
+ version: 1;
59
+ graphVersion: string;
60
+ projectId: string;
61
+ projectPath?: string;
62
+ generatedAt: string;
63
+ sourceHash: string;
64
+ sourceHashes: Record<string, string>;
65
+ metadata: {
66
+ nodeCount: number;
67
+ edgeCount: number;
68
+ sourceCount: number;
69
+ reprocessedSourceCount: number;
70
+ skippedSourceCount: number;
71
+ };
72
+ nodes: ProjectGraphNode[];
73
+ edges: ProjectGraphEdge[];
74
+ }
75
+ export interface ProjectGraphSource {
76
+ id: string;
77
+ kind: string;
78
+ path: string;
79
+ content: string;
80
+ hash: string;
81
+ }
82
+ export interface ProjectGraphExtractionResult {
83
+ nodes: ProjectGraphNode[];
84
+ edges: ProjectGraphEdge[];
85
+ }
86
+ export interface ProjectGraphBuildResult {
87
+ graph: ProjectKnowledgeGraph;
88
+ graphPath: string;
89
+ cachePath: string;
90
+ reprocessedSources: string[];
91
+ skippedSources: string[];
92
+ usedCache: boolean;
93
+ }
94
+ export interface ProjectGraphFreshness {
95
+ exists: boolean;
96
+ stale: boolean;
97
+ reason: 'missing' | 'fresh' | 'expired' | 'source_changed' | 'corrupt';
98
+ graphPath: string;
99
+ generatedAt?: string;
100
+ changedSources: string[];
101
+ }
102
+ export interface ProjectGraphSlice {
103
+ graphVersion: string;
104
+ generatedAt: string;
105
+ freshness: ProjectGraphFreshness;
106
+ nodes: ProjectGraphNode[];
107
+ edges: ProjectGraphEdge[];
108
+ summary: {
109
+ specs: number;
110
+ criteria: number;
111
+ files: number;
112
+ tests: number;
113
+ decisions: number;
114
+ risks: number;
115
+ tools: number;
116
+ releases: number;
117
+ };
118
+ tokenSavings: {
119
+ compactNodes: number;
120
+ compactEdges: number;
121
+ estimatedRawContextAvoided: number;
122
+ };
123
+ }
124
+ export interface ProjectGraphQueryInput {
125
+ projectId?: string;
126
+ projectPath: string;
127
+ specId?: string;
128
+ filePath?: string;
129
+ nodeId?: string;
130
+ maxNodes?: number;
131
+ maxEdges?: number;
132
+ }
133
+ export interface GraphCoverageReport {
134
+ freshness?: ProjectGraphFreshness;
135
+ gaps: string[];
136
+ compactNodes: number;
137
+ compactEdges: number;
138
+ }
139
+ //# sourceMappingURL=project-knowledge-graph.d.ts.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=project-knowledge-graph.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@planu/cli",
3
- "version": "4.6.1",
3
+ "version": "4.7.1",
4
4
  "description": "Planu — MCP Server for Spec Driven Development with native Rust acceleration for hot paths. Cross-platform (Linux/macOS/Windows, x64/arm64, glibc/musl).",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -34,14 +34,14 @@
34
34
  "packageName": "@planu/core"
35
35
  },
36
36
  "optionalDependencies": {
37
- "@planu/core-darwin-arm64": "4.6.1",
38
- "@planu/core-darwin-x64": "4.6.1",
39
- "@planu/core-linux-arm64-gnu": "4.6.1",
40
- "@planu/core-linux-arm64-musl": "4.6.1",
41
- "@planu/core-linux-x64-gnu": "4.6.1",
42
- "@planu/core-linux-x64-musl": "4.6.1",
43
- "@planu/core-win32-arm64-msvc": "4.6.1",
44
- "@planu/core-win32-x64-msvc": "4.6.1"
37
+ "@planu/core-darwin-arm64": "4.7.1",
38
+ "@planu/core-darwin-x64": "4.7.1",
39
+ "@planu/core-linux-arm64-gnu": "4.7.1",
40
+ "@planu/core-linux-arm64-musl": "4.7.1",
41
+ "@planu/core-linux-x64-gnu": "4.7.1",
42
+ "@planu/core-linux-x64-musl": "4.7.1",
43
+ "@planu/core-win32-arm64-msvc": "4.7.1",
44
+ "@planu/core-win32-x64-msvc": "4.7.1"
45
45
  },
46
46
  "engines": {
47
47
  "node": ">=24.0.0"
@@ -147,6 +147,7 @@
147
147
  "lodash-es": ">=4.18.0",
148
148
  "hono": ">=4.12.14",
149
149
  "postcss": ">=8.5.10",
150
+ "esbuild": "0.28.1",
150
151
  "fast-uri": ">=3.1.2",
151
152
  "qs": ">=6.15.2"
152
153
  },
@@ -186,11 +187,11 @@
186
187
  "@vitejs/plugin-vue": "^6.0.7",
187
188
  "@vitest/coverage-v8": "^4.1.8",
188
189
  "@vue/test-utils": "^2.4.11",
189
- "eslint": "^10.4.1",
190
+ "eslint": "^10.5.0",
190
191
  "eslint-config-prettier": "^10.1.8",
191
192
  "eslint-import-resolver-typescript": "^4.4.5",
192
193
  "eslint-plugin-import": "^2.32.0",
193
- "happy-dom": "^20.10.2",
194
+ "happy-dom": "^20.10.3",
194
195
  "husky": "^9.1.7",
195
196
  "javascript-obfuscator": "^5.4.3",
196
197
  "knip": "^6.16.1",
package/planu-native.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dev.planu.native",
3
3
  "displayName": "Planu Native Lightweight Surface",
4
- "version": "4.6.1",
4
+ "version": "4.7.1",
5
5
  "packageName": "@planu/cli",
6
6
  "modes": {
7
7
  "lightweight": {
package/planu-plugin.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "dev.planu.cli",
3
3
  "displayName": "Planu — Spec Driven Development",
4
4
  "description": "Manage software specs, estimations, and autonomous SDD workflows. Language-agnostic MCP server for Claude Code.",
5
- "version": "4.6.1",
5
+ "version": "4.7.1",
6
6
  "icon": "assets/plugin/icon.svg",
7
7
  "command": [
8
8
  "npx",