@planu/cli 4.6.0 → 4.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/config/project-knowledge-graph.json +65 -0
  3. package/dist/engine/project-graph/builder.d.ts +7 -0
  4. package/dist/engine/project-graph/builder.js +92 -0
  5. package/dist/engine/project-graph/cache.d.ts +26 -0
  6. package/dist/engine/project-graph/cache.js +160 -0
  7. package/dist/engine/project-graph/extractors/git-extractor.d.ts +7 -0
  8. package/dist/engine/project-graph/extractors/git-extractor.js +89 -0
  9. package/dist/engine/project-graph/extractors/handoff-extractor.d.ts +3 -0
  10. package/dist/engine/project-graph/extractors/handoff-extractor.js +55 -0
  11. package/dist/engine/project-graph/extractors/spec-extractor.d.ts +3 -0
  12. package/dist/engine/project-graph/extractors/spec-extractor.js +189 -0
  13. package/dist/engine/project-graph/extractors/validation-extractor.d.ts +3 -0
  14. package/dist/engine/project-graph/extractors/validation-extractor.js +36 -0
  15. package/dist/engine/project-graph/index.d.ts +4 -0
  16. package/dist/engine/project-graph/index.js +4 -0
  17. package/dist/engine/project-graph/query.d.ts +9 -0
  18. package/dist/engine/project-graph/query.js +161 -0
  19. package/dist/engine/validator/spec-compliance-runner.js +39 -9
  20. package/dist/tools/create-spec/post-creation.js +13 -1
  21. package/dist/tools/package-handoff.js +31 -2
  22. package/dist/tools/schemas/index.d.ts +1 -0
  23. package/dist/tools/schemas/index.js +1 -0
  24. package/dist/tools/schemas/project-graph.d.ts +18 -0
  25. package/dist/tools/schemas/project-graph.js +8 -0
  26. package/dist/tools/schemas/token-intelligence.d.ts +1 -0
  27. package/dist/tools/schemas/token-intelligence.js +3 -2
  28. package/dist/tools/status-handler.js +9 -2
  29. package/dist/tools/token-intelligence-handler.js +28 -1
  30. package/dist/tools/validate.js +75 -30
  31. package/dist/types/index.d.ts +1 -0
  32. package/dist/types/index.js +1 -0
  33. package/dist/types/project-knowledge-graph.d.ts +139 -0
  34. package/dist/types/project-knowledge-graph.js +2 -0
  35. package/package.json +12 -11
  36. package/planu-native.json +1 -1
  37. package/planu-plugin.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,3 +1,21 @@
1
+ ## [4.7.0] - 2026-06-12
2
+
3
+ ### Features
4
+ - feat(SPEC-1085): add project knowledge graph
5
+
6
+ ### Bug Fixes
7
+ - fix(security): override esbuild patched release
8
+
9
+ ### Chores
10
+ - chore(deps): refresh push-gate dependencies
11
+
12
+
13
+ ## [4.6.1] - 2026-06-12
14
+
15
+ ### Bug Fixes
16
+ - fix(SPEC-1086): handle Vitest 4 JSON paths in validate
17
+
18
+
1
19
  ## [4.6.0] - 2026-06-11
2
20
 
3
21
  ### Features
