@event4u/agent-config 17.3.0 → 17.4.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.
- package/.augment-plugin/marketplace.json +3 -3
- package/.augment-plugin/plugin.json +1 -1
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +164 -342
- package/CREDITS.md +2 -2
- package/README.md +1 -1
- package/dist/agent-src/skills/code-intelligence/SKILL.md +3 -0
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/cli/registry.js +2 -2
- package/dist/cli-delegate/{chunk-DHSSP7V3.js → chunk-5AYJGUA3.js} +0 -2
- package/dist/cli-delegate/chunk-DIFZBP4G.js +1336 -0
- package/dist/cli-delegate/{chunk-MT2WOOCY.js → chunk-WY5ZZPGL.js} +2 -2
- package/dist/cli-delegate/{chunk-ORJPBER2.js → chunk-X6LZ7W5A.js} +73 -10
- package/dist/cli-delegate/cmd_conformance.js +175 -183
- package/dist/cli-delegate/cmd_doctor.js +5 -5
- package/dist/cli-delegate/cmd_doctor_neighbours.js +3 -3
- package/dist/cli-delegate/cmd_explain.js +1 -1
- package/dist/cli-delegate/cmd_handoff.js +2 -2
- package/dist/cli-delegate/cmd_memory_get.js +1 -1
- package/dist/cli-delegate/cmd_route_audit.js +1 -1
- package/dist/cli-delegate/cmd_session_recycle.js +2 -2
- package/dist/cli-delegate/cmd_update.js +24 -13
- package/dist/cli-delegate/cmd_upgrade.js +2 -2
- package/dist/cli-delegate/dist-25AZAHOP.js +3240 -0
- package/dist/cli-delegate/dist-P7MFPMF3.js +411 -0
- package/dist/cli-delegate/dist-XAS5ZLQB.js +6 -0
- package/dist/cli-delegate/{upgrade_unattended-VVL6JLSX.js → upgrade_unattended-QCWOKTUN.js} +178 -163
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +3 -3
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +1 -1
- package/dist/discovery/dormancy-report.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +2 -2
- package/dist/discovery/trust-report.md +1 -1
- package/dist/discovery/workspaces.json +2 -2
- package/dist/hooks/code_graph_refresh.chunk.cjs +56 -27
- package/dist/hooks/code_graph_refresh.chunk.cjs.sha256 +1 -1
- package/dist/hooks/dispatch.js +982 -929
- package/dist/hooks/dispatch.sha256 +1 -1
- package/dist/hooks/egress_engine.chunk.cjs +56 -6
- package/dist/hooks/egress_engine.chunk.cjs.sha256 +1 -1
- package/dist/hooks/update_notice.chunk.cjs +914 -0
- package/dist/hooks/update_notice.chunk.cjs.sha256 +1 -0
- package/dist/mcp/registry-manifest.json +1 -1
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +71259 -66437
- package/dist/scripts/condense.js +9 -6
- package/package.json +4 -1
- package/src/config/evaluator-budgets.json +9 -8
- package/src/config/gate-violation-baselines.json +2 -2
- package/src/config/hook-bundle-budget.json +31 -6
- package/src/config/packed-binary-manifest.json +9 -0
- package/src/config/sigstore/npm_keys.json +50 -0
- package/src/config/sigstore/trust-material.json +9 -0
- package/src/config/sigstore/trusted_root.json +126 -0
- package/src/config/skill-body-budget.json +7 -1
- package/src/scripts/_cli/cmd_update.ts +48 -20
- package/src/scripts/_cli/cmd_upgrade.ts +7 -1
- package/src/scripts/_cli/upgrade_unattended.ts +90 -76
- package/src/scripts/_dispatch.bash +3 -2
- package/src/scripts/_lib/confusables.ts +0 -19
- package/src/scripts/_lib/egress_normalize.ts +53 -23
- package/src/scripts/_lib/egress_text.ts +5 -3
- package/src/scripts/_lib/egress_token.ts +7 -5
- package/src/scripts/_lib/gate_collector_experimental.ts +633 -0
- package/src/scripts/_lib/git_common_dir.ts +10 -0
- package/src/scripts/_lib/graph_feeder_record.ts +3 -2
- package/src/scripts/_lib/install_reach_checks.ts +32 -2
- package/src/scripts/_lib/lineage_backfill.ts +136 -0
- package/src/scripts/_lib/lineage_ledger.ts +211 -0
- package/src/scripts/_lib/lineage_map.ts +55 -0
- package/src/scripts/_lib/lineage_query.ts +140 -0
- package/src/scripts/_lib/lineage_seed.ts +116 -0
- package/src/scripts/_lib/lineage_watch.ts +249 -0
- package/src/scripts/_lib/link_crypto.ts +8 -4
- package/src/scripts/_lib/production_reach.ts +3 -3
- package/src/scripts/_lib/project_registry.ts +227 -0
- package/src/scripts/_lib/provenance_verify.ts +14 -242
- package/src/scripts/_lib/sigstore_provenance.ts +177 -0
- package/src/scripts/_lib/source_id.ts +48 -0
- package/src/scripts/_lib/source_shape.ts +4 -0
- package/src/scripts/_lib/unattended_gates.ts +6 -1
- package/src/scripts/advisories.ts +26 -10
- package/src/scripts/ai_council/council_egress.ts +5 -6
- package/src/scripts/bench_graph_feeder_latency.ts +10 -3
- package/src/scripts/check_dependency_floors.ts +7 -0
- package/src/scripts/check_egress_mutations.ts +1 -0
- package/src/scripts/code_graph/build.ts +67 -3
- package/src/scripts/code_graph/cli.ts +131 -28
- package/src/scripts/code_graph/conventions_default.ts +20 -1
- package/src/scripts/code_graph/detect.ts +5 -5
- package/src/scripts/code_graph/estate_cli.ts +135 -0
- package/src/scripts/code_graph/estate_index.ts +394 -0
- package/src/scripts/code_graph/estate_manifests.ts +223 -0
- package/src/scripts/code_graph/estate_map.ts +141 -0
- package/src/scripts/code_graph/extract.ts +4 -1
- package/src/scripts/code_graph/extract_more.ts +137 -2
- package/src/scripts/code_graph/graph_budget.ts +158 -0
- package/src/scripts/code_graph/graph_home.ts +80 -0
- package/src/scripts/code_graph/lineage_cli.ts +315 -0
- package/src/scripts/code_graph/map.ts +22 -13
- package/src/scripts/code_graph/map_model.ts +67 -12
- package/src/scripts/code_graph/map_verify.ts +68 -5
- package/src/scripts/code_graph/map_viewer/viewer.css +12 -0
- package/src/scripts/code_graph/map_viewer/viewer.js +45 -15
- package/src/scripts/code_graph/project_ref.ts +25 -0
- package/src/scripts/code_graph/projects_cli.ts +254 -0
- package/src/scripts/code_graph/providers/catalog.ts +14 -5
- package/src/scripts/code_graph/providers/django.ts +52 -9
- package/src/scripts/code_graph/providers/index.ts +2 -0
- package/src/scripts/code_graph/providers/rails.ts +204 -0
- package/src/scripts/code_graph/providers/spring.ts +38 -1
- package/src/scripts/code_graph/query.ts +9 -9
- package/src/scripts/code_graph/refresh_lock.ts +12 -8
- package/src/scripts/code_graph/repo_identity.ts +169 -0
- package/src/scripts/code_graph/sqlite_store.ts +22 -25
- package/src/scripts/code_graph/types.ts +6 -2
- package/src/scripts/condense.ts +6 -6
- package/src/scripts/hook_chunks.mjs +7 -0
- package/src/scripts/hook_manifest.json +1 -1
- package/src/scripts/hook_manifest.yaml +20 -17
- package/src/scripts/hooks/code_graph_refresh_hook.ts +10 -7
- package/src/scripts/hooks/dispatch_hook.ts +6 -7
- package/src/scripts/hooks/host_semantics.ts +72 -0
- package/src/scripts/hooks/lazy_concern.ts +4 -0
- package/src/scripts/hooks/user_notice.ts +105 -0
- package/src/scripts/lint_harvest_provenance.ts +137 -11
- package/src/scripts/mcp_server/consumer_tool_catalog.json +69 -15
- package/src/scripts/mcp_server/graph_tools.ts +161 -41
- package/src/scripts/network_sources.ts +24 -6
- package/src/scripts/schemas/provenance-mechanism.schema.json +46 -0
- package/src/scripts/update_notice_hook.ts +75 -7
- package/src/vendor/grammars/README.md +7 -6
- package/src/vendor/grammars/tree-sitter-ruby.wasm +0 -0
- /package/dist/cli-delegate/{chunk-FKYZOWVD.js → chunk-QBAFP67O.js} +0 -0
- /package/dist/cli-delegate/{chunk-BMGUGSY6.js → chunk-TQG6UMPC.js} +0 -0
|
@@ -66,7 +66,8 @@ import { gitEnv } from './_lib/git_env.js';
|
|
|
66
66
|
import { graphUntestedVerdict } from './_lib/graph_feeder_record.js';
|
|
67
67
|
import { LexicalIndex } from './_lib/lexical_index.js';
|
|
68
68
|
import { buildFromRepo } from './code_graph/build.js';
|
|
69
|
-
import { graphState
|
|
69
|
+
import { graphState } from './code_graph/detect.js';
|
|
70
|
+
import { graphHome } from './code_graph/graph_home.js';
|
|
70
71
|
import { loadGraph } from './code_graph/query.js';
|
|
71
72
|
import { emitSqliteTwin, sqliteTwinPath } from './code_graph/sqlite_store.js';
|
|
72
73
|
import type { CodeEdge, CodeGraph } from './code_graph/types.js';
|
|
@@ -159,7 +160,13 @@ export async function makeFixture(root: string, files: number, withGraph: boolea
|
|
|
159
160
|
git(dir, ['init', '-q']);
|
|
160
161
|
git(dir, ['add', '-A']);
|
|
161
162
|
git(dir, ['commit', '-q', '-m', 'fixture']);
|
|
162
|
-
if (withGraph)
|
|
163
|
+
if (withGraph) {
|
|
164
|
+
// A project home, as in every repository the feeder serves: the hooks the
|
|
165
|
+
// bench spawns run under their own user-global root, where a global
|
|
166
|
+
// graph home would not be found.
|
|
167
|
+
fs.mkdirSync(path.join(dir, 'agents'));
|
|
168
|
+
await buildFromRepo(dir, graphHome(dir).cachePath);
|
|
169
|
+
}
|
|
163
170
|
fs.writeFileSync(path.join(dir, 'src', 'service.ts'), 'export function handle(x: string): string {\n return x.trim();\n}\n');
|
|
164
171
|
return dir;
|
|
165
172
|
}
|
|
@@ -404,7 +411,7 @@ export function benchLoadSplit(opts: { graph: string; runs?: number | undefined
|
|
|
404
411
|
if (!v.ok) throw new Error(`invalid graph at ${opts.graph}: ${v.errors.slice(0, 3).join('; ')}`);
|
|
405
412
|
const graph = parsed as CodeGraph;
|
|
406
413
|
const t3 = performance.now();
|
|
407
|
-
const wrote = emitSqliteTwin(graph,
|
|
414
|
+
const wrote = emitSqliteTwin(graph, copy);
|
|
408
415
|
const t4 = performance.now();
|
|
409
416
|
const byId = new Map(graph.nodes.map((n) => [n.id, n]));
|
|
410
417
|
const outM = new Map<string, CodeEdge[]>();
|
|
@@ -60,6 +60,13 @@ export const EXACT_PIN_EXCEPTIONS: Readonly<Record<string, string>> = {
|
|
|
60
60
|
'which would make every grammar load throw rather than degrade — so the runtime ' +
|
|
61
61
|
'and its grammars move together, in one reviewed change, or not at all. ' +
|
|
62
62
|
'Refresh procedure: src/vendor/grammars/README.md.',
|
|
63
|
+
// The three sigstore modules are the trusted computing base of the
|
|
64
|
+
// unattended upgrade's provenance check (sigstore_provenance.ts): a
|
|
65
|
+
// patch release of any of them changes what "verified" means, so each
|
|
66
|
+
// version is reviewed rather than resolved. They move together.
|
|
67
|
+
'@sigstore/verify': 'Trusted computing base of the release provenance check; moves only in a reviewed change, with its two siblings.',
|
|
68
|
+
'@sigstore/bundle': 'Parses the bundle the provenance check verifies; pinned with @sigstore/verify for the same reason.',
|
|
69
|
+
'@sigstore/protobuf-specs': 'Decodes the pinned trust root and npm keys; pinned with @sigstore/verify for the same reason.',
|
|
63
70
|
};
|
|
64
71
|
|
|
65
72
|
/**
|
|
@@ -79,6 +79,16 @@ const CODE_EXTS: ReadonlySet<string> = new Set(Object.keys(EXT_LANG));
|
|
|
79
79
|
/** Generated output under the project root: the engine never reads its own artefacts back. */
|
|
80
80
|
const GENERATED_DIRS: ReadonlySet<string> = new Set(['agents/map', 'agents/runtime']);
|
|
81
81
|
|
|
82
|
+
/** Whether the discovery walk would enter every directory of repo-relative `rel` (posix). */
|
|
83
|
+
export function walkedPath(rel: string): boolean {
|
|
84
|
+
const dirs = rel.split('/').slice(0, -1);
|
|
85
|
+
for (let i = 0; i < dirs.length; i++) {
|
|
86
|
+
const d = dirs[i] as string;
|
|
87
|
+
if (IGNORE_DIRS.has(d) || d.startsWith('.') || GENERATED_DIRS.has(dirs.slice(0, i + 1).join('/'))) return false;
|
|
88
|
+
}
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
|
|
82
92
|
export function discoverFiles(root: string, exts: ReadonlySet<string> = CODE_EXTS): string[] {
|
|
83
93
|
return walkFiles(root, (name) => exts.has(path.extname(name).toLowerCase()));
|
|
84
94
|
}
|
|
@@ -128,7 +138,7 @@ function walkFiles(root: string, accept: (name: string) => boolean): string[] {
|
|
|
128
138
|
}
|
|
129
139
|
|
|
130
140
|
/** Manifests a provider may read to detect a stack, by basename. */
|
|
131
|
-
const MANIFEST_NAME = /^(?:composer\.json|package\.json|pyproject\.toml|requirements[\w.-]*\.txt|pom\.xml|build\.gradle(?:\.kts)?|go\.mod|[\w.-]+\.csproj|Gemfile)$/;
|
|
141
|
+
export const MANIFEST_NAME = /^(?:composer\.json|package\.json|pyproject\.toml|requirements[\w.-]*\.txt|pom\.xml|build\.gradle(?:\.kts)?|go\.mod|[\w.-]+\.csproj|Gemfile)$/;
|
|
132
142
|
|
|
133
143
|
/** The project's manifests and marker files, for provider detection (ADR-298). */
|
|
134
144
|
export function readProjectProbe(root: string): ProjectProbe {
|
|
@@ -266,6 +276,8 @@ function resolveSpecifier(fromFile: string, spec: string, fileIds: ReadonlySet<s
|
|
|
266
276
|
* go `example.com/x/mod` → every non-test `.go` file of the most specific
|
|
267
277
|
* directory whose path ends the import path — a Go import names a
|
|
268
278
|
* package, and a package is all of its files.
|
|
279
|
+
* ruby `require_relative 'x'` → `x.rb` beside the importer; `require 'a/b'`
|
|
280
|
+
* → the one `a/b.rb` under any directory (a load-path root).
|
|
269
281
|
* csharp `using A.B` → the one file declaring namespace `A.B` (`namespaces`).
|
|
270
282
|
* A namespace declared by several files is left to name lookup: it
|
|
271
283
|
* binds no file, since a `using` names no single one.
|
|
@@ -305,6 +317,15 @@ export function resolveLangModule(
|
|
|
305
317
|
}
|
|
306
318
|
return best ? goFiles.filter((f) => idDirname(f) === best).sort() : [];
|
|
307
319
|
}
|
|
320
|
+
if (lang === 'ruby') {
|
|
321
|
+
if (spec.startsWith('.')) {
|
|
322
|
+
const dir = idDirname(fromFile);
|
|
323
|
+
const joined = path.posix.normalize(dir && dir !== '.' ? `${dir}/${spec}` : spec);
|
|
324
|
+
const c = joined.endsWith('.rb') ? joined : `${joined}.rb`;
|
|
325
|
+
return fileIds.includes(c) ? [c] : [];
|
|
326
|
+
}
|
|
327
|
+
return unique([spec.endsWith('.rb') ? spec : `${spec}.rb`]);
|
|
328
|
+
}
|
|
308
329
|
if (lang === 'csharp') return [...(namespaces.get(spec) ?? [])];
|
|
309
330
|
return [];
|
|
310
331
|
}
|
|
@@ -429,6 +450,19 @@ export function buildGraph(
|
|
|
429
450
|
const extraImports: Array<{ source: string; target: string }> = [];
|
|
430
451
|
const namespaceFiles = new Map<string, string[]>();
|
|
431
452
|
for (const ex of extracts) for (const ns of ex.namespaces ?? []) push(namespaceFiles, ns, ex.file);
|
|
453
|
+
// C# name lookup is scoped by namespace, not by file: a type is visible
|
|
454
|
+
// from its own namespace, every enclosing one, and each `using`. Within
|
|
455
|
+
// that set a unique name IS the binding the compiler makes.
|
|
456
|
+
const csTypes = new Map<string, string[]>();
|
|
457
|
+
const csVisible = new Map<string, string[]>();
|
|
458
|
+
for (const ex of extracts) {
|
|
459
|
+
if (ex.lang !== 'csharp') continue;
|
|
460
|
+
for (const [id, ns] of Object.entries(ex.typeNamespaces ?? {})) push(csTypes, `${ns}\0${id.slice(id.indexOf('#') + 1)}`, id);
|
|
461
|
+
const seen = new Set<string>();
|
|
462
|
+
for (const ns of ex.namespaces ?? []) for (let p = ns; p; p = p.includes('.') ? p.slice(0, p.lastIndexOf('.')) : '') seen.add(p);
|
|
463
|
+
for (const r of ex.rawEdges) if (r.relation === 'imports' && r.importKind === 'namespace' && !r.localName && r.moduleSpecifier) seen.add(r.moduleSpecifier);
|
|
464
|
+
csVisible.set(ex.file, [...seen]);
|
|
465
|
+
}
|
|
432
466
|
for (const ex of extracts) {
|
|
433
467
|
const locals = new Map<string, string[]>();
|
|
434
468
|
for (const n of ex.nodes) if (n.kind !== 'file') push(locals, n.label, n.id);
|
|
@@ -466,7 +500,7 @@ export function buildGraph(
|
|
|
466
500
|
continue;
|
|
467
501
|
}
|
|
468
502
|
if (ex.lang === 'csharp' && (namespaceFiles.get(spec)?.length ?? 0) > 1) continue;
|
|
469
|
-
if (ex.lang === 'python' || ex.lang === 'java' || ex.lang === 'go' || ex.lang === 'csharp') {
|
|
503
|
+
if (ex.lang === 'python' || ex.lang === 'java' || ex.lang === 'go' || ex.lang === 'csharp' || ex.lang === 'ruby') {
|
|
470
504
|
const hits = resolveLangModule(ex.lang, ex.file, spec, fileIdList, namespaceFiles);
|
|
471
505
|
const first = hits[0];
|
|
472
506
|
if (first === undefined) {
|
|
@@ -549,6 +583,11 @@ export function buildGraph(
|
|
|
549
583
|
// reported as ones.
|
|
550
584
|
via: bound.via,
|
|
551
585
|
};
|
|
586
|
+
if (lang === 'csharp') {
|
|
587
|
+
const ids = new Set((csVisible.get(file) ?? []).flatMap((ns) => csTypes.get(`${ns}\0${name}`) ?? []));
|
|
588
|
+
const fit = [...ids].filter((id) => satisfies(nodeKind.get(id), want));
|
|
589
|
+
if (fit.length === 1) return { target: fit[0] as string, confidence: 'EXTRACTED', via: 'namespace-scope' };
|
|
590
|
+
}
|
|
552
591
|
if (scopeOnly) return null;
|
|
553
592
|
const wide =
|
|
554
593
|
want === 'constructible'
|
|
@@ -654,6 +693,9 @@ export function buildGraph(
|
|
|
654
693
|
// against a same-named declaration elsewhere in the repository is
|
|
655
694
|
// the defect this roadmap removes, applied to a second relation.
|
|
656
695
|
const hit = resolveInScope(ex.file, r.targetName, 'any', ex.lang, true);
|
|
696
|
+
// A C# type reference to a framework type (`List`, `Task`) names no
|
|
697
|
+
// project file, and a `symbol:` edge per mention would only be noise.
|
|
698
|
+
if (!hit && ex.lang === 'csharp') return;
|
|
657
699
|
emit({
|
|
658
700
|
source: r.sourceId,
|
|
659
701
|
target: hit?.target ?? `symbol:${r.targetName}`,
|
|
@@ -717,6 +759,28 @@ export function buildGraph(
|
|
|
717
759
|
}
|
|
718
760
|
return;
|
|
719
761
|
}
|
|
762
|
+
// scoped `C::m()` whose class the file binds by a stated mechanism
|
|
763
|
+
// (an import, PSR-4, a local declaration, a C# namespace): the class
|
|
764
|
+
// is known, so a method it declares or inherits is the target, and the
|
|
765
|
+
// edge is exactly as trustworthy as the class binding.
|
|
766
|
+
if ((r.callStyle === 'scoped' || r.callStyle === 'dynamic') && r.scopeName) {
|
|
767
|
+
const cls = resolveInScope(ex.file, r.scopeName, 'any', ex.lang, true);
|
|
768
|
+
const kind = cls ? nodeKind.get(cls.target) : undefined;
|
|
769
|
+
if (cls && isStatedResolution(cls.via) && (kind === 'class' || kind === 'interface' || kind === 'enum')) {
|
|
770
|
+
const m = resolveHierMethod(cls.target, r.targetName);
|
|
771
|
+
if (m) {
|
|
772
|
+
emit({
|
|
773
|
+
source: r.sourceId,
|
|
774
|
+
target: m,
|
|
775
|
+
relation: 'calls',
|
|
776
|
+
confidence: m.startsWith(`${cls.target}::`) ? cls.confidence : 'INFERRED',
|
|
777
|
+
resolved_via: cls.via,
|
|
778
|
+
provider: 'native',
|
|
779
|
+
});
|
|
780
|
+
return;
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
}
|
|
720
784
|
// scoped (Class::m unresolved / facade) or dynamic ($obj->m) → AMBIGUOUS
|
|
721
785
|
const cands = methodCandidates(r.targetName, ex.lang);
|
|
722
786
|
if (r.callStyle === 'dynamic' && cands.length === 0) {
|
|
@@ -1129,7 +1193,7 @@ export async function buildFromRepo(
|
|
|
1129
1193
|
write_atomic(sidecarPath(cachePath), JSON.stringify(nextSidecar));
|
|
1130
1194
|
// Derived SQLite twin (ADR-129): best-effort accelerator beside the
|
|
1131
1195
|
// canonical JSON — never fails the build, no-op on old Node.
|
|
1132
|
-
emitSqliteTwin(graph,
|
|
1196
|
+
emitSqliteTwin(graph, cachePath);
|
|
1133
1197
|
}
|
|
1134
1198
|
return { graph, json, fileCount: files.length, reExtracted, reused };
|
|
1135
1199
|
}
|
|
@@ -12,6 +12,11 @@
|
|
|
12
12
|
* is discarded and retried once
|
|
13
13
|
* auto-refresh on|off|status [--root P] opt this project in to the
|
|
14
14
|
* session-start background refresh
|
|
15
|
+
* projects list|add|remove|prune the user-global registry of
|
|
16
|
+
* projects this machine works in
|
|
17
|
+
* (ADR-299, `projects_cli.ts`)
|
|
18
|
+
* estate where-used|projects-using| estate questions from the local
|
|
19
|
+
* advisory-impact index alone (`estate_cli.ts`)
|
|
15
20
|
*
|
|
16
21
|
* `detect --format json` (Phase 2, road-to-reachable-code-memory) emits a
|
|
17
22
|
* single-line, stable-key-order JSON verdict on stdout — no timestamps, safe
|
|
@@ -29,8 +34,10 @@
|
|
|
29
34
|
* completed temp output — on budget-exceeded, build failure, or the child
|
|
30
35
|
* never producing output, the rename never happens and the OLD cache is left
|
|
31
36
|
* byte-for-byte untouched. The cache itself (default
|
|
32
|
-
* `agents/runtime/state/code-graph-v1.json
|
|
33
|
-
*
|
|
37
|
+
* `agents/runtime/state/code-graph-v1.json`; for a root without `agents/` or a
|
|
38
|
+
* project install, the user-global `runtime/graphs/<repo id>/state/`, ADR-299)
|
|
39
|
+
* is local-only and disposable — gitignored, rebuildable from source at any
|
|
40
|
+
* time; never a source of truth.
|
|
34
41
|
*
|
|
35
42
|
* impact --diff <rev> [--depth N] callers/dependents/test files
|
|
36
43
|
* reachable from the changed
|
|
@@ -46,16 +53,24 @@
|
|
|
46
53
|
* three-state staleness — so a caller can tell "no callers" from "no callers I
|
|
47
54
|
* would trust", and "dead" from "dead as of a graph 40 commits behind".
|
|
48
55
|
*
|
|
56
|
+
* Every verb that reads a graph also takes `--project <id|name|path>`: a
|
|
57
|
+
* project in the user-global registry, resolved to its root and graph home
|
|
58
|
+
* (ADR-299). Only a registered project resolves.
|
|
59
|
+
*
|
|
49
60
|
* Query subcommands (query/path/explain/affected) are added by the Phase-3
|
|
50
61
|
* query tier. Deterministic, LLM-free, no network. Exit codes:
|
|
51
62
|
* 0 ok · 1 not-found / validation-failed / refresh-budget-exceeded ·
|
|
52
63
|
* 2 usage · 3 internal.
|
|
53
64
|
*
|
|
54
65
|
* map [--root P] [--out DIR] [--open] draw the project map into
|
|
55
|
-
* [--no-verify] [--diff <rev>] agents/map/ (ADR-297)
|
|
66
|
+
* [--no-verify] [--diff <rev>] agents/map/ (ADR-297), or the
|
|
67
|
+
* graph home's map/ (ADR-299); renders it
|
|
56
68
|
* [--prs] (open PRs by area, via gh)
|
|
57
69
|
* at three widths when Playwright
|
|
58
70
|
* is present
|
|
71
|
+
* map --estate [--out DIR] [--no-verify] one page of the registered projects
|
|
72
|
+
* and their links, under the user-global
|
|
73
|
+
* home; refused inside a git work tree
|
|
59
74
|
*
|
|
60
75
|
* suggest-verb "<question>" (Phase 9 D3) print the verb
|
|
61
76
|
* (query/affected/path/explain)
|
|
@@ -81,16 +96,21 @@ import {
|
|
|
81
96
|
detectSources,
|
|
82
97
|
type GraphState,
|
|
83
98
|
graphState,
|
|
84
|
-
NATIVE_CACHE_REL,
|
|
85
99
|
pickSource,
|
|
86
100
|
} from './detect.js';
|
|
101
|
+
import { graphHome } from './graph_home.js';
|
|
87
102
|
import { suggestVerb } from './intent.js';
|
|
103
|
+
import { estateMain } from './estate_cli.js';
|
|
104
|
+
import { estateMapMain } from './estate_map.js';
|
|
105
|
+
import { touchHome } from './graph_budget.js';
|
|
106
|
+
import { resolveProjectRef } from './project_ref.js';
|
|
107
|
+
import { projectsMain, registerUse } from './projects_cli.js';
|
|
88
108
|
import { buildMap, graphGeneration, mapGeneration, recordOutcome } from './map.js';
|
|
89
109
|
import { cliPackage, usableCli } from './refresh_cli.js';
|
|
90
110
|
import {
|
|
91
|
-
|
|
92
|
-
LOCK_REL,
|
|
111
|
+
STALE_LOCK_MS,
|
|
93
112
|
acquire as acquireRefreshLock,
|
|
113
|
+
pidAlive,
|
|
94
114
|
readRefreshRecords,
|
|
95
115
|
reclaimMutexPath,
|
|
96
116
|
recordRefresh,
|
|
@@ -121,24 +141,34 @@ import {
|
|
|
121
141
|
untested,
|
|
122
142
|
type UntestedResult,
|
|
123
143
|
} from './verbs.js';
|
|
144
|
+
import { main as lineageMain } from './lineage_cli.js';
|
|
124
145
|
|
|
125
146
|
const _HERE = fileURLToPath(import.meta.url);
|
|
126
147
|
export const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..', '..');
|
|
127
148
|
/**
|
|
128
|
-
* The cache belongs to the project it describes: `<root>/agents/runtime/state
|
|
129
|
-
* the
|
|
149
|
+
* The cache belongs to the project it describes: `<root>/agents/runtime/state/`
|
|
150
|
+
* when the root has a project home, its user-global graph home otherwise
|
|
151
|
+
* (ADR-299) — the path MCP, the context hook and `detect` read. Anchoring it to
|
|
130
152
|
* the package tree put a consumer's graph inside `node_modules`, where `npm ci`
|
|
131
153
|
* deleted it, two projects sharing an install overwrote each other, and MCP
|
|
132
154
|
* answered "no graph" right after the CLI reported a fresh one.
|
|
133
155
|
*/
|
|
134
156
|
export function cacheFor(root: string): string {
|
|
135
|
-
return
|
|
157
|
+
return graphHome(root).cachePath;
|
|
158
|
+
}
|
|
159
|
+
/** A path relative to the root when it lies inside it; a graph home outside the root prints in full. */
|
|
160
|
+
function shown(root: string, p: string): string {
|
|
161
|
+
const rel = path.relative(root, p);
|
|
162
|
+
return rel.startsWith('..') || path.isAbsolute(rel) ? p : rel;
|
|
136
163
|
}
|
|
137
164
|
/** Local `tsx` binary — `refresh` spawns `build` as a child of THIS file
|
|
138
165
|
* through it, so a budget-exceeded kill only ever terminates the child. */
|
|
139
166
|
const TSX_BIN = path.join(REPO_ROOT, 'node_modules', '.bin', process.platform === 'win32' ? 'tsx.cmd' : 'tsx');
|
|
140
167
|
const DEFAULT_BUDGET_SECONDS = 60;
|
|
141
168
|
|
|
169
|
+
/** A refusal that names what the caller asked for wrongly; exits 1, not 3. */
|
|
170
|
+
class UsageError extends Error {}
|
|
171
|
+
|
|
142
172
|
function flag(argv: string[], name: string): string | null {
|
|
143
173
|
const i = argv.indexOf(name);
|
|
144
174
|
return i >= 0 && i + 1 < argv.length ? (argv[i + 1] as string) : null;
|
|
@@ -157,8 +187,13 @@ function flag(argv: string[], name: string): string | null {
|
|
|
157
187
|
*/
|
|
158
188
|
function resolveRoot(argv: string[]): string {
|
|
159
189
|
const explicit = flag(argv, '--root');
|
|
190
|
+
const project = flag(argv, '--project');
|
|
160
191
|
let candidate: string;
|
|
161
|
-
if (
|
|
192
|
+
if (project !== null) {
|
|
193
|
+
const r = resolveProjectRef(project);
|
|
194
|
+
if ('error' in r) throw new UsageError(r.error);
|
|
195
|
+
candidate = r.root;
|
|
196
|
+
} else if (explicit !== null) {
|
|
162
197
|
candidate = explicit;
|
|
163
198
|
} else {
|
|
164
199
|
try {
|
|
@@ -197,6 +232,7 @@ async function cmdBuild(argv: string[]): Promise<number> {
|
|
|
197
232
|
}
|
|
198
233
|
const update = argv.includes('--update');
|
|
199
234
|
const { graph, fileCount, reExtracted, reused } = await buildFromRepo(root, out, { update });
|
|
235
|
+
if (out === cacheFor(root)) registerUse(root, graph, git(root, 'rev-parse', 'HEAD'));
|
|
200
236
|
const c = graph.edge_confidence_counts;
|
|
201
237
|
process.stdout.write(
|
|
202
238
|
`✅ code-graph ${update ? 'updated' : 'built'} — ${fileCount} files · ${graph.nodes.length} nodes · ${graph.edges.length} edges\n` +
|
|
@@ -204,7 +240,7 @@ async function cmdBuild(argv: string[]): Promise<number> {
|
|
|
204
240
|
` edges: EXTRACTED ${c.EXTRACTED} · INFERRED ${c.INFERRED} · AMBIGUOUS ${c.AMBIGUOUS}\n` +
|
|
205
241
|
` resolved_via: ${resolvedViaHistogram(graph)}\n` +
|
|
206
242
|
(update ? ` incremental: ${reExtracted} re-extracted · ${reused} reused\n` : '') +
|
|
207
|
-
(out ? ` cache: ${
|
|
243
|
+
(out ? ` cache: ${shown(root, out)}\n` : '') +
|
|
208
244
|
legacyCacheNotice(root),
|
|
209
245
|
);
|
|
210
246
|
return 0;
|
|
@@ -261,12 +297,12 @@ function cmdValidate(argv: string[]): number {
|
|
|
261
297
|
// content-addressed cache key, so agreement here means the twin was emitted
|
|
262
298
|
// from THIS graph and not merely from a graph.
|
|
263
299
|
const twinState = twinChecksumState(p, (parsed as CodeGraph).source_checksum);
|
|
264
|
-
const rel =
|
|
300
|
+
const rel = shown(root, p);
|
|
265
301
|
if (twinState.kind === 'mismatch') {
|
|
266
302
|
process.stderr.write(
|
|
267
303
|
`❌ SQLite twin disagrees with the canonical JSON: twin ${twinState.twin} ≠ json ${twinState.json}\n` +
|
|
268
304
|
' The twin is derived and disposable — delete it and re-run `code-graph build`.\n' +
|
|
269
|
-
` ${
|
|
305
|
+
` ${shown(root, sqliteTwinPath(p))}\n`,
|
|
270
306
|
);
|
|
271
307
|
return 1;
|
|
272
308
|
}
|
|
@@ -309,7 +345,8 @@ function resolveGraph(argv: string[]): { g: LoadedGraph; note: string } | { err:
|
|
|
309
345
|
if (!picked) return { err: 'no code-graph source found — run `code_graph build` or ship a graph.json' };
|
|
310
346
|
if (picked.kind === 'scip') return { err: `${picked.note ?? 'SCIP detected'} — run \`code-graph build\` to merge it into the native graph` };
|
|
311
347
|
const staleNote = picked.stale ? ` ⚠ index is ${picked.commits_behind ?? 'N'} commit(s) behind — rebuild before trusting` : '';
|
|
312
|
-
|
|
348
|
+
touchHome(root);
|
|
349
|
+
return { g: loadGraph(picked.path, `${picked.kind}:${shown(root, picked.path)}`), note: staleNote };
|
|
313
350
|
}
|
|
314
351
|
|
|
315
352
|
function render(r: QueryResult, staleNote: string): number {
|
|
@@ -429,7 +466,8 @@ function isFlagValue(argv: string[], token: string): boolean {
|
|
|
429
466
|
*/
|
|
430
467
|
function stateOf(argv: string[], answeredBy?: string): GraphState {
|
|
431
468
|
const explicit = flag(argv, '--graph');
|
|
432
|
-
|
|
469
|
+
const root = resolveRoot(argv);
|
|
470
|
+
return graphState(root, explicit ?? answeredBy ?? cacheFor(root));
|
|
433
471
|
}
|
|
434
472
|
|
|
435
473
|
/** The two histogram lines + the staleness line, in one place so the four
|
|
@@ -635,7 +673,7 @@ function cmdDetect(argv: string[]): number {
|
|
|
635
673
|
}
|
|
636
674
|
for (const v of verdicts) {
|
|
637
675
|
const fresh = v.stale === undefined ? '' : v.stale ? ` · STALE${v.commits_behind !== undefined ? ` (${v.commits_behind} behind)` : ''}` : ' · fresh';
|
|
638
|
-
process.stdout.write(`${v.kind.padEnd(9)} ${
|
|
676
|
+
process.stdout.write(`${v.kind.padEnd(9)} ${shown(root, v.path)}${fresh}${v.note ? ` · ${v.note}` : ''}\n`);
|
|
639
677
|
}
|
|
640
678
|
const picked = pickSource(verdicts);
|
|
641
679
|
if (picked) process.stdout.write(`→ query would use: ${picked.kind}\n`);
|
|
@@ -649,10 +687,21 @@ function budgetSecondsOf(argv: string[]): number {
|
|
|
649
687
|
return Number.isFinite(n) ? n : DEFAULT_BUDGET_SECONDS;
|
|
650
688
|
}
|
|
651
689
|
|
|
652
|
-
/** Best-effort removal of a temp build output
|
|
653
|
-
*
|
|
654
|
-
|
|
655
|
-
|
|
690
|
+
/** Best-effort removal of a temp build output, its sidecar, its SQLite twin and
|
|
691
|
+
* the twin's own half-written temp. Never throws — cleanup failure must not mask
|
|
692
|
+
* the real result (budget-exceeded / error). */
|
|
693
|
+
export function cleanupTemp(tmpOut: string): void {
|
|
694
|
+
const twin = sqliteTwinPath(tmpOut);
|
|
695
|
+
let twinTemps: string[] = [];
|
|
696
|
+
try {
|
|
697
|
+
twinTemps = fs
|
|
698
|
+
.readdirSync(path.dirname(tmpOut))
|
|
699
|
+
.filter((f) => f.startsWith(`${path.basename(twin)}.tmp-`))
|
|
700
|
+
.map((f) => path.join(path.dirname(tmpOut), f));
|
|
701
|
+
} catch {
|
|
702
|
+
/* the directory is gone, so nothing in it is left to remove */
|
|
703
|
+
}
|
|
704
|
+
for (const p of [tmpOut, sidecarPath(tmpOut), twin, ...twinTemps]) {
|
|
656
705
|
try {
|
|
657
706
|
fs.unlinkSync(p);
|
|
658
707
|
} catch {
|
|
@@ -661,6 +710,33 @@ function cleanupTemp(tmpOut: string): void {
|
|
|
661
710
|
}
|
|
662
711
|
}
|
|
663
712
|
|
|
713
|
+
/**
|
|
714
|
+
* Remove what a refresh killed before its own cleanup left behind. A file is
|
|
715
|
+
* only an orphan when the pid in its name is gone AND it is older than a stale
|
|
716
|
+
* lock: the pid check alone would race a refresh that is starting right now.
|
|
717
|
+
*/
|
|
718
|
+
function sweepOrphanedTemps(out: string): void {
|
|
719
|
+
const dir = path.dirname(out);
|
|
720
|
+
const prefix = `.${path.basename(out)}.refresh-tmp-`;
|
|
721
|
+
let names: string[];
|
|
722
|
+
try {
|
|
723
|
+
names = fs.readdirSync(dir);
|
|
724
|
+
} catch {
|
|
725
|
+
return;
|
|
726
|
+
}
|
|
727
|
+
for (const name of names) {
|
|
728
|
+
if (!name.startsWith(prefix)) continue;
|
|
729
|
+
const pid = Number(name.slice(prefix.length).split('-')[0]);
|
|
730
|
+
if (!Number.isInteger(pid) || pid <= 0 || pidAlive(pid)) continue;
|
|
731
|
+
const full = path.join(dir, name);
|
|
732
|
+
try {
|
|
733
|
+
if (Date.now() - fs.statSync(full).mtimeMs > STALE_LOCK_MS) fs.unlinkSync(full);
|
|
734
|
+
} catch {
|
|
735
|
+
/* raced another sweeper — fine */
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
|
|
664
740
|
/**
|
|
665
741
|
* Run `build` (optionally `--update`) as a CHILD process against a temp
|
|
666
742
|
* `--out` path, under a hard wall-clock budget. Only on a clean, in-budget
|
|
@@ -736,6 +812,7 @@ function runBudgetedBuild(root: string, out: string, update: boolean, budgetSeco
|
|
|
736
812
|
return 1;
|
|
737
813
|
}
|
|
738
814
|
const dir = path.dirname(out);
|
|
815
|
+
sweepOrphanedTemps(out);
|
|
739
816
|
const tmpOut = path.join(dir, `.${path.basename(out)}.refresh-tmp-${process.pid}-${randomBytes(4).toString('hex')}.json`);
|
|
740
817
|
if (update) {
|
|
741
818
|
const realSidecar = sidecarPath(out);
|
|
@@ -795,7 +872,13 @@ function runBudgetedBuild(root: string, out: string, update: boolean, budgetSeco
|
|
|
795
872
|
fs.renameSync(tmpOut, out); // same-dir → atomic; real cache only ever changes here
|
|
796
873
|
const tmpSidecar = sidecarPath(tmpOut);
|
|
797
874
|
if (fs.existsSync(tmpSidecar)) fs.renameSync(tmpSidecar, sidecarPath(out));
|
|
798
|
-
|
|
875
|
+
// The twin records the JSON's size and mtime; a rename keeps both, so the
|
|
876
|
+
// promoted twin matches the promoted JSON. Without a twin, the old one goes:
|
|
877
|
+
// it describes a JSON that no longer exists.
|
|
878
|
+
const tmpTwin = sqliteTwinPath(tmpOut);
|
|
879
|
+
if (fs.existsSync(tmpTwin)) fs.renameSync(tmpTwin, sqliteTwinPath(out));
|
|
880
|
+
else fs.rmSync(sqliteTwinPath(out), { force: true });
|
|
881
|
+
process.stdout.write(`✅ code-graph refreshed — cache: ${shown(root, out)}\n`);
|
|
799
882
|
return 0;
|
|
800
883
|
}
|
|
801
884
|
|
|
@@ -844,6 +927,7 @@ async function cmdRefresh(argv: string[]): Promise<number> {
|
|
|
844
927
|
record('failed', `build exited ${rc}`);
|
|
845
928
|
return rc;
|
|
846
929
|
}
|
|
930
|
+
if (out === cacheFor(root)) registerUse(root, null, git(root, 'rev-parse', 'HEAD'));
|
|
847
931
|
if (argv.includes('--map') && mapIsBehind(root, out)) {
|
|
848
932
|
const mrc = await drawMap(['--root', root, '--no-build', '--no-verify']);
|
|
849
933
|
record(mrc === 0 ? 'refreshed-with-map' : 'map-failed', mrc === 0 ? undefined : `map exited ${mrc}`);
|
|
@@ -864,8 +948,18 @@ async function cmdRefresh(argv: string[]): Promise<number> {
|
|
|
864
948
|
function cmdAutoRefresh(argv: string[]): number {
|
|
865
949
|
const root = resolveRoot(argv);
|
|
866
950
|
const action = argv.find((a) => !a.startsWith('--') && !isFlagValue(argv, a)) ?? 'status';
|
|
867
|
-
const
|
|
951
|
+
const home = graphHome(root);
|
|
952
|
+
const marker = home.autoRefreshPath;
|
|
868
953
|
if (action === 'on') {
|
|
954
|
+
if (home.kind === 'global') {
|
|
955
|
+
// The session concern serves project homes only; its chunk cannot carry the
|
|
956
|
+
// user-global resolver, so an opt-in here would be a promise nothing keeps.
|
|
957
|
+
process.stderr.write(
|
|
958
|
+
`code-graph: auto-refresh needs a project home (agents/ or a project install); ${root} has none.\n` +
|
|
959
|
+
'Its graph lives in the user-global home — refresh it with `code-graph refresh`.\n',
|
|
960
|
+
);
|
|
961
|
+
return 1;
|
|
962
|
+
}
|
|
869
963
|
fs.mkdirSync(path.dirname(marker), { recursive: true });
|
|
870
964
|
fs.writeFileSync(marker, `${JSON.stringify({ enabled_at: new Date().toISOString() })}\n`);
|
|
871
965
|
const cli = usableCli(root);
|
|
@@ -896,10 +990,10 @@ function cmdAutoRefresh(argv: string[]): number {
|
|
|
896
990
|
const cli = usableCli(root);
|
|
897
991
|
const last = readRefreshRecords(root);
|
|
898
992
|
const line = (r: (typeof last)['refresh']): string => (r ? `${r.outcome} at ${r.at}${r.detail ? ` (${r.detail})` : ''}` : 'never');
|
|
899
|
-
const mapDir =
|
|
993
|
+
const mapDir = home.mapDir;
|
|
900
994
|
const gen = graphGeneration(cacheFor(root));
|
|
901
995
|
const drawnFrom = mapGeneration(mapDir);
|
|
902
|
-
const mutex = reclaimMutexPath(
|
|
996
|
+
const mutex = reclaimMutexPath(home.lockPath);
|
|
903
997
|
process.stdout.write(
|
|
904
998
|
`auto-refresh: ${fs.existsSync(marker) ? 'on' : 'off'}\n` +
|
|
905
999
|
`cli: ${'reason' in cli ? (cli.reason === 'no-cli' ? 'none usable — nothing will run' : `${cli.cli} ${cliPackage(cli.cli)?.version ?? '?'} — too old, nothing will run`) : `${cli.cli} ${cliPackage(cli.cli)?.version ?? '?'}`}\n` +
|
|
@@ -908,7 +1002,7 @@ function cmdAutoRefresh(argv: string[]): number {
|
|
|
908
1002
|
`graph: ${computeVerdict(root, cacheFor(root)).verdict}\n` +
|
|
909
1003
|
`map: ${!fs.existsSync(mapDir) ? 'absent' : drawnFrom === null ? 'present (generation unknown — redrawn on the next refresh)' : drawnFrom === gen ? 'present, drawn from the current graph' : 'present, drawn from an older graph'}\n` +
|
|
910
1004
|
(fs.existsSync(mutex) && mutexAgeMs(mutex) > 60_000
|
|
911
|
-
? `reclaim mutex: ${
|
|
1005
|
+
? `reclaim mutex: ${shown(root, mutex)} left by a crash — stale locks stay held until it is removed by hand\n`
|
|
912
1006
|
: ''),
|
|
913
1007
|
);
|
|
914
1008
|
return 0;
|
|
@@ -928,7 +1022,7 @@ function mutexAgeMs(p: string): number {
|
|
|
928
1022
|
* copied or restored file carries wrongly. No map → never draw one unbidden.
|
|
929
1023
|
*/
|
|
930
1024
|
function mapIsBehind(root: string, cache: string): boolean {
|
|
931
|
-
const mapDir =
|
|
1025
|
+
const mapDir = graphHome(root).mapDir;
|
|
932
1026
|
if (!fs.existsSync(mapDir)) return false;
|
|
933
1027
|
const gen = graphGeneration(cache);
|
|
934
1028
|
return gen !== null && mapGeneration(mapDir) !== gen;
|
|
@@ -940,6 +1034,7 @@ function mapIsBehind(root: string, cache: string): boolean {
|
|
|
940
1034
|
* Builds or updates the graph first when it is absent or behind.
|
|
941
1035
|
*/
|
|
942
1036
|
async function cmdMap(argv: string[]): Promise<number> {
|
|
1037
|
+
if (argv.includes('--estate')) return estateMapMain(argv);
|
|
943
1038
|
const root = resolveRoot(argv);
|
|
944
1039
|
if (!fs.existsSync(root)) {
|
|
945
1040
|
process.stderr.write(`code-graph: root not found: ${root}\n`);
|
|
@@ -990,7 +1085,7 @@ async function drawMap(argv: string[]): Promise<number> {
|
|
|
990
1085
|
? ` diff ${r.model.diff.rev}: ${r.model.diff.changed.length} changed · ${r.model.diff.impacted.length} linked into them · ${r.model.diff.areas.length} area(s) touched\n`
|
|
991
1086
|
: '') +
|
|
992
1087
|
` wrote: ${r.written.length} files under ${rel}/ — open ${path.join(rel, 'index.html')}\n` +
|
|
993
|
-
(fs.existsSync(
|
|
1088
|
+
(fs.existsSync(graphHome(root).autoRefreshPath)
|
|
994
1089
|
? ''
|
|
995
1090
|
: ' keep it current at every agent session start: agent-config code-graph auto-refresh on\n'),
|
|
996
1091
|
);
|
|
@@ -1082,15 +1177,23 @@ export async function main(argv: string[] = process.argv.slice(2)): Promise<numb
|
|
|
1082
1177
|
if (sub === 'untested') return cmdUntested(argv.slice(1));
|
|
1083
1178
|
if (sub === 'dead') return cmdDead(argv.slice(1));
|
|
1084
1179
|
if (sub === 'suggest-verb') return cmdSuggestVerb(argv.slice(1));
|
|
1180
|
+
if (sub === 'lineage' || (sub === 'map' && argv.includes('--lineage')))
|
|
1181
|
+
return await lineageMain(sub === 'lineage' ? argv.slice(1) : ['map', ...argv.slice(1)]);
|
|
1085
1182
|
if (sub === 'map') return await cmdMap(argv.slice(1));
|
|
1086
1183
|
if (sub === 'outcome') return cmdOutcome(argv.slice(1));
|
|
1087
1184
|
if (sub === 'auto-refresh') return cmdAutoRefresh(argv.slice(1));
|
|
1185
|
+
if (sub === 'projects') return projectsMain(argv.slice(1));
|
|
1186
|
+
if (sub === 'estate') return estateMain(argv.slice(1));
|
|
1088
1187
|
process.stderr.write(
|
|
1089
1188
|
'usage: code_graph/cli.ts <build|validate|detect|refresh|query|explain|affected|path|' +
|
|
1090
|
-
'impact|tests-for|untested|dead|suggest-verb|map|outcome|auto-refresh> [options]\n',
|
|
1189
|
+
'impact|tests-for|untested|dead|suggest-verb|map|outcome|auto-refresh|projects|estate|lineage> [options]\n',
|
|
1091
1190
|
);
|
|
1092
1191
|
return 2;
|
|
1093
1192
|
} catch (e) {
|
|
1193
|
+
if (e instanceof UsageError) {
|
|
1194
|
+
process.stderr.write(`code-graph: ${e.message}\n`);
|
|
1195
|
+
return 1;
|
|
1196
|
+
}
|
|
1094
1197
|
process.stderr.write(`code-graph: internal error: ${(e as Error).message}\n`);
|
|
1095
1198
|
return 3;
|
|
1096
1199
|
}
|
|
@@ -19,6 +19,21 @@ import type { SourceFile } from './build.js';
|
|
|
19
19
|
import type { CodeEdge, CodeNode, ResolvedVia } from './types.js';
|
|
20
20
|
import { EXT_LANG, MAX_FILE_BYTES } from './types.js';
|
|
21
21
|
|
|
22
|
+
/** A line that is a log, print or throw call up to this point: its literals are messages, not files. */
|
|
23
|
+
const MESSAGE_CALL =
|
|
24
|
+
/(?:\bconsole\.\w+|\blog(?:ger|ging)?\.\w+|\bLog::\w+|\b(?:print|println|printf|echo|error_log)\b|\bthrow\s+new\s+\w+)\s*\(?/;
|
|
25
|
+
|
|
26
|
+
/** `src` with comments and Python docstrings blanked to spaces, every offset and newline kept. */
|
|
27
|
+
function maskComments(src: string, lang: string | undefined): string {
|
|
28
|
+
const blank = (m: string): string => m.replace(/[^\n]/g, ' ');
|
|
29
|
+
let out = src
|
|
30
|
+
.replace(/\/\*[\s\S]*?\*\//g, blank)
|
|
31
|
+
.replace(/(^|[^:'"\\])(\/\/[^\n]*)/g, (_m, pre: string, c: string) => pre + blank(c));
|
|
32
|
+
if (lang === 'python' || lang === 'php') out = out.replace(/(^|[ \t])(#[^\n]*)/gm, (_m, pre: string, c: string) => pre + blank(c));
|
|
33
|
+
if (lang === 'python') out = out.replace(/("""|''')[\s\S]*?\1/g, blank);
|
|
34
|
+
return out;
|
|
35
|
+
}
|
|
36
|
+
|
|
22
37
|
/** The text right before a JS/TS module specifier: `from`, `import`, `import(`, `require(`. */
|
|
23
38
|
const IMPORT_POSITION = /(?:\bfrom|\bimport|\bimport\s*\(|\brequire\s*\()\s*$/;
|
|
24
39
|
|
|
@@ -177,8 +192,12 @@ export function conventionsDefault(input: DefaultInput): { nodes: CodeNode[]; ed
|
|
|
177
192
|
const jsLike = lang === 'typescript' || lang === 'javascript';
|
|
178
193
|
if (on['literal-path']) {
|
|
179
194
|
// Single and double quotes only: a backtick span is usually a mention in a comment.
|
|
180
|
-
|
|
195
|
+
// Comments and docstrings are blanked first, so a path they mention is never read.
|
|
196
|
+
const code = maskComments(f.source, lang);
|
|
197
|
+
for (const m of code.matchAll(/(['"])([^'"`\s\\${}()<>|,;]{3,200})\1/g)) {
|
|
181
198
|
const lit = m[2] as string;
|
|
199
|
+
const lineStart = code.lastIndexOf('\n', m.index as number) + 1;
|
|
200
|
+
if (MESSAGE_CALL.test(code.slice(lineStart, m.index as number))) continue;
|
|
182
201
|
// A module specifier belongs to the import tier: resolved, it is an
|
|
183
202
|
// `imports` edge already; unresolved, it is an external module — a
|
|
184
203
|
// read in neither case.
|
|
@@ -19,6 +19,7 @@ import * as path from 'node:path';
|
|
|
19
19
|
|
|
20
20
|
import { load_agent_settings } from '../_lib/agent_settings.js';
|
|
21
21
|
import { hardenedSpawnEnv } from '../_lib/spawn_env.js';
|
|
22
|
+
import { projectHome } from './graph_home.js';
|
|
22
23
|
import { EXT_LANG } from './types.js';
|
|
23
24
|
import { validateGraph } from './validate.js';
|
|
24
25
|
|
|
@@ -226,9 +227,6 @@ export function computeVerdict(root: string, nativeCache: string): VerdictJSON {
|
|
|
226
227
|
};
|
|
227
228
|
}
|
|
228
229
|
|
|
229
|
-
/** Cache path a repository root's native graph lives at, repo-relative. */
|
|
230
|
-
export const NATIVE_CACHE_REL = path.join('agents', 'runtime', 'state', 'code-graph-v1.json');
|
|
231
|
-
|
|
232
230
|
/**
|
|
233
231
|
* The four-state staleness token every consumer of the graph reports.
|
|
234
232
|
*
|
|
@@ -328,10 +326,12 @@ function hasUncommittedIndexedEdit(root: string): boolean {
|
|
|
328
326
|
* report `absent` while answering from that very file — the state of a cache
|
|
329
327
|
* nobody asked about. Passing the graph the verb is actually reading makes the
|
|
330
328
|
* printed staleness a statement about the answer rather than about a
|
|
331
|
-
* convention.
|
|
329
|
+
* convention. The default is the project home only: the hooks that call this
|
|
330
|
+
* ship in the dispatch bundle, which cannot carry the user-global resolver, so
|
|
331
|
+
* a caller serving a user-global home passes `graphHome(root).cachePath`.
|
|
332
332
|
*/
|
|
333
333
|
export function graphState(root: string, nativeCache?: string): GraphState {
|
|
334
|
-
const picked = pickSource(detectSources(root, nativeCache ??
|
|
334
|
+
const picked = pickSource(detectSources(root, nativeCache ?? projectHome(root).cachePath));
|
|
335
335
|
if (!picked) return 'absent';
|
|
336
336
|
if (picked.stale === true) {
|
|
337
337
|
const behind = picked.commits_behind;
|