@planu/cli 4.10.11 → 4.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/dist/config/project-knowledge-graph.json +42 -5
  3. package/dist/engine/core-bridge-project-graph.d.ts +13 -0
  4. package/dist/engine/core-bridge-project-graph.js +499 -0
  5. package/dist/engine/core-bridge.d.ts +7 -2
  6. package/dist/engine/core-bridge.js +55 -0
  7. package/dist/engine/frontmatter-parser.js +73 -23
  8. package/dist/engine/model-tier-resolver.d.ts +8 -7
  9. package/dist/engine/model-tier-resolver.js +70 -73
  10. package/dist/engine/next-spec-resolver/orchestration-planner.d.ts +5 -0
  11. package/dist/engine/next-spec-resolver/orchestration-planner.js +34 -5
  12. package/dist/engine/project-graph/builder.js +271 -36
  13. package/dist/engine/project-graph/cache.d.ts +22 -4
  14. package/dist/engine/project-graph/cache.js +412 -33
  15. package/dist/engine/project-graph/index.d.ts +1 -0
  16. package/dist/engine/project-graph/index.js +1 -0
  17. package/dist/engine/project-graph/native.d.ts +3 -0
  18. package/dist/engine/project-graph/native.js +36 -0
  19. package/dist/engine/project-graph/query.js +34 -2
  20. package/dist/engine/provider-adapters/adapters/claude.js +38 -14
  21. package/dist/engine/scan-project/index.js +88 -15
  22. package/dist/engine/spec-format/lean-spec-generator.d.ts +2 -2
  23. package/dist/engine/spec-format/lean-spec-generator.js +65 -50
  24. package/dist/engine/spec-format/metadata-value-policy.d.ts +161 -0
  25. package/dist/engine/spec-format/metadata-value-policy.js +87 -0
  26. package/dist/engine/spec-format/value-only-spec-serializer.d.ts +12 -0
  27. package/dist/engine/spec-format/value-only-spec-serializer.js +18 -0
  28. package/dist/engine/spec-generator/fallback-generator.js +4 -2
  29. package/dist/engine/spec-generator/opus-generator.js +5 -2
  30. package/dist/engine/spec-migrator/lean-migration.js +26 -13
  31. package/dist/storage/spec-store.js +6 -6
  32. package/dist/tools/create-spec.js +1027 -739
  33. package/dist/tools/render-spec-for-provider.js +4 -3
  34. package/dist/tools/reverse-engineer/handler.js +76 -43
  35. package/dist/tools/spec-split-handler.js +36 -75
  36. package/dist/types/conventions.d.ts +9 -0
  37. package/dist/types/core-bridge.d.ts +72 -0
  38. package/dist/types/next-spec.d.ts +2 -1
  39. package/dist/types/project-knowledge-graph.d.ts +58 -0
  40. package/dist/types/spec/core.d.ts +7 -4
  41. package/dist/types/spec-format.d.ts +2 -1
  42. package/dist/types/spec-generator.d.ts +8 -2
  43. package/package.json +11 -9
  44. package/planu-native.json +1 -1
  45. package/planu-plugin.json +1 -1
  46. package/dist/engine/spec-format/model-budget-deriver.d.ts +0 -5
  47. package/dist/engine/spec-format/model-budget-deriver.js +0 -7
@@ -1,15 +1,24 @@
1
- import { collectProjectGraphSources, diffSourceHashes, hashText, loadProjectGraphPolicy, projectGraphCachePath, projectGraphPath, readProjectGraph, readProjectGraphSourceHashes, writeProjectGraph, writeProjectGraphSourceHashes, } from './cache.js';
1
+ import { randomUUID } from 'node:crypto';
2
+ import { collectProjectGraphSources, diffSourceHashes, hashText, hydrateProjectGraphSource, loadProjectGraphPolicy, projectGraphCachePath, projectGraphExtractorVersion, projectGraphPath, readProjectGraphArtifact, readProjectGraphCache, withProjectGraphBuildLock, writeProjectGraphArtifacts, } from './cache.js';
2
3
  import { extractGitGraph, extractReleaseGraph } from './extractors/git-extractor.js';
