@planu/cli 4.10.12 → 4.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/dist/config/project-knowledge-graph.json +42 -5
- package/dist/engine/core-bridge-project-graph.d.ts +13 -0
- package/dist/engine/core-bridge-project-graph.js +499 -0
- package/dist/engine/core-bridge.d.ts +7 -2
- package/dist/engine/core-bridge.js +55 -0
- package/dist/engine/frontmatter-parser.js +73 -23
- package/dist/engine/model-tier-resolver.d.ts +8 -7
- package/dist/engine/model-tier-resolver.js +70 -73
- package/dist/engine/next-spec-resolver/orchestration-planner.d.ts +5 -0
- package/dist/engine/next-spec-resolver/orchestration-planner.js +34 -5
- package/dist/engine/project-graph/builder.js +271 -36
- package/dist/engine/project-graph/cache.d.ts +22 -4
- package/dist/engine/project-graph/cache.js +412 -33
- package/dist/engine/project-graph/index.d.ts +1 -0
- package/dist/engine/project-graph/index.js +1 -0
- package/dist/engine/project-graph/native.d.ts +3 -0
- package/dist/engine/project-graph/native.js +36 -0
- package/dist/engine/project-graph/query.js +34 -2
- package/dist/engine/provider-adapters/adapters/claude.js +38 -14
- package/dist/engine/scan-project/index.js +88 -15
- package/dist/engine/spec-format/lean-spec-generator.d.ts +2 -2
- package/dist/engine/spec-format/lean-spec-generator.js +65 -50
- package/dist/engine/spec-format/metadata-value-policy.d.ts +161 -0
- package/dist/engine/spec-format/metadata-value-policy.js +87 -0
- package/dist/engine/spec-format/value-only-spec-serializer.d.ts +12 -0
- package/dist/engine/spec-format/value-only-spec-serializer.js +18 -0
- package/dist/engine/spec-generator/fallback-generator.js +4 -2
- package/dist/engine/spec-generator/opus-generator.js +5 -2
- package/dist/engine/spec-migrator/lean-migration.js +26 -13
- package/dist/storage/spec-store.js +6 -6
- package/dist/tools/create-spec.js +1027 -739
- package/dist/tools/render-spec-for-provider.js +4 -3
- package/dist/tools/reverse-engineer/handler.js +76 -43
- package/dist/tools/spec-split-handler.js +36 -75
- package/dist/types/conventions.d.ts +9 -0
- package/dist/types/core-bridge.d.ts +72 -0
- package/dist/types/next-spec.d.ts +2 -1
- package/dist/types/project-knowledge-graph.d.ts +58 -0
- package/dist/types/spec/core.d.ts +7 -4
- package/dist/types/spec-format.d.ts +2 -1
- package/dist/types/spec-generator.d.ts +8 -2
- package/package.json +11 -9
- package/planu-native.json +1 -1
- package/planu-plugin.json +1 -1
- package/dist/engine/spec-format/model-budget-deriver.d.ts +0 -5
- package/dist/engine/spec-format/model-budget-deriver.js +0 -7
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,19 @@
|
|
|
1
|
+
## [4.11.1] - 2026-07-09
|
|
2
|
+
|
|
3
|
+
### Bug Fixes
|
|
4
|
+
- fix: benchmark representative graph sources
|
|
5
|
+
- fix: verify release lockfile with pinned pnpm
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
## [4.11.0] - 2026-07-09
|
|
9
|
+
|
|
10
|
+
### Features
|
|
11
|
+
- feat: add native project graph and value-only specs
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
- fix: stabilize native graph release benchmark
|
|
15
|
+
|
|
16
|
+
|
|
1
17
|
## [4.10.12] - 2026-07-08
|
|
2
18
|
|
|
3
19
|
### Chores
|
|
@@ -25,7 +25,11 @@
|
|
|
25
25
|
"architecture_risk",
|
|
26
26
|
"drift",
|
|
27
27
|
"contract",
|
|
28
|
-
"git_change"
|
|
28
|
+
"git_change",
|
|
29
|
+
"symbol",
|
|
30
|
+
"tool_handler",
|
|
31
|
+
"module",
|
|
32
|
+
"scenario"
|
|
29
33
|
],
|
|
30
34
|
"edgeTypes": [
|
|
31
35
|
"contains",
|
|
@@ -44,13 +48,34 @@
|
|
|
44
48
|
"depends_on",
|
|
45
49
|
"has_evidence",
|
|
46
50
|
"has_status",
|
|
47
|
-
"has_contract"
|
|
51
|
+
"has_contract",
|
|
52
|
+
"imports",
|
|
53
|
+
"re_exports",
|
|
54
|
+
"exports",
|
|
55
|
+
"defines"
|
|
48
56
|
],
|
|
49
57
|
"classifications": ["extracted", "inferred", "ambiguous"],
|
|
50
58
|
"confidenceLabels": ["high", "medium", "low"],
|
|
51
59
|
"freshness": {
|
|
52
60
|
"staleAfterMinutes": 1440
|
|
53
61
|
},
|
|
62
|
+
"limits": {
|
|
63
|
+
"operational": {
|
|
64
|
+
"maxFiles": 20000,
|
|
65
|
+
"maxFileBytes": 2097152,
|
|
66
|
+
"maxNodes": 60,
|
|
67
|
+
"maxEdges": 120,
|
|
68
|
+
"maxDepth": 3
|
|
69
|
+
},
|
|
70
|
+
"hard": {
|
|
71
|
+
"maxFiles": 50000,
|
|
72
|
+
"maxFileBytes": 8388608,
|
|
73
|
+
"maxNodes": 200,
|
|
74
|
+
"maxEdges": 1000,
|
|
75
|
+
"maxDepth": 16
|
|
76
|
+
},
|
|
77
|
+
"evidence": "Operational limits preserve the existing project-graph contract. Hard caps bound native allocation and traversal to under 8 MiB per source and 1,000 returned edges."
|
|
78
|
+
},
|
|
54
79
|
"query": {
|
|
55
80
|
"maxNodes": 60,
|
|
56
81
|
"maxEdges": 120,
|
|
@@ -83,7 +108,11 @@
|
|
|
83
108
|
"risk",
|
|
84
109
|
"tool",
|
|
85
110
|
"contract",
|
|
86
|
-
"git_change"
|
|
111
|
+
"git_change",
|
|
112
|
+
"symbol",
|
|
113
|
+
"tool_handler",
|
|
114
|
+
"module",
|
|
115
|
+
"scenario"
|
|
87
116
|
],
|
|
88
117
|
"edgeTypes": [
|
|
89
118
|
"contains",
|
|
@@ -99,7 +128,11 @@
|
|
|
99
128
|
"depends_on",
|
|
100
129
|
"has_evidence",
|
|
101
130
|
"has_status",
|
|
102
|
-
"has_contract"
|
|
131
|
+
"has_contract",
|
|
132
|
+
"imports",
|
|
133
|
+
"re_exports",
|
|
134
|
+
"exports",
|
|
135
|
+
"defines"
|
|
103
136
|
],
|
|
104
137
|
"response": {
|
|
105
138
|
"defaultLimit": 60,
|
|
@@ -117,7 +150,11 @@
|
|
|
117
150
|
"architecture_risk": ["path", "ruleId", "severity"],
|
|
118
151
|
"drift": ["specId", "status", "severity"],
|
|
119
152
|
"contract": ["kind", "passed"],
|
|
120
|
-
"git_change": ["path", "status"]
|
|
153
|
+
"git_change": ["path", "status"],
|
|
154
|
+
"symbol": ["path", "name"],
|
|
155
|
+
"tool_handler": ["path", "name"],
|
|
156
|
+
"module": ["specifier"],
|
|
157
|
+
"scenario": ["path", "name"]
|
|
121
158
|
}
|
|
122
159
|
},
|
|
123
160
|
"toolNamePatterns": [
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { AffectedNodeRs, CompactGraphSliceRs, GraphEdgeInputRs, GraphNodeInputRs, ProjectGraphLimitsPolicyRs, ProjectGraphSourceFileRs, ResolvedProjectGraphLimitsRs, ShortestPathResultRs, TsRelationRs } from '../types/core-bridge.js';
|
|
2
|
+
export declare class ProjectGraphError extends Error {
|
|
3
|
+
readonly code: string;
|
|
4
|
+
constructor(code: string, message: string);
|
|
5
|
+
}
|
|
6
|
+
export declare function resolveProjectGraphLimits(policy: ProjectGraphLimitsPolicyRs, overrides?: Partial<ResolvedProjectGraphLimitsRs>): ResolvedProjectGraphLimitsRs;
|
|
7
|
+
export declare function validateResolvedLimits(limits: ResolvedProjectGraphLimitsRs): void;
|
|
8
|
+
export declare function tsScanProjectGraphSources(rootPath: string, limits: ResolvedProjectGraphLimitsRs): ProjectGraphSourceFileRs[];
|
|
9
|
+
export declare function tsExtractTsRelations(rootPath: string, paths: string[], limits: ResolvedProjectGraphLimitsRs): TsRelationRs[];
|
|
10
|
+
export declare function tsQueryAffectedNodes(seed: string, edges: GraphEdgeInputRs[], relationFilter: string[], limits: ResolvedProjectGraphLimitsRs): AffectedNodeRs[];
|
|
11
|
+
export declare function tsQueryShortestPath(from: string, to: string, edges: GraphEdgeInputRs[], relationFilter: string[], limits: ResolvedProjectGraphLimitsRs): ShortestPathResultRs;
|
|
12
|
+
export declare function tsSelectCompactGraphSlice(seeds: string[], nodes: GraphNodeInputRs[], edges: GraphEdgeInputRs[], limits: ResolvedProjectGraphLimitsRs): CompactGraphSliceRs;
|
|
13
|
+
//# sourceMappingURL=core-bridge-project-graph.d.ts.map
|
|
@@ -0,0 +1,499 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { closeSync, fstatSync, lstatSync, openSync, readSync, readdirSync, realpathSync, } from 'node:fs';
|
|
3
|
+
import { extname, isAbsolute, join, relative, sep } from 'node:path';
|
|
4
|
+
const PROJECT_GRAPH_EXTS = new Set(['.ts', '.tsx', '.js', '.jsx', '.json', '.md']);
|
|
5
|
+
const PROJECT_GRAPH_CODE_EXTS = new Set(['.ts', '.tsx', '.js', '.jsx']);
|
|
6
|
+
const IGNORED_DIRS = new Set([
|
|
7
|
+
'node_modules',
|
|
8
|
+
'.git',
|
|
9
|
+
'dist',
|
|
10
|
+
'build',
|
|
11
|
+
'coverage',
|
|
12
|
+
'.next',
|
|
13
|
+
'target',
|
|
14
|
+
'npm',
|
|
15
|
+
]);
|
|
16
|
+
export class ProjectGraphError extends Error {
|
|
17
|
+
code;
|
|
18
|
+
constructor(code, message) {
|
|
19
|
+
super(`${code}: ${message}`);
|
|
20
|
+
this.code = code;
|
|
21
|
+
this.name = 'ProjectGraphError';
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function compareText(left, right) {
|
|
25
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
26
|
+
}
|
|
27
|
+
function compareTuple(left, right) {
|
|
28
|
+
for (let index = 0; index < Math.max(left.length, right.length); index += 1) {
|
|
29
|
+
const compared = compareText(left[index] ?? '', right[index] ?? '');
|
|
30
|
+
if (compared !== 0) {
|
|
31
|
+
return compared;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return 0;
|
|
35
|
+
}
|
|
36
|
+
function validateLimit(name, value, hard) {
|
|
37
|
+
if (!Number.isSafeInteger(value) || !Number.isSafeInteger(hard) || value <= 0 || value > hard) {
|
|
38
|
+
throw new ProjectGraphError('PROJECT_GRAPH_INVALID_LIMIT', `${name} must be an integer between 1 and its hard cap (${String(hard)}), received ${String(value)}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
export function resolveProjectGraphLimits(policy, overrides = {}) {
|
|
42
|
+
const candidate = policy;
|
|
43
|
+
if (candidate.operational === undefined || candidate.hard === undefined) {
|
|
44
|
+
throw new ProjectGraphError('PROJECT_GRAPH_INVALID_LIMIT', 'project graph limits policy must include operational and hard values');
|
|
45
|
+
}
|
|
46
|
+
const resolved = {
|
|
47
|
+
maxFiles: overrides.maxFiles ?? policy.operational.maxFiles,
|
|
48
|
+
maxFileBytes: overrides.maxFileBytes ?? policy.operational.maxFileBytes,
|
|
49
|
+
maxNodes: overrides.maxNodes ?? policy.operational.maxNodes,
|
|
50
|
+
maxEdges: overrides.maxEdges ?? policy.operational.maxEdges,
|
|
51
|
+
maxDepth: overrides.maxDepth ?? policy.operational.maxDepth,
|
|
52
|
+
hardMaxFiles: policy.hard.maxFiles,
|
|
53
|
+
hardMaxFileBytes: policy.hard.maxFileBytes,
|
|
54
|
+
hardMaxNodes: policy.hard.maxNodes,
|
|
55
|
+
hardMaxEdges: policy.hard.maxEdges,
|
|
56
|
+
hardMaxDepth: policy.hard.maxDepth,
|
|
57
|
+
};
|
|
58
|
+
validateResolvedLimits(resolved);
|
|
59
|
+
return resolved;
|
|
60
|
+
}
|
|
61
|
+
export function validateResolvedLimits(limits) {
|
|
62
|
+
validateLimit('maxFiles', limits.maxFiles, limits.hardMaxFiles);
|
|
63
|
+
validateLimit('maxFileBytes', limits.maxFileBytes, limits.hardMaxFileBytes);
|
|
64
|
+
validateLimit('maxNodes', limits.maxNodes, limits.hardMaxNodes);
|
|
65
|
+
validateLimit('maxEdges', limits.maxEdges, limits.hardMaxEdges);
|
|
66
|
+
validateLimit('maxDepth', limits.maxDepth, limits.hardMaxDepth);
|
|
67
|
+
}
|
|
68
|
+
function canonicalRoot(rootPath) {
|
|
69
|
+
if (rootPath.includes('\0') || !isAbsolute(rootPath)) {
|
|
70
|
+
throw new ProjectGraphError('PROJECT_GRAPH_INVALID_PATH', 'root path must be absolute and contain no NUL');
|
|
71
|
+
}
|
|
72
|
+
let root;
|
|
73
|
+
try {
|
|
74
|
+
root = realpathSync.native(rootPath);
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
throw new ProjectGraphError('PROJECT_GRAPH_INVALID_ROOT', `cannot canonicalize root: ${error.message}`);
|
|
78
|
+
}
|
|
79
|
+
if (!lstatSync(root).isDirectory()) {
|
|
80
|
+
throw new ProjectGraphError('PROJECT_GRAPH_INVALID_ROOT', 'root is not a directory');
|
|
81
|
+
}
|
|
82
|
+
return root;
|
|
83
|
+
}
|
|
84
|
+
function validateRelativeCandidate(candidate) {
|
|
85
|
+
if (candidate.length === 0 ||
|
|
86
|
+
candidate.includes('\0') ||
|
|
87
|
+
isAbsolute(candidate) ||
|
|
88
|
+
candidate.startsWith('/') ||
|
|
89
|
+
candidate.startsWith('\\') ||
|
|
90
|
+
/^[A-Za-z]:[/\\]/u.test(candidate)) {
|
|
91
|
+
throw new ProjectGraphError('PROJECT_GRAPH_INVALID_PATH', `candidate must be a non-empty relative path: ${candidate}`);
|
|
92
|
+
}
|
|
93
|
+
const parts = candidate.split(/[\\/]/u);
|
|
94
|
+
if (parts.some((part) => part.length === 0 || part === '.' || part === '..')) {
|
|
95
|
+
throw new ProjectGraphError('PROJECT_GRAPH_INVALID_PATH', `candidate must be normalized and relative: ${candidate}`);
|
|
96
|
+
}
|
|
97
|
+
return parts;
|
|
98
|
+
}
|
|
99
|
+
function normalizedRelative(root, path) {
|
|
100
|
+
const result = relative(root, path);
|
|
101
|
+
if (result === '' || result === '..' || result.startsWith(`..${sep}`) || isAbsolute(result)) {
|
|
102
|
+
throw new ProjectGraphError('PROJECT_GRAPH_PATH_OUTSIDE_ROOT', `${path} is outside ${root}`);
|
|
103
|
+
}
|
|
104
|
+
return result.split(sep).join('/');
|
|
105
|
+
}
|
|
106
|
+
function resolveRegularCandidate(root, candidate) {
|
|
107
|
+
const parts = validateRelativeCandidate(candidate);
|
|
108
|
+
let current = root;
|
|
109
|
+
for (const part of parts) {
|
|
110
|
+
current = join(current, part);
|
|
111
|
+
let metadata;
|
|
112
|
+
try {
|
|
113
|
+
metadata = lstatSync(current);
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
throw new ProjectGraphError('PROJECT_GRAPH_INVALID_PATH', `cannot inspect ${candidate}: ${error.message}`);
|
|
117
|
+
}
|
|
118
|
+
if (metadata.isSymbolicLink()) {
|
|
119
|
+
throw new ProjectGraphError('PROJECT_GRAPH_SYMLINK_FORBIDDEN', `symlink component is forbidden: ${candidate}`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
const canonical = realpathSync.native(current);
|
|
123
|
+
normalizedRelative(root, canonical);
|
|
124
|
+
const metadata = lstatSync(canonical);
|
|
125
|
+
if (!metadata.isFile()) {
|
|
126
|
+
throw new ProjectGraphError('PROJECT_GRAPH_NON_REGULAR_FILE', `candidate is not a regular file: ${candidate}`);
|
|
127
|
+
}
|
|
128
|
+
return { path: canonical, size: metadata.size, mtimeMs: metadata.mtimeMs };
|
|
129
|
+
}
|
|
130
|
+
function ensureFileSize(candidate, size, maxFileBytes) {
|
|
131
|
+
if (size > maxFileBytes) {
|
|
132
|
+
throw new ProjectGraphError('PROJECT_GRAPH_FILE_TOO_LARGE', `${candidate} exceeds maxFileBytes (${String(maxFileBytes)})`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
function hashFile(path, candidate, maxFileBytes) {
|
|
136
|
+
const descriptor = openSync(path, 'r');
|
|
137
|
+
try {
|
|
138
|
+
ensureFileSize(candidate, fstatSync(descriptor).size, maxFileBytes);
|
|
139
|
+
const hash = createHash('sha256');
|
|
140
|
+
const buffer = Buffer.allocUnsafe(64 * 1024);
|
|
141
|
+
let total = 0;
|
|
142
|
+
for (;;) {
|
|
143
|
+
const bytesRead = readSync(descriptor, buffer, 0, buffer.length, null);
|
|
144
|
+
if (bytesRead === 0) {
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
total += bytesRead;
|
|
148
|
+
if (total > maxFileBytes) {
|
|
149
|
+
throw new ProjectGraphError('PROJECT_GRAPH_FILE_TOO_LARGE', `${candidate} grew beyond maxFileBytes (${String(maxFileBytes)}) while hashing`);
|
|
150
|
+
}
|
|
151
|
+
hash.update(buffer.subarray(0, bytesRead));
|
|
152
|
+
}
|
|
153
|
+
return hash.digest('hex');
|
|
154
|
+
}
|
|
155
|
+
finally {
|
|
156
|
+
closeSync(descriptor);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
function readBoundedUtf8(path, candidate, maxFileBytes) {
|
|
160
|
+
const descriptor = openSync(path, 'r');
|
|
161
|
+
try {
|
|
162
|
+
const initialSize = fstatSync(descriptor).size;
|
|
163
|
+
ensureFileSize(candidate, initialSize, maxFileBytes);
|
|
164
|
+
const bytes = Buffer.allocUnsafe(initialSize);
|
|
165
|
+
let offset = 0;
|
|
166
|
+
while (offset < bytes.length) {
|
|
167
|
+
const bytesRead = readSync(descriptor, bytes, offset, bytes.length - offset, null);
|
|
168
|
+
if (bytesRead === 0) {
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
171
|
+
offset += bytesRead;
|
|
172
|
+
}
|
|
173
|
+
const growthProbe = Buffer.allocUnsafe(1);
|
|
174
|
+
if (readSync(descriptor, growthProbe, 0, 1, null) !== 0) {
|
|
175
|
+
throw new ProjectGraphError('PROJECT_GRAPH_FILE_TOO_LARGE', `${candidate} changed while reading`);
|
|
176
|
+
}
|
|
177
|
+
try {
|
|
178
|
+
return new TextDecoder('utf-8', { fatal: true }).decode(bytes.subarray(0, offset));
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
throw new ProjectGraphError('PROJECT_GRAPH_INVALID_UTF8', `${candidate} is not valid UTF-8`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
finally {
|
|
185
|
+
closeSync(descriptor);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
function walkFiles(root) {
|
|
189
|
+
const output = [];
|
|
190
|
+
const visit = (directory) => {
|
|
191
|
+
const entries = readdirSync(directory, { withFileTypes: true }).sort((left, right) => compareText(left.name, right.name));
|
|
192
|
+
for (const entry of entries) {
|
|
193
|
+
if (entry.isSymbolicLink()) {
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
const path = join(directory, entry.name);
|
|
197
|
+
if (entry.isDirectory()) {
|
|
198
|
+
if (!IGNORED_DIRS.has(entry.name)) {
|
|
199
|
+
visit(path);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
else if (entry.isFile()) {
|
|
203
|
+
output.push(path);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
visit(root);
|
|
208
|
+
return output;
|
|
209
|
+
}
|
|
210
|
+
export function tsScanProjectGraphSources(rootPath, limits) {
|
|
211
|
+
validateResolvedLimits(limits);
|
|
212
|
+
const root = canonicalRoot(rootPath);
|
|
213
|
+
return walkFiles(root)
|
|
214
|
+
.filter((path) => PROJECT_GRAPH_EXTS.has(extname(path).toLowerCase()))
|
|
215
|
+
.map((path) => ({ path, candidate: normalizedRelative(root, path) }))
|
|
216
|
+
.sort((left, right) => compareText(left.candidate, right.candidate))
|
|
217
|
+
.slice(0, limits.maxFiles)
|
|
218
|
+
.map(({ candidate }) => {
|
|
219
|
+
const resolved = resolveRegularCandidate(root, candidate);
|
|
220
|
+
ensureFileSize(candidate, resolved.size, limits.maxFileBytes);
|
|
221
|
+
return {
|
|
222
|
+
path: candidate,
|
|
223
|
+
hash: hashFile(resolved.path, candidate, limits.maxFileBytes),
|
|
224
|
+
size: resolved.size,
|
|
225
|
+
mtime: resolved.mtimeMs,
|
|
226
|
+
};
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
const IMPORT_RE = /^\s*import(?:\s+type)?(?:[^'"]*from\s*)?['"]([^'"]+)['"]/gmu;
|
|
230
|
+
const EXPORT_FROM_RE = /^\s*export\s+[^'"]*from\s*['"]([^'"]+)['"]/gmu;
|
|
231
|
+
const EXPORT_SYMBOL_RE = /^\s*export\s+(?:async\s+)?(?:function|class|interface|type|const|let|var|enum)\s+([A-Za-z_$][\w$]*)/gmu;
|
|
232
|
+
const DEFINE_SYMBOL_RE = /^\s*(?:export\s+)?(?:async\s+)?(?:function|class|interface|type|const|let|var|enum)\s+([A-Za-z_$][\w$]*)/gmu;
|
|
233
|
+
const TEST_RE = /\b(describe|it|test)\s*\(\s*['"]([^'"]+)['"]/gu;
|
|
234
|
+
function pushMatches(output, content, candidate, regex, relation, targetPrefix, confidence = 'high') {
|
|
235
|
+
for (const match of content.matchAll(new RegExp(regex.source, regex.flags))) {
|
|
236
|
+
const target = match[1]?.trim();
|
|
237
|
+
if (target) {
|
|
238
|
+
output.push({
|
|
239
|
+
from: `file:${candidate}`,
|
|
240
|
+
to: targetPrefix.endsWith('#') ? `${targetPrefix}${target}` : `${targetPrefix}:${target}`,
|
|
241
|
+
relation,
|
|
242
|
+
sourceFile: candidate,
|
|
243
|
+
confidence,
|
|
244
|
+
classification: 'extracted',
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
function relationTuple(relation) {
|
|
250
|
+
return [
|
|
251
|
+
relation.from,
|
|
252
|
+
relation.to,
|
|
253
|
+
relation.relation,
|
|
254
|
+
relation.sourceFile,
|
|
255
|
+
relation.confidence,
|
|
256
|
+
relation.classification,
|
|
257
|
+
];
|
|
258
|
+
}
|
|
259
|
+
export function tsExtractTsRelations(rootPath, paths, limits) {
|
|
260
|
+
validateResolvedLimits(limits);
|
|
261
|
+
const root = canonicalRoot(rootPath);
|
|
262
|
+
if (paths.length > limits.maxFiles) {
|
|
263
|
+
throw new ProjectGraphError('PROJECT_GRAPH_LIMIT_EXCEEDED', `candidate count exceeds maxFiles (${String(limits.maxFiles)})`);
|
|
264
|
+
}
|
|
265
|
+
const candidates = [
|
|
266
|
+
...new Set(paths.length > 0
|
|
267
|
+
? paths
|
|
268
|
+
: tsScanProjectGraphSources(rootPath, limits).map((source) => source.path)),
|
|
269
|
+
].sort(compareText);
|
|
270
|
+
const output = [];
|
|
271
|
+
for (const candidate of candidates) {
|
|
272
|
+
const resolved = resolveRegularCandidate(root, candidate);
|
|
273
|
+
ensureFileSize(candidate, resolved.size, limits.maxFileBytes);
|
|
274
|
+
if (!PROJECT_GRAPH_CODE_EXTS.has(extname(candidate).toLowerCase())) {
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
const content = readBoundedUtf8(resolved.path, candidate, limits.maxFileBytes);
|
|
278
|
+
pushMatches(output, content, candidate, IMPORT_RE, 'imports', 'module');
|
|
279
|
+
pushMatches(output, content, candidate, EXPORT_FROM_RE, 're_exports', 'module');
|
|
280
|
+
pushMatches(output, content, candidate, EXPORT_SYMBOL_RE, 'exports', `symbol:${candidate}#`);
|
|
281
|
+
pushMatches(output, content, candidate, DEFINE_SYMBOL_RE, 'defines', `symbol:${candidate}#`, 'medium');
|
|
282
|
+
if (candidate.includes('test.') ||
|
|
283
|
+
candidate.endsWith('.test.ts') ||
|
|
284
|
+
candidate.endsWith('.test.tsx')) {
|
|
285
|
+
for (const match of content.matchAll(new RegExp(TEST_RE.source, TEST_RE.flags))) {
|
|
286
|
+
const name = match[2]?.trim();
|
|
287
|
+
if (name) {
|
|
288
|
+
output.push({
|
|
289
|
+
from: `test:${candidate}`,
|
|
290
|
+
to: `scenario:${candidate}#${name}`,
|
|
291
|
+
relation: 'tests',
|
|
292
|
+
sourceFile: candidate,
|
|
293
|
+
confidence: 'medium',
|
|
294
|
+
classification: 'extracted',
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
return output.sort((left, right) => compareTuple(relationTuple(left), relationTuple(right)));
|
|
301
|
+
}
|
|
302
|
+
function validateText(name, value) {
|
|
303
|
+
if (value.length === 0 || value.includes('\0')) {
|
|
304
|
+
throw new ProjectGraphError('PROJECT_GRAPH_INVALID_INPUT', `${name} must be non-empty and contain no NUL`);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
function validateEdges(edges, limits) {
|
|
308
|
+
if (edges.length > limits.hardMaxEdges) {
|
|
309
|
+
throw new ProjectGraphError('PROJECT_GRAPH_LIMIT_EXCEEDED', `edge count exceeds hard maxEdges (${String(limits.hardMaxEdges)})`);
|
|
310
|
+
}
|
|
311
|
+
const ids = new Set();
|
|
312
|
+
for (const edge of edges) {
|
|
313
|
+
validateText('edge.id', edge.id);
|
|
314
|
+
validateText('edge.from', edge.from);
|
|
315
|
+
validateText('edge.to', edge.to);
|
|
316
|
+
validateText('edge.relation', edge.relation);
|
|
317
|
+
if (ids.has(edge.id)) {
|
|
318
|
+
throw new ProjectGraphError('PROJECT_GRAPH_INVALID_INPUT', `duplicate edge id: ${edge.id}`);
|
|
319
|
+
}
|
|
320
|
+
ids.add(edge.id);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
function relationFilterSet(relationFilter) {
|
|
324
|
+
for (const relation of relationFilter) {
|
|
325
|
+
validateText('relationFilter', relation);
|
|
326
|
+
}
|
|
327
|
+
return new Set(relationFilter);
|
|
328
|
+
}
|
|
329
|
+
function relationAllowed(relation, filter) {
|
|
330
|
+
return filter.size === 0 || filter.has(relation);
|
|
331
|
+
}
|
|
332
|
+
function sortedIncoming(edges, filter) {
|
|
333
|
+
const incoming = new Map();
|
|
334
|
+
for (const edge of edges) {
|
|
335
|
+
if (relationAllowed(edge.relation, filter)) {
|
|
336
|
+
const values = incoming.get(edge.to) ?? [];
|
|
337
|
+
values.push(edge);
|
|
338
|
+
incoming.set(edge.to, values);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
for (const values of incoming.values()) {
|
|
342
|
+
values.sort((left, right) => compareTuple([left.from, left.id, left.relation], [right.from, right.id, right.relation]));
|
|
343
|
+
}
|
|
344
|
+
return incoming;
|
|
345
|
+
}
|
|
346
|
+
export function tsQueryAffectedNodes(seed, edges, relationFilter, limits) {
|
|
347
|
+
validateResolvedLimits(limits);
|
|
348
|
+
validateText('seed', seed);
|
|
349
|
+
validateEdges(edges, limits);
|
|
350
|
+
const incoming = sortedIncoming(edges, relationFilterSet(relationFilter));
|
|
351
|
+
const seen = new Set([seed]);
|
|
352
|
+
const queue = [{ node: seed, depth: 0 }];
|
|
353
|
+
const output = [];
|
|
354
|
+
let cursor = 0;
|
|
355
|
+
while (cursor < queue.length && output.length < limits.maxNodes) {
|
|
356
|
+
const current = queue[cursor++];
|
|
357
|
+
if (!current || current.depth >= limits.maxDepth) {
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
for (const edge of incoming.get(current.node) ?? []) {
|
|
361
|
+
if (output.length >= limits.maxNodes) {
|
|
362
|
+
break;
|
|
363
|
+
}
|
|
364
|
+
if (seen.has(edge.from)) {
|
|
365
|
+
continue;
|
|
366
|
+
}
|
|
367
|
+
seen.add(edge.from);
|
|
368
|
+
const nextDepth = current.depth + 1;
|
|
369
|
+
output.push({ nodeId: edge.from, depth: nextDepth, viaRelation: edge.relation });
|
|
370
|
+
queue.push({ node: edge.from, depth: nextDepth });
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
return output;
|
|
374
|
+
}
|
|
375
|
+
function undirectedAdjacency(edges, filter) {
|
|
376
|
+
const adjacency = new Map();
|
|
377
|
+
const add = (from, neighbor) => {
|
|
378
|
+
const values = adjacency.get(from) ?? [];
|
|
379
|
+
values.push(neighbor);
|
|
380
|
+
adjacency.set(from, values);
|
|
381
|
+
};
|
|
382
|
+
for (const edge of edges) {
|
|
383
|
+
if (relationAllowed(edge.relation, filter)) {
|
|
384
|
+
add(edge.from, { node: edge.to, edgeId: edge.id, relation: edge.relation });
|
|
385
|
+
add(edge.to, { node: edge.from, edgeId: edge.id, relation: edge.relation });
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
for (const values of adjacency.values()) {
|
|
389
|
+
values.sort((left, right) => compareTuple([left.node, left.edgeId, left.relation], [right.node, right.edgeId, right.relation]));
|
|
390
|
+
}
|
|
391
|
+
return adjacency;
|
|
392
|
+
}
|
|
393
|
+
export function tsQueryShortestPath(from, to, edges, relationFilter, limits) {
|
|
394
|
+
validateResolvedLimits(limits);
|
|
395
|
+
validateText('from', from);
|
|
396
|
+
validateText('to', to);
|
|
397
|
+
validateEdges(edges, limits);
|
|
398
|
+
if (from === to) {
|
|
399
|
+
return { nodeIds: [from], edgeIds: [] };
|
|
400
|
+
}
|
|
401
|
+
const adjacency = undirectedAdjacency(edges, relationFilterSet(relationFilter));
|
|
402
|
+
const queue = [from];
|
|
403
|
+
const seen = new Set([from]);
|
|
404
|
+
const predecessor = new Map();
|
|
405
|
+
let cursor = 0;
|
|
406
|
+
while (cursor < queue.length) {
|
|
407
|
+
const current = queue[cursor++];
|
|
408
|
+
if (!current) {
|
|
409
|
+
continue;
|
|
410
|
+
}
|
|
411
|
+
for (const neighbor of adjacency.get(current) ?? []) {
|
|
412
|
+
if (seen.has(neighbor.node)) {
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
if (seen.size >= limits.maxNodes) {
|
|
416
|
+
throw new ProjectGraphError('PROJECT_GRAPH_TRAVERSAL_LIMIT', `shortest-path traversal exceeded maxNodes (${String(limits.maxNodes)})`);
|
|
417
|
+
}
|
|
418
|
+
seen.add(neighbor.node);
|
|
419
|
+
predecessor.set(neighbor.node, { node: current, edgeId: neighbor.edgeId });
|
|
420
|
+
if (neighbor.node === to) {
|
|
421
|
+
const nodeIds = [to];
|
|
422
|
+
const edgeIds = [];
|
|
423
|
+
let pathCursor = to;
|
|
424
|
+
while (pathCursor !== from) {
|
|
425
|
+
const previous = predecessor.get(pathCursor);
|
|
426
|
+
if (!previous) {
|
|
427
|
+
throw new ProjectGraphError('PROJECT_GRAPH_INTERNAL_ERROR', 'missing predecessor while reconstructing path');
|
|
428
|
+
}
|
|
429
|
+
edgeIds.push(previous.edgeId);
|
|
430
|
+
nodeIds.push(previous.node);
|
|
431
|
+
pathCursor = previous.node;
|
|
432
|
+
}
|
|
433
|
+
return { nodeIds: nodeIds.reverse(), edgeIds: edgeIds.reverse() };
|
|
434
|
+
}
|
|
435
|
+
queue.push(neighbor.node);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
return { nodeIds: [], edgeIds: [] };
|
|
439
|
+
}
|
|
440
|
+
export function tsSelectCompactGraphSlice(seeds, nodes, edges, limits) {
|
|
441
|
+
validateResolvedLimits(limits);
|
|
442
|
+
if (nodes.length > limits.hardMaxNodes) {
|
|
443
|
+
throw new ProjectGraphError('PROJECT_GRAPH_LIMIT_EXCEEDED', `node count exceeds hard maxNodes (${String(limits.hardMaxNodes)})`);
|
|
444
|
+
}
|
|
445
|
+
validateEdges(edges, limits);
|
|
446
|
+
const known = new Set();
|
|
447
|
+
for (const node of nodes) {
|
|
448
|
+
validateText('node.id', node.id);
|
|
449
|
+
if (known.has(node.id)) {
|
|
450
|
+
throw new ProjectGraphError('PROJECT_GRAPH_INVALID_INPUT', `duplicate node id: ${node.id}`);
|
|
451
|
+
}
|
|
452
|
+
known.add(node.id);
|
|
453
|
+
}
|
|
454
|
+
const sortedSeeds = [...new Set(seeds)].sort(compareText);
|
|
455
|
+
for (const seed of sortedSeeds) {
|
|
456
|
+
validateText('seed', seed);
|
|
457
|
+
}
|
|
458
|
+
const selectedNodes = new Set(sortedSeeds.filter((seed) => known.size === 0 || known.has(seed)).slice(0, limits.maxNodes));
|
|
459
|
+
const queue = [...selectedNodes].sort(compareText);
|
|
460
|
+
const edgeById = new Map(edges.map((edge) => [edge.id, edge]));
|
|
461
|
+
const adjacency = undirectedAdjacency(edges, new Set());
|
|
462
|
+
const selectedEdgeIds = new Set();
|
|
463
|
+
const edgeIds = [];
|
|
464
|
+
let cursor = 0;
|
|
465
|
+
while (cursor < queue.length && edgeIds.length < limits.maxEdges) {
|
|
466
|
+
const current = queue[cursor++];
|
|
467
|
+
if (!current) {
|
|
468
|
+
continue;
|
|
469
|
+
}
|
|
470
|
+
for (const neighbor of adjacency.get(current) ?? []) {
|
|
471
|
+
if (edgeIds.length >= limits.maxEdges) {
|
|
472
|
+
break;
|
|
473
|
+
}
|
|
474
|
+
if (known.size > 0 && !known.has(neighbor.node)) {
|
|
475
|
+
continue;
|
|
476
|
+
}
|
|
477
|
+
const newNode = !selectedNodes.has(neighbor.node);
|
|
478
|
+
if (newNode && selectedNodes.size >= limits.maxNodes) {
|
|
479
|
+
continue;
|
|
480
|
+
}
|
|
481
|
+
if (newNode) {
|
|
482
|
+
selectedNodes.add(neighbor.node);
|
|
483
|
+
queue.push(neighbor.node);
|
|
484
|
+
}
|
|
485
|
+
if (!selectedEdgeIds.has(neighbor.edgeId)) {
|
|
486
|
+
selectedEdgeIds.add(neighbor.edgeId);
|
|
487
|
+
const edge = edgeById.get(neighbor.edgeId);
|
|
488
|
+
if (!edge) {
|
|
489
|
+
throw new ProjectGraphError('PROJECT_GRAPH_INTERNAL_ERROR', 'adjacency references unknown edge');
|
|
490
|
+
}
|
|
491
|
+
if (selectedNodes.has(edge.from) && selectedNodes.has(edge.to)) {
|
|
492
|
+
edgeIds.push(neighbor.edgeId);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
return { nodeIds: [...selectedNodes].sort(compareText), edgeIds };
|
|
498
|
+
}
|
|
499
|
+
//# sourceMappingURL=core-bridge-project-graph.js.map
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { FileHash, FileMetadata, SpecAnnotation, DriftResult, PlanuCore, DuplicateRawBlockRs, DuplicateBlockLocationRs, FastGrepMatch, FileContent, SpecBriefRs, AmbiguousCriterionRs, GapsLogChainResultRs, SearchResult, VectorNode } from '../types/core-bridge.js';
|
|
2
|
-
export type { FileHash, FileMetadata, SpecAnnotation, DriftResult, PlanuCore, DuplicateRawBlockRs, DuplicateBlockLocationRs, FastGrepMatch, FileContent, SpecBriefRs, AmbiguousCriterionRs, GapsLogChainResultRs, SearchResult, VectorNode, };
|
|
1
|
+
import type { FileHash, FileMetadata, SpecAnnotation, DriftResult, PlanuCore, DuplicateRawBlockRs, DuplicateBlockLocationRs, FastGrepMatch, FileContent, SpecBriefRs, AmbiguousCriterionRs, GapsLogChainResultRs, SearchResult, VectorNode, ProjectGraphSourceFileRs, TsRelationRs, GraphNodeInputRs, GraphEdgeInputRs, AffectedNodeRs, ShortestPathResultRs, CompactGraphSliceRs, CompactGraphEdgesRs, ProjectGraphLimitsPolicyRs, ResolvedProjectGraphLimitsRs } from '../types/core-bridge.js';
|
|
2
|
+
export type { FileHash, FileMetadata, SpecAnnotation, DriftResult, PlanuCore, DuplicateRawBlockRs, DuplicateBlockLocationRs, FastGrepMatch, FileContent, SpecBriefRs, AmbiguousCriterionRs, GapsLogChainResultRs, SearchResult, VectorNode, ProjectGraphSourceFileRs, TsRelationRs, GraphNodeInputRs, GraphEdgeInputRs, AffectedNodeRs, ShortestPathResultRs, CompactGraphSliceRs, CompactGraphEdgesRs, ProjectGraphLimitsPolicyRs, ResolvedProjectGraphLimitsRs, };
|
|
3
3
|
interface LoadDiagnostic {
|
|
4
4
|
attempted: string[];
|
|
5
5
|
errors: string[];
|
|
@@ -25,6 +25,11 @@ export declare function fastCosineSimilarity(v1: number[], v2: number[]): number
|
|
|
25
25
|
export declare function fastHnswSearchFlat(query: number[], nodes: VectorNode[], topK: number): SearchResult[];
|
|
26
26
|
export declare function fastHmacSign(secret: string, data: string): string;
|
|
27
27
|
export declare function fastHmacVerify(secret: string, data: string, signature: string): boolean;
|
|
28
|
+
export declare function fastScanProjectGraphSources(rootPath: string, policy: ProjectGraphLimitsPolicyRs, overrides?: Partial<ResolvedProjectGraphLimitsRs>): ProjectGraphSourceFileRs[];
|
|
29
|
+
export declare function fastExtractTsRelations(rootPath: string, paths: string[], policy: ProjectGraphLimitsPolicyRs, overrides?: Partial<ResolvedProjectGraphLimitsRs>): TsRelationRs[];
|
|
30
|
+
export declare function fastQueryAffectedNodes(seed: string, edges: GraphEdgeInputRs[], relationFilter: string[], policy: ProjectGraphLimitsPolicyRs, overrides?: Partial<ResolvedProjectGraphLimitsRs>): AffectedNodeRs[];
|
|
31
|
+
export declare function fastQueryShortestPath(from: string, to: string, edges: GraphEdgeInputRs[], relationFilter: string[], policy: ProjectGraphLimitsPolicyRs, overrides?: Partial<ResolvedProjectGraphLimitsRs>): ShortestPathResultRs;
|
|
32
|
+
export declare function fastSelectCompactGraphSlice(seeds: string[], nodes: GraphNodeInputRs[], edges: GraphEdgeInputRs[], policy: ProjectGraphLimitsPolicyRs, overrides?: Partial<ResolvedProjectGraphLimitsRs>): CompactGraphSliceRs;
|
|
28
33
|
export declare function startNativeWatcher(rootPath: string, callback: (err: Error | null, event: string) => void): void;
|
|
29
34
|
export declare const isNativeActive: () => boolean;
|
|
30
35
|
export declare const nativeLoadDiagnostic: () => LoadDiagnostic;
|