@event4u/agent-config 17.1.0 → 17.2.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/.augment-plugin/marketplace.json +3 -3
- package/.augment-plugin/plugin.json +1 -1
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +92 -0
- package/README.md +2 -2
- package/dist/agent-src/commands/project/analyze.md +9 -6
- package/dist/agent-src/commands/project/map.md +58 -0
- package/dist/agent-src/commands/project.md +5 -3
- package/dist/agent-src/skills/code-intelligence/SKILL.md +6 -0
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/cli/registry.js +1 -1
- package/dist/cli-delegate/cmd_conformance.js +23 -39
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +39 -13
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +3 -3
- package/dist/discovery/dormancy-report.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +6 -5
- package/dist/discovery/trust-report.md +2 -2
- package/dist/discovery/workspaces.json +5 -4
- package/dist/hooks/dispatch.js +65 -74
- package/dist/hooks/dispatch.sha256 +1 -1
- package/dist/hooks/instructions_observer.chunk.cjs +301 -0
- package/dist/hooks/instructions_observer.chunk.cjs.sha256 +1 -0
- package/dist/mcp/registry-manifest.json +2 -2
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +33058 -22930
- package/package.json +4 -1
- package/src/config/continuity-surface.json +7 -0
- package/src/config/evaluator-budgets.json +4 -4
- package/src/config/gate-violation-baselines.json +3 -3
- package/src/config/gitignore-block.txt +6 -0
- package/src/config/hook-bundle-budget.json +17 -2
- package/src/config/hook-token-budget.json +1 -1
- package/src/config/packed-binary-manifest.json +27 -0
- package/src/config/skill-body-budget.json +7 -1
- package/src/scripts/_lib/bench_ab_complexity.ts +14 -2
- package/src/scripts/_lib/collector_record.ts +1 -0
- package/src/scripts/_lib/runtime_journal.ts +7 -7
- package/src/scripts/code_graph/build.ts +210 -10
- package/src/scripts/code_graph/cli.ts +168 -46
- package/src/scripts/code_graph/detect.ts +5 -4
- package/src/scripts/code_graph/docs_layer.ts +57 -0
- package/src/scripts/code_graph/extract.ts +58 -13
- package/src/scripts/code_graph/extract_more.ts +314 -0
- package/src/scripts/code_graph/laravel_overlay.ts +454 -0
- package/src/scripts/code_graph/map.ts +846 -0
- package/src/scripts/code_graph/map_model.ts +977 -0
- package/src/scripts/code_graph/map_verify.ts +140 -0
- package/src/scripts/code_graph/map_viewer/viewer.css +278 -0
- package/src/scripts/code_graph/map_viewer/viewer.js +756 -0
- package/src/scripts/code_graph/scip_reader.ts +201 -0
- package/src/scripts/code_graph/sqlite_store.ts +8 -1
- package/src/scripts/code_graph/types.ts +81 -15
- package/src/scripts/code_graph/validate.ts +16 -26
- package/src/scripts/code_graph/verbs.ts +7 -0
- package/src/scripts/hook_chunks.mjs +7 -0
- package/src/scripts/hook_manifest.json +1 -1
- package/src/scripts/hook_manifest.yaml +20 -0
- package/src/scripts/hooks/dispatch_hook.ts +2 -2
- package/src/scripts/hooks/instructions_observer_hook.ts +25 -6
- package/src/scripts/hooks/lazy_concern.ts +4 -0
- package/src/scripts/install-hooks.sh +5 -0
- package/src/scripts/lint_agents_layout.ts +2 -0
- package/src/scripts/lint_hook_manifest.ts +1 -0
- package/src/scripts/mcp_server/consumer_tool_catalog.json +53 -6
- package/src/scripts/mcp_server/graph_tools.ts +73 -1
- package/src/scripts/mcp_server/tools.ts +19 -36
- package/src/vendor/grammars/README.md +10 -5
- package/src/vendor/grammars/tree-sitter-go.wasm +0 -0
- package/src/vendor/grammars/tree-sitter-java.wasm +0 -0
- package/src/vendor/grammars/tree-sitter-python.wasm +0 -0
|
@@ -13,9 +13,12 @@
|
|
|
13
13
|
import { createHash } from 'node:crypto';
|
|
14
14
|
import * as fs from 'node:fs';
|
|
15
15
|
import * as path from 'node:path';
|
|
16
|
+
import { fileURLToPath } from 'node:url';
|
|
16
17
|
|
|
17
18
|
import { write_atomic } from '../_lib/fs_atomic.js';
|
|
18
19
|
import { extractFile, type FileExtract, type RawEdge } from './extract.js';
|
|
20
|
+
import { buildLaravelOverlay } from './laravel_overlay.js';
|
|
21
|
+
import { grammarWasmPath } from './loader.js';
|
|
19
22
|
import {
|
|
20
23
|
ALIAS_VIA,
|
|
21
24
|
aliasRulesFrom,
|
|
@@ -27,6 +30,8 @@ import {
|
|
|
27
30
|
resolveAlias,
|
|
28
31
|
resolvePsr4,
|
|
29
32
|
} from './resolution_tiers.js';
|
|
33
|
+
import { DOC_EXTS, docsLayer } from './docs_layer.js';
|
|
34
|
+
import { readScipIndex, type ScipDocument, scipEdges, supersededByScip } from './scip_reader.js';
|
|
30
35
|
import { emitSqliteTwin } from './sqlite_store.js';
|
|
31
36
|
import {
|
|
32
37
|
type CodeEdge,
|
|
@@ -34,6 +39,7 @@ import {
|
|
|
34
39
|
type EdgeConfidence,
|
|
35
40
|
EXPECTED_GRAMMAR_ABI,
|
|
36
41
|
EXT_LANG,
|
|
42
|
+
GRAMMAR_WASM,
|
|
37
43
|
isStatedResolution,
|
|
38
44
|
type Lang,
|
|
39
45
|
type ResolvedVia,
|
|
@@ -51,6 +57,10 @@ const IGNORE_DIRS = new Set([
|
|
|
51
57
|
'coverage',
|
|
52
58
|
'.cache',
|
|
53
59
|
'storage',
|
|
60
|
+
'__pycache__',
|
|
61
|
+
'venv',
|
|
62
|
+
'site-packages',
|
|
63
|
+
'target',
|
|
54
64
|
]);
|
|
55
65
|
|
|
56
66
|
export interface SourceFile {
|
|
@@ -62,7 +72,12 @@ export interface SourceFile {
|
|
|
62
72
|
// ── deterministic file discovery (path-confined) ─────────────────────────────
|
|
63
73
|
|
|
64
74
|
/** Recursively list launch-set source files under `root`, path-confined. */
|
|
65
|
-
|
|
75
|
+
const CODE_EXTS: ReadonlySet<string> = new Set(Object.keys(EXT_LANG));
|
|
76
|
+
|
|
77
|
+
/** Generated output under the project root: the engine never reads its own artefacts back. */
|
|
78
|
+
const GENERATED_DIRS: ReadonlySet<string> = new Set(['agents/map', 'agents/runtime']);
|
|
79
|
+
|
|
80
|
+
export function discoverFiles(root: string, exts: ReadonlySet<string> = CODE_EXTS): string[] {
|
|
66
81
|
const abs = path.resolve(root);
|
|
67
82
|
// Resolve the root itself first: a repo checked out under a symlinked
|
|
68
83
|
// path (e.g. macOS /tmp → /private/tmp) must still confine correctly —
|
|
@@ -85,12 +100,14 @@ export function discoverFiles(root: string): string[] {
|
|
|
85
100
|
const full = path.join(dir, e.name);
|
|
86
101
|
if (e.isSymbolicLink()) continue; // never follow symlinks
|
|
87
102
|
if (e.isDirectory()) {
|
|
88
|
-
if (
|
|
103
|
+
if (IGNORE_DIRS.has(e.name) || e.name.startsWith('.')) continue;
|
|
104
|
+
if (GENERATED_DIRS.has(path.relative(abs, full).split(path.sep).join('/'))) continue;
|
|
105
|
+
walk(full);
|
|
89
106
|
continue;
|
|
90
107
|
}
|
|
91
108
|
if (!e.isFile()) continue;
|
|
92
109
|
const ext = path.extname(e.name).toLowerCase();
|
|
93
|
-
if (!(ext
|
|
110
|
+
if (!exts.has(ext)) continue;
|
|
94
111
|
let real: string;
|
|
95
112
|
try {
|
|
96
113
|
real = fs.realpathSync(full);
|
|
@@ -207,6 +224,49 @@ function resolveSpecifier(fromFile: string, spec: string, fileIds: ReadonlySet<s
|
|
|
207
224
|
return null;
|
|
208
225
|
}
|
|
209
226
|
|
|
227
|
+
/**
|
|
228
|
+
* The file(s) a Python, Java or Go import names, or `[]` when the tree does not
|
|
229
|
+
* hold it (a standard-library or third-party module).
|
|
230
|
+
*
|
|
231
|
+
* python `pkg.mod` → `pkg/mod.py` or `pkg/mod/__init__.py` (any source root,
|
|
232
|
+
* unique suffix match); `.mod` / `..mod` relative to the importer.
|
|
233
|
+
* java `a.b.C` → `a/b/C.java` (unique suffix match).
|
|
234
|
+
* go `example.com/x/mod` → every non-test `.go` file of the most specific
|
|
235
|
+
* directory whose path ends the import path — a Go import names a
|
|
236
|
+
* package, and a package is all of its files.
|
|
237
|
+
*
|
|
238
|
+
* A suffix that matches two files is refused rather than guessed.
|
|
239
|
+
*/
|
|
240
|
+
export function resolveLangModule(lang: Lang, fromFile: string, spec: string, fileIds: readonly string[]): string[] {
|
|
241
|
+
const unique = (suffixes: string[]): string[] => {
|
|
242
|
+
const hits = fileIds.filter((f) => suffixes.some((s) => f === s || f.endsWith(`/${s}`)));
|
|
243
|
+
return hits.length === 1 ? hits : [];
|
|
244
|
+
};
|
|
245
|
+
if (lang === 'python') {
|
|
246
|
+
const rel = /^(\.+)(.*)$/.exec(spec);
|
|
247
|
+
if (rel) {
|
|
248
|
+
let dir = idDirname(fromFile);
|
|
249
|
+
for (let i = 1; i < (rel[1] as string).length; i++) dir = idDirname(dir);
|
|
250
|
+
const sub = (rel[2] as string).split('.').filter(Boolean).join('/');
|
|
251
|
+
const base = sub ? (dir && dir !== '.' ? `${dir}/${sub}` : sub) : dir;
|
|
252
|
+
return [`${base}.py`, `${base}/__init__.py`].filter((c) => fileIds.includes(c)).slice(0, 1);
|
|
253
|
+
}
|
|
254
|
+
const p = spec.split('.').join('/');
|
|
255
|
+
return unique([`${p}.py`, `${p}/__init__.py`]);
|
|
256
|
+
}
|
|
257
|
+
if (lang === 'java') return unique([`${spec.split('.').join('/')}.java`]);
|
|
258
|
+
if (lang === 'go') {
|
|
259
|
+
const goFiles = fileIds.filter((f) => f.endsWith('.go') && !f.endsWith('_test.go'));
|
|
260
|
+
let best = '';
|
|
261
|
+
for (const f of goFiles) {
|
|
262
|
+
const d = idDirname(f);
|
|
263
|
+
if ((spec === d || spec.endsWith(`/${d}`)) && d.length > best.length) best = d;
|
|
264
|
+
}
|
|
265
|
+
return best ? goFiles.filter((f) => idDirname(f) === best).sort() : [];
|
|
266
|
+
}
|
|
267
|
+
return [];
|
|
268
|
+
}
|
|
269
|
+
|
|
210
270
|
function push(map: Map<string, string[]>, k: string, v: string): void {
|
|
211
271
|
const arr = map.get(k);
|
|
212
272
|
if (arr) {
|
|
@@ -247,6 +307,8 @@ export function buildGraph(
|
|
|
247
307
|
files: readonly SourceFile[],
|
|
248
308
|
extracts: readonly FileExtract[],
|
|
249
309
|
resolution: ResolutionConfig = EMPTY_RESOLUTION_CONFIG,
|
|
310
|
+
scip: readonly ScipDocument[] = [],
|
|
311
|
+
docs: readonly SourceFile[] = [],
|
|
250
312
|
): CodeGraph {
|
|
251
313
|
const nodes = extracts.flatMap((e) => e.nodes);
|
|
252
314
|
const nodeIds = new Set(nodes.map((n) => n.id));
|
|
@@ -318,6 +380,9 @@ export function buildGraph(
|
|
|
318
380
|
const nodeKind = new Map(nodes.map((n) => [n.id, n.kind as string]));
|
|
319
381
|
const fileIds = new Set(nodes.filter((n) => n.kind === 'file').map((n) => n.id));
|
|
320
382
|
const scopes = new Map<string, FileScope>();
|
|
383
|
+
const fileIdList = [...fileIds].sort();
|
|
384
|
+
/** Go imports name a package of several files; the binding takes the first, these carry the rest. */
|
|
385
|
+
const extraImports: Array<{ source: string; target: string }> = [];
|
|
321
386
|
for (const ex of extracts) {
|
|
322
387
|
const locals = new Map<string, string[]>();
|
|
323
388
|
for (const n of ex.nodes) if (n.kind !== 'file') push(locals, n.label, n.id);
|
|
@@ -354,6 +419,26 @@ export function buildGraph(
|
|
|
354
419
|
if (hit) imports.set(local, { target: hit, exact: false, via: 'name-lookup' });
|
|
355
420
|
continue;
|
|
356
421
|
}
|
|
422
|
+
if (ex.lang === 'python' || ex.lang === 'java' || ex.lang === 'go') {
|
|
423
|
+
const hits = resolveLangModule(ex.lang, ex.file, spec, fileIdList);
|
|
424
|
+
const first = hits[0];
|
|
425
|
+
if (first === undefined) {
|
|
426
|
+
imports.set(local, {
|
|
427
|
+
target: r.importKind === 'named' ? `external:${spec}#${r.targetName}` : `external:${spec}`,
|
|
428
|
+
exact: true,
|
|
429
|
+
via: 'import-specifier',
|
|
430
|
+
});
|
|
431
|
+
continue;
|
|
432
|
+
}
|
|
433
|
+
const member = `${first}#${r.targetName}`;
|
|
434
|
+
imports.set(local, {
|
|
435
|
+
target: r.importKind === 'named' && nodeKind.has(member) ? member : first,
|
|
436
|
+
exact: true,
|
|
437
|
+
via: 'import-specifier',
|
|
438
|
+
});
|
|
439
|
+
for (const extra of hits.slice(1)) extraImports.push({ source: ex.file, target: extra });
|
|
440
|
+
continue;
|
|
441
|
+
}
|
|
357
442
|
// TIER 0: a relative specifier — the file says where it points.
|
|
358
443
|
// TIER 1 (2.3): tsconfig `paths`. `@shared/x` IS `src/shared/x`
|
|
359
444
|
// because the project declares it, so this is a fact and not a
|
|
@@ -470,6 +555,16 @@ export function buildGraph(
|
|
|
470
555
|
}
|
|
471
556
|
for (const r of ex.rawEdges) resolveRawEdge(r, ex);
|
|
472
557
|
}
|
|
558
|
+
for (const x of extraImports) {
|
|
559
|
+
emit({
|
|
560
|
+
source: x.source,
|
|
561
|
+
target: x.target,
|
|
562
|
+
relation: 'imports',
|
|
563
|
+
confidence: 'EXTRACTED',
|
|
564
|
+
resolved_via: 'import-specifier',
|
|
565
|
+
provider: 'native',
|
|
566
|
+
});
|
|
567
|
+
}
|
|
473
568
|
|
|
474
569
|
function resolveRawEdge(r: RawEdge, ex: FileExtract): void {
|
|
475
570
|
if (r.relation === 'member') {
|
|
@@ -659,6 +754,40 @@ export function buildGraph(
|
|
|
659
754
|
});
|
|
660
755
|
}
|
|
661
756
|
|
|
757
|
+
// Framework links no import states (routes, views, tables); rule-read,
|
|
758
|
+
// emitted only when the target exists. Also drops the base edges it
|
|
759
|
+
// proves false: a fluent `->middleware()` bound by name to some
|
|
760
|
+
// unrelated in-repo `middleware()`.
|
|
761
|
+
const overlay = buildLaravelOverlay(files, nodes, resolution);
|
|
762
|
+
let routeBuilderCalls = 0;
|
|
763
|
+
if (overlay.nodes.length > 0 || overlay.edges.length > 0) {
|
|
764
|
+
const kept = edges.filter((e) => !overlay.dropEdge(e));
|
|
765
|
+
routeBuilderCalls = edges.length - kept.length;
|
|
766
|
+
// Element-wise: a spread of ~200k edges overflows the call stack.
|
|
767
|
+
edges.length = 0;
|
|
768
|
+
for (const e of kept) edges.push(e);
|
|
769
|
+
for (const e of overlay.edges) edges.push(e);
|
|
770
|
+
for (const n of overlay.nodes) nodes.push(n);
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
if (docs.length > 0) {
|
|
774
|
+
const allFiles = new Set([...files.map((f) => f.path), ...docs.map((d) => d.path)]);
|
|
775
|
+
const layer = docsLayer(docs, allFiles);
|
|
776
|
+
for (const n of layer.nodes) nodes.push(n);
|
|
777
|
+
for (const e of layer.edges) edges.push(e);
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
// A project's own SCIP index states references the compiler resolved;
|
|
781
|
+
// each one supersedes the native name guess it duplicates.
|
|
782
|
+
if (scip.length > 0) {
|
|
783
|
+
const stated = scipEdges(scip, nodes);
|
|
784
|
+
const dropped = supersededByScip(stated, nodes);
|
|
785
|
+
const kept = edges.filter((e) => !dropped(e));
|
|
786
|
+
edges.length = 0;
|
|
787
|
+
for (const e of kept) edges.push(e);
|
|
788
|
+
for (const e of stated) edges.push(e);
|
|
789
|
+
}
|
|
790
|
+
|
|
662
791
|
// deterministic ordering
|
|
663
792
|
nodes.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
664
793
|
edges.sort((a, b) => {
|
|
@@ -669,7 +798,10 @@ export function buildGraph(
|
|
|
669
798
|
|
|
670
799
|
const counts = { EXTRACTED: 0, INFERRED: 0, AMBIGUOUS: 0 };
|
|
671
800
|
for (const e of edges) counts[e.confidence] += 1;
|
|
672
|
-
const suppressed = {
|
|
801
|
+
const suppressed: CodeGraph['suppressed_edge_counts'] = {
|
|
802
|
+
dynamic_no_candidate: suppressedKeys.size,
|
|
803
|
+
...(routeBuilderCalls > 0 ? { route_builder_call: routeBuilderCalls } : {}),
|
|
804
|
+
};
|
|
673
805
|
|
|
674
806
|
const languages = [...new Set(extracts.map((e) => e.lang))].sort() as Lang[];
|
|
675
807
|
|
|
@@ -682,6 +814,18 @@ export function buildGraph(
|
|
|
682
814
|
h.update('\n');
|
|
683
815
|
}
|
|
684
816
|
|
|
817
|
+
for (const d of [...docs].sort((a, b) => (a.path < b.path ? -1 : 1))) {
|
|
818
|
+
h.update(`doc\0${d.path}\0`);
|
|
819
|
+
h.update(createHash('sha256').update(d.source, 'utf-8').digest('hex'));
|
|
820
|
+
h.update('\n');
|
|
821
|
+
}
|
|
822
|
+
// The SCIP index is an input like any source file: a new index must not
|
|
823
|
+
// read as the same graph.
|
|
824
|
+
for (const d of [...scip].sort((a, b) => (a.path < b.path ? -1 : 1))) {
|
|
825
|
+
h.update(`scip\0${d.path}\0`);
|
|
826
|
+
for (const o of d.occurrences) h.update(`${o.line}:${o.definition ? 1 : 0}:${o.symbol}\n`);
|
|
827
|
+
}
|
|
828
|
+
|
|
685
829
|
return {
|
|
686
830
|
schema_version: SCHEMA_VERSION,
|
|
687
831
|
source_checksum: h.digest('hex'),
|
|
@@ -820,19 +964,57 @@ function contentHash(s: string): string {
|
|
|
820
964
|
* source of truth: a full build reproduces the identical graph. */
|
|
821
965
|
interface ExtractSidecar {
|
|
822
966
|
version: number;
|
|
967
|
+
/** `extractorFingerprint()` at write time — see there. */
|
|
968
|
+
fingerprint?: string;
|
|
823
969
|
files: Record<string, { hash: string; extract: FileExtract }>;
|
|
824
970
|
}
|
|
825
971
|
|
|
972
|
+
const _BUILD_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
973
|
+
let _fingerprint: string | null = null;
|
|
974
|
+
|
|
975
|
+
/**
|
|
976
|
+
* What a cached per-file extract was produced BY: schema × package version ×
|
|
977
|
+
* extractor source × every grammar's bytes. Keyed on the schema alone, an
|
|
978
|
+
* upgrade that changed how a file is extracted kept serving the old facts on
|
|
979
|
+
* `--update` for every file whose content had not moved. Hashing the extractor
|
|
980
|
+
* source rather than trusting a hand-bumped constant means a change nobody
|
|
981
|
+
* remembered to version still invalidates the cache.
|
|
982
|
+
*/
|
|
983
|
+
export function extractorFingerprint(): string {
|
|
984
|
+
if (_fingerprint !== null) return _fingerprint;
|
|
985
|
+
const h = createHash('sha256');
|
|
986
|
+
h.update(`schema:${SCHEMA_VERSION}\n`);
|
|
987
|
+
try {
|
|
988
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(_BUILD_DIR, '..', '..', '..', 'package.json'), 'utf-8')) as {
|
|
989
|
+
version?: string;
|
|
990
|
+
};
|
|
991
|
+
h.update(`package:${pkg.version ?? '?'}\n`);
|
|
992
|
+
} catch {
|
|
993
|
+
h.update('package:?\n');
|
|
994
|
+
}
|
|
995
|
+
for (const f of ['extract.ts', 'extract.js']) {
|
|
996
|
+
const p = path.join(_BUILD_DIR, f);
|
|
997
|
+
if (fs.existsSync(p)) h.update(fs.readFileSync(p));
|
|
998
|
+
}
|
|
999
|
+
for (const lang of Object.keys(GRAMMAR_WASM).sort() as Lang[]) {
|
|
1000
|
+
const p = grammarWasmPath(lang);
|
|
1001
|
+
h.update(`grammar:${lang}\n`);
|
|
1002
|
+
if (fs.existsSync(p)) h.update(fs.readFileSync(p));
|
|
1003
|
+
}
|
|
1004
|
+
_fingerprint = h.digest('hex').slice(0, 16);
|
|
1005
|
+
return _fingerprint;
|
|
1006
|
+
}
|
|
1007
|
+
|
|
826
1008
|
/** Exported so `refresh` (cli.ts) can locate/seed/promote the sidecar next to
|
|
827
1009
|
* a temp `--out` path without duplicating the naming convention. */
|
|
828
1010
|
export function sidecarPath(cachePath: string): string {
|
|
829
1011
|
return cachePath.replace(/\.json$/, '.extracts.json');
|
|
830
1012
|
}
|
|
831
1013
|
|
|
832
|
-
function readSidecar(p: string): ExtractSidecar | null {
|
|
1014
|
+
function readSidecar(p: string, fingerprint: string): ExtractSidecar | null {
|
|
833
1015
|
try {
|
|
834
1016
|
const s = JSON.parse(fs.readFileSync(p, 'utf-8')) as ExtractSidecar;
|
|
835
|
-
return s.version === SCHEMA_VERSION ? s : null;
|
|
1017
|
+
return s.version === SCHEMA_VERSION && s.fingerprint === fingerprint ? s : null;
|
|
836
1018
|
} catch {
|
|
837
1019
|
return null;
|
|
838
1020
|
}
|
|
@@ -848,7 +1030,7 @@ function readSidecar(p: string): ExtractSidecar | null {
|
|
|
848
1030
|
export async function buildFromRepo(
|
|
849
1031
|
root: string,
|
|
850
1032
|
cachePath: string | null,
|
|
851
|
-
opts: { update?: boolean } = {},
|
|
1033
|
+
opts: { update?: boolean; fingerprint?: string } = {},
|
|
852
1034
|
): Promise<BuildResult> {
|
|
853
1035
|
const abs = path.resolve(root);
|
|
854
1036
|
const fullPaths = discoverFiles(abs);
|
|
@@ -864,9 +1046,10 @@ export async function buildFromRepo(
|
|
|
864
1046
|
files.push({ path: rel, source });
|
|
865
1047
|
}
|
|
866
1048
|
|
|
867
|
-
const
|
|
1049
|
+
const fingerprint = opts.fingerprint ?? extractorFingerprint();
|
|
1050
|
+
const prior = opts.update && cachePath ? readSidecar(sidecarPath(cachePath), fingerprint) : null;
|
|
868
1051
|
const priorFiles = prior?.files ?? {};
|
|
869
|
-
const nextSidecar: ExtractSidecar = { version: SCHEMA_VERSION, files: {} };
|
|
1052
|
+
const nextSidecar: ExtractSidecar = { version: SCHEMA_VERSION, fingerprint, files: {} };
|
|
870
1053
|
const extracts: FileExtract[] = [];
|
|
871
1054
|
let reExtracted = 0;
|
|
872
1055
|
let reused = 0;
|
|
@@ -886,7 +1069,24 @@ export async function buildFromRepo(
|
|
|
886
1069
|
nextSidecar.files[f.path] = { hash, extract };
|
|
887
1070
|
}
|
|
888
1071
|
|
|
889
|
-
const
|
|
1072
|
+
const scipPath = path.join(abs, 'index.scip');
|
|
1073
|
+
let scip: ScipDocument[] = [];
|
|
1074
|
+
if (fs.existsSync(scipPath)) {
|
|
1075
|
+
try {
|
|
1076
|
+
scip = readScipIndex(fs.readFileSync(scipPath));
|
|
1077
|
+
} catch (e) {
|
|
1078
|
+
process.stderr.write(`code-graph: index.scip not read — ${(e as Error).message}\n`);
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
const docs: SourceFile[] = [];
|
|
1082
|
+
for (const full of discoverFiles(abs, DOC_EXTS)) {
|
|
1083
|
+
try {
|
|
1084
|
+
docs.push({ path: toPosixRel(abs, full), source: fs.readFileSync(full, 'utf-8') });
|
|
1085
|
+
} catch {
|
|
1086
|
+
continue;
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
const graph = buildGraph(files, extracts, readResolutionConfig(root), scip, docs);
|
|
890
1090
|
const json = serializeGraph(graph);
|
|
891
1091
|
if (cachePath) {
|
|
892
1092
|
write_atomic(cachePath, json);
|