@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.
- package/CHANGELOG.md +18 -0
- package/dist/config/project-knowledge-graph.json +65 -0
- package/dist/engine/project-graph/builder.d.ts +7 -0
- package/dist/engine/project-graph/builder.js +92 -0
- package/dist/engine/project-graph/cache.d.ts +26 -0
- package/dist/engine/project-graph/cache.js +160 -0
- package/dist/engine/project-graph/extractors/git-extractor.d.ts +7 -0
- package/dist/engine/project-graph/extractors/git-extractor.js +89 -0
- package/dist/engine/project-graph/extractors/handoff-extractor.d.ts +3 -0
- package/dist/engine/project-graph/extractors/handoff-extractor.js +55 -0
- package/dist/engine/project-graph/extractors/spec-extractor.d.ts +3 -0
- package/dist/engine/project-graph/extractors/spec-extractor.js +189 -0
- package/dist/engine/project-graph/extractors/validation-extractor.d.ts +3 -0
- package/dist/engine/project-graph/extractors/validation-extractor.js +36 -0
- package/dist/engine/project-graph/index.d.ts +4 -0
- package/dist/engine/project-graph/index.js +4 -0
- package/dist/engine/project-graph/query.d.ts +9 -0
- package/dist/engine/project-graph/query.js +161 -0
- package/dist/engine/validator/spec-compliance-runner.js +39 -9
- package/dist/tools/create-spec/post-creation.js +13 -1
- package/dist/tools/package-handoff.js +31 -2
- package/dist/tools/schemas/index.d.ts +1 -0
- package/dist/tools/schemas/index.js +1 -0
- package/dist/tools/schemas/project-graph.d.ts +18 -0
- package/dist/tools/schemas/project-graph.js +8 -0
- package/dist/tools/schemas/token-intelligence.d.ts +1 -0
- package/dist/tools/schemas/token-intelligence.js +3 -2
- package/dist/tools/status-handler.js +9 -2
- package/dist/tools/token-intelligence-handler.js +28 -1
- package/dist/tools/validate.js +75 -30
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.js +1 -0
- package/dist/types/project-knowledge-graph.d.ts +139 -0
- package/dist/types/project-knowledge-graph.js +2 -0
- package/package.json +12 -11
- package/planu-native.json +1 -1
- 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, '<').replace(/>/g, '>');
|
|
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
|
-
|
|
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(
|
|
242
|
+
testFileStatus.set(testFilePath, allPassed ? 'pass' : 'fail');
|
|
219
243
|
// Also index by relative path
|
|
220
|
-
const relPath =
|
|
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
|
|
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(`${
|
|
271
|
+
evidence.push(`${linkPath}: file not found`);
|
|
242
272
|
verdicts.push('missing');
|
|
243
273
|
continue;
|
|
244
274
|
}
|
|
245
|
-
const status = testFileStatus.get(
|
|
275
|
+
const status = testFileStatus.get(linkPath) ?? (vitestResult ? 'fail' : 'missing');
|
|
246
276
|
if (status === 'pass') {
|
|
247
|
-
evidence.push(`${
|
|
277
|
+
evidence.push(`${linkPath}: passed`);
|
|
248
278
|
verdicts.push('pass');
|
|
249
279
|
}
|
|
250
280
|
else if (status === 'missing') {
|
|
251
|
-
evidence.push(`${
|
|
281
|
+
evidence.push(`${linkPath}: not found in vitest output`);
|
|
252
282
|
verdicts.push('missing');
|
|
253
283
|
}
|
|
254
284
|
else {
|
|
255
|
-
evidence.push(`${
|
|
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: [
|
|
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
|
|
235
|
-
|
|
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
|
|
@@ -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
|