@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.
- 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/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
|
|
@@ -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
|
|
@@ -18,6 +18,7 @@ import { readPlanuConfig } from '../engine/planu-config-writer.js';
|
|
|
18
18
|
import { computePlanuDriftScore } from '../engine/dashboard/drift-score.js';
|
|
19
19
|
import { findStaleBranches, findStaleWorktrees, findStaleStashes, } from '../engine/housekeeping/index.js';
|
|
20
20
|
import { loadTokenWastePolicy } from '../engine/token-optimizer/index.js';
|
|
21
|
+
import { getProjectGraphFreshnessHint } from '../engine/project-graph/index.js';
|
|
21
22
|
// SPEC-1011 Bug G: fire-and-forget status.json reconciliation
|
|
22
23
|
import { reconcileStatusFromDisk } from '../engine/status-reconciler/index.js';
|
|
23
24
|
const execFile = promisify(execFileCb);
|
|
@@ -128,7 +129,7 @@ function buildSuggestion(snapshot) {
|
|
|
128
129
|
// Output builder
|
|
129
130
|
// ---------------------------------------------------------------------------
|
|
130
131
|
function buildOutput(params) {
|
|
131
|
-
const { snapshot, git, ci, slaBreaches, workMode, updateBanner, autoCompleted, sessionTip, tokenWasteHint, } = params;
|
|
132
|
+
const { snapshot, git, ci, slaBreaches, workMode, updateBanner, autoCompleted, sessionTip, tokenWasteHint, graphFreshnessHint, } = params;
|
|
132
133
|
const lines = ['Planu Status', '━━━━━━━━━━━━━━━'];
|
|
133
134
|
if (snapshot.active !== null) {
|
|
134
135
|
const title = snapshot.active.title.slice(0, 40);
|
|
@@ -166,6 +167,9 @@ function buildOutput(params) {
|
|
|
166
167
|
if (tokenWasteHint !== undefined) {
|
|
167
168
|
lines.push(`TOKEN WASTE ${tokenWasteHint}`);
|
|
168
169
|
}
|
|
170
|
+
if (graphFreshnessHint !== undefined) {
|
|
171
|
+
lines.push(`GRAPH ${graphFreshnessHint}`);
|
|
172
|
+
}
|
|
169
173
|
lines.push(`SUGGEST ${buildSuggestion(snapshot)}`);
|
|
170
174
|
if (workMode !== undefined) {
|
|
171
175
|
lines.push(`WORK MODE ${workMode}`);
|
|
@@ -210,7 +214,7 @@ export async function handlePlanStatus(args) {
|
|
|
210
214
|
/* best-effort */
|
|
211
215
|
});
|
|
212
216
|
}
|
|
213
|
-
const [snapshot, git, ci, slaBreaches, planuConfig, autoCompleted, sessionState, driftScore, pendingCleanup, tokenWastePolicy,] = await Promise.all([
|
|
217
|
+
const [snapshot, git, ci, slaBreaches, planuConfig, autoCompleted, sessionState, driftScore, pendingCleanup, tokenWastePolicy, graphFreshnessHint,] = await Promise.all([
|
|
214
218
|
getStatusSpecSnapshot(projectId),
|
|
215
219
|
getGitState(args.projectPath),
|
|
216
220
|
getCiState(args.projectPath),
|
|
@@ -244,6 +248,7 @@ export async function handlePlanStatus(args) {
|
|
|
244
248
|
loadTokenWastePolicy(args.projectPath)
|
|
245
249
|
.then((loaded) => loaded.policy)
|
|
246
250
|
.catch(() => null),
|
|
251
|
+
getProjectGraphFreshnessHint({ projectId, projectPath: args.projectPath }).catch(() => undefined),
|
|
247
252
|
]);
|
|
248
253
|
// SPEC-1012: Detect stale release status (best-effort, non-blocking)
|
|
249
254
|
let staleStatusWarnings = [];
|
|
@@ -277,6 +282,7 @@ export async function handlePlanStatus(args) {
|
|
|
277
282
|
autoCompleted,
|
|
278
283
|
sessionTip,
|
|
279
284
|
tokenWasteHint,
|
|
285
|
+
graphFreshnessHint,
|
|
280
286
|
});
|
|
281
287
|
// SPEC-256: Always show /btw hint as educational tip (no context percent available from MCP layer)
|
|
282
288
|
const DEMO_CONTEXT_PERCENT = 65;
|
|
@@ -301,6 +307,7 @@ export async function handlePlanStatus(args) {
|
|
|
301
307
|
...(autoCompleted.length > 0 ? { autoCompleted } : {}),
|
|
302
308
|
...(sessionTip !== undefined ? { sessionTip } : {}),
|
|
303
309
|
...(tokenWasteHint !== undefined ? { tokenWasteHint } : {}),
|
|
310
|
+
...(graphFreshnessHint !== undefined ? { graphFreshnessHint } : {}),
|
|
304
311
|
...(btwHint !== undefined ? { btw_hint: btwHint } : {}),
|
|
305
312
|
...(updateBanner !== undefined ? { update_banner: updateBanner } : {}),
|
|
306
313
|
// SPEC-751: pending cleanup counters
|