3
4
  import { extractHandoffGraph } from './extractors/handoff-extractor.js';
4
5
  import { extractDecisionStoreGraph } from './extractors/decision-store-extractor.js';
5
6
  import { extractSpecGraph } from './extractors/spec-extractor.js';
6
7
  import { extractValidationGraph } from './extractors/validation-extractor.js';
8
+ import { createNativeProjectGraphRuntime } from './native.js';
9
+ const NATIVE_PROJECT_GRAPH_IMPLEMENTATION_VERSION = 'native-project-graph-v2';
7
10
  function uniqueNodes(nodes) {
8
11
  return [...new Map(nodes.map((node) => [node.id, node])).values()];
9
12
  }
10
13
  function uniqueEdges(edges) {
11
14
  return [...new Map(edges.map((edge) => [edge.id, edge])).values()];
12
15
  }
16
+ function sameSourceHashes(graphHashes, cacheHashes) {
17
+ const graphEntries = Object.entries(graphHashes);
18
+ const cacheEntries = Object.entries(cacheHashes);
19
+ return (graphEntries.length === cacheEntries.length &&
20
+ graphEntries.every(([sourceId, hash]) => cacheHashes[sourceId] === hash));
21
+ }
13
22
  function validateAgainstPolicy(result, policy) {
14
23
  const nodes = result.nodes.filter((node) => policy.nodeTypes.includes(node.type));
15
24
  const edges = result.edges.filter((edge) => policy.edgeTypes.includes(edge.type) &&
@@ -17,61 +26,271 @@ function validateAgainstPolicy(result, policy) {
17
26
  policy.classifications.includes(edge.classification));
18
27
  return { nodes, edges };
19
28
  }
20
- export async function buildProjectKnowledgeGraph(args) {
21
- const policy = args.policy ?? (await loadProjectGraphPolicy());
22
- const sources = await collectProjectGraphSources({
29
+ function codeNode(args) {
30
+ return {
31
+ id: args.id,
32
+ type: args.type,
33
+ label: args.label,
34
+ source: 'project-code-graph',
35
+ evidence: [{ path: args.path }],
36
+ metadata: args.metadata,
37
+ updatedAt: args.updatedAt,
38
+ };
39
+ }
40
+ function relationTargetNode(args) {
41
+ if (args.id.startsWith('module:')) {
42
+ const specifier = args.id.slice('module:'.length);
43
+ return codeNode({
44
+ id: args.id,
45
+ type: 'module',
46
+ label: specifier,
47
+ path: args.sourceFile,
48
+ updatedAt: args.updatedAt,
49
+ metadata: { specifier },
50
+ });
51
+ }
52
+ if (args.id.startsWith('scenario:')) {
53
+ const name = args.id.split('#').slice(1).join('#') || args.id;
54
+ return codeNode({
55
+ id: args.id,
56
+ type: 'scenario',
57
+ label: name,
58
+ path: args.sourceFile,
59
+ updatedAt: args.updatedAt,
60
+ metadata: { path: args.sourceFile, name },
61
+ });
62
+ }
63
+ const name = args.id.split('#').slice(1).join('#') || args.id;
64
+ const type = args.sourceFile.startsWith('src/tools/') && /^handle[A-Z]/.test(name)
65
+ ? 'tool_handler'
66
+ : 'symbol';
67
+ return codeNode({
68
+ id: args.id,
69
+ type,
70
+ label: name,
71
+ path: args.sourceFile,
72
+ updatedAt: args.updatedAt,
73
+ metadata: { path: args.sourceFile, name },
74
+ });
75
+ }
76
+ function extractNativeCodeFragment(args) {
77
+ const updatedAt = new Date().toISOString();
78
+ const relations = args.runtime.extractTypeScriptRelations(args.projectPath, [args.source.path], args.policy.limits.operational.maxFiles);
79
+ const fileType = args.source.path.includes('.test.') || args.source.path.endsWith('.test.ts') ? 'test' : 'file';
80
+ const nodes = [
81
+ codeNode({
82
+ id: `${fileType}:${args.source.path}`,
83
+ type: fileType,
84
+ label: args.source.path,
85
+ path: args.source.path,
86
+ updatedAt,
87
+ metadata: {
88
+ path: args.source.path,
89
+ hash: args.source.hash,
90
+ ...(args.source.size === undefined ? {} : { size: args.source.size }),
91
+ native: args.runtime.nativeActive,
92
+ },
93
+ }),
94
+ ];
95
+ nodes.push(...relations.flatMap((relation) => [
96
+ relationTargetNode({ id: relation.to, sourceFile: relation.sourceFile, updatedAt }),
97
+ relation.from.startsWith('test:')
98
+ ? codeNode({
99
+ id: relation.from,
100
+ type: 'test',
101
+ label: relation.sourceFile,
102
+ path: relation.sourceFile,
103
+ updatedAt,
104
+ metadata: { path: relation.sourceFile },
105
+ })
106
+ : codeNode({
107
+ id: relation.from,
108
+ type: 'file',
109
+ label: relation.sourceFile,
110
+ path: relation.sourceFile,
111
+ updatedAt,
112
+ metadata: { path: relation.sourceFile },
113
+ }),
114
+ ]));
115
+ const edges = relations.map((relation) => ({
116
+ id: `code:${relation.sourceFile}:${relation.relation}:${relation.from}->${relation.to}`,
117
+ from: relation.from,
118
+ to: relation.to,
119
+ type: relation.relation,
120
+ source: 'project-code-graph',
121
+ confidence: relation.confidence === 'low' || relation.confidence === 'medium'
122
+ ? relation.confidence
123
+ : 'high',
124
+ evidence: { path: relation.sourceFile },
125
+ classification: relation.classification === 'ambiguous' || relation.classification === 'inferred'
126
+ ? relation.classification
127
+ : 'extracted',
128
+ updatedAt,
129
+ }));
130
+ return validateAgainstPolicy({ nodes, edges }, args.policy);
131
+ }
132
+ function nativeCoreCacheVersion(runtime) {
133
+ const mode = runtime.nativeActive
134
+ ? `native:${process.platform}-${process.arch}`
135
+ : 'typescript-fallback';
136
+ return `${NATIVE_PROJECT_GRAPH_IMPLEMENTATION_VERSION}:${mode}`;
137
+ }
138
+ async function collectGitSource(projectPath) {
139
+ try {
140
+ const { execFile } = await import('node:child_process');
141
+ const stdout = await new Promise((resolveOutput, reject) => {
142
+ execFile('git', ['rev-parse', 'HEAD'], { cwd: projectPath }, (error, output) => {
143
+ if (error) {
144
+ reject(new Error('git rev-parse failed', { cause: error }));
145
+ return;
146
+ }
147
+ resolveOutput(output);
148
+ });
149
+ });
150
+ const head = stdout.trim();
151
+ if (head.length === 0) {
152
+ return null;
153
+ }
154
+ return {
155
+ id: 'project-git',
156
+ kind: 'project-git',
157
+ path: '.git/log',
158
+ content: head,
159
+ hash: hashText(head),
160
+ size: Buffer.byteLength(head),
161
+ mtimeMs: 0,
162
+ contentLoaded: true,
163
+ };
164
+ }
165
+ catch {
166
+ return null;
167
+ }
168
+ }
169
+ async function extractSourceFragment(args) {
170
+ if (args.source.kind === 'spec') {
171
+ return extractSpecGraph(args.source, args.policy);
172
+ }
173
+ if (args.source.kind === 'validation') {
174
+ return extractValidationGraph(args.source, args.policy);
175
+ }
176
+ if (args.source.kind === 'handoff') {
177
+ return extractHandoffGraph(args.source, args.policy);
178
+ }
179
+ if (args.source.kind === 'decisions') {
180
+ return extractDecisionStoreGraph(args.source, args.policy);
181
+ }
182
+ if (args.source.kind === 'release-events') {
183
+ return extractReleaseGraph(args.source, args.policy);
184
+ }
185
+ if (args.source.kind === 'project-code') {
186
+ return extractNativeCodeFragment(args);
187
+ }
188
+ if (args.source.kind === 'project-git') {
189
+ return extractGitGraph({ projectPath: args.projectPath, policy: args.policy });
190
+ }
191
+ return { nodes: [], edges: [] };
192
+ }
193
+ function createFragment(args) {
194
+ return {
195
+ version: 1,
196
+ sourceId: args.source.id,
197
+ kind: args.source.kind,
198
+ contentHash: args.source.hash,
199
+ extractorVersion: projectGraphExtractorVersion(args.source.kind),
200
+ nativeCoreVersion: args.source.kind === 'project-code' ? args.nativeCoreVersion : 'not-applicable',
201
+ nodes: args.result.nodes,
202
+ edges: args.result.edges,
203
+ };
204
+ }
205
+ // eslint-disable-next-line max-lines-per-function -- the lock owner keeps one coherent snapshot transaction.
206
+ async function buildProjectKnowledgeGraphUnlocked(args) {
207
+ const runtime = createNativeProjectGraphRuntime(args.policy.limits);
208
+ const nativeCoreVersion = nativeCoreCacheVersion(runtime);
209
+ const cacheResult = await readProjectGraphCache(args.projectId, args.policy);
210
+ const previousCache = cacheResult.cache;
211
+ const previousRecords = previousCache?.records ?? {};
212
+ const collected = await collectProjectGraphSources({
23
213
  projectId: args.projectId,
24
214
  projectPath: args.projectPath,
25
- policy,
215
+ policy: args.policy,
216
+ previous: previousRecords,
217
+ nativeCoreVersion,
218
+ });
219
+ const gitSource = await collectGitSource(args.projectPath);
220
+ let sources = gitSource === null ? collected : [...collected, gitSource];
221
+ sources.sort((a, b) => a.id.localeCompare(b.id));
222
+ let diff = diffSourceHashes(sources, previousRecords, nativeCoreVersion, {
223
+ fragments: previousCache?.fragments,
224
+ knownSourceIds: cacheResult.knownSourceIds,
26
225
  });
27
- const previousHashes = await readProjectGraphSourceHashes(args.projectId, policy);
28
- const diff = diffSourceHashes(sources, previousHashes);
29
- const existingGraph = await readProjectGraph(args.projectId, policy);
30
- if (existingGraph !== null && diff.changed.length === 0) {
226
+ const changed = new Set(diff.changed);
227
+ const hydrated = await Promise.all(sources.map(async (source) => changed.has(source.id) && source.contentLoaded === false
228
+ ? await hydrateProjectGraphSource(source, args.policy.limits.operational.maxFileBytes)
229
+ : source));
230
+ sources = hydrated.filter((source) => source !== null);
231
+ diff = diffSourceHashes(sources, previousRecords, nativeCoreVersion, {
232
+ fragments: previousCache?.fragments,
233
+ knownSourceIds: cacheResult.knownSourceIds,
234
+ });
235
+ const existingGraph = await readProjectGraphArtifact(args.projectId, args.policy);
236
+ const hasConsistentExistingGraph = existingGraph !== null &&
237
+ typeof existingGraph.generation === 'string' &&
238
+ existingGraph.generation.length > 0 &&
239
+ existingGraph.generation === previousCache?.generation &&
240
+ sameSourceHashes(existingGraph.sourceHashes, diff.next) &&
241
+ Array.isArray(existingGraph.nodes) &&
242
+ Array.isArray(existingGraph.edges);
243
+ if (hasConsistentExistingGraph && diff.changed.length === 0 && diff.removed.length === 0) {
31
244
  return {
32
245
  graph: existingGraph,
33
- graphPath: projectGraphPath(args.projectId, policy),
34
- cachePath: projectGraphCachePath(args.projectId, policy),
246
+ generation: existingGraph.generation,
247
+ graphPath: projectGraphPath(args.projectId, args.policy),
248
+ cachePath: projectGraphCachePath(args.projectId, args.policy),
35
249
  reprocessedSources: [],
250
+ removedSources: [],
36
251
  skippedSources: diff.skipped,
37
252
  usedCache: true,
38
253
  };
39
254
  }
40
- const extracted = [];
255
+ const changedIds = new Set(diff.changed);
256
+ const fragments = {};
41
257
  for (const source of sources) {
42
- if (source.kind === 'spec') {
43
- extracted.push(extractSpecGraph(source, policy));
44
- }
45
- else if (source.kind === 'validation') {
46
- extracted.push(extractValidationGraph(source, policy));
47
- }
48
- else if (source.kind === 'handoff') {
49
- extracted.push(extractHandoffGraph(source, policy));
50
- }
51
- else if (source.kind === 'decisions') {
52
- extracted.push(extractDecisionStoreGraph(source, policy));
53
- }
54
- else if (source.kind === 'release-events') {
55
- extracted.push(extractReleaseGraph(source, policy));
258
+ if (!changedIds.has(source.id)) {
259
+ const cached = previousCache?.fragments[source.id];
260
+ if (cached !== undefined) {
261
+ fragments[source.id] = cached;
262
+ continue;
263
+ }
56
264
  }
265
+ const extracted = await extractSourceFragment({
266
+ source,
267
+ projectPath: args.projectPath,
268
+ policy: args.policy,
269
+ runtime,
270
+ });
271
+ fragments[source.id] = createFragment({
272
+ source,
273
+ result: validateAgainstPolicy(extracted, args.policy),
274
+ nativeCoreVersion,
275
+ });
57
276
  }
58
- extracted.push(await extractGitGraph({ projectPath: args.projectPath, policy }));
59
- const validated = extracted.map((result) => validateAgainstPolicy(result, policy));
277
+ const validated = Object.values(fragments).map((fragment) => validateAgainstPolicy(fragment, args.policy));
60
278
  const nodes = uniqueNodes(validated.flatMap((result) => result.nodes));
61
279
  const nodeIds = new Set(nodes.map((node) => node.id));
62
280
  const edges = uniqueEdges(validated
63
281
  .flatMap((result) => result.edges)
64
282
  .filter((edge) => nodeIds.has(edge.from) && nodeIds.has(edge.to)));
65
- const sourceHashes = diff.next;
66
- const sourceHash = hashText(JSON.stringify(sourceHashes));
283
+ const generation = randomUUID();
284
+ const sourceHash = hashText(JSON.stringify(diff.next));
67
285
  const graph = {
68
286
  version: 1,
69
- graphVersion: policy.graphVersion,
287
+ graphVersion: args.policy.graphVersion,
288
+ generation,
70
289
  projectId: args.projectId,
71
290
  projectPath: args.projectPath,
72
291
  generatedAt: new Date().toISOString(),
73
292
  sourceHash,
74
- sourceHashes,
293
+ sourceHashes: diff.next,
75
294
  metadata: {
76
295
  nodeCount: nodes.length,
77
296
  edgeCount: edges.length,
@@ -82,15 +301,31 @@ export async function buildProjectKnowledgeGraph(args) {
82
301
  nodes,
83
302
  edges,
84
303
  };
85
- await writeProjectGraph(args.projectId, policy, graph);
86
- await writeProjectGraphSourceHashes(args.projectId, policy, sourceHashes);
304
+ const cache = {
305
+ version: 2,
306
+ generation,
307
+ records: diff.records,
308
+ fragments,
309
+ };
310
+ await writeProjectGraphArtifacts({
311
+ projectId: args.projectId,
312
+ policy: args.policy,
313
+ graph,
314
+ cache,
315
+ });
87
316
  return {
88
317
  graph,
89
- graphPath: projectGraphPath(args.projectId, policy),
90
- cachePath: projectGraphCachePath(args.projectId, policy),
318
+ generation,
319
+ graphPath: projectGraphPath(args.projectId, args.policy),
320
+ cachePath: projectGraphCachePath(args.projectId, args.policy),
91
321
  reprocessedSources: diff.changed,
322
+ removedSources: diff.removed,
92
323
  skippedSources: diff.skipped,
93
324
  usedCache: false,
94
325
  };
95
326
  }
327
+ export async function buildProjectKnowledgeGraph(args) {
328
+ const policy = args.policy ?? (await loadProjectGraphPolicy());
329
+ return withProjectGraphBuildLock(args.projectId, policy, () => buildProjectKnowledgeGraphUnlocked({ ...args, policy }));
330
+ }
96
331
  //# sourceMappingURL=builder.js.map
@@ -1,22 +1,40 @@
1
- import type { ProjectGraphFreshness, ProjectGraphPolicy, ProjectGraphSource, ProjectKnowledgeGraph } from '../../types/project-knowledge-graph.js';
1
+ import type { ProjectGraphCacheEnvelope, ProjectGraphCacheReadResult, ProjectGraphFreshness, ProjectGraphPolicy, ProjectGraphSource, ProjectGraphSourceCacheRecord, ProjectGraphSourceFragment, ProjectKnowledgeGraph } from '../../types/project-knowledge-graph.js';
2
2
  export declare function loadProjectGraphPolicy(): Promise<ProjectGraphPolicy>;
3
3
  export declare function hashText(text: string): string;
4
+ export declare function projectGraphExtractorVersion(kind: string): string;
4
5
  export declare function projectGraphDir(projectId: string, policy: ProjectGraphPolicy): string;
5
6
  export declare function projectGraphPath(projectId: string, policy: ProjectGraphPolicy): string;
6
7
  export declare function projectGraphCachePath(projectId: string, policy: ProjectGraphPolicy): string;
8
+ export declare function readProjectGraphCache(projectId: string, policy: ProjectGraphPolicy): Promise<ProjectGraphCacheReadResult>;
9
+ export declare function readProjectGraphArtifact(projectId: string, policy: ProjectGraphPolicy): Promise<ProjectKnowledgeGraph | null>;
7
10
  export declare function readProjectGraph(projectId: string, policy: ProjectGraphPolicy): Promise<ProjectKnowledgeGraph | null>;
8
11
  export declare function writeProjectGraph(projectId: string, policy: ProjectGraphPolicy, graph: ProjectKnowledgeGraph): Promise<void>;
9
- export declare function readProjectGraphSourceHashes(projectId: string, policy: ProjectGraphPolicy): Promise<Record<string, string>>;
10
- export declare function writeProjectGraphSourceHashes(projectId: string, policy: ProjectGraphPolicy, hashes: Record<string, string>): Promise<void>;
11
- export declare function diffSourceHashes(sources: ProjectGraphSource[], previous: Record<string, string>): {
12
+ export declare function readProjectGraphSourceHashes(projectId: string, policy: ProjectGraphPolicy): Promise<Record<string, ProjectGraphSourceCacheRecord>>;
13
+ export declare function writeProjectGraphSourceHashes(projectId: string, policy: ProjectGraphPolicy, hashes: Record<string, ProjectGraphSourceCacheRecord>): Promise<void>;
14
+ export declare function writeProjectGraphArtifacts(args: {
15
+ projectId: string;
16
+ policy: ProjectGraphPolicy;
17
+ graph: ProjectKnowledgeGraph;
18
+ cache: ProjectGraphCacheEnvelope;
19
+ }): Promise<void>;
20
+ export declare function withProjectGraphBuildLock<T>(projectId: string, policy: ProjectGraphPolicy, task: () => Promise<T>): Promise<T>;
21
+ export declare function diffSourceHashes(sources: ProjectGraphSource[], previous: Record<string, ProjectGraphSourceCacheRecord>, nativeCoreVersion?: string, options?: {
22
+ fragments?: Record<string, ProjectGraphSourceFragment>;
23
+ knownSourceIds?: string[];
24
+ }): {
12
25
  changed: string[];
26
+ removed: string[];
13
27
  skipped: string[];
14
28
  next: Record<string, string>;
29
+ records: Record<string, ProjectGraphSourceCacheRecord>;
15
30
  };
31
+ export declare function hydrateProjectGraphSource(source: ProjectGraphSource, maxFileBytes: number): Promise<ProjectGraphSource | null>;
16
32
  export declare function collectProjectGraphSources(args: {
17
33
  projectId: string;
18
34
  projectPath: string;
19
35
  policy: ProjectGraphPolicy;
36
+ previous?: Record<string, ProjectGraphSourceCacheRecord>;
37
+ nativeCoreVersion?: string;
20
38
  }): Promise<ProjectGraphSource[]>;
21
39
  export declare function getProjectGraphFreshness(args: {
22
40
  projectId: string;