@planu/cli 4.8.0 → 4.9.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 +9 -0
- package/dist/config/project-knowledge-graph.json +70 -2
- package/dist/engine/project-graph/builder.js +9 -5
- package/dist/engine/project-graph/cache.js +4 -0
- package/dist/engine/project-graph/extractors/decision-store-extractor.d.ts +3 -0
- package/dist/engine/project-graph/extractors/decision-store-extractor.js +57 -0
- package/dist/engine/structural-memory/architecture-snapshot.d.ts +3 -0
- package/dist/engine/structural-memory/architecture-snapshot.js +132 -0
- package/dist/engine/structural-memory/artifact-policy.d.ts +5 -0
- package/dist/engine/structural-memory/artifact-policy.js +17 -0
- package/dist/engine/structural-memory/change-impact.d.ts +3 -0
- package/dist/engine/structural-memory/change-impact.js +159 -0
- package/dist/engine/structural-memory/contracts.d.ts +2 -0
- package/dist/engine/structural-memory/contracts.js +2 -0
- package/dist/engine/structural-memory/helpers.d.ts +28 -0
- package/dist/engine/structural-memory/helpers.js +122 -0
- package/dist/engine/structural-memory/index.d.ts +7 -0
- package/dist/engine/structural-memory/index.js +6 -0
- package/dist/engine/structural-memory/query.d.ts +3 -0
- package/dist/engine/structural-memory/query.js +67 -0
- package/dist/engine/structural-memory/status.d.ts +3 -0
- package/dist/engine/structural-memory/status.js +30 -0
- package/dist/tools/tool-registry.js +3 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/index.js +2 -0
- package/dist/types/project-knowledge-graph.d.ts +10 -0
- package/dist/types/structural-memory.d.ts +138 -0
- package/dist/types/structural-memory.js +2 -0
- package/package.json +20 -20
- package/planu-native.json +1 -1
- package/planu-plugin.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -18,7 +18,14 @@
|
|
|
18
18
|
"tool",
|
|
19
19
|
"handoff",
|
|
20
20
|
"validation",
|
|
21
|
-
"commit"
|
|
21
|
+
"commit",
|
|
22
|
+
"adr",
|
|
23
|
+
"validation_evidence",
|
|
24
|
+
"release_risk",
|
|
25
|
+
"architecture_risk",
|
|
26
|
+
"drift",
|
|
27
|
+
"contract",
|
|
28
|
+
"git_change"
|
|
22
29
|
],
|
|
23
30
|
"edgeTypes": [
|
|
24
31
|
"contains",
|
|
@@ -31,7 +38,13 @@
|
|
|
31
38
|
"has_risk",
|
|
32
39
|
"uses_tool",
|
|
33
40
|
"mentions",
|
|
34
|
-
"neighbors"
|
|
41
|
+
"neighbors",
|
|
42
|
+
"affects",
|
|
43
|
+
"covers",
|
|
44
|
+
"depends_on",
|
|
45
|
+
"has_evidence",
|
|
46
|
+
"has_status",
|
|
47
|
+
"has_contract"
|
|
35
48
|
],
|
|
36
49
|
"classifications": ["extracted", "inferred", "ambiguous"],
|
|
37
50
|
"confidenceLabels": ["high", "medium", "low"],
|
|
@@ -52,6 +65,61 @@
|
|
|
52
65
|
"(api[_-]?key|token|secret|password)\\s*[:=]\\s*[^\\s,;]+"
|
|
53
66
|
]
|
|
54
67
|
},
|
|
68
|
+
"structuralMemory": {
|
|
69
|
+
"schemaVersion": "1.0.0",
|
|
70
|
+
"nodeTypes": [
|
|
71
|
+
"spec",
|
|
72
|
+
"criterion",
|
|
73
|
+
"file",
|
|
74
|
+
"test",
|
|
75
|
+
"adr",
|
|
76
|
+
"decision",
|
|
77
|
+
"validation_evidence",
|
|
78
|
+
"validation",
|
|
79
|
+
"release",
|
|
80
|
+
"release_risk",
|
|
81
|
+
"architecture_risk",
|
|
82
|
+
"drift",
|
|
83
|
+
"risk",
|
|
84
|
+
"tool",
|
|
85
|
+
"contract",
|
|
86
|
+
"git_change"
|
|
87
|
+
],
|
|
88
|
+
"edgeTypes": [
|
|
89
|
+
"contains",
|
|
90
|
+
"references",
|
|
91
|
+
"implements",
|
|
92
|
+
"tests",
|
|
93
|
+
"validated_by",
|
|
94
|
+
"decides",
|
|
95
|
+
"has_risk",
|
|
96
|
+
"uses_tool",
|
|
97
|
+
"affects",
|
|
98
|
+
"covers",
|
|
99
|
+
"depends_on",
|
|
100
|
+
"has_evidence",
|
|
101
|
+
"has_status",
|
|
102
|
+
"has_contract"
|
|
103
|
+
],
|
|
104
|
+
"response": {
|
|
105
|
+
"defaultLimit": 60,
|
|
106
|
+
"maxLimit": 200
|
|
107
|
+
},
|
|
108
|
+
"propertySchemas": {
|
|
109
|
+
"spec": ["specId", "status"],
|
|
110
|
+
"criterion": ["specId", "index"],
|
|
111
|
+
"file": ["path"],
|
|
112
|
+
"test": ["path"],
|
|
113
|
+
"decision": ["specId", "decisionId"],
|
|
114
|
+
"adr": ["specId", "decisionId"],
|
|
115
|
+
"validation_evidence": ["specId", "kind", "status"],
|
|
116
|
+
"release_risk": ["specId", "severity"],
|
|
117
|
+
"architecture_risk": ["path", "ruleId", "severity"],
|
|
118
|
+
"drift": ["specId", "status", "severity"],
|
|
119
|
+
"contract": ["kind", "passed"],
|
|
120
|
+
"git_change": ["path", "status"]
|
|
121
|
+
}
|
|
122
|
+
},
|
|
55
123
|
"toolNamePatterns": [
|
|
56
124
|
"package_handoff",
|
|
57
125
|
"validate",
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { collectProjectGraphSources, diffSourceHashes, hashText, loadProjectGraphPolicy, projectGraphCachePath, projectGraphPath, readProjectGraph, readProjectGraphSourceHashes, writeProjectGraph, writeProjectGraphSourceHashes, } from './cache.js';
|
|
2
2
|
import { extractGitGraph, extractReleaseGraph } from './extractors/git-extractor.js';
|
|
3
3
|
import { extractHandoffGraph } from './extractors/handoff-extractor.js';
|
|
4
|
+
import { extractDecisionStoreGraph } from './extractors/decision-store-extractor.js';
|
|
4
5
|
import { extractSpecGraph } from './extractors/spec-extractor.js';
|
|
5
6
|
import { extractValidationGraph } from './extractors/validation-extractor.js';
|
|
6
7
|
function uniqueNodes(nodes) {
|
|
@@ -11,10 +12,7 @@ function uniqueEdges(edges) {
|
|
|
11
12
|
}
|
|
12
13
|
function validateAgainstPolicy(result, policy) {
|
|
13
14
|
const nodes = result.nodes.filter((node) => policy.nodeTypes.includes(node.type));
|
|
14
|
-
const
|
|
15
|
-
const edges = result.edges.filter((edge) => nodeIds.has(edge.from) &&
|
|
16
|
-
nodeIds.has(edge.to) &&
|
|
17
|
-
policy.edgeTypes.includes(edge.type) &&
|
|
15
|
+
const edges = result.edges.filter((edge) => policy.edgeTypes.includes(edge.type) &&
|
|
18
16
|
policy.confidenceLabels.includes(edge.confidence) &&
|
|
19
17
|
policy.classifications.includes(edge.classification));
|
|
20
18
|
return { nodes, edges };
|
|
@@ -50,6 +48,9 @@ export async function buildProjectKnowledgeGraph(args) {
|
|
|
50
48
|
else if (source.kind === 'handoff') {
|
|
51
49
|
extracted.push(extractHandoffGraph(source, policy));
|
|
52
50
|
}
|
|
51
|
+
else if (source.kind === 'decisions') {
|
|
52
|
+
extracted.push(extractDecisionStoreGraph(source, policy));
|
|
53
|
+
}
|
|
53
54
|
else if (source.kind === 'release-events') {
|
|
54
55
|
extracted.push(extractReleaseGraph(source, policy));
|
|
55
56
|
}
|
|
@@ -57,7 +58,10 @@ export async function buildProjectKnowledgeGraph(args) {
|
|
|
57
58
|
extracted.push(await extractGitGraph({ projectPath: args.projectPath, policy }));
|
|
58
59
|
const validated = extracted.map((result) => validateAgainstPolicy(result, policy));
|
|
59
60
|
const nodes = uniqueNodes(validated.flatMap((result) => result.nodes));
|
|
60
|
-
const
|
|
61
|
+
const nodeIds = new Set(nodes.map((node) => node.id));
|
|
62
|
+
const edges = uniqueEdges(validated
|
|
63
|
+
.flatMap((result) => result.edges)
|
|
64
|
+
.filter((edge) => nodeIds.has(edge.from) && nodeIds.has(edge.to)));
|
|
61
65
|
const sourceHashes = diff.next;
|
|
62
66
|
const sourceHash = hashText(JSON.stringify(sourceHashes));
|
|
63
67
|
const graph = {
|
|
@@ -83,6 +83,7 @@ export async function collectProjectGraphSources(args) {
|
|
|
83
83
|
const specFiles = await collectFiles(join(projectRoot, 'planu', 'specs'), (path) => path.endsWith('/spec.md'));
|
|
84
84
|
const handoffFiles = await collectFiles(join(dataRoot, 'handoffs'), (path) => /\.(json|md)$/.test(path));
|
|
85
85
|
const validationFiles = handoffFiles.filter((path) => path.includes('validation'));
|
|
86
|
+
const decisions = join(dataRoot, 'decisions.json');
|
|
86
87
|
const transitionLog = join(dataRoot, 'transition-log.jsonl');
|
|
87
88
|
const releaseEvents = join(dataRoot, '..', '..', 'release-events.jsonl');
|
|
88
89
|
const raw = [
|
|
@@ -99,6 +100,9 @@ export async function collectProjectGraphSources(args) {
|
|
|
99
100
|
...((await fileExists(transitionLog))
|
|
100
101
|
? [{ id: 'transition-log', kind: 'transition-log', path: transitionLog }]
|
|
101
102
|
: []),
|
|
103
|
+
...((await fileExists(decisions))
|
|
104
|
+
? [{ id: 'decisions', kind: 'decisions', path: decisions }]
|
|
105
|
+
: []),
|
|
102
106
|
...((await fileExists(releaseEvents))
|
|
103
107
|
? [{ id: 'release-events', kind: 'release-events', path: releaseEvents }]
|
|
104
108
|
: []),
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { ProjectGraphExtractionResult, ProjectGraphPolicy, ProjectGraphSource } from '../../../types/project-knowledge-graph.js';
|
|
2
|
+
export declare function extractDecisionStoreGraph(source: ProjectGraphSource, policy: ProjectGraphPolicy): ProjectGraphExtractionResult;
|
|
3
|
+
//# sourceMappingURL=decision-store-extractor.d.ts.map
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { safeJsonParse } from '../../../storage/base-store.js';
|
|
2
|
+
import { hashText } from '../cache.js';
|
|
3
|
+
import { redactGraphText } from '../query.js';
|
|
4
|
+
function isDecision(value) {
|
|
5
|
+
return (value !== null &&
|
|
6
|
+
typeof value === 'object' &&
|
|
7
|
+
typeof value.id === 'string' &&
|
|
8
|
+
typeof value.what === 'string' &&
|
|
9
|
+
typeof value.why === 'string');
|
|
10
|
+
}
|
|
11
|
+
function decisionsFromSource(source) {
|
|
12
|
+
const parsed = safeJsonParse(source.content, { decisions: [] });
|
|
13
|
+
return Array.isArray(parsed.decisions) ? parsed.decisions.filter(isDecision) : [];
|
|
14
|
+
}
|
|
15
|
+
function node(args) {
|
|
16
|
+
return {
|
|
17
|
+
id: `decision:${args.decision.id}`,
|
|
18
|
+
type: 'decision',
|
|
19
|
+
label: redactGraphText(`${args.decision.what}: ${args.decision.why}`, args.policy),
|
|
20
|
+
source: 'decision-store-extractor',
|
|
21
|
+
evidence: [{ path: args.source.path, hash: args.source.hash }],
|
|
22
|
+
metadata: {
|
|
23
|
+
decisionId: args.decision.id,
|
|
24
|
+
specId: args.decision.specId ?? null,
|
|
25
|
+
category: args.decision.category,
|
|
26
|
+
status: args.decision.status,
|
|
27
|
+
decidedBy: args.decision.decidedBy,
|
|
28
|
+
},
|
|
29
|
+
updatedAt: args.decision.date,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
function edge(decision, source) {
|
|
33
|
+
if (!decision.specId) {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
const decisionId = `decision:${decision.id}`;
|
|
37
|
+
return {
|
|
38
|
+
id: `edge:${hashText(`spec:${decision.specId}:decides:${decisionId}`)}`,
|
|
39
|
+
from: `spec:${decision.specId}`,
|
|
40
|
+
to: decisionId,
|
|
41
|
+
type: 'decides',
|
|
42
|
+
source: 'decision-store-extractor',
|
|
43
|
+
confidence: 'high',
|
|
44
|
+
classification: 'extracted',
|
|
45
|
+
evidence: { path: source.path, hash: source.hash },
|
|
46
|
+
updatedAt: decision.date,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
export function extractDecisionStoreGraph(source, policy) {
|
|
50
|
+
const decisions = decisionsFromSource(source);
|
|
51
|
+
const nodes = decisions.map((decision) => node({ decision, source, policy }));
|
|
52
|
+
const edges = decisions
|
|
53
|
+
.map((decision) => edge(decision, source))
|
|
54
|
+
.filter((item) => item !== null);
|
|
55
|
+
return { nodes, edges };
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=decision-store-extractor.js.map
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { clampLimit, loadFreshGraph, nextActionForFreshness, pagination, resolveProjectId, safeRelativeProjectPath, structuralPolicy, } from './helpers.js';
|
|
2
|
+
function aspects(input) {
|
|
3
|
+
const requested = input.aspects ?? ['all'];
|
|
4
|
+
if (requested.includes('all')) {
|
|
5
|
+
return new Set(['packages', 'hotspots', 'boundaries', 'releaseRisks', 'tests', 'decisions']);
|
|
6
|
+
}
|
|
7
|
+
return new Set(requested);
|
|
8
|
+
}
|
|
9
|
+
function packageName(path) {
|
|
10
|
+
const parts = path.split('/');
|
|
11
|
+
return parts.length > 1 ? parts.slice(0, 2).join('/') : (parts[0] ?? path);
|
|
12
|
+
}
|
|
13
|
+
function packageAspect(nodes, limit) {
|
|
14
|
+
const counts = new Map();
|
|
15
|
+
for (const node of nodes.filter((item) => item.type === 'file' || item.type === 'test')) {
|
|
16
|
+
counts.set(packageName(node.label), (counts.get(packageName(node.label)) ?? 0) + 1);
|
|
17
|
+
}
|
|
18
|
+
return [...counts.entries()]
|
|
19
|
+
.map(([name, fileCount]) => ({ name, fileCount }))
|
|
20
|
+
.sort((a, b) => b.fileCount - a.fileCount)
|
|
21
|
+
.slice(0, limit);
|
|
22
|
+
}
|
|
23
|
+
function hotspotAspect(nodes, edges, limit) {
|
|
24
|
+
const degree = new Map();
|
|
25
|
+
for (const edge of edges) {
|
|
26
|
+
degree.set(edge.from, (degree.get(edge.from) ?? 0) + 1);
|
|
27
|
+
degree.set(edge.to, (degree.get(edge.to) ?? 0) + 1);
|
|
28
|
+
}
|
|
29
|
+
return nodes
|
|
30
|
+
.map((node) => ({
|
|
31
|
+
id: node.id,
|
|
32
|
+
label: node.label,
|
|
33
|
+
type: node.type,
|
|
34
|
+
degree: degree.get(node.id) ?? 0,
|
|
35
|
+
}))
|
|
36
|
+
.filter((node) => node.degree > 0)
|
|
37
|
+
.sort((a, b) => b.degree - a.degree)
|
|
38
|
+
.slice(0, limit);
|
|
39
|
+
}
|
|
40
|
+
function boundaryAspect(edges, limit) {
|
|
41
|
+
return edges
|
|
42
|
+
.filter((edge) => edge.type === 'references' || edge.type === 'implements' || edge.type === 'tests')
|
|
43
|
+
.map((edge) => ({ from: edge.from, to: edge.to, type: edge.type }))
|
|
44
|
+
.slice(0, limit);
|
|
45
|
+
}
|
|
46
|
+
function simpleNodes(nodes, types, limit) {
|
|
47
|
+
return nodes
|
|
48
|
+
.filter((node) => types.includes(node.type))
|
|
49
|
+
.map((node) => ({ id: node.id, label: node.label, type: node.type }))
|
|
50
|
+
.slice(0, limit);
|
|
51
|
+
}
|
|
52
|
+
function snapshotAspects(args) {
|
|
53
|
+
const result = {};
|
|
54
|
+
if (args.requested.has('packages')) {
|
|
55
|
+
result.packages = packageAspect(args.nodes, args.limit);
|
|
56
|
+
}
|
|
57
|
+
if (args.requested.has('hotspots')) {
|
|
58
|
+
result.hotspots = hotspotAspect(args.nodes, args.edges, args.limit);
|
|
59
|
+
}
|
|
60
|
+
if (args.requested.has('boundaries')) {
|
|
61
|
+
result.boundaries = boundaryAspect(args.edges, args.limit);
|
|
62
|
+
}
|
|
63
|
+
if (args.requested.has('releaseRisks')) {
|
|
64
|
+
result.releaseRisks = simpleNodes(args.nodes, ['risk', 'release_risk', 'architecture_risk'], args.limit).map((node) => ({ id: node.id, label: node.label, type: node.type ?? 'risk' }));
|
|
65
|
+
}
|
|
66
|
+
if (args.requested.has('tests')) {
|
|
67
|
+
result.tests = simpleNodes(args.nodes, ['test'], args.limit).map(({ id, label }) => ({
|
|
68
|
+
id,
|
|
69
|
+
label,
|
|
70
|
+
}));
|
|
71
|
+
}
|
|
72
|
+
if (args.requested.has('decisions')) {
|
|
73
|
+
result.decisions = simpleNodes(args.nodes, ['decision', 'adr'], args.limit).map(({ id, label }) => ({ id, label }));
|
|
74
|
+
}
|
|
75
|
+
return result;
|
|
76
|
+
}
|
|
77
|
+
export async function getArchitectureSnapshot(input) {
|
|
78
|
+
const policy = await structuralPolicy();
|
|
79
|
+
const projectId = resolveProjectId(input);
|
|
80
|
+
const limit = clampLimit(input.limit, policy);
|
|
81
|
+
const { graph, freshness } = await loadFreshGraph({
|
|
82
|
+
projectId,
|
|
83
|
+
projectPath: input.projectPath,
|
|
84
|
+
policy,
|
|
85
|
+
});
|
|
86
|
+
const prefix = input.pathPrefix === undefined
|
|
87
|
+
? undefined
|
|
88
|
+
: safeRelativeProjectPath(input.projectPath, input.pathPrefix);
|
|
89
|
+
if (input.pathPrefix !== undefined && prefix === null) {
|
|
90
|
+
const page = pagination(0, limit, 0);
|
|
91
|
+
return {
|
|
92
|
+
freshness,
|
|
93
|
+
pagination: page,
|
|
94
|
+
...page,
|
|
95
|
+
nextAction: {
|
|
96
|
+
type: 'inspect_evidence',
|
|
97
|
+
message: 'Rejecting unsafe pathPrefix filter; pass a project-relative path inside the repo.',
|
|
98
|
+
},
|
|
99
|
+
aspects: {},
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
const page = pagination(graph?.nodes.length ?? 0, limit, 0);
|
|
103
|
+
if (graph === null) {
|
|
104
|
+
return {
|
|
105
|
+
freshness,
|
|
106
|
+
pagination: page,
|
|
107
|
+
...page,
|
|
108
|
+
nextAction: nextActionForFreshness(freshness, true),
|
|
109
|
+
aspects: {},
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
const scopedNodes = graph.nodes.filter((node) => {
|
|
113
|
+
if (prefix === undefined) {
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
return node.label === prefix || node.label.startsWith(`${prefix}/`);
|
|
117
|
+
});
|
|
118
|
+
const scopedIds = new Set(scopedNodes.map((node) => node.id));
|
|
119
|
+
const scopedEdges = graph.edges.filter((edge) => scopedIds.has(edge.from) && scopedIds.has(edge.to));
|
|
120
|
+
const requested = aspects(input);
|
|
121
|
+
const result = snapshotAspects({ nodes: scopedNodes, edges: scopedEdges, requested, limit });
|
|
122
|
+
const scopedPage = pagination(scopedNodes.length, limit, 0);
|
|
123
|
+
return {
|
|
124
|
+
graphVersion: graph.graphVersion,
|
|
125
|
+
freshness,
|
|
126
|
+
pagination: scopedPage,
|
|
127
|
+
...scopedPage,
|
|
128
|
+
nextAction: nextActionForFreshness(freshness, scopedNodes.length === 0),
|
|
129
|
+
aspects: result,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
//# sourceMappingURL=architecture-snapshot.js.map
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { ProjectGraphPolicy } from '../../types/project-knowledge-graph.js';
|
|
2
|
+
import type { StructuralMemoryArtifactPolicy } from '../../types/structural-memory.js';
|
|
3
|
+
export declare function createStructuralMemoryArtifactPolicy(projectId: string, policy: ProjectGraphPolicy): StructuralMemoryArtifactPolicy;
|
|
4
|
+
export declare function isStructuralMemoryArtifactPathAllowed(path: string): boolean;
|
|
5
|
+
//# sourceMappingURL=artifact-policy.d.ts.map
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { projectGraphDir, projectGraphPath } from '../project-graph/cache.js';
|
|
2
|
+
export function createStructuralMemoryArtifactPolicy(projectId, policy) {
|
|
3
|
+
const artifactRoot = projectGraphDir(projectId, policy);
|
|
4
|
+
return {
|
|
5
|
+
artifactRoot,
|
|
6
|
+
graphPath: projectGraphPath(projectId, policy),
|
|
7
|
+
projectDataDirectory: artifactRoot.split('/project-graph')[0] ?? artifactRoot,
|
|
8
|
+
forbiddenPathFragment: 'planu/specs',
|
|
9
|
+
specDirectoryPolicy: 'spec.md-only',
|
|
10
|
+
evidenceAndGraphArtifactsRemainOutsideSpecs: true,
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
export function isStructuralMemoryArtifactPathAllowed(path) {
|
|
14
|
+
const normalized = path.replace(/\\/g, '/');
|
|
15
|
+
return !normalized.includes('/planu/specs/') && !normalized.endsWith('/planu/specs');
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=artifact-policy.js.map
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { execFile as execFileCb } from 'node:child_process';
|
|
2
|
+
import { promisify } from 'node:util';
|
|
3
|
+
import { clampLimit, loadFreshGraph, nextActionForFreshness, pagination, resolveProjectId, safeRelativeProjectPath, structuralPolicy, } from './helpers.js';
|
|
4
|
+
const execFile = promisify(execFileCb);
|
|
5
|
+
function safeGitRef(ref) {
|
|
6
|
+
return (/^[A-Za-z0-9][A-Za-z0-9._/@{}:+-]{0,200}$/.test(ref) &&
|
|
7
|
+
!ref.includes('..') &&
|
|
8
|
+
!ref.startsWith('-'));
|
|
9
|
+
}
|
|
10
|
+
function statusFromCode(code) {
|
|
11
|
+
if (code.includes('A')) {
|
|
12
|
+
return 'added';
|
|
13
|
+
}
|
|
14
|
+
if (code.includes('D')) {
|
|
15
|
+
return 'deleted';
|
|
16
|
+
}
|
|
17
|
+
if (code.includes('R')) {
|
|
18
|
+
return 'renamed';
|
|
19
|
+
}
|
|
20
|
+
if (code.includes('?')) {
|
|
21
|
+
return 'untracked';
|
|
22
|
+
}
|
|
23
|
+
if (code.trim().length > 0) {
|
|
24
|
+
return 'modified';
|
|
25
|
+
}
|
|
26
|
+
return 'unknown';
|
|
27
|
+
}
|
|
28
|
+
function parseNameStatus(stdout, projectPath) {
|
|
29
|
+
return stdout
|
|
30
|
+
.split('\n')
|
|
31
|
+
.map((line) => line.trimEnd())
|
|
32
|
+
.filter((line) => line.length > 0)
|
|
33
|
+
.flatMap((line) => {
|
|
34
|
+
const [status, ...paths] = line.split(/\s+/);
|
|
35
|
+
const path = paths.at(-1);
|
|
36
|
+
if (!status || !path) {
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
39
|
+
const safePath = safeRelativeProjectPath(projectPath, path);
|
|
40
|
+
return safePath === null ? [] : [{ path: safePath, status: statusFromCode(status) }];
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
function parsePorcelain(stdout, projectPath, includeUntracked) {
|
|
44
|
+
return stdout
|
|
45
|
+
.split('\n')
|
|
46
|
+
.map((line) => line.trimEnd())
|
|
47
|
+
.filter((line) => line.length > 0)
|
|
48
|
+
.flatMap((line) => {
|
|
49
|
+
const code = line.slice(0, 2);
|
|
50
|
+
if (!includeUntracked && code === '??') {
|
|
51
|
+
return [];
|
|
52
|
+
}
|
|
53
|
+
const rawPath = line.slice(3).split(' -> ').at(-1);
|
|
54
|
+
if (!rawPath) {
|
|
55
|
+
return [];
|
|
56
|
+
}
|
|
57
|
+
const safePath = safeRelativeProjectPath(projectPath, rawPath);
|
|
58
|
+
return safePath === null ? [] : [{ path: safePath, status: statusFromCode(code) }];
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
async function gitChangedFiles(input) {
|
|
62
|
+
if (input.changedFiles !== undefined) {
|
|
63
|
+
return input.changedFiles.flatMap((path) => {
|
|
64
|
+
const safePath = safeRelativeProjectPath(input.projectPath, path);
|
|
65
|
+
return safePath === null ? [] : [{ path: safePath, status: 'unknown' }];
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
if (input.baseRef !== undefined) {
|
|
69
|
+
if (!safeGitRef(input.baseRef)) {
|
|
70
|
+
throw new Error('Unsafe git baseRef for structural change impact.');
|
|
71
|
+
}
|
|
72
|
+
const { stdout } = await execFile('git', ['diff', '--name-status', input.baseRef, '--'], {
|
|
73
|
+
cwd: input.projectPath,
|
|
74
|
+
maxBuffer: 1024 * 1024,
|
|
75
|
+
});
|
|
76
|
+
return parseNameStatus(stdout, input.projectPath);
|
|
77
|
+
}
|
|
78
|
+
const { stdout } = await execFile('git', ['status', '--porcelain'], {
|
|
79
|
+
cwd: input.projectPath,
|
|
80
|
+
maxBuffer: 1024 * 1024,
|
|
81
|
+
});
|
|
82
|
+
return parsePorcelain(stdout, input.projectPath, input.includeUntracked ?? true);
|
|
83
|
+
}
|
|
84
|
+
function uniqueNodes(nodes) {
|
|
85
|
+
return [...new Map(nodes.map((node) => [node.id, node])).values()];
|
|
86
|
+
}
|
|
87
|
+
function collectAffected(graphNodes, graphEdges, changedFiles) {
|
|
88
|
+
const changed = new Set(changedFiles.map((file) => file.path));
|
|
89
|
+
const fileNodeIds = new Set(graphNodes
|
|
90
|
+
.filter((node) => node.type === 'file' && changed.has(node.label))
|
|
91
|
+
.map((node) => node.id));
|
|
92
|
+
const affectedIds = new Set(fileNodeIds);
|
|
93
|
+
for (let depth = 0; depth < 3; depth += 1) {
|
|
94
|
+
for (const edge of graphEdges) {
|
|
95
|
+
if (affectedIds.has(edge.from)) {
|
|
96
|
+
affectedIds.add(edge.to);
|
|
97
|
+
}
|
|
98
|
+
if (affectedIds.has(edge.to)) {
|
|
99
|
+
affectedIds.add(edge.from);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
const nodes = graphNodes.filter((node) => affectedIds.has(node.id));
|
|
104
|
+
const ids = new Set(nodes.map((node) => node.id));
|
|
105
|
+
const edges = graphEdges.filter((edge) => ids.has(edge.from) && ids.has(edge.to));
|
|
106
|
+
return { nodes, edges };
|
|
107
|
+
}
|
|
108
|
+
export async function detectStructuralChangeImpact(input) {
|
|
109
|
+
const policy = await structuralPolicy();
|
|
110
|
+
const projectId = resolveProjectId(input);
|
|
111
|
+
const limit = clampLimit(input.limit, policy);
|
|
112
|
+
const changedFiles = await gitChangedFiles(input);
|
|
113
|
+
const { graph, freshness } = await loadFreshGraph({
|
|
114
|
+
projectId,
|
|
115
|
+
projectPath: input.projectPath,
|
|
116
|
+
policy,
|
|
117
|
+
});
|
|
118
|
+
const page = pagination(changedFiles.length, limit, 0);
|
|
119
|
+
if (graph === null) {
|
|
120
|
+
return {
|
|
121
|
+
freshness,
|
|
122
|
+
pagination: page,
|
|
123
|
+
...page,
|
|
124
|
+
nextAction: nextActionForFreshness(freshness, true),
|
|
125
|
+
changedFiles: changedFiles.slice(0, limit),
|
|
126
|
+
affected: {
|
|
127
|
+
specs: [],
|
|
128
|
+
criteria: [],
|
|
129
|
+
tests: [],
|
|
130
|
+
decisions: [],
|
|
131
|
+
evidence: [],
|
|
132
|
+
risks: [],
|
|
133
|
+
releaseRisk: [],
|
|
134
|
+
},
|
|
135
|
+
affectedEdges: [],
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
const affected = collectAffected(graph.nodes, graph.edges, changedFiles);
|
|
139
|
+
const byType = (types) => uniqueNodes(affected.nodes.filter((node) => types.includes(node.type))).slice(0, limit);
|
|
140
|
+
return {
|
|
141
|
+
graphVersion: graph.graphVersion,
|
|
142
|
+
freshness,
|
|
143
|
+
pagination: page,
|
|
144
|
+
...page,
|
|
145
|
+
nextAction: nextActionForFreshness(freshness, affected.nodes.length === 0),
|
|
146
|
+
changedFiles: changedFiles.slice(0, limit),
|
|
147
|
+
affected: {
|
|
148
|
+
specs: byType(['spec']),
|
|
149
|
+
criteria: byType(['criterion']),
|
|
150
|
+
tests: byType(['test']),
|
|
151
|
+
decisions: byType(['decision', 'adr']),
|
|
152
|
+
evidence: byType(['handoff', 'validation', 'validation_evidence', 'contract']),
|
|
153
|
+
risks: byType(['risk', 'architecture_risk', 'drift']),
|
|
154
|
+
releaseRisk: byType(['release', 'release_risk']),
|
|
155
|
+
},
|
|
156
|
+
affectedEdges: affected.edges.slice(0, policy.query.maxEdges),
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
//# sourceMappingURL=change-impact.js.map
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { ProjectGraphEdge, ProjectGraphFreshness, ProjectGraphNode, ProjectGraphPolicy } from '../../types/project-knowledge-graph.js';
|
|
2
|
+
import { readProjectGraph } from '../project-graph/cache.js';
|
|
3
|
+
import type { StructuralMemoryFreshness, StructuralMemoryNextAction, StructuralMemoryPagination } from './contracts.js';
|
|
4
|
+
export declare function resolveProjectId(input: {
|
|
5
|
+
projectId?: string;
|
|
6
|
+
projectPath: string;
|
|
7
|
+
}): string;
|
|
8
|
+
export declare function freshnessStatus(freshness: ProjectGraphFreshness): StructuralMemoryFreshness;
|
|
9
|
+
export declare function nextActionForFreshness(freshness: StructuralMemoryFreshness, empty?: boolean): StructuralMemoryNextAction;
|
|
10
|
+
export declare function pagination(total: number, limit: number, offset: number): StructuralMemoryPagination;
|
|
11
|
+
export declare function clampLimit(limit: number | undefined, policy: ProjectGraphPolicy): number;
|
|
12
|
+
export declare function safeRelativeProjectPath(projectPath: string, candidate: string): string | null;
|
|
13
|
+
export declare function loadFreshGraph(args: {
|
|
14
|
+
projectId: string;
|
|
15
|
+
projectPath: string;
|
|
16
|
+
policy: ProjectGraphPolicy;
|
|
17
|
+
}): Promise<{
|
|
18
|
+
graph: Awaited<ReturnType<typeof readProjectGraph>>;
|
|
19
|
+
freshness: StructuralMemoryFreshness;
|
|
20
|
+
}>;
|
|
21
|
+
export declare function summarizeNodes(nodes: ProjectGraphNode[]): Record<string, number>;
|
|
22
|
+
export declare function relatedNodeIds(nodes: ProjectGraphNode[], edges: ProjectGraphEdge[], input: {
|
|
23
|
+
specId?: string;
|
|
24
|
+
filePath?: string;
|
|
25
|
+
nodeId?: string;
|
|
26
|
+
}): Set<string>;
|
|
27
|
+
export declare function structuralPolicy(): Promise<ProjectGraphPolicy>;
|
|
28
|
+
//# sourceMappingURL=helpers.d.ts.map
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { isAbsolute, normalize, relative, resolve } from 'node:path';
|
|
2
|
+
import { hashProjectPath } from '../../storage/base-store.js';
|
|
3
|
+
import { getProjectGraphFreshness, loadProjectGraphPolicy, readProjectGraph, } from '../project-graph/cache.js';
|
|
4
|
+
export function resolveProjectId(input) {
|
|
5
|
+
return input.projectId ?? hashProjectPath(input.projectPath);
|
|
6
|
+
}
|
|
7
|
+
export function freshnessStatus(freshness) {
|
|
8
|
+
const status = freshness.reason === 'missing'
|
|
9
|
+
? 'missing'
|
|
10
|
+
: freshness.reason === 'corrupt'
|
|
11
|
+
? 'corrupt'
|
|
12
|
+
: freshness.stale
|
|
13
|
+
? 'stale'
|
|
14
|
+
: 'fresh';
|
|
15
|
+
return {
|
|
16
|
+
status,
|
|
17
|
+
graphPath: freshness.graphPath,
|
|
18
|
+
generatedAt: freshness.generatedAt,
|
|
19
|
+
changedSources: freshness.changedSources,
|
|
20
|
+
reason: freshness.reason,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
export function nextActionForFreshness(freshness, empty = false) {
|
|
24
|
+
if (freshness.status === 'missing' || freshness.status === 'corrupt') {
|
|
25
|
+
return {
|
|
26
|
+
type: 'rebuild_graph',
|
|
27
|
+
message: 'Rebuild the project graph before trusting structural memory results.',
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
if (freshness.status === 'stale' || freshness.status === 'partial') {
|
|
31
|
+
return {
|
|
32
|
+
type: 'run_validate',
|
|
33
|
+
message: 'Run validation or rebuild graph context before using this result as gate evidence.',
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
if (empty) {
|
|
37
|
+
return {
|
|
38
|
+
type: 'broaden_filters',
|
|
39
|
+
message: 'No structural memory nodes matched; broaden filters or inspect evidence coverage.',
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
return { type: 'none', message: 'Structural memory result is ready to use.' };
|
|
43
|
+
}
|
|
44
|
+
export function pagination(total, limit, offset) {
|
|
45
|
+
const hasMore = offset + limit < total;
|
|
46
|
+
return {
|
|
47
|
+
total,
|
|
48
|
+
limit,
|
|
49
|
+
offset,
|
|
50
|
+
hasMore,
|
|
51
|
+
has_more: hasMore,
|
|
52
|
+
truncated: hasMore,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
export function clampLimit(limit, policy) {
|
|
56
|
+
const defaultLimit = policy.structuralMemory?.response.defaultLimit ?? policy.query.maxNodes;
|
|
57
|
+
const maxLimit = policy.structuralMemory?.response.maxLimit ?? policy.query.maxNodes;
|
|
58
|
+
return Math.max(1, Math.min(limit ?? defaultLimit, maxLimit));
|
|
59
|
+
}
|
|
60
|
+
export function safeRelativeProjectPath(projectPath, candidate) {
|
|
61
|
+
if (candidate.trim().length === 0 || isAbsolute(candidate) || candidate.includes('\0')) {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
const normalized = normalize(candidate).replace(/\\/g, '/');
|
|
65
|
+
if (normalized === '..' || normalized.startsWith('../') || normalized.includes('/../')) {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
const root = resolve(projectPath);
|
|
69
|
+
const absolute = resolve(root, normalized);
|
|
70
|
+
const rel = relative(root, absolute).replace(/\\/g, '/');
|
|
71
|
+
if (rel === '..' || rel.startsWith('../') || isAbsolute(rel)) {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
return rel;
|
|
75
|
+
}
|
|
76
|
+
export async function loadFreshGraph(args) {
|
|
77
|
+
const rawFreshness = await getProjectGraphFreshness(args);
|
|
78
|
+
return {
|
|
79
|
+
graph: await readProjectGraph(args.projectId, args.policy),
|
|
80
|
+
freshness: freshnessStatus(rawFreshness),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
export function summarizeNodes(nodes) {
|
|
84
|
+
return nodes.reduce((acc, node) => {
|
|
85
|
+
return { ...acc, [node.type]: (acc[node.type] ?? 0) + 1 };
|
|
86
|
+
}, {});
|
|
87
|
+
}
|
|
88
|
+
export function relatedNodeIds(nodes, edges, input) {
|
|
89
|
+
const seeds = new Set();
|
|
90
|
+
for (const node of nodes) {
|
|
91
|
+
const specMatch = input.specId !== undefined &&
|
|
92
|
+
(node.id === `spec:${input.specId}` || node.metadata?.specId === input.specId);
|
|
93
|
+
const fileMatch = input.filePath !== undefined &&
|
|
94
|
+
(node.id === `file:${input.filePath}` || node.label === input.filePath);
|
|
95
|
+
const nodeMatch = input.nodeId !== undefined && node.id === input.nodeId;
|
|
96
|
+
if (specMatch || fileMatch || nodeMatch) {
|
|
97
|
+
seeds.add(node.id);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (seeds.size === 0 &&
|
|
101
|
+
input.specId === undefined &&
|
|
102
|
+
input.filePath === undefined &&
|
|
103
|
+
input.nodeId === undefined) {
|
|
104
|
+
for (const node of nodes) {
|
|
105
|
+
seeds.add(node.id);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
const related = new Set(seeds);
|
|
109
|
+
for (const edge of edges) {
|
|
110
|
+
if (seeds.has(edge.from)) {
|
|
111
|
+
related.add(edge.to);
|
|
112
|
+
}
|
|
113
|
+
if (seeds.has(edge.to)) {
|
|
114
|
+
related.add(edge.from);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return related;
|
|
118
|
+
}
|
|
119
|
+
export async function structuralPolicy() {
|
|
120
|
+
return loadProjectGraphPolicy();
|
|
121
|
+
}
|
|
122
|
+
//# sourceMappingURL=helpers.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export type * from './contracts.js';
|
|
2
|
+
export { queryStructuralMemory } from './query.js';
|
|
3
|
+
export { detectStructuralChangeImpact } from './change-impact.js';
|
|
4
|
+
export { getArchitectureSnapshot } from './architecture-snapshot.js';
|
|
5
|
+
export { getStructuralMemoryStatus } from './status.js';
|
|
6
|
+
export { createStructuralMemoryArtifactPolicy, isStructuralMemoryArtifactPathAllowed, } from './artifact-policy.js';
|
|
7
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { queryStructuralMemory } from './query.js';
|
|
2
|
+
export { detectStructuralChangeImpact } from './change-impact.js';
|
|
3
|
+
export { getArchitectureSnapshot } from './architecture-snapshot.js';
|
|
4
|
+
export { getStructuralMemoryStatus } from './status.js';
|
|
5
|
+
export { createStructuralMemoryArtifactPolicy, isStructuralMemoryArtifactPathAllowed, } from './artifact-policy.js';
|
|
6
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { clampLimit, loadFreshGraph, nextActionForFreshness, pagination, relatedNodeIds, resolveProjectId, safeRelativeProjectPath, structuralPolicy, summarizeNodes, } from './helpers.js';
|
|
2
|
+
export async function queryStructuralMemory(input) {
|
|
3
|
+
const policy = await structuralPolicy();
|
|
4
|
+
const projectId = resolveProjectId(input);
|
|
5
|
+
const filePath = input.filePath === undefined
|
|
6
|
+
? undefined
|
|
7
|
+
: safeRelativeProjectPath(input.projectPath, input.filePath);
|
|
8
|
+
const freshnessGraph = await loadFreshGraph({
|
|
9
|
+
projectId,
|
|
10
|
+
projectPath: input.projectPath,
|
|
11
|
+
policy,
|
|
12
|
+
});
|
|
13
|
+
const limit = clampLimit(input.limit, policy);
|
|
14
|
+
const offset = Math.max(0, input.offset ?? 0);
|
|
15
|
+
if (input.filePath !== undefined && filePath === null) {
|
|
16
|
+
const page = pagination(0, limit, offset);
|
|
17
|
+
return {
|
|
18
|
+
freshness: freshnessGraph.freshness,
|
|
19
|
+
pagination: page,
|
|
20
|
+
...page,
|
|
21
|
+
nextAction: {
|
|
22
|
+
type: 'inspect_evidence',
|
|
23
|
+
message: 'Rejecting unsafe filePath filter; pass a project-relative path inside the repo.',
|
|
24
|
+
},
|
|
25
|
+
nodes: [],
|
|
26
|
+
edges: [],
|
|
27
|
+
summary: {},
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
const graph = freshnessGraph.graph;
|
|
31
|
+
if (graph === null) {
|
|
32
|
+
const page = pagination(0, limit, offset);
|
|
33
|
+
return {
|
|
34
|
+
freshness: freshnessGraph.freshness,
|
|
35
|
+
pagination: page,
|
|
36
|
+
...page,
|
|
37
|
+
nextAction: nextActionForFreshness(freshnessGraph.freshness, true),
|
|
38
|
+
nodes: [],
|
|
39
|
+
edges: [],
|
|
40
|
+
summary: {},
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
const ids = relatedNodeIds(graph.nodes, graph.edges, {
|
|
44
|
+
specId: input.specId,
|
|
45
|
+
nodeId: input.nodeId,
|
|
46
|
+
filePath: filePath ?? undefined,
|
|
47
|
+
});
|
|
48
|
+
const typeFilter = new Set(input.nodeTypes ?? []);
|
|
49
|
+
const filteredNodes = graph.nodes.filter((node) => ids.has(node.id) && (typeFilter.size === 0 || typeFilter.has(node.type)));
|
|
50
|
+
const nodePage = filteredNodes.slice(offset, offset + limit);
|
|
51
|
+
const nodeIds = new Set(nodePage.map((node) => node.id));
|
|
52
|
+
const edges = graph.edges.filter((edge) => nodeIds.has(edge.from) && nodeIds.has(edge.to));
|
|
53
|
+
const page = pagination(filteredNodes.length, limit, offset);
|
|
54
|
+
return {
|
|
55
|
+
graphVersion: graph.graphVersion,
|
|
56
|
+
freshness: freshnessGraph.freshness,
|
|
57
|
+
pagination: page,
|
|
58
|
+
...page,
|
|
59
|
+
nextAction: nextActionForFreshness(freshnessGraph.freshness, filteredNodes.length === 0),
|
|
60
|
+
nodes: nodePage,
|
|
61
|
+
edges,
|
|
62
|
+
summary: summarizeNodes(nodePage),
|
|
63
|
+
sourceHash: graph.sourceHash,
|
|
64
|
+
sourceHashes: graph.sourceHashes,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=query.js.map
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { createStructuralMemoryArtifactPolicy, isStructuralMemoryArtifactPathAllowed, } from './artifact-policy.js';
|
|
2
|
+
import { loadFreshGraph, nextActionForFreshness, resolveProjectId, structuralPolicy, } from './helpers.js';
|
|
3
|
+
export async function getStructuralMemoryStatus(input) {
|
|
4
|
+
const policy = await structuralPolicy();
|
|
5
|
+
const projectId = resolveProjectId(input);
|
|
6
|
+
const { graph, freshness } = await loadFreshGraph({
|
|
7
|
+
projectId,
|
|
8
|
+
projectPath: input.projectPath,
|
|
9
|
+
policy,
|
|
10
|
+
});
|
|
11
|
+
const artifactPolicy = createStructuralMemoryArtifactPolicy(projectId, policy);
|
|
12
|
+
if (!isStructuralMemoryArtifactPathAllowed(artifactPolicy.graphPath)) {
|
|
13
|
+
throw new Error('Structural memory graph artifacts must remain outside planu/specs.');
|
|
14
|
+
}
|
|
15
|
+
return {
|
|
16
|
+
freshness,
|
|
17
|
+
nextAction: nextActionForFreshness(freshness, graph === null),
|
|
18
|
+
artifactPolicy,
|
|
19
|
+
graph: graph === null
|
|
20
|
+
? undefined
|
|
21
|
+
: {
|
|
22
|
+
graphVersion: graph.graphVersion,
|
|
23
|
+
generatedAt: graph.generatedAt,
|
|
24
|
+
sourceHash: graph.sourceHash,
|
|
25
|
+
counts: graph.metadata,
|
|
26
|
+
sourceHashCount: Object.keys(graph.sourceHashes).length,
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=status.js.map
|
|
@@ -22,6 +22,9 @@ export function withToolTracking(server) {
|
|
|
22
22
|
// Return a wrapper function that calls the original method and captures the result
|
|
23
23
|
return (...args) => {
|
|
24
24
|
const name = args[0];
|
|
25
|
+
if (registeredTools.has(name)) {
|
|
26
|
+
throw new Error(`Duplicate MCP tool registration: ${name}`);
|
|
27
|
+
}
|
|
25
28
|
const originalMethod = Reflect.get(target, prop, receiver);
|
|
26
29
|
const registered = originalMethod.apply(target, args);
|
|
27
30
|
registeredTools.set(name, registered);
|
package/dist/types/index.d.ts
CHANGED
|
@@ -5,6 +5,8 @@ export * from './common/index.js';
|
|
|
5
5
|
export * from './estimation.js';
|
|
6
6
|
export * from './spec/index.js';
|
|
7
7
|
export * from './project.js';
|
|
8
|
+
export * from './project-knowledge-graph.js';
|
|
9
|
+
export * from './structural-memory.js';
|
|
8
10
|
export * from './schema.js';
|
|
9
11
|
export * from './ui.js';
|
|
10
12
|
export * from './analysis.js';
|
package/dist/types/index.js
CHANGED
|
@@ -6,6 +6,8 @@ export * from './common/index.js';
|
|
|
6
6
|
export * from './estimation.js';
|
|
7
7
|
export * from './spec/index.js';
|
|
8
8
|
export * from './project.js';
|
|
9
|
+
export * from './project-knowledge-graph.js';
|
|
10
|
+
export * from './structural-memory.js';
|
|
9
11
|
export * from './schema.js';
|
|
10
12
|
export * from './ui.js';
|
|
11
13
|
export * from './analysis.js';
|
|
@@ -27,6 +27,16 @@ export interface ProjectGraphPolicy {
|
|
|
27
27
|
maxSnippetChars: number;
|
|
28
28
|
redactPatterns: string[];
|
|
29
29
|
};
|
|
30
|
+
structuralMemory?: {
|
|
31
|
+
schemaVersion: string;
|
|
32
|
+
nodeTypes: string[];
|
|
33
|
+
edgeTypes: string[];
|
|
34
|
+
response: {
|
|
35
|
+
defaultLimit: number;
|
|
36
|
+
maxLimit: number;
|
|
37
|
+
};
|
|
38
|
+
propertySchemas: Record<string, string[]>;
|
|
39
|
+
};
|
|
30
40
|
toolNamePatterns: string[];
|
|
31
41
|
}
|
|
32
42
|
export interface ProjectGraphEvidencePointer {
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import type { ProjectGraphEdge, ProjectGraphFreshness, ProjectGraphNode, ProjectKnowledgeGraph } from './project-knowledge-graph.js';
|
|
2
|
+
export type StructuralMemoryStatus = 'fresh' | 'stale' | 'missing' | 'corrupt' | 'partial';
|
|
3
|
+
export type StructuralMemoryNextActionType = 'none' | 'rebuild_graph' | 'broaden_filters' | 'inspect_evidence' | 'run_validate';
|
|
4
|
+
export interface StructuralMemoryNextAction {
|
|
5
|
+
type: StructuralMemoryNextActionType;
|
|
6
|
+
message: string;
|
|
7
|
+
}
|
|
8
|
+
export interface StructuralMemoryPagination {
|
|
9
|
+
total: number;
|
|
10
|
+
limit: number;
|
|
11
|
+
offset: number;
|
|
12
|
+
hasMore: boolean;
|
|
13
|
+
has_more: boolean;
|
|
14
|
+
truncated: boolean;
|
|
15
|
+
}
|
|
16
|
+
export interface StructuralMemoryFreshness {
|
|
17
|
+
status: StructuralMemoryStatus;
|
|
18
|
+
graphPath: string;
|
|
19
|
+
generatedAt?: string;
|
|
20
|
+
changedSources: string[];
|
|
21
|
+
reason: ProjectGraphFreshness['reason'] | 'partial';
|
|
22
|
+
}
|
|
23
|
+
export interface StructuralMemoryEnvelope {
|
|
24
|
+
graphVersion?: string;
|
|
25
|
+
freshness: StructuralMemoryFreshness;
|
|
26
|
+
pagination: StructuralMemoryPagination;
|
|
27
|
+
total: number;
|
|
28
|
+
limit: number;
|
|
29
|
+
offset: number;
|
|
30
|
+
hasMore: boolean;
|
|
31
|
+
has_more: boolean;
|
|
32
|
+
truncated: boolean;
|
|
33
|
+
nextAction: StructuralMemoryNextAction;
|
|
34
|
+
}
|
|
35
|
+
export interface StructuralMemoryQueryInput {
|
|
36
|
+
projectId?: string;
|
|
37
|
+
projectPath: string;
|
|
38
|
+
specId?: string;
|
|
39
|
+
filePath?: string;
|
|
40
|
+
nodeId?: string;
|
|
41
|
+
nodeTypes?: string[];
|
|
42
|
+
limit?: number;
|
|
43
|
+
offset?: number;
|
|
44
|
+
}
|
|
45
|
+
export interface StructuralMemoryQueryResult extends StructuralMemoryEnvelope {
|
|
46
|
+
nodes: ProjectGraphNode[];
|
|
47
|
+
edges: ProjectGraphEdge[];
|
|
48
|
+
summary: Record<string, number>;
|
|
49
|
+
sourceHash?: string;
|
|
50
|
+
sourceHashes?: Record<string, string>;
|
|
51
|
+
}
|
|
52
|
+
export type ArchitectureAspect = 'packages' | 'hotspots' | 'boundaries' | 'releaseRisks' | 'tests' | 'decisions' | 'all';
|
|
53
|
+
export interface ArchitectureSnapshotInput {
|
|
54
|
+
projectId?: string;
|
|
55
|
+
projectPath: string;
|
|
56
|
+
pathPrefix?: string;
|
|
57
|
+
aspects?: ArchitectureAspect[];
|
|
58
|
+
limit?: number;
|
|
59
|
+
}
|
|
60
|
+
export interface ArchitectureSnapshotResult extends StructuralMemoryEnvelope {
|
|
61
|
+
aspects: {
|
|
62
|
+
packages?: {
|
|
63
|
+
name: string;
|
|
64
|
+
fileCount: number;
|
|
65
|
+
}[];
|
|
66
|
+
hotspots?: {
|
|
67
|
+
id: string;
|
|
68
|
+
label: string;
|
|
69
|
+
type: string;
|
|
70
|
+
degree: number;
|
|
71
|
+
}[];
|
|
72
|
+
boundaries?: {
|
|
73
|
+
from: string;
|
|
74
|
+
to: string;
|
|
75
|
+
type: string;
|
|
76
|
+
}[];
|
|
77
|
+
releaseRisks?: {
|
|
78
|
+
id: string;
|
|
79
|
+
label: string;
|
|
80
|
+
type: string;
|
|
81
|
+
}[];
|
|
82
|
+
tests?: {
|
|
83
|
+
id: string;
|
|
84
|
+
label: string;
|
|
85
|
+
}[];
|
|
86
|
+
decisions?: {
|
|
87
|
+
id: string;
|
|
88
|
+
label: string;
|
|
89
|
+
}[];
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
export interface StructuralMemoryStatusInput {
|
|
93
|
+
projectId?: string;
|
|
94
|
+
projectPath: string;
|
|
95
|
+
}
|
|
96
|
+
export interface StructuralMemoryArtifactPolicy {
|
|
97
|
+
artifactRoot: string;
|
|
98
|
+
graphPath: string;
|
|
99
|
+
projectDataDirectory: string;
|
|
100
|
+
forbiddenPathFragment: 'planu/specs';
|
|
101
|
+
specDirectoryPolicy: 'spec.md-only';
|
|
102
|
+
evidenceAndGraphArtifactsRemainOutsideSpecs: true;
|
|
103
|
+
}
|
|
104
|
+
export interface StructuralMemoryStatusResult {
|
|
105
|
+
freshness: StructuralMemoryFreshness;
|
|
106
|
+
nextAction: StructuralMemoryNextAction;
|
|
107
|
+
artifactPolicy: StructuralMemoryArtifactPolicy;
|
|
108
|
+
graph?: Pick<ProjectKnowledgeGraph, 'graphVersion' | 'generatedAt' | 'sourceHash'> & {
|
|
109
|
+
counts: ProjectKnowledgeGraph['metadata'];
|
|
110
|
+
sourceHashCount: number;
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
export interface StructuralChangeImpactInput {
|
|
114
|
+
projectId?: string;
|
|
115
|
+
projectPath: string;
|
|
116
|
+
baseRef?: string;
|
|
117
|
+
changedFiles?: string[];
|
|
118
|
+
includeUntracked?: boolean;
|
|
119
|
+
limit?: number;
|
|
120
|
+
}
|
|
121
|
+
export interface StructuralChangedFile {
|
|
122
|
+
path: string;
|
|
123
|
+
status: 'added' | 'modified' | 'deleted' | 'renamed' | 'untracked' | 'unknown';
|
|
124
|
+
}
|
|
125
|
+
export interface StructuralChangeImpactResult extends StructuralMemoryEnvelope {
|
|
126
|
+
changedFiles: StructuralChangedFile[];
|
|
127
|
+
affected: {
|
|
128
|
+
specs: ProjectGraphNode[];
|
|
129
|
+
criteria: ProjectGraphNode[];
|
|
130
|
+
tests: ProjectGraphNode[];
|
|
131
|
+
decisions: ProjectGraphNode[];
|
|
132
|
+
evidence: ProjectGraphNode[];
|
|
133
|
+
risks: ProjectGraphNode[];
|
|
134
|
+
releaseRisk: ProjectGraphNode[];
|
|
135
|
+
};
|
|
136
|
+
affectedEdges: ProjectGraphEdge[];
|
|
137
|
+
}
|
|
138
|
+
//# sourceMappingURL=structural-memory.d.ts.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@planu/cli",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.9.0",
|
|
4
4
|
"description": "Planu — MCP Server for Spec Driven Development with native Rust acceleration for hot paths. Cross-platform (Linux/macOS/Windows, x64/arm64, glibc/musl).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -34,14 +34,14 @@
|
|
|
34
34
|
"packageName": "@planu/core"
|
|
35
35
|
},
|
|
36
36
|
"optionalDependencies": {
|
|
37
|
-
"@planu/core-darwin-arm64": "4.
|
|
38
|
-
"@planu/core-darwin-x64": "4.
|
|
39
|
-
"@planu/core-linux-arm64-gnu": "4.
|
|
40
|
-
"@planu/core-linux-arm64-musl": "4.
|
|
41
|
-
"@planu/core-linux-x64-gnu": "4.
|
|
42
|
-
"@planu/core-linux-x64-musl": "4.
|
|
43
|
-
"@planu/core-win32-arm64-msvc": "4.
|
|
44
|
-
"@planu/core-win32-x64-msvc": "4.
|
|
37
|
+
"@planu/core-darwin-arm64": "4.9.0",
|
|
38
|
+
"@planu/core-darwin-x64": "4.9.0",
|
|
39
|
+
"@planu/core-linux-arm64-gnu": "4.9.0",
|
|
40
|
+
"@planu/core-linux-arm64-musl": "4.9.0",
|
|
41
|
+
"@planu/core-linux-x64-gnu": "4.9.0",
|
|
42
|
+
"@planu/core-linux-x64-musl": "4.9.0",
|
|
43
|
+
"@planu/core-win32-arm64-msvc": "4.9.0",
|
|
44
|
+
"@planu/core-win32-x64-msvc": "4.9.0"
|
|
45
45
|
},
|
|
46
46
|
"engines": {
|
|
47
47
|
"node": ">=24.0.0"
|
|
@@ -129,7 +129,7 @@
|
|
|
129
129
|
],
|
|
130
130
|
"license": "SEE LICENSE IN LICENSE",
|
|
131
131
|
"dependencies": {
|
|
132
|
-
"@anthropic-ai/sdk": "^0.
|
|
132
|
+
"@anthropic-ai/sdk": "^0.107.0",
|
|
133
133
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
134
134
|
"glob": "^13.0.6",
|
|
135
135
|
"yaml": "^2.9.0",
|
|
@@ -168,8 +168,8 @@
|
|
|
168
168
|
}
|
|
169
169
|
},
|
|
170
170
|
"devDependencies": {
|
|
171
|
-
"@commitlint/cli": "^21.
|
|
172
|
-
"@commitlint/config-conventional": "^21.
|
|
171
|
+
"@commitlint/cli": "^21.2.0",
|
|
172
|
+
"@commitlint/config-conventional": "^21.2.0",
|
|
173
173
|
"@eslint/js": "^10.0.1",
|
|
174
174
|
"@napi-rs/cli": "^3.7.2",
|
|
175
175
|
"@secretlint/secretlint-rule-no-homedir": "^13.0.2",
|
|
@@ -182,30 +182,30 @@
|
|
|
182
182
|
"@semantic-release/release-notes-generator": "^14.1.1",
|
|
183
183
|
"@stryker-mutator/core": "^9.6.1",
|
|
184
184
|
"@stryker-mutator/vitest-runner": "^9.6.1",
|
|
185
|
-
"@supabase/supabase-js": "^2.
|
|
186
|
-
"@types/node": "^26.0.
|
|
185
|
+
"@supabase/supabase-js": "^2.110.0",
|
|
186
|
+
"@types/node": "^26.0.1",
|
|
187
187
|
"@vitejs/plugin-vue": "^6.0.7",
|
|
188
188
|
"@vitest/coverage-v8": "^4.1.9",
|
|
189
189
|
"@vue/test-utils": "^2.4.11",
|
|
190
|
-
"eslint": "^10.
|
|
190
|
+
"eslint": "^10.6.0",
|
|
191
191
|
"eslint-config-prettier": "^10.1.8",
|
|
192
192
|
"eslint-import-resolver-typescript": "^4.4.5",
|
|
193
193
|
"eslint-plugin-import": "^2.32.0",
|
|
194
194
|
"happy-dom": "^20.10.6",
|
|
195
195
|
"husky": "^9.1.7",
|
|
196
196
|
"javascript-obfuscator": "^5.4.3",
|
|
197
|
-
"knip": "^6.
|
|
197
|
+
"knip": "^6.23.0",
|
|
198
198
|
"lint-staged": "^17.0.8",
|
|
199
199
|
"madge": "^8.0.0",
|
|
200
|
-
"prettier": "^3.
|
|
200
|
+
"prettier": "^3.9.4",
|
|
201
201
|
"secretlint": "^13.0.2",
|
|
202
202
|
"semantic-release": "^25.0.5",
|
|
203
203
|
"tsc-alias": "^1.8.17",
|
|
204
204
|
"type-coverage": "^2.29.7",
|
|
205
205
|
"typescript": "^6.0.3",
|
|
206
|
-
"typescript-eslint": "^8.62.
|
|
207
|
-
"vite": "^8.1.
|
|
206
|
+
"typescript-eslint": "^8.62.1",
|
|
207
|
+
"vite": "^8.1.1",
|
|
208
208
|
"vitest": "^4.1.9",
|
|
209
|
-
"vue": "^3.5.
|
|
209
|
+
"vue": "^3.5.39"
|
|
210
210
|
}
|
|
211
211
|
}
|
package/planu-native.json
CHANGED
package/planu-plugin.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "dev.planu.cli",
|
|
3
3
|
"displayName": "Planu — Spec Driven Development",
|
|
4
4
|
"description": "Manage software specs, estimations, and autonomous SDD workflows. Language-agnostic MCP server for Claude Code.",
|
|
5
|
-
"version": "4.
|
|
5
|
+
"version": "4.9.0",
|
|
6
6
|
"icon": "assets/plugin/icon.svg",
|
|
7
7
|
"command": [
|
|
8
8
|
"npx",
|