@@ -0,0 +1,65 @@
1
+ {
2
+ "version": 1,
3
+ "graphVersion": "1.0.0",
4
+ "enabled": true,
5
+ "artifactPaths": {
6
+ "directory": "project-graph",
7
+ "graph": "project-knowledge-graph.json",
8
+ "sourceHashes": "source-hashes.json"
9
+ },
10
+ "nodeTypes": [
11
+ "spec",
12
+ "criterion",
13
+ "file",
14
+ "test",
15
+ "release",
16
+ "decision",
17
+ "risk",
18
+ "tool",
19
+ "handoff",
20
+ "validation",
21
+ "commit"
22
+ ],
23
+ "edgeTypes": [
24
+ "contains",
25
+ "references",
26
+ "implements",
27
+ "tests",
28
+ "validated_by",
29
+ "released_in",
30
+ "decides",
31
+ "has_risk",
32
+ "uses_tool",
33
+ "mentions",
34
+ "neighbors"
35
+ ],
36
+ "classifications": ["extracted", "inferred", "ambiguous"],
37
+ "confidenceLabels": ["high", "medium", "low"],
38
+ "freshness": {
39
+ "staleAfterMinutes": 1440
40
+ },
41
+ "query": {
42
+ "maxNodes": 60,
43
+ "maxEdges": 120,
44
+ "maxListItems": 12
45
+ },
46
+ "redaction": {
47
+ "maxSnippetChars": 240,
48
+ "redactPatterns": [
49
+ "sk-[A-Za-z0-9_-]+",
50
+ "ghp_[A-Za-z0-9_]+",
51
+ "xox[baprs]-[A-Za-z0-9-]+",
52
+ "(api[_-]?key|token|secret|password)\\s*[:=]\\s*[^\\s,;]+"
53
+ ]
54
+ },
55
+ "toolNamePatterns": [
56
+ "package_handoff",
57
+ "validate",
58
+ "create_spec",
59
+ "planu_status",
60
+ "token_intelligence",
61
+ "check_readiness",
62
+ "update_status",
63
+ "session_checkpoint"
64
+ ]
65
+ }
@@ -0,0 +1,7 @@
1
+ import type { ProjectGraphBuildResult, ProjectGraphPolicy } from '../../types/project-knowledge-graph.js';
2
+ export declare function buildProjectKnowledgeGraph(args: {
3
+ projectId: string;
4
+ projectPath: string;
5
+ policy?: ProjectGraphPolicy;
6
+ }): Promise<ProjectGraphBuildResult>;
7
+ //# sourceMappingURL=builder.d.ts.map
@@ -0,0 +1,92 @@
1
+ import { collectProjectGraphSources, diffSourceHashes, hashText, loadProjectGraphPolicy, projectGraphCachePath, projectGraphPath, readProjectGraph, readProjectGraphSourceHashes, writeProjectGraph, writeProjectGraphSourceHashes, } from './cache.js';
2
+ import { extractGitGraph, extractReleaseGraph } from './extractors/git-extractor.js';
3
+ import { extractHandoffGraph } from './extractors/handoff-extractor.js';
4
+ import { extractSpecGraph } from './extractors/spec-extractor.js';
5
+ import { extractValidationGraph } from './extractors/validation-extractor.js';
6
+ function uniqueNodes(nodes) {
7
+ return [...new Map(nodes.map((node) => [node.id, node])).values()];
8
+ }
9
+ function uniqueEdges(edges) {
10
+ return [...new Map(edges.map((edge) => [edge.id, edge])).values()];
11
+ }
12
+ function validateAgainstPolicy(result, policy) {
13
+ const nodes = result.nodes.filter((node) => policy.nodeTypes.includes(node.type));
14
+ const nodeIds = new Set(nodes.map((node) => node.id));
15
+ const edges = result.edges.filter((edge) => nodeIds.has(edge.from) &&
16
+ nodeIds.has(edge.to) &&
17
+ policy.edgeTypes.includes(edge.type) &&
18
+ policy.confidenceLabels.includes(edge.confidence) &&
19
+ policy.classifications.includes(edge.classification));
20
+ return { nodes, edges };
21
+ }
22
+ export async function buildProjectKnowledgeGraph(args) {
23
+ const policy = args.policy ?? (await loadProjectGraphPolicy());
24
+ const sources = await collectProjectGraphSources({
25
+ projectId: args.projectId,
26
+ projectPath: args.projectPath,
27
+ policy,
28
+ });
29
+ const previousHashes = await readProjectGraphSourceHashes(args.projectId, policy);
30
+ const diff = diffSourceHashes(sources, previousHashes);
31
+ const existingGraph = await readProjectGraph(args.projectId, policy);
32
+ if (existingGraph !== null && diff.changed.length === 0) {
33
+ return {
34
+ graph: existingGraph,
35
+ graphPath: projectGraphPath(args.projectId, policy),
36
+ cachePath: projectGraphCachePath(args.projectId, policy),
37
+ reprocessedSources: [],
38
+ skippedSources: diff.skipped,
39
+ usedCache: true,
40
+ };
41
+ }
42
+ const extracted = [];
43
+ for (const source of sources) {
44
+ if (source.kind === 'spec') {
45
+ extracted.push(extractSpecGraph(source, policy));
46
+ }
47
+ else if (source.kind === 'validation') {
48
+ extracted.push(extractValidationGraph(source, policy));
49
+ }
50
+ else if (source.kind === 'handoff') {
51
+ extracted.push(extractHandoffGraph(source, policy));
52
+ }
53
+ else if (source.kind === 'release-events') {
54
+ extracted.push(extractReleaseGraph(source, policy));
55
+ }
56
+ }
57
+ extracted.push(await extractGitGraph({ projectPath: args.projectPath, policy }));
58
+ const validated = extracted.map((result) => validateAgainstPolicy(result, policy));
59
+ const nodes = uniqueNodes(validated.flatMap((result) => result.nodes));
60
+ const edges = uniqueEdges(validated.flatMap((result) => result.edges));
61
+ const sourceHashes = diff.next;
62
+ const sourceHash = hashText(JSON.stringify(sourceHashes));
63
+ const graph = {
64
+ version: 1,
65
+ graphVersion: policy.graphVersion,
66
+ projectId: args.projectId,
67
+ projectPath: args.projectPath,
68
+ generatedAt: new Date().toISOString(),
69
+ sourceHash,
70
+ sourceHashes,
71
+ metadata: {
72
+ nodeCount: nodes.length,
73
+ edgeCount: edges.length,
74
+ sourceCount: sources.length,
75
+ reprocessedSourceCount: diff.changed.length,
76
+ skippedSourceCount: diff.skipped.length,
77
+ },
78
+ nodes,
79
+ edges,
80
+ };
81
+ await writeProjectGraph(args.projectId, policy, graph);
82
+ await writeProjectGraphSourceHashes(args.projectId, policy, sourceHashes);
83
+ return {
84
+ graph,
85
+ graphPath: projectGraphPath(args.projectId, policy),
86
+ cachePath: projectGraphCachePath(args.projectId, policy),
87
+ reprocessedSources: diff.changed,
88
+ skippedSources: diff.skipped,
89
+ usedCache: false,
90
+ };
91
+ }
92
+ //# sourceMappingURL=builder.js.map
@@ -0,0 +1,26 @@
1
+ import type { ProjectGraphFreshness, ProjectGraphPolicy, ProjectGraphSource, ProjectKnowledgeGraph } from '../../types/project-knowledge-graph.js';
2
+ export declare function loadProjectGraphPolicy(): Promise<ProjectGraphPolicy>;
3
+ export declare function hashText(text: string): string;
4
+ export declare function projectGraphDir(projectId: string, policy: ProjectGraphPolicy): string;
5
+ export declare function projectGraphPath(projectId: string, policy: ProjectGraphPolicy): string;
6
+ export declare function projectGraphCachePath(projectId: string, policy: ProjectGraphPolicy): string;
7
+ export declare function readProjectGraph(projectId: string, policy: ProjectGraphPolicy): Promise<ProjectKnowledgeGraph | null>;
8
+ 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
+ changed: string[];
13
+ skipped: string[];
14
+ next: Record<string, string>;
15
+ };
16
+ export declare function collectProjectGraphSources(args: {
17
+ projectId: string;
18
+ projectPath: string;
19
+ policy: ProjectGraphPolicy;
20
+ }): Promise<ProjectGraphSource[]>;
21
+ export declare function getProjectGraphFreshness(args: {
22
+ projectId: string;
23
+ projectPath: string;
24
+ policy?: ProjectGraphPolicy;
25
+ }): Promise<ProjectGraphFreshness>;
26
+ //# sourceMappingURL=cache.d.ts.map
@@ -0,0 +1,160 @@
1
+ // @crash-shield-ignore-file — graph artifacts are Planu-owned JSON written by this subsystem.
2
+ import { createHash } from 'node:crypto';
3
+ import { readdir, readFile, stat } from 'node:fs/promises';
4
+ import { dirname, join, relative, resolve } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { projectDataDir, readJson, writeJson } from '../../storage/base-store.js';
7
+ const POLICY_PATH = join(dirname(fileURLToPath(import.meta.url)), '../../config/project-knowledge-graph.json');
8
+ export async function loadProjectGraphPolicy() {
9
+ const raw = await readFile(POLICY_PATH, 'utf-8');
10
+ return JSON.parse(raw);
11
+ }
12
+ export function hashText(text) {
13
+ return createHash('sha256').update(text, 'utf8').digest('hex');
14
+ }
15
+ export function projectGraphDir(projectId, policy) {
16
+ return join(projectDataDir(projectId), policy.artifactPaths.directory);
17
+ }
18
+ export function projectGraphPath(projectId, policy) {
19
+ return join(projectGraphDir(projectId, policy), policy.artifactPaths.graph);
20
+ }
21
+ export function projectGraphCachePath(projectId, policy) {
22
+ return join(projectGraphDir(projectId, policy), policy.artifactPaths.sourceHashes);
23
+ }
24
+ export async function readProjectGraph(projectId, policy) {
25
+ return readJson(projectGraphPath(projectId, policy), null);
26
+ }
27
+ export async function writeProjectGraph(projectId, policy, graph) {
28
+ await writeJson(projectGraphPath(projectId, policy), graph);
29
+ }
30
+ export async function readProjectGraphSourceHashes(projectId, policy) {
31
+ return readJson(projectGraphCachePath(projectId, policy), {});
32
+ }
33
+ export async function writeProjectGraphSourceHashes(projectId, policy, hashes) {
34
+ await writeJson(projectGraphCachePath(projectId, policy), hashes);
35
+ }
36
+ export function diffSourceHashes(sources, previous) {
37
+ const next = Object.fromEntries(sources.map((source) => [source.id, source.hash]));
38
+ const changed = sources
39
+ .filter((source) => previous[source.id] !== source.hash)
40
+ .map((source) => source.id);
41
+ const skipped = sources
42
+ .filter((source) => previous[source.id] === source.hash)
43
+ .map((source) => source.id);
44
+ return { changed, skipped, next };
45
+ }
46
+ async function fileExists(path) {
47
+ try {
48
+ const info = await stat(path);
49
+ return info.isFile();
50
+ }
51
+ catch {
52
+ return false;
53
+ }
54
+ }
55
+ async function collectFiles(dir, predicate) {
56
+ try {
57
+ const entries = await readdir(dir, { withFileTypes: true });
58
+ const nested = await Promise.all(entries.map(async (entry) => {
59
+ const path = join(dir, entry.name);
60
+ if (entry.isDirectory()) {
61
+ return collectFiles(path, predicate);
62
+ }
63
+ return entry.isFile() && predicate(path) ? [path] : [];
64
+ }));
65
+ return nested.flat();
66
+ }
67
+ catch {
68
+ return [];
69
+ }
70
+ }
71
+ async function sourceFromFile(args) {
72
+ try {
73
+ const content = await readFile(args.path, 'utf-8');
74
+ return { ...args, content, hash: hashText(content) };
75
+ }
76
+ catch {
77
+ return null;
78
+ }
79
+ }
80
+ export async function collectProjectGraphSources(args) {
81
+ const projectRoot = resolve(args.projectPath);
82
+ const dataRoot = projectDataDir(args.projectId);
83
+ const specFiles = await collectFiles(join(projectRoot, 'planu', 'specs'), (path) => path.endsWith('/spec.md'));
84
+ const handoffFiles = await collectFiles(join(dataRoot, 'handoffs'), (path) => /\.(json|md)$/.test(path));
85
+ const validationFiles = handoffFiles.filter((path) => path.includes('validation'));
86
+ const transitionLog = join(dataRoot, 'transition-log.jsonl');
87
+ const releaseEvents = join(dataRoot, '..', '..', 'release-events.jsonl');
88
+ const raw = [
89
+ ...specFiles.map((path) => ({
90
+ id: `spec:${relative(projectRoot, path)}`,
91
+ kind: 'spec',
92
+ path,
93
+ })),
94
+ ...handoffFiles.map((path) => ({
95
+ id: `handoff:${relative(dataRoot, path)}`,
96
+ kind: validationFiles.includes(path) ? 'validation' : 'handoff',
97
+ path,
98
+ })),
99
+ ...((await fileExists(transitionLog))
100
+ ? [{ id: 'transition-log', kind: 'transition-log', path: transitionLog }]
101
+ : []),
102
+ ...((await fileExists(releaseEvents))
103
+ ? [{ id: 'release-events', kind: 'release-events', path: releaseEvents }]
104
+ : []),
105
+ ];
106
+ const sources = await Promise.all(raw.map((source) => sourceFromFile(source)));
107
+ return sources.filter((source) => source !== null);
108
+ }
109
+ export async function getProjectGraphFreshness(args) {
110
+ const policy = args.policy ?? (await loadProjectGraphPolicy());
111
+ const graphPath = projectGraphPath(args.projectId, policy);
112
+ const graph = await readProjectGraph(args.projectId, policy);
113
+ if (graph === null) {
114
+ return { exists: false, stale: true, reason: 'missing', graphPath, changedSources: [] };
115
+ }
116
+ if (!Array.isArray(graph.nodes) || !Array.isArray(graph.edges)) {
117
+ return { exists: true, stale: true, reason: 'corrupt', graphPath, changedSources: [] };
118
+ }
119
+ const generatedAtMs = Date.parse(graph.generatedAt);
120
+ if (Number.isFinite(generatedAtMs)) {
121
+ const ageMinutes = (Date.now() - generatedAtMs) / 60_000;
122
+ if (ageMinutes > policy.freshness.staleAfterMinutes) {
123
+ return {
124
+ exists: true,
125
+ stale: true,
126
+ reason: 'expired',
127
+ graphPath,
128
+ generatedAt: graph.generatedAt,
129
+ changedSources: [],
130
+ };
131
+ }
132
+ }
133
+ const sources = await collectProjectGraphSources({
134
+ projectId: args.projectId,
135
+ projectPath: args.projectPath,
136
+ policy,
137
+ });
138
+ const changedSources = sources
139
+ .filter((source) => graph.sourceHashes[source.id] !== source.hash)
140
+ .map((source) => source.id);
141
+ if (changedSources.length > 0) {
142
+ return {
143
+ exists: true,
144
+ stale: true,
145
+ reason: 'source_changed',
146
+ graphPath,
147
+ generatedAt: graph.generatedAt,
148
+ changedSources,
149
+ };
150
+ }
151
+ return {
152
+ exists: true,
153
+ stale: false,
154
+ reason: 'fresh',
155
+ graphPath,
156
+ generatedAt: graph.generatedAt,
157
+ changedSources: [],
158
+ };
159
+ }
160
+ //# sourceMappingURL=cache.js.map
@@ -0,0 +1,7 @@
1
+ import type { ProjectGraphExtractionResult, ProjectGraphPolicy, ProjectGraphSource } from '../../../types/project-knowledge-graph.js';
2
+ export declare function extractGitGraph(args: {
3
+ projectPath: string;
4
+ policy: ProjectGraphPolicy;
5
+ }): Promise<ProjectGraphExtractionResult>;
6
+ export declare function extractReleaseGraph(source: ProjectGraphSource, policy: ProjectGraphPolicy): ProjectGraphExtractionResult;
7
+ //# sourceMappingURL=git-extractor.d.ts.map
@@ -0,0 +1,89 @@
1
+ import { promisify } from 'node:util';
2
+ import { hashText } from '../cache.js';
3
+ import { redactGraphText } from '../query.js';
4
+ export async function extractGitGraph(args) {
5
+ try {
6
+ const childProcess = await import('node:child_process');
7
+ if (typeof childProcess.execFile !== 'function') {
8
+ return { nodes: [], edges: [] };
9
+ }
10
+ const execFileAsync = promisify(childProcess.execFile);
11
+ const { stdout } = await execFileAsync('git', ['log', '--max-count=20', '--pretty=format:%H%x09%s'], { cwd: args.projectPath });
12
+ const nodes = [];
13
+ const edges = [];
14
+ for (const line of stdout.split('\n').filter(Boolean)) {
15
+ const [sha, ...messageParts] = line.split('\t');
16
+ if (sha === undefined) {
17
+ continue;
18
+ }
19
+ const message = messageParts.join('\t');
20
+ const commitId = `commit:${sha.slice(0, 12)}`;
21
+ nodes.push({
22
+ id: commitId,
23
+ type: 'commit',
24
+ label: redactGraphText(message, args.policy),
25
+ source: 'git-extractor',
26
+ evidence: [{ path: '.git/log', selector: sha }],
27
+ updatedAt: new Date().toISOString(),
28
+ });
29
+ const specId = /SPEC-\d+/i.exec(message)?.[0]?.toUpperCase();
30
+ if (specId !== undefined) {
31
+ edges.push({
32
+ id: `edge:${hashText(`${commitId}:references:${specId}`)}`,
33
+ from: commitId,
34
+ to: `spec:${specId}`,
35
+ type: 'references',
36
+ source: 'git-extractor',
37
+ confidence: 'medium',
38
+ classification: 'inferred',
39
+ evidence: { path: '.git/log', selector: sha },
40
+ updatedAt: new Date().toISOString(),
41
+ });
42
+ }
43
+ }
44
+ return { nodes, edges };
45
+ }
46
+ catch {
47
+ return { nodes: [], edges: [] };
48
+ }
49
+ }
50
+ export function extractReleaseGraph(source, policy) {
51
+ const nodes = [];
52
+ const edges = [];
53
+ for (const line of source.content.split('\n').filter(Boolean).slice(-20)) {
54
+ try {
55
+ const item = JSON.parse(line);
56
+ if (typeof item.version !== 'string') {
57
+ continue;
58
+ }
59
+ const releaseId = `release:${item.version}`;
60
+ nodes.push({
61
+ id: releaseId,
62
+ type: 'release',
63
+ label: redactGraphText(item.version, policy),
64
+ source: 'git-extractor',
65
+ evidence: [{ path: source.path, hash: source.hash }],
66
+ metadata: { commitSha: item.commitSha ?? null, timestamp: item.timestamp ?? null },
67
+ updatedAt: new Date().toISOString(),
68
+ });
69
+ if (typeof item.commitSha === 'string') {
70
+ edges.push({
71
+ id: `edge:${hashText(`${releaseId}:released_in:${item.commitSha}`)}`,
72
+ from: `commit:${item.commitSha.slice(0, 12)}`,
73
+ to: releaseId,
74
+ type: 'released_in',
75
+ source: 'git-extractor',
76
+ confidence: 'medium',
77
+ classification: 'extracted',
78
+ evidence: { path: source.path, hash: source.hash },
79
+ updatedAt: new Date().toISOString(),
80
+ });
81
+ }
82
+ }
83
+ catch {
84
+ continue;
85
+ }
86
+ }
87
+ return { nodes, edges };
88
+ }
89
+ //# sourceMappingURL=git-extractor.js.map
@@ -0,0 +1,3 @@
1
+ import type { ProjectGraphExtractionResult, ProjectGraphPolicy, ProjectGraphSource } from '../../../types/project-knowledge-graph.js';
2
+ export declare function extractHandoffGraph(source: ProjectGraphSource, policy: ProjectGraphPolicy): ProjectGraphExtractionResult;
3
+ //# sourceMappingURL=handoff-extractor.d.ts.map
@@ -0,0 +1,55 @@
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
+ function node(args) {
10
+ return {
11
+ id: args.id,
12
+ type: args.type,
13
+ label: redactGraphText(args.label, args.policy),
14
+ source: 'handoff-extractor',
15
+ evidence: [{ path: args.source.path, hash: args.source.hash }],
16
+ updatedAt: new Date().toISOString(),
17
+ };
18
+ }
19
+ function edge(from, to, type, source) {
20
+ return {
21
+ id: `edge:${hashText(`${from}:${type}:${to}:${source.id}`)}`,
22
+ from,
23
+ to,
24
+ type,
25
+ source: 'handoff-extractor',
26
+ confidence: 'medium',
27
+ classification: 'extracted',
28
+ evidence: { path: source.path, hash: source.hash },
29
+ updatedAt: new Date().toISOString(),
30
+ };
31
+ }
32
+ export function extractHandoffGraph(source, policy) {
33
+ const specId = specIdFromPath(source.path);
34
+ if (specId === undefined) {
35
+ return { nodes: [], edges: [] };
36
+ }
37
+ const specNodeId = `spec:${specId}`;
38
+ const handoffId = `handoff:${specId}:${hashText(source.path).slice(0, 10)}`;
39
+ const nodes = [
40
+ node({ id: handoffId, type: 'handoff', label: basename(source.path), source, policy }),
41
+ ];
42
+ const edges = [edge(specNodeId, handoffId, 'references', source)];
43
+ for (const match of source.content.matchAll(/`((?:src|tests|planu)\/[^`]+)`/g)) {
44
+ const path = (match[1] ?? '').trim();
45
+ if (path.length === 0) {
46
+ continue;
47
+ }
48
+ const type = path.startsWith('tests/') ? 'test' : 'file';
49
+ const id = `${type}:${path}`;
50
+ nodes.push(node({ id, type, label: path, source, policy }));
51
+ edges.push(edge(handoffId, id, type === 'test' ? 'tests' : 'references', source));
52
+ }
53
+ return { nodes, edges };
54
+ }
55
+ //# sourceMappingURL=handoff-extractor.js.map
@@ -0,0 +1,3 @@
1
+ import type { ProjectGraphExtractionResult, ProjectGraphPolicy, ProjectGraphSource } from '../../../types/project-knowledge-graph.js';
2
+ export declare function extractSpecGraph(source: ProjectGraphSource, policy: ProjectGraphPolicy): ProjectGraphExtractionResult;
3
+ //# sourceMappingURL=spec-extractor.d.ts.map