@planu/cli 4.6.0 → 4.7.0

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 (37) 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/engine/validator/spec-compliance-runner.js +39 -9
  20. package/dist/tools/create-spec/post-creation.js +13 -1
  21. package/dist/tools/package-handoff.js +31 -2
  22. package/dist/tools/schemas/index.d.ts +1 -0
  23. package/dist/tools/schemas/index.js +1 -0
  24. package/dist/tools/schemas/project-graph.d.ts +18 -0
  25. package/dist/tools/schemas/project-graph.js +8 -0
  26. package/dist/tools/schemas/token-intelligence.d.ts +1 -0
  27. package/dist/tools/schemas/token-intelligence.js +3 -2
  28. package/dist/tools/status-handler.js +9 -2
  29. package/dist/tools/token-intelligence-handler.js +28 -1
  30. package/dist/tools/validate.js +75 -30
  31. package/dist/types/index.d.ts +1 -0
  32. package/dist/types/index.js +1 -0
  33. package/dist/types/project-knowledge-graph.d.ts +139 -0
  34. package/dist/types/project-knowledge-graph.js +2 -0
  35. package/package.json +12 -11
  36. package/planu-native.json +1 -1
  37. package/planu-plugin.json +1 -1
@@ -0,0 +1,189 @@
1
+ import YAML from 'yaml';
2
+ import { hashText } from '../cache.js';
3
+ import { redactGraphText } from '../query.js';
4
+ function frontmatter(content) {
5
+ if (!content.startsWith('---')) {
6
+ return {};
7
+ }
8
+ const end = content.indexOf('\n---', 3);
9
+ if (end === -1) {
10
+ return {};
11
+ }
12
+ try {
13
+ const parsed = YAML.parse(content.slice(3, end));
14
+ return parsed !== null && typeof parsed === 'object' ? parsed : {};
15
+ }
16
+ catch {
17
+ return {};
18
+ }
19
+ }
20
+ function asString(value) {
21
+ return typeof value === 'string' && value.trim().length > 0 ? value.trim() : undefined;
22
+ }
23
+ function criterionTexts(fm, content) {
24
+ const raw = Array.isArray(fm.criteria) ? fm.criteria : [];
25
+ const fromFrontmatter = raw
26
+ .map((item) => typeof item === 'object' && item !== null && 'text' in item
27
+ ? asString(item.text)
28
+ : undefined)
29
+ .filter((item) => item !== undefined);
30
+ if (fromFrontmatter.length > 0) {
31
+ return fromFrontmatter;
32
+ }
33
+ return [...content.matchAll(/^- \[[ x]\]\s+(.+)$/gim)]
34
+ .map((match) => (match[1] ?? '').trim())
35
+ .filter((item) => item.length > 0);
36
+ }
37
+ function extractBacktickPaths(text, prefix) {
38
+ const idx = text.indexOf(prefix);
39
+ if (idx === -1) {
40
+ return [];
41
+ }
42
+ const fragment = text.slice(idx + prefix.length);
43
+ return [...fragment.matchAll(/`([^`]+)`/g)]
44
+ .map((match) => (match[1] ?? '').trim())
45
+ .filter((item) => item.length > 0);
46
+ }
47
+ function uniqueById(items) {
48
+ return [...new Map(items.map((item) => [item.id, item])).values()];
49
+ }
50
+ function node(args) {
51
+ return {
52
+ id: args.id,
53
+ type: args.type,
54
+ label: redactGraphText(args.label, args.policy),
55
+ source: 'spec-extractor',
56
+ evidence: [{ path: args.source.path, hash: args.source.hash }],
57
+ metadata: args.metadata,
58
+ updatedAt: new Date().toISOString(),
59
+ };
60
+ }
61
+ function edge(args) {
62
+ const id = `edge:${hashText(`${args.from}:${args.type}:${args.to}:${args.selector ?? ''}`)}`;
63
+ return {
64
+ id,
65
+ from: args.from,
66
+ to: args.to,
67
+ type: args.type,
68
+ source: 'spec-extractor',
69
+ confidence: args.confidence ?? 'high',
70
+ classification: args.classification ?? 'extracted',
71
+ evidence: { path: args.source.path, selector: args.selector, hash: args.source.hash },
72
+ updatedAt: new Date().toISOString(),
73
+ };
74
+ }
75
+ function sectionBullets(content, heading) {
76
+ const pattern = new RegExp(`^##+\\s+${heading}\\s*$`, 'im');
77
+ const match = pattern.exec(content);
78
+ if (!match) {
79
+ return [];
80
+ }
81
+ const start = match.index + match[0].length;
82
+ const rest = content.slice(start);
83
+ const nextHeading = rest.search(/^##+\s+/m);
84
+ const section = nextHeading === -1 ? rest : rest.slice(0, nextHeading);
85
+ return [...section.matchAll(/^-+\s+(.+)$/gm)]
86
+ .map((item) => (item[1] ?? '').trim())
87
+ .filter((item) => item.length > 0)
88
+ .slice(0, 20);
89
+ }
90
+ function appendRiskAndDecisionNodes(args) {
91
+ for (const [index, risk] of [
92
+ ...sectionBullets(args.source.content, 'Risks'),
93
+ ...sectionBullets(args.source.content, 'Edge Cases And Failure Modes'),
94
+ ].entries()) {
95
+ const riskId = `risk:${args.specId}:${String(index + 1)}`;
96
+ args.nodes.push(node({
97
+ id: riskId,
98
+ type: 'risk',
99
+ label: risk,
100
+ source: args.source,
101
+ policy: args.policy,
102
+ metadata: { specId: args.specId },
103
+ }));
104
+ args.edges.push(edge({ from: args.specNodeId, to: riskId, type: 'has_risk', source: args.source }));
105
+ }
106
+ for (const [index, decision] of [
107
+ ...sectionBullets(args.source.content, 'Hard constraints'),
108
+ ...sectionBullets(args.source.content, 'Non-Goals And Forbidden Approaches'),
109
+ ].entries()) {
110
+ const decisionId = `decision:${args.specId}:${String(index + 1)}`;
111
+ args.nodes.push(node({
112
+ id: decisionId,
113
+ type: 'decision',
114
+ label: decision,
115
+ source: args.source,
116
+ policy: args.policy,
117
+ metadata: { specId: args.specId },
118
+ }));
119
+ args.edges.push(edge({ from: args.specNodeId, to: decisionId, type: 'decides', source: args.source }));
120
+ }
121
+ }
122
+ function appendToolNodes(args) {
123
+ for (const toolName of args.policy.toolNamePatterns) {
124
+ if (args.source.content.includes(toolName)) {
125
+ const toolId = `tool:${toolName}`;
126
+ args.nodes.push(node({
127
+ id: toolId,
128
+ type: 'tool',
129
+ label: toolName,
130
+ source: args.source,
131
+ policy: args.policy,
132
+ }));
133
+ args.edges.push(edge({
134
+ from: args.specNodeId,
135
+ to: toolId,
136
+ type: 'uses_tool',
137
+ source: args.source,
138
+ confidence: 'medium',
139
+ classification: 'inferred',
140
+ }));
141
+ }
142
+ }
143
+ }
144
+ export function extractSpecGraph(source, policy) {
145
+ const fm = frontmatter(source.content);
146
+ const specId = asString(fm.id) ?? source.path.split('/').find((part) => /^SPEC-\d+/.test(part));
147
+ if (specId === undefined) {
148
+ return { nodes: [], edges: [] };
149
+ }
150
+ const title = asString(fm.title) ?? specId;
151
+ const specNodeId = `spec:${specId}`;
152
+ const nodes = [
153
+ node({
154
+ id: specNodeId,
155
+ type: 'spec',
156
+ label: `${specId} ${title}`,
157
+ source,
158
+ policy,
159
+ metadata: { specId, status: asString(fm.status) ?? null },
160
+ }),
161
+ ];
162
+ const edges = [];
163
+ for (const [index, text] of criterionTexts(fm, source.content).entries()) {
164
+ const criterionId = `criterion:${specId}:${String(index + 1)}`;
165
+ nodes.push(node({
166
+ id: criterionId,
167
+ type: 'criterion',
168
+ label: text,
169
+ source,
170
+ policy,
171
+ metadata: { specId, index: index + 1 },
172
+ }));
173
+ edges.push(edge({ from: specNodeId, to: criterionId, type: 'contains', source }));
174
+ for (const file of extractBacktickPaths(text, 'FILES:')) {
175
+ const fileId = `file:${file}`;
176
+ nodes.push(node({ id: fileId, type: 'file', label: file, source, policy }));
177
+ edges.push(edge({ from: criterionId, to: fileId, type: 'implements', source }));
178
+ }
179
+ for (const test of extractBacktickPaths(text, 'TEST:')) {
180
+ const testId = `test:${test}`;
181
+ nodes.push(node({ id: testId, type: 'test', label: test, source, policy }));
182
+ edges.push(edge({ from: criterionId, to: testId, type: 'tests', source }));
183
+ }
184
+ }
185
+ appendRiskAndDecisionNodes({ nodes, edges, specNodeId, specId, source, policy });
186
+ appendToolNodes({ nodes, edges, specNodeId, source, policy });
187
+ return { nodes: uniqueById(nodes), edges: uniqueById(edges) };
188
+ }
189
+ //# sourceMappingURL=spec-extractor.js.map
@@ -0,0 +1,3 @@
1
+ import type { ProjectGraphExtractionResult, ProjectGraphPolicy, ProjectGraphSource } from '../../../types/project-knowledge-graph.js';
2
+ export declare function extractValidationGraph(source: ProjectGraphSource, policy: ProjectGraphPolicy): ProjectGraphExtractionResult;
3
+ //# sourceMappingURL=validation-extractor.d.ts.map
@@ -0,0 +1,36 @@
1
+ import { hashText } from '../cache.js';
2
+ import { redactGraphText } from '../query.js';
3
+ function specIdFromPath(path) {
4
+ return path.split('/').find((part) => /^SPEC-\d+$/i.test(part));
5
+ }
6
+ function basename(path) {
7
+ return path.split('/').slice(-1)[0] ?? path;
8
+ }
9
+ export function extractValidationGraph(source, policy) {
10
+ const specId = specIdFromPath(source.path);
11
+ if (specId === undefined || !source.path.includes('validation')) {
12
+ return { nodes: [], edges: [] };
13
+ }
14
+ const validationId = `validation:${specId}:${hashText(source.path).slice(0, 10)}`;
15
+ const node = {
16
+ id: validationId,
17
+ type: 'validation',
18
+ label: redactGraphText(basename(source.path), policy),
19
+ source: 'validation-extractor',
20
+ evidence: [{ path: source.path, hash: source.hash }],
21
+ updatedAt: new Date().toISOString(),
22
+ };
23
+ const edge = {
24
+ id: `edge:${hashText(`spec:${specId}:validated_by:${validationId}`)}`,
25
+ from: `spec:${specId}`,
26
+ to: validationId,
27
+ type: 'validated_by',
28
+ source: 'validation-extractor',
29
+ confidence: 'medium',
30
+ classification: 'extracted',
31
+ evidence: { path: source.path, hash: source.hash },
32
+ updatedAt: new Date().toISOString(),
33
+ };
34
+ return { nodes: [node], edges: [edge] };
35
+ }
36
+ //# sourceMappingURL=validation-extractor.js.map
@@ -0,0 +1,4 @@
1
+ export { buildProjectKnowledgeGraph } from './builder.js';
2
+ export { queryProjectGraphSlice, formatProjectGraphContext, getProjectGraphFreshnessHint, redactGraphText, } from './query.js';
3
+ export { getProjectGraphFreshness, loadProjectGraphPolicy, projectGraphPath, projectGraphCachePath, } from './cache.js';
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,4 @@
1
+ export { buildProjectKnowledgeGraph } from './builder.js';
2
+ export { queryProjectGraphSlice, formatProjectGraphContext, getProjectGraphFreshnessHint, redactGraphText, } from './query.js';
3
+ export { getProjectGraphFreshness, loadProjectGraphPolicy, projectGraphPath, projectGraphCachePath, } from './cache.js';
4
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,9 @@
1
+ import type { ProjectGraphPolicy, ProjectGraphQueryInput, ProjectGraphSlice } from '../../types/project-knowledge-graph.js';
2
+ export declare function redactGraphText(text: string, policy: ProjectGraphPolicy, maxChars?: number): string;
3
+ export declare function queryProjectGraphSlice(input: ProjectGraphQueryInput): Promise<ProjectGraphSlice | null>;
4
+ export declare function formatProjectGraphContext(slice: ProjectGraphSlice): Promise<string>;
5
+ export declare function getProjectGraphFreshnessHint(args: {
6
+ projectId?: string;
7
+ projectPath: string;
8
+ }): Promise<string | undefined>;
9
+ //# sourceMappingURL=query.d.ts.map
@@ -0,0 +1,161 @@
1
+ import { hashProjectPath } from '../../storage/base-store.js';
2
+ import { buildProjectKnowledgeGraph } from './builder.js';
3
+ import { getProjectGraphFreshness, loadProjectGraphPolicy, readProjectGraph } from './cache.js';
4
+ function escapeInert(text) {
5
+ return text.replace(/</g, '&lt;').replace(/>/g, '&gt;');
6
+ }
7
+ export function redactGraphText(text, policy, maxChars = policy.redaction.maxSnippetChars) {
8
+ const truncated = text.slice(0, maxChars);
9
+ return policy.redaction.redactPatterns.reduce((current, pattern) => {
10
+ const re = new RegExp(pattern, 'gi');
11
+ return current.replace(re, '[redacted]');
12
+ }, truncated);
13
+ }
14
+ function sanitizeLabel(label, policy) {
15
+ return escapeInert(redactGraphText(label, policy));
16
+ }
17
+ function relatedNodeIds(nodes, edges, input) {
18
+ const seeds = new Set();
19
+ for (const node of nodes) {
20
+ const specMatch = input.specId !== undefined &&
21
+ (node.id === `spec:${input.specId}` || node.metadata?.specId === input.specId);
22
+ const fileMatch = input.filePath !== undefined &&
23
+ (node.id === `file:${input.filePath}` || node.label === input.filePath);
24
+ const nodeMatch = input.nodeId !== undefined && node.id === input.nodeId;
25
+ if (specMatch || fileMatch || nodeMatch) {
26
+ seeds.add(node.id);
27
+ }
28
+ }
29
+ if (seeds.size === 0 && input.specId === undefined && input.filePath === undefined) {
30
+ for (const node of nodes.slice(0, 20)) {
31
+ seeds.add(node.id);
32
+ }
33
+ }
34
+ const related = new Set(seeds);
35
+ for (const edge of edges) {
36
+ if (seeds.has(edge.from)) {
37
+ related.add(edge.to);
38
+ }
39
+ if (seeds.has(edge.to)) {
40
+ related.add(edge.from);
41
+ }
42
+ }
43
+ return related;
44
+ }
45
+ function summarize(nodes) {
46
+ const count = (type) => nodes.filter((node) => node.type === type).length;
47
+ return {
48
+ specs: count('spec'),
49
+ criteria: count('criterion'),
50
+ files: count('file'),
51
+ tests: count('test'),
52
+ decisions: count('decision'),
53
+ risks: count('risk'),
54
+ tools: count('tool'),
55
+ releases: count('release'),
56
+ };
57
+ }
58
+ export async function queryProjectGraphSlice(input) {
59
+ const projectId = input.projectId ?? hashProjectPath(input.projectPath);
60
+ const policy = await loadProjectGraphPolicy();
61
+ if (!policy.enabled) {
62
+ return null;
63
+ }
64
+ let freshness = await getProjectGraphFreshness({
65
+ projectId,
66
+ projectPath: input.projectPath,
67
+ policy,
68
+ });
69
+ if (!freshness.exists ||
70
+ freshness.reason === 'source_changed' ||
71
+ freshness.reason === 'corrupt') {
72
+ await buildProjectKnowledgeGraph({ projectId, projectPath: input.projectPath, policy });
73
+ freshness = await getProjectGraphFreshness({
74
+ projectId,
75
+ projectPath: input.projectPath,
76
+ policy,
77
+ });
78
+ }
79
+ const graph = await readProjectGraph(projectId, policy);
80
+ if (graph === null) {
81
+ return null;
82
+ }
83
+ const maxNodes = input.maxNodes ?? policy.query.maxNodes;
84
+ const maxEdges = input.maxEdges ?? policy.query.maxEdges;
85
+ const ids = relatedNodeIds(graph.nodes, graph.edges, input);
86
+ const nodes = graph.nodes.filter((node) => ids.has(node.id)).slice(0, maxNodes);
87
+ const nodeIds = new Set(nodes.map((node) => node.id));
88
+ const edges = graph.edges
89
+ .filter((edge) => nodeIds.has(edge.from) && nodeIds.has(edge.to))
90
+ .slice(0, maxEdges);
91
+ return {
92
+ graphVersion: graph.graphVersion,
93
+ generatedAt: graph.generatedAt,
94
+ freshness,
95
+ nodes,
96
+ edges,
97
+ summary: summarize(nodes),
98
+ tokenSavings: {
99
+ compactNodes: nodes.length,
100
+ compactEdges: edges.length,
101
+ estimatedRawContextAvoided: Math.max(0, graph.nodes.length + graph.edges.length - nodes.length - edges.length),
102
+ },
103
+ };
104
+ }
105
+ function listByType(slice, type, policy) {
106
+ return slice.nodes
107
+ .filter((node) => node.type === type)
108
+ .map((node) => sanitizeLabel(node.label, policy))
109
+ .slice(0, policy.query.maxListItems);
110
+ }
111
+ export async function formatProjectGraphContext(slice) {
112
+ const policy = await loadProjectGraphPolicy();
113
+ const files = listByType(slice, 'file', policy);
114
+ const tests = listByType(slice, 'test', policy);
115
+ const decisions = listByType(slice, 'decision', policy);
116
+ const risks = listByType(slice, 'risk', policy);
117
+ const specs = listByType(slice, 'spec', policy);
118
+ const tools = listByType(slice, 'tool', policy);
119
+ const lines = ['## Graph Context', ''];
120
+ lines.push(`Freshness: ${slice.freshness.stale ? `stale (${slice.freshness.reason})` : 'fresh'}; compact slice: ${String(slice.nodes.length)} nodes / ${String(slice.edges.length)} edges.`);
121
+ if (files.length > 0) {
122
+ lines.push(`Related files: ${files.join(', ')}`);
123
+ }
124
+ if (tests.length > 0) {
125
+ lines.push(`Related tests: ${tests.join(', ')}`);
126
+ }
127
+ if (decisions.length > 0) {
128
+ lines.push(`Decisions: ${decisions.join('; ')}`);
129
+ }
130
+ if (risks.length > 0) {
131
+ lines.push(`Risks: ${risks.join('; ')}`);
132
+ }
133
+ if (specs.length > 1) {
134
+ lines.push(`Neighboring specs: ${specs.join(', ')}`);
135
+ }
136
+ if (tools.length > 0) {
137
+ lines.push(`Tools: ${tools.join(', ')}`);
138
+ }
139
+ lines.push('');
140
+ return lines.join('\n');
141
+ }
142
+ export async function getProjectGraphFreshnessHint(args) {
143
+ const projectId = args.projectId ?? hashProjectPath(args.projectPath);
144
+ const policy = await loadProjectGraphPolicy();
145
+ if (!policy.enabled) {
146
+ return undefined;
147
+ }
148
+ const freshness = await getProjectGraphFreshness({
149
+ projectId,
150
+ projectPath: args.projectPath,
151
+ policy,
152
+ });
153
+ if (!freshness.stale) {
154
+ return undefined;
155
+ }
156
+ const suffix = freshness.changedSources.length > 0
157
+ ? `; ${String(freshness.changedSources.length)} changed source(s)`
158
+ : '';
159
+ return `project graph ${freshness.reason}${suffix}`;
160
+ }
161
+ //# sourceMappingURL=query.js.map
@@ -160,6 +160,23 @@ function runVitest(testPaths, projectPath, timeoutMs = 120_000) {
160
160
  });
161
161
  });
162
162
  }
163
+ function resolveVitestTestFilePath(result) {
164
+ // Vitest JSON returns the file path under `testResults[].name` in v4.
165
+ const candidates = [result.testFilePath, result.name, result.filepath, result.filePath];
166
+ for (const candidate of candidates) {
167
+ if (typeof candidate === 'string' && candidate.trim().length > 0) {
168
+ return candidate;
169
+ }
170
+ }
171
+ return null;
172
+ }
173
+ function toProjectRelativeTestPath(testFilePath, projectPath) {
174
+ const prefix = projectPath.endsWith('/') ? projectPath : `${projectPath}/`;
175
+ return testFilePath.startsWith(prefix) ? testFilePath.slice(prefix.length) : testFilePath;
176
+ }
177
+ function resolveScenarioTestPath(link) {
178
+ return typeof link.path === 'string' && link.path.trim().length > 0 ? link.path : null;
179
+ }
163
180
  // ---------------------------------------------------------------------------
164
181
  // Public API
165
182
  // ---------------------------------------------------------------------------
@@ -186,7 +203,10 @@ export async function runSpecCompliance(spec, projectPath) {
186
203
  const allTestPaths = new Set();
187
204
  for (const scenario of scenarios) {
188
205
  for (const link of scenario.tests ?? []) {
189
- allTestPaths.add(link.path);
206
+ const linkPath = resolveScenarioTestPath(link);
207
+ if (linkPath) {
208
+ allTestPaths.add(linkPath);
209
+ }
190
210
  }
191
211
  }
192
212
  const command = allTestPaths.size > 0
@@ -214,10 +234,14 @@ export async function runSpecCompliance(spec, projectPath) {
214
234
  const testFileStatus = new Map();
215
235
  if (vitestResult) {
216
236
  for (const tr of vitestResult.testResults) {
237
+ const testFilePath = resolveVitestTestFilePath(tr);
238
+ if (!testFilePath) {
239
+ continue;
240
+ }
217
241
  const allPassed = tr.assertionResults.every((r) => r.status === 'passed');
218
- testFileStatus.set(tr.testFilePath, allPassed ? 'pass' : 'fail');
242
+ testFileStatus.set(testFilePath, allPassed ? 'pass' : 'fail');
219
243
  // Also index by relative path
220
- const relPath = tr.testFilePath.replace(projectPath + '/', '');
244
+ const relPath = toProjectRelativeTestPath(testFilePath, projectPath);
221
245
  testFileStatus.set(relPath, allPassed ? 'pass' : 'fail');
222
246
  }
223
247
  }
@@ -236,23 +260,29 @@ export async function runSpecCompliance(spec, projectPath) {
236
260
  // Check all links
237
261
  const verdicts = [];
238
262
  for (const link of links) {
239
- const exists = existenceMap.get(link.path) !== false;
263
+ const linkPath = resolveScenarioTestPath(link);
264
+ if (!linkPath) {
265
+ evidence.push('Malformed test link: missing path');
266
+ verdicts.push('missing');
267
+ continue;
268
+ }
269
+ const exists = existenceMap.get(linkPath) !== false;
240
270
  if (!exists) {
241
- evidence.push(`${link.path}: file not found`);
271
+ evidence.push(`${linkPath}: file not found`);
242
272
  verdicts.push('missing');
243
273
  continue;
244
274
  }
245
- const status = testFileStatus.get(link.path) ?? (vitestResult ? 'fail' : 'missing');
275
+ const status = testFileStatus.get(linkPath) ?? (vitestResult ? 'fail' : 'missing');
246
276
  if (status === 'pass') {
247
- evidence.push(`${link.path}: passed`);
277
+ evidence.push(`${linkPath}: passed`);
248
278
  verdicts.push('pass');
249
279
  }
250
280
  else if (status === 'missing') {
251
- evidence.push(`${link.path}: not found in vitest output`);
281
+ evidence.push(`${linkPath}: not found in vitest output`);
252
282
  verdicts.push('missing');
253
283
  }
254
284
  else {
255
- evidence.push(`${link.path}: failed`);
285
+ evidence.push(`${linkPath}: failed`);
256
286
  verdicts.push('fail');
257
287
  }
258
288
  }
@@ -13,6 +13,7 @@ import { join, dirname } from 'node:path';
13
13
  import { hashProjectPath, projectDataDir } from '../../storage/base-store.js';
14
14
  import { appendAutopilotLogEntry } from '../../storage/autopilot-log-store.js';
15
15
  import { analyzeContextPreflight, buildTokenWasteReport, loadTokenWastePolicy, recommendRelevantTools, toolsFromPolicyGroups, } from '../../engine/token-optimizer/index.js';
16
+ import { queryProjectGraphSlice } from '../../engine/project-graph/index.js';
16
17
  const ASYNC_ANALYSIS_HOOK = 'create-spec-async-analysis';
17
18
  export function getAsyncAnalysisPath(projectPath, specId) {
18
19
  const projectId = hashProjectPath(projectPath);
@@ -24,10 +25,21 @@ export async function buildInitialTokenWasteMetadata(specId, projectPath, descri
24
25
  if (!policy.enabled) {
25
26
  return null;
26
27
  }
28
+ const graphSlice = await queryProjectGraphSlice({ projectPath, specId }).catch(() => null);
29
+ const graphCandidates = graphSlice?.nodes
30
+ .filter((node) => node.type === 'file' || node.type === 'test')
31
+ .slice(0, 12)
32
+ .map((node) => ({
33
+ path: node.label,
34
+ reason: 'Related by Project Knowledge Graph compact slice.',
35
+ })) ?? [];
27
36
  const context = analyzeContextPreflight({
28
37
  action: 'create_spec',
29
38
  policy,
30
- candidates: [{ path: 'planu/specs', reason: 'New spec workspace metadata.' }],
39
+ candidates: [
40
+ ...graphCandidates,
41
+ { path: 'planu/specs', reason: 'New spec workspace metadata.' },
42
+ ],
31
43
  spec: { id: specId },
32
44
  });
33
45
  const tools = recommendRelevantTools({
@@ -5,6 +5,7 @@ import { packageHandoff } from '../engine/handoff-packager.js';
5
5
  import { detectParadigms } from '../engine/paradigm-detector.js';
6
6
  import { analyzeContextPreflight, buildTokenWasteReport, formatTokenWasteReport, loadTokenWastePolicy, recommendRelevantTools, toolsFromPolicyGroups, } from '../engine/token-optimizer/index.js';
7
7
  import { appendTransitionEvent } from '../storage/transition-log.js';
8
+ import { formatProjectGraphContext, queryProjectGraphSlice, } from '../engine/project-graph/index.js';
8
9
  // ── Formatting helpers ───────────────────────────────────────────────────────
9
10
  function formatHandoff(pkg) {
10
11
  const lines = [];
@@ -176,6 +177,25 @@ async function buildHandoffTokenWasteReport(pkg, knowledge, spec) {
176
177
  return null;
177
178
  }
178
179
  }
180
+ async function buildGraphContextSection(args) {
181
+ if (args.projectPath === undefined) {
182
+ return null;
183
+ }
184
+ try {
185
+ const slice = await queryProjectGraphSlice({
186
+ projectId: args.projectId,
187
+ projectPath: args.projectPath,
188
+ specId: args.specId,
189
+ });
190
+ if (slice === null || slice.nodes.length === 0) {
191
+ return null;
192
+ }
193
+ return { text: await formatProjectGraphContext(slice), structured: slice };
194
+ }
195
+ catch {
196
+ return null;
197
+ }
198
+ }
179
199
  // ── Handler ──────────────────────────────────────────────────────────────────
180
200
  export async function handlePackageHandoff(args) {
181
201
  const { projectId, specId } = args;
@@ -231,9 +251,17 @@ export async function handlePackageHandoff(args) {
231
251
  constraints: paradigmConstraints,
232
252
  };
233
253
  const tokenWasteReport = await buildHandoffTokenWasteReport(pkgWithScore, knowledge, spec);
234
- const formatted = tokenWasteReport !== null
235
- ? `${formatHandoff(pkgWithScore)}\n${formatTokenWasteReport(tokenWasteReport)}`
254
+ const graphContext = await buildGraphContextSection({
255
+ projectId,
256
+ projectPath: knowledge.projectPath,
257
+ specId,
258
+ });
259
+ const handoffText = graphContext !== null
260
+ ? `${formatHandoff(pkgWithScore)}\n${graphContext.text}`
236
261
  : formatHandoff(pkgWithScore);
262
+ const formatted = tokenWasteReport !== null
263
+ ? `${handoffText}\n${formatTokenWasteReport(tokenWasteReport)}`
264
+ : handoffText;
237
265
  void appendTransitionEvent({
238
266
  projectId,
239
267
  specId,
@@ -255,6 +283,7 @@ export async function handlePackageHandoff(args) {
255
283
  handoffPath: pkgWithScore.handoffPath,
256
284
  contextHash: pkgWithScore.contextHash,
257
285
  ...(tokenWasteReport !== null ? { tokenWaste: tokenWasteReport } : {}),
286
+ ...(graphContext !== null ? { graphContext: graphContext.structured } : {}),
258
287
  },
259
288
  };
260
289
  }
@@ -13,6 +13,7 @@ export { SecurityReportTimeRangeEnum, SecurityReportFormatEnum } from './runtime
13
13
  export { WorkerStatusInputSchema, ConfigureWorkersInputSchema } from './workers-schema.js';
14
14
  export { TokenUsagePeriodEnum, TokenUsageGroupByEnum } from './token-optimization.js';
15
15
  export { TokenIntelligencePeriodEnum, TokenIntelligenceGroupByEnum, TokenIntelligenceViewEnum, } from './token-intelligence.js';
16
+ export { ProjectGraphActionEnum, ProjectGraphQueryKindEnum } from './project-graph.js';
16
17
  export { ConfigureLLMProvidersActionEnum, LoadBalancingStrategyEnum, } from './llm-provider-schemas.js';
17
18
  export { PluginActionEnum } from './plugins-schemas.js';
18
19
  export { ListSpecsOutputSchema, EstimateOutputSchema, ValidateOutputSchema, CheckReadinessOutputSchema, LicenseStatusOutputSchema, } from './output-schemas.js';
@@ -14,6 +14,7 @@ export { SecurityReportTimeRangeEnum, SecurityReportFormatEnum } from './runtime
14
14
  export { WorkerStatusInputSchema, ConfigureWorkersInputSchema } from './workers-schema.js';
15
15
  export { TokenUsagePeriodEnum, TokenUsageGroupByEnum } from './token-optimization.js';
16
16
  export { TokenIntelligencePeriodEnum, TokenIntelligenceGroupByEnum, TokenIntelligenceViewEnum, } from './token-intelligence.js';
17
+ export { ProjectGraphActionEnum, ProjectGraphQueryKindEnum } from './project-graph.js';
17
18
  export { ConfigureLLMProvidersActionEnum, LoadBalancingStrategyEnum, } from './llm-provider-schemas.js';
18
19
  export { PluginActionEnum } from './plugins-schemas.js';
19
20
  export { ListSpecsOutputSchema, EstimateOutputSchema, ValidateOutputSchema, CheckReadinessOutputSchema, LicenseStatusOutputSchema, } from './output-schemas.js';
@@ -0,0 +1,18 @@
1
+ import { z } from 'zod';
2
+ export declare const ProjectGraphActionEnum: z.ZodEnum<{
3
+ status: "status";
4
+ query: "query";
5
+ explain: "explain";
6
+ rebuild: "rebuild";
7
+ }>;
8
+ export declare const ProjectGraphQueryKindEnum: z.ZodEnum<{
9
+ release: "release";
10
+ decision: "decision";
11
+ spec: "spec";
12
+ file: "file";
13
+ risk: "risk";
14
+ tool: "tool";
15
+ test: "test";
16
+ criterion: "criterion";
17
+ }>;
18
+ //# sourceMappingURL=project-graph.d.ts.map
@@ -0,0 +1,8 @@
1
+ import { z } from 'zod';
2
+ export const ProjectGraphActionEnum = z
3
+ .enum(['status', 'query', 'explain', 'rebuild'])
4
+ .describe('Project graph inspection action.');
5
+ export const ProjectGraphQueryKindEnum = z
6
+ .enum(['spec', 'file', 'criterion', 'test', 'release', 'decision', 'risk', 'tool'])
7
+ .describe('Graph entity kind to query.');
8
+ //# sourceMappingURL=project-graph.js.map
@@ -12,6 +12,7 @@ export declare const TokenIntelligenceGroupByEnum: z.ZodEnum<{
12
12
  day: "day";
13
13
  }>;
14
14
  export declare const TokenIntelligenceViewEnum: z.ZodEnum<{
15
+ graph: "graph";
15
16
  summary: "summary";
16
17
  budget: "budget";
17
18
  detailed: "detailed";
@@ -9,11 +9,12 @@ export const TokenIntelligenceGroupByEnum = z
9
9
  .describe('Group results by: tool (per MCP tool name), model (per AI model), ' +
10
10
  'spec (per spec ID), day (daily breakdown)');
11
11
  export const TokenIntelligenceViewEnum = z
12
- .enum(['summary', 'detailed', 'budget', 'reconciliation', 'trends', 'autopilot'])
12
+ .enum(['summary', 'detailed', 'budget', 'reconciliation', 'trends', 'autopilot', 'graph'])
13
13
  .describe('Dashboard view: summary (high-level overview with top consumers), ' +
14
14
  'detailed (full per-tool and per-model breakdown), ' +
15
15
  'budget (spending vs monthly limits with projections), ' +
16
16
  'reconciliation (estimated vs actual cost accuracy per spec), ' +
17
17
  'trends (usage patterns over time, week-over-week changes, and anomaly alerts), ' +
18
- 'autopilot (policy-driven recommendations to reduce wasted context, output, and tool usage)');
18
+ 'autopilot (policy-driven recommendations to reduce wasted context, output, and tool usage), ' +
19
+ 'graph (project knowledge graph freshness, compact context savings, and stale graph risks)');
19
20
  //# sourceMappingURL=token-intelligence.js.map