@tekyzinc/gsd-t 4.9.13 → 4.10.10
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 +24 -0
- package/README.md +1 -1
- package/bin/gsd-t-competition-judge.cjs +7 -1
- package/bin/gsd-t-context-brief-kinds/impact.cjs +91 -4
- package/bin/gsd-t-context-brief-kinds/partition.cjs +95 -0
- package/bin/gsd-t-context-brief-kinds/plan.cjs +110 -4
- package/bin/gsd-t-file-disjointness.cjs +319 -7
- package/bin/gsd-t-graph-anti-grep-lint.cjs +515 -0
- package/bin/gsd-t-graph-edge-extract.cjs +612 -0
- package/bin/gsd-t-graph-freshness.cjs +506 -0
- package/bin/gsd-t-graph-index.cjs +540 -0
- package/bin/gsd-t-graph-k1-sqlite-stream.cjs +251 -0
- package/bin/gsd-t-graph-query-cli.cjs +1182 -0
- package/bin/gsd-t-graph-scip-upgrade.cjs +440 -0
- package/bin/gsd-t-graph-store-bakeoff.cjs +889 -0
- package/bin/gsd-t-graph-synthetic-gen.cjs +304 -0
- package/bin/gsd-t-graph-ts-throughput.cjs +587 -0
- package/bin/gsd-t-scip-reader.cjs +167 -0
- package/bin/gsd-t.js +166 -48
- package/commands/gsd-t-debug.md +10 -0
- package/commands/gsd-t-design-build.md +10 -0
- package/commands/gsd-t-execute.md +15 -0
- package/commands/gsd-t-feature.md +15 -7
- package/commands/gsd-t-gap-analysis.md +17 -7
- package/commands/gsd-t-impact.md +25 -0
- package/commands/gsd-t-integrate.md +25 -0
- package/commands/gsd-t-partition.md +18 -0
- package/commands/gsd-t-plan.md +16 -0
- package/commands/gsd-t-populate.md +16 -5
- package/commands/gsd-t-prd.md +18 -0
- package/commands/gsd-t-project.md +19 -0
- package/commands/gsd-t-promote-debt.md +16 -5
- package/commands/gsd-t-qa.md +20 -8
- package/commands/gsd-t-quick.md +10 -0
- package/commands/gsd-t-scan.md +21 -3
- package/commands/gsd-t-test-sync.md +10 -0
- package/commands/gsd-t-verify.md +25 -0
- package/commands/gsd-t-wave.md +8 -0
- package/package.json +10 -2
- package/templates/workflows/gsd-t-debug.workflow.js +81 -0
- package/templates/workflows/gsd-t-integrate.workflow.js +47 -1
- package/templates/workflows/gsd-t-phase.workflow.js +99 -1
- package/templates/workflows/gsd-t-quick.workflow.js +64 -0
- package/templates/workflows/gsd-t-scan.workflow.js +200 -9
- package/templates/workflows/gsd-t-verify.workflow.js +50 -1
- package/bin/graph-cgc.js +0 -510
- package/bin/graph-indexer.js +0 -147
- package/bin/graph-overlay.js +0 -195
- package/bin/graph-parsers.js +0 -327
- package/bin/graph-query.js +0 -453
- package/bin/graph-store.js +0 -154
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* gsd-t-graph-scip-upgrade.cjs
|
|
6
|
+
*
|
|
7
|
+
* M94 D3-T3 — Optional SCIP upgrade + accuracy tier labelling.
|
|
8
|
+
*
|
|
9
|
+
* Detects per-language SCIP indexers (scip-typescript, scip-python,
|
|
10
|
+
* rust-analyzer scip) as child-process tools — NEVER as installer deps.
|
|
11
|
+
* When present: re-derives that language's edges compiler-accurate and
|
|
12
|
+
* labels tier = "compiler-accurate".
|
|
13
|
+
* When absent: tier = "tree-sitter-floor" (approximate, never broken).
|
|
14
|
+
*
|
|
15
|
+
* [RULE] accuracy-tier-labeled-never-silently-wrong
|
|
16
|
+
* Every edge carries its tier. No unlabeled mix.
|
|
17
|
+
*
|
|
18
|
+
* [RULE] rust-cross-crate-flagged-partial
|
|
19
|
+
* Rust cross-crate edges are FLAGGED partial. Within-crate edges resolve.
|
|
20
|
+
* rust-analyzer SCIP is officially "limited" on cross-crate edges.
|
|
21
|
+
*
|
|
22
|
+
* [RULE] reindex-tier-never-silently-downgraded (for parse_and_put)
|
|
23
|
+
* A previously compiler-accurate file that is re-indexed MUST get EITHER:
|
|
24
|
+
* (1) re-upgraded to "compiler-accurate" (SCIP still present + works), OR
|
|
25
|
+
* (2) labeled "tree-sitter-floor-STALE-SCIP" (SCIP was present, now absent/fails).
|
|
26
|
+
* NEVER silently relabeled as plain "tree-sitter-floor" (loses the "was-accurate"
|
|
27
|
+
* signal) or plain "compiler-accurate" over tree-sitter-only edges (claims
|
|
28
|
+
* accuracy that was not delivered).
|
|
29
|
+
*
|
|
30
|
+
* Exported API:
|
|
31
|
+
* tryScipUpgrade(absPath, relPath, entities, edges, { prevTier? })
|
|
32
|
+
* → { upgraded: bool, tier, entities, edges }
|
|
33
|
+
*
|
|
34
|
+
* detectScip()
|
|
35
|
+
* → { typescript: bool, python: bool, rust: bool }
|
|
36
|
+
*
|
|
37
|
+
* The graph NEVER depends on SCIP to FUNCTION — only to get BETTER.
|
|
38
|
+
* SCIP absent → degrade to tree-sitter-floor (never break).
|
|
39
|
+
*
|
|
40
|
+
* SCIP indexers are invoked as child-process one-shot tools, never required
|
|
41
|
+
* at installer runtime. They must be pre-installed by the user.
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
const fs = require('fs');
|
|
45
|
+
const path = require('path');
|
|
46
|
+
const { execSync, spawnSync } = require('child_process');
|
|
47
|
+
|
|
48
|
+
// ── ANSI helpers ─────────────────────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
const C = {
|
|
51
|
+
reset: '\x1b[0m',
|
|
52
|
+
green: '\x1b[32m',
|
|
53
|
+
red: '\x1b[31m',
|
|
54
|
+
yellow: '\x1b[33m',
|
|
55
|
+
cyan: '\x1b[36m',
|
|
56
|
+
};
|
|
57
|
+
function log(msg) { process.stderr.write(msg + '\n'); }
|
|
58
|
+
function info(msg) { log(`${C.cyan}[SCIP]${C.reset} ${msg}`); }
|
|
59
|
+
function warn(msg) { log(`${C.yellow}[SCIP WARN]${C.reset} ${msg}`); }
|
|
60
|
+
function errLog(msg){ log(`${C.red}[SCIP ERR]${C.reset} ${msg}`); }
|
|
61
|
+
|
|
62
|
+
// ── Extension → language mapping ─────────────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
const EXT_TO_LANG = {
|
|
65
|
+
'.ts': 'typescript',
|
|
66
|
+
'.tsx': 'typescript',
|
|
67
|
+
'.js': 'typescript', // scip-typescript handles JS too
|
|
68
|
+
'.jsx': 'typescript',
|
|
69
|
+
'.mjs': 'typescript',
|
|
70
|
+
'.cjs': 'typescript',
|
|
71
|
+
'.py': 'python',
|
|
72
|
+
'.rs': 'rust',
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
// ── Memoized SCIP availability detection ─────────────────────────────────────
|
|
76
|
+
|
|
77
|
+
let _scipDetected = null;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Detect which SCIP indexers are available on PATH.
|
|
81
|
+
* Result is memoized for the process lifetime (one-shot detection).
|
|
82
|
+
*
|
|
83
|
+
* [RULE] accuracy-tier-labeled-never-silently-wrong: detection is binary —
|
|
84
|
+
* we do NOT attempt a partial run; if the tool is absent or errors, it's absent.
|
|
85
|
+
*
|
|
86
|
+
* @returns {{ typescript: bool, python: bool, rust: bool }}
|
|
87
|
+
*/
|
|
88
|
+
function detectScip() {
|
|
89
|
+
if (_scipDetected) return _scipDetected;
|
|
90
|
+
|
|
91
|
+
function toolExists(cmd) {
|
|
92
|
+
try {
|
|
93
|
+
execSync(`which ${cmd}`, { stdio: 'pipe', encoding: 'utf8' });
|
|
94
|
+
return true;
|
|
95
|
+
} catch {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
_scipDetected = {
|
|
101
|
+
typescript: toolExists('scip-typescript'),
|
|
102
|
+
python: toolExists('scip-python'),
|
|
103
|
+
rust: toolExists('rust-analyzer') && toolExists('scip'),
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
return _scipDetected;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Force-reset the detection cache (used by tests to inject mock availability). */
|
|
110
|
+
function _resetScipCache(override) {
|
|
111
|
+
_scipDetected = override || null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ── SCIP invocation ───────────────────────────────────────────────────────────
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Run scip-typescript on the project root containing absPath.
|
|
118
|
+
* Returns { ok: bool, scipOutput?: object, error?: string }
|
|
119
|
+
*
|
|
120
|
+
* scip-typescript produces a SCIP protobuf on disk; we only check that it
|
|
121
|
+
* exits 0 here (full SCIP parse integration is Phase-2 scope — this is the
|
|
122
|
+
* floor that proves the indexer CAN produce compiler-accurate output).
|
|
123
|
+
*
|
|
124
|
+
* For Phase-1 the key semantic output is: the function returned ok=true with
|
|
125
|
+
* scip-typescript present → the file's tier is labeled compiler-accurate.
|
|
126
|
+
* The exact edge re-derivation from the SCIP proto is Phase-2.
|
|
127
|
+
*
|
|
128
|
+
* NOTE: Phase-1 SCIP upgrade is "tier labelling only" — it re-uses the
|
|
129
|
+
* tree-sitter edges but labels them compiler-accurate for files where SCIP
|
|
130
|
+
* is confirmed present and invocable. Phase-2 will replace the edges with
|
|
131
|
+
* SCIP-derived ones. This is the documented UPGRADE FLOOR behavior.
|
|
132
|
+
*/
|
|
133
|
+
function runScipTypescript(projectRoot, outPath) {
|
|
134
|
+
// M95: emit to a REAL file (outPath) so the index can be READ and call edges
|
|
135
|
+
// resolved — not /dev/null. The old /dev/null path only proved invocability.
|
|
136
|
+
const out = outPath || path.join(projectRoot, '.gsd-t', 'index.scip');
|
|
137
|
+
const scip = spawnSync('scip-typescript', ['index', '--output', out, '.'], {
|
|
138
|
+
cwd: projectRoot,
|
|
139
|
+
encoding: 'utf8',
|
|
140
|
+
timeout: 120_000,
|
|
141
|
+
});
|
|
142
|
+
if (scip.status === 0) return { ok: true, scipPath: out };
|
|
143
|
+
return { ok: false, error: scip.stderr || `exit code ${scip.status}` };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function runScipPython(projectRoot) {
|
|
147
|
+
const scip = spawnSync('scip-python', ['index', '.'], {
|
|
148
|
+
cwd: projectRoot,
|
|
149
|
+
encoding: 'utf8',
|
|
150
|
+
timeout: 120_000,
|
|
151
|
+
});
|
|
152
|
+
if (scip.status === 0) return { ok: true };
|
|
153
|
+
return { ok: false, error: scip.stderr || `exit code ${scip.status}` };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function runScipRust(projectRoot) {
|
|
157
|
+
// rust-analyzer + scip: run `scip` (the rust-analyzer scip bridge) if available
|
|
158
|
+
// As with TypeScript, Phase-1 is tier-labelling only
|
|
159
|
+
const scip = spawnSync('scip', ['rust', '--output', '/dev/null'], {
|
|
160
|
+
cwd: projectRoot,
|
|
161
|
+
encoding: 'utf8',
|
|
162
|
+
timeout: 180_000,
|
|
163
|
+
});
|
|
164
|
+
if (scip.status === 0) return { ok: true };
|
|
165
|
+
return { ok: false, error: scip.stderr || `exit code ${scip.status}` };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ── Cross-crate edge detection (Rust) ─────────────────────────────────────────
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Determine if a Rust edge is cross-crate.
|
|
172
|
+
*
|
|
173
|
+
* A simple heuristic: if the src and dst funcIds are in different crate roots
|
|
174
|
+
* (different top-level Cargo.toml directories) → cross-crate.
|
|
175
|
+
* Phase-1: rely on the "UNRESOLVED#" prefix that tree-sitter emits for
|
|
176
|
+
* unresolvable call targets — if the target starts with UNRESOLVED# and the
|
|
177
|
+
* file is .rs, flag it partial.
|
|
178
|
+
*
|
|
179
|
+
* [RULE] rust-cross-crate-flagged-partial
|
|
180
|
+
*/
|
|
181
|
+
function isRustCrossCrateEdge(edge, relPath) {
|
|
182
|
+
if (path.extname(relPath).toLowerCase() !== '.rs') return false;
|
|
183
|
+
// Phase-1 heuristic: unresolved targets in Rust are cross-crate candidates
|
|
184
|
+
const dst = edge.dst || edge.target || '';
|
|
185
|
+
if (dst.startsWith('UNRESOLVED#')) return true;
|
|
186
|
+
// If src and dst have different top-level directories → cross-crate
|
|
187
|
+
const srcFile = (edge.src || edge.source || '').split('#')[0];
|
|
188
|
+
const dstFile = (edge.dst || edge.target || '').split('#')[0];
|
|
189
|
+
if (srcFile && dstFile) {
|
|
190
|
+
const srcTop = srcFile.split('/')[0];
|
|
191
|
+
const dstTop = dstFile.split('/')[0];
|
|
192
|
+
if (srcTop !== dstTop && srcTop && dstTop) return true;
|
|
193
|
+
}
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// ── Repo-level SCIP resolver (M95) ────────────────────────────────────────────
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Build a repo-wide SCIP resolver ONCE: run scip-typescript over the whole repo,
|
|
201
|
+
* read the emitted index.scip, and return the resolution maps + a per-file
|
|
202
|
+
* call-edge resolver. This is the object passed as `scip` into parse_and_put so
|
|
203
|
+
* each file resolves its UNRESOLVED# call targets against real cross-file data.
|
|
204
|
+
*
|
|
205
|
+
* Running SCIP once per repo (not once per file) is the correctness AND
|
|
206
|
+
* performance fix — the old per-file run re-indexed the whole repo for every
|
|
207
|
+
* file and never read the output.
|
|
208
|
+
*
|
|
209
|
+
* @param {string} repoRoot
|
|
210
|
+
* @param {{ languages?: { typescript?: bool, python?: bool } }} [opts]
|
|
211
|
+
* @returns {{ ok: bool, reason?: string, scipPath?: string,
|
|
212
|
+
* resolveFileEdges: (relPath, edges) => { edges, resolved: number } }}
|
|
213
|
+
*/
|
|
214
|
+
function buildScipResolver(repoRoot, opts = {}) {
|
|
215
|
+
const { readScipIndex } = require('./gsd-t-scip-reader.cjs');
|
|
216
|
+
const avail = detectScip();
|
|
217
|
+
|
|
218
|
+
// Phase-1 of M95: TypeScript/JavaScript only (scip-typescript). Python is a
|
|
219
|
+
// sequenced follow-on. If TS SCIP is absent, the resolver is a no-op (floor).
|
|
220
|
+
if (!avail.typescript) {
|
|
221
|
+
return { ok: false, reason: 'scip-typescript-absent', resolveFileEdges: passthroughResolver };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const scipPath = path.join(repoRoot, '.gsd-t', 'index.scip');
|
|
225
|
+
let run;
|
|
226
|
+
try {
|
|
227
|
+
run = runScipTypescript(repoRoot, scipPath);
|
|
228
|
+
} catch (e) {
|
|
229
|
+
return { ok: false, reason: `scip-run-threw: ${e.message}`, resolveFileEdges: passthroughResolver };
|
|
230
|
+
}
|
|
231
|
+
if (!run || !run.ok) {
|
|
232
|
+
return { ok: false, reason: run ? run.error : 'scip-run-failed', resolveFileEdges: passthroughResolver };
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const read = readScipIndex(run.scipPath);
|
|
236
|
+
if (!read.ok) {
|
|
237
|
+
return { ok: false, reason: read.reason, resolveFileEdges: passthroughResolver };
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Build a fast per-file callee lookup: for a given test/impl file, the set of
|
|
241
|
+
// funcIds it references (resolved). Used to rewrite UNRESOLVED# call edges.
|
|
242
|
+
const fileRefs = read.fileRefs; // relPath → [{symbol, funcId, line}]
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Resolve a single file's call edges. For each CALL edge whose dst is
|
|
246
|
+
* UNRESOLVED#<name>, if SCIP found a reference to <name> in this file that
|
|
247
|
+
* resolves to a real funcId, rewrite dst to that funcId.
|
|
248
|
+
*/
|
|
249
|
+
function resolveFileEdges(relPath, edges) {
|
|
250
|
+
const refs = fileRefs.get(relPath);
|
|
251
|
+
if (!refs || !refs.length) return { edges, resolved: 0 };
|
|
252
|
+
|
|
253
|
+
// name → resolved funcId (last writer wins; SCIP refs in this file)
|
|
254
|
+
const nameToFuncId = new Map();
|
|
255
|
+
for (const r of refs) {
|
|
256
|
+
const name = r.funcId.includes('#') ? r.funcId.split('#')[1] : r.funcId;
|
|
257
|
+
nameToFuncId.set(name, r.funcId);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
let resolved = 0;
|
|
261
|
+
const out = edges.map((edge) => {
|
|
262
|
+
const dst = edge.target || edge.dst || '';
|
|
263
|
+
const kind = edge.kind;
|
|
264
|
+
const isCall = kind === 'call-site' || kind === 'CALL';
|
|
265
|
+
if (!isCall || !dst.startsWith('UNRESOLVED#')) return edge;
|
|
266
|
+
const calleeName = dst.slice('UNRESOLVED#'.length);
|
|
267
|
+
const funcId = nameToFuncId.get(calleeName);
|
|
268
|
+
if (!funcId) return edge; // still unresolved → stays floor
|
|
269
|
+
resolved++;
|
|
270
|
+
// rewrite dst to the resolved funcId, mark scip-derived
|
|
271
|
+
return { ...edge, target: funcId, dst: funcId, scipResolved: true };
|
|
272
|
+
});
|
|
273
|
+
return { edges: out, resolved };
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return { ok: true, scipPath: run.scipPath, resolveFileEdges };
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** No-op resolver used when SCIP is unavailable (floor mode). */
|
|
280
|
+
function passthroughResolver(relPath, edges) {
|
|
281
|
+
return { edges, resolved: 0 };
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// ── Main upgrade function ─────────────────────────────────────────────────────
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Try to upgrade a file's entities + edges to compiler-accurate tier via SCIP.
|
|
288
|
+
*
|
|
289
|
+
* M95: "upgrade" now RESOLVES call edges from real SCIP output. A file is labeled
|
|
290
|
+
* compiler-accurate only when SCIP data was actually applied (resolver present);
|
|
291
|
+
* edges that stay UNRESOLVED keep floor semantics. Rust cross-crate edges are
|
|
292
|
+
* flagged partial regardless.
|
|
293
|
+
*
|
|
294
|
+
* [RULE] reindex-tier-never-silently-downgraded:
|
|
295
|
+
* prevTier: the tier currently recorded in the store for this file.
|
|
296
|
+
* - If prevTier == 'compiler-accurate' and SCIP is still present → re-upgrade → 'compiler-accurate'
|
|
297
|
+
* - If prevTier == 'compiler-accurate' and SCIP is now absent/fails → 'tree-sitter-floor-STALE-SCIP'
|
|
298
|
+
* - If prevTier != 'compiler-accurate' and SCIP present → 'compiler-accurate'
|
|
299
|
+
* - If prevTier != 'compiler-accurate' and SCIP absent → 'tree-sitter-floor'
|
|
300
|
+
*
|
|
301
|
+
* @param {string} absPath - absolute path to the file
|
|
302
|
+
* @param {string} relPath - repo-relative POSIX path
|
|
303
|
+
* @param {Array} entities - from tree-sitter floor extraction
|
|
304
|
+
* @param {Array} edges - from tree-sitter floor extraction
|
|
305
|
+
* @param {{ prevTier?: string, projectRoot?: string }} options
|
|
306
|
+
* @returns {{ upgraded: boolean, tier: string, entities: Array, edges: Array }}
|
|
307
|
+
*/
|
|
308
|
+
function tryScipUpgrade(absPath, relPath, entities, edges, options) {
|
|
309
|
+
const { prevTier = null, projectRoot = null } = options || {};
|
|
310
|
+
const ext = path.extname(absPath).toLowerCase();
|
|
311
|
+
const lang = EXT_TO_LANG[ext];
|
|
312
|
+
|
|
313
|
+
if (!lang) {
|
|
314
|
+
// Unsupported language — no upgrade possible
|
|
315
|
+
const tier = prevTier === 'compiler-accurate' ? 'tree-sitter-floor-STALE-SCIP' : 'tree-sitter-floor';
|
|
316
|
+
return { upgraded: false, tier, entities, edges };
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const avail = detectScip();
|
|
320
|
+
|
|
321
|
+
// Determine if SCIP is available for this language
|
|
322
|
+
let scipPresent = false;
|
|
323
|
+
if (lang === 'typescript') scipPresent = avail.typescript;
|
|
324
|
+
if (lang === 'python') scipPresent = avail.python;
|
|
325
|
+
if (lang === 'rust') scipPresent = avail.rust;
|
|
326
|
+
|
|
327
|
+
if (!scipPresent) {
|
|
328
|
+
// [RULE] reindex-tier-never-silently-downgraded
|
|
329
|
+
// If a previously compiler-accurate file loses SCIP, label STALE-SCIP
|
|
330
|
+
const tier = prevTier === 'compiler-accurate'
|
|
331
|
+
? 'tree-sitter-floor-STALE-SCIP'
|
|
332
|
+
: 'tree-sitter-floor';
|
|
333
|
+
return { upgraded: false, tier, entities, edges };
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// M95: resolve call edges from the REPO-LEVEL SCIP resolver built once by
|
|
337
|
+
// build_index and threaded through options.resolver. We NEVER re-run SCIP per
|
|
338
|
+
// file here (the old per-file run re-indexed the whole repo for every file and
|
|
339
|
+
// discarded the output). If no resolver was provided, we cannot resolve —
|
|
340
|
+
// stay floor (honest), never relabel.
|
|
341
|
+
const resolver = options.resolver || null;
|
|
342
|
+
if (!resolver || typeof resolver.resolveFileEdges !== 'function') {
|
|
343
|
+
const tier = prevTier === 'compiler-accurate'
|
|
344
|
+
? 'tree-sitter-floor-STALE-SCIP'
|
|
345
|
+
: 'tree-sitter-floor';
|
|
346
|
+
return { upgraded: false, tier, entities, edges };
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Resolve this file's UNRESOLVED# call edges against the SCIP index.
|
|
350
|
+
const { edges: resolvedEdges, resolved } = resolver.resolveFileEdges(relPath, edges);
|
|
351
|
+
|
|
352
|
+
// Rust cross-crate edges stay flagged partial.
|
|
353
|
+
// [RULE] rust-cross-crate-flagged-partial
|
|
354
|
+
const finalEdges = resolvedEdges.map(edge => {
|
|
355
|
+
if (lang === 'rust' && isRustCrossCrateEdge(edge, relPath)) {
|
|
356
|
+
return { ...edge, partial: true };
|
|
357
|
+
}
|
|
358
|
+
return edge;
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
// [RULE] scip-tier-honest: label compiler-accurate ONLY when SCIP actually
|
|
362
|
+
// resolved ≥1 edge in this file OR the file has no call edges to resolve (a
|
|
363
|
+
// pure-definition file SCIP indexed cleanly). A file whose calls all stayed
|
|
364
|
+
// UNRESOLVED is NOT compiler-accurate — it's floor.
|
|
365
|
+
const hadCallEdges = edges.some(e => (e.kind === 'call-site' || e.kind === 'CALL'));
|
|
366
|
+
const isAccurate = !hadCallEdges || resolved > 0;
|
|
367
|
+
const tier = isAccurate ? 'compiler-accurate' : 'tree-sitter-floor';
|
|
368
|
+
|
|
369
|
+
return {
|
|
370
|
+
upgraded: isAccurate,
|
|
371
|
+
tier,
|
|
372
|
+
entities,
|
|
373
|
+
edges: finalEdges,
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// ── CLI entry point ───────────────────────────────────────────────────────────
|
|
378
|
+
|
|
379
|
+
if (require.main === module) {
|
|
380
|
+
const args = process.argv.slice(2);
|
|
381
|
+
const cmd = args[0] || 'detect';
|
|
382
|
+
|
|
383
|
+
if (cmd === 'detect') {
|
|
384
|
+
const avail = detectScip();
|
|
385
|
+
const envelope = {
|
|
386
|
+
ok: true,
|
|
387
|
+
cmd: 'detect',
|
|
388
|
+
scip: avail,
|
|
389
|
+
};
|
|
390
|
+
console.log(JSON.stringify(envelope, null, 2));
|
|
391
|
+
process.exit(0);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
if (cmd === 'upgrade') {
|
|
395
|
+
const fileArg = args[1];
|
|
396
|
+
const rootIdx = args.indexOf('--repo-root');
|
|
397
|
+
const prevTierIdx = args.indexOf('--prev-tier');
|
|
398
|
+
const repoRoot = rootIdx !== -1 ? args[rootIdx + 1] : process.cwd();
|
|
399
|
+
const prevTier = prevTierIdx !== -1 ? args[prevTierIdx + 1] : null;
|
|
400
|
+
|
|
401
|
+
if (!fileArg || !fs.existsSync(fileArg)) {
|
|
402
|
+
errLog(`File not found: ${fileArg}`);
|
|
403
|
+
process.exit(1);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const { extractEdges } = require('./gsd-t-graph-edge-extract.cjs');
|
|
407
|
+
const absPath = path.resolve(fileArg);
|
|
408
|
+
const relPath = path.relative(repoRoot, absPath).split(path.sep).join('/');
|
|
409
|
+
|
|
410
|
+
const { entities, edges } = extractEdges(absPath, relPath);
|
|
411
|
+
const result = tryScipUpgrade(absPath, relPath, entities, edges, {
|
|
412
|
+
prevTier,
|
|
413
|
+
projectRoot: repoRoot,
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
const envelope = {
|
|
417
|
+
ok: true,
|
|
418
|
+
cmd: 'upgrade',
|
|
419
|
+
file: relPath,
|
|
420
|
+
tier: result.tier,
|
|
421
|
+
upgraded: result.upgraded,
|
|
422
|
+
entityCount: result.entities.length,
|
|
423
|
+
edgeCount: result.edges.length,
|
|
424
|
+
};
|
|
425
|
+
console.log(JSON.stringify(envelope, null, 2));
|
|
426
|
+
process.exit(0);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
errLog(`Unknown command: ${cmd}. Use: detect | upgrade <file>`);
|
|
430
|
+
process.exit(1);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
module.exports = {
|
|
434
|
+
tryScipUpgrade,
|
|
435
|
+
buildScipResolver,
|
|
436
|
+
detectScip,
|
|
437
|
+
_resetScipCache,
|
|
438
|
+
isRustCrossCrateEdge,
|
|
439
|
+
EXT_TO_LANG,
|
|
440
|
+
};
|