@event4u/agent-config 17.2.0 → 17.3.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/.claude-plugin/skills/install-agent-config/SKILL.md +9 -9
- package/CHANGELOG.md +174 -0
- package/dist/agent-src/skills/code-intelligence/SKILL.md +13 -12
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/scripts/check_memory.ts +1 -1
- package/dist/cli/commands/recordTriggerEval.js +1 -1
- package/dist/cli-delegate/{chunk-KR5ZIFPO.js → chunk-DHSSP7V3.js} +1 -1
- package/dist/cli-delegate/chunk-E7ACTJTJ.js +6792 -0
- package/dist/cli-delegate/{chunk-BZJ4Y3CI.js → chunk-MT2WOOCY.js} +1 -1
- package/dist/cli-delegate/{chunk-CMZEKSNN.js → chunk-ORJPBER2.js} +2 -2
- package/dist/cli-delegate/{chunk-VYZGJLCA.js → chunk-RKNT4LIK.js} +22 -1286
- package/dist/cli-delegate/cmd_conformance.js +20 -14
- package/dist/cli-delegate/cmd_doctor.js +3 -3
- package/dist/cli-delegate/cmd_doctor_neighbours.js +2 -2
- package/dist/cli-delegate/cmd_explain.js +1 -1
- package/dist/cli-delegate/cmd_export.js +2 -2
- package/dist/cli-delegate/cmd_prune.js +2 -2
- package/dist/cli-delegate/cmd_settings_migrate.js +2 -2
- package/dist/cli-delegate/cmd_settings_set.js +8 -4
- package/dist/cli-delegate/cmd_sync.js +2 -2
- package/dist/cli-delegate/cmd_uninstall.js +2 -2
- package/dist/cli-delegate/cmd_validate.js +2 -2
- 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 +414 -0
- package/dist/hooks/code_graph_refresh.chunk.cjs.sha256 +1 -0
- package/dist/hooks/dispatch.js +21 -11
- package/dist/hooks/dispatch.sha256 +1 -1
- package/dist/hooks/egress_engine.chunk.cjs +1 -1
- package/dist/hooks/egress_engine.chunk.cjs.sha256 +1 -1
- package/dist/install/install.mjs +6974 -6037
- package/dist/mcp/mcp-cloudflare-catalogue.json +1 -0
- package/dist/mcp/registry-manifest.json +3 -2
- package/dist/mcp/server.json +13 -4
- package/dist/mcp/server.mjs +5561 -2463
- package/dist/scripts/_lib/egress_rules.js +8 -2
- package/dist/scripts/condense.js +9 -9
- package/dist/server/routes/discovery.js +1 -1
- package/dist/server/routes/install.js +1 -1
- package/dist/server/routes/ping.js +1 -1
- package/dist/server/routes/schema.js +4 -4
- package/dist/server/routes/settings.js +2 -6
- package/dist/server/routes/wizard.js +6 -6
- package/dist/server/schemas/settings.js +42 -42
- package/dist/shared/settingsSurface.js +1 -1
- package/dist/shared/userMd/schema.js +1 -1
- package/dist/shared/zodJsonSchema.js +47 -0
- package/dist/ui/assets/index-B0bcIgi0.js +77 -0
- package/dist/ui/index.html +1 -1
- package/package.json +3 -3
- package/src/agent-src/templates/scripts/check_memory.ts +1 -1
- package/src/config/ci-local-parity.yml +11 -0
- package/src/config/continuity-surface.json +9 -2
- package/src/config/gate-violation-baselines.json +3 -3
- package/src/config/gitignore-block.txt +6 -5
- package/src/config/hook-bundle-budget.json +16 -1
- package/src/config/pack-size-budget.json +12 -1
- package/src/config/packed-binary-manifest.json +9 -0
- package/src/scripts/_cli/cmd_settings_set.ts +3 -2
- package/src/scripts/_dispatch.bash +1 -1
- package/src/scripts/_lib/egress_rules.ts +8 -2
- package/src/scripts/_lib/listing_manifests.ts +85 -0
- package/src/scripts/_lib/neighbour_census.ts +1 -0
- package/src/scripts/_lib/release_settings_surface.ts +2 -6
- package/src/scripts/adoption_status.ts +231 -27
- package/src/scripts/build_mcp_registry_manifest.ts +23 -12
- package/src/scripts/check_dependency_floors.ts +7 -0
- package/src/scripts/check_release_manifest_content.ts +17 -2
- package/src/scripts/check_release_pr_shape.ts +5 -0
- package/src/scripts/code_graph/build.ts +80 -44
- package/src/scripts/code_graph/cli.ts +260 -16
- package/src/scripts/code_graph/conventions_default.ts +228 -0
- package/src/scripts/code_graph/extract.ts +5 -2
- package/src/scripts/code_graph/extract_more.ts +105 -1
- package/src/scripts/code_graph/loader.ts +10 -2
- package/src/scripts/code_graph/map.ts +174 -41
- package/src/scripts/code_graph/map_model.ts +53 -15
- package/src/scripts/code_graph/map_verify.ts +12 -0
- package/src/scripts/code_graph/map_viewer/viewer.css +6 -0
- package/src/scripts/code_graph/map_viewer/viewer.js +114 -13
- package/src/scripts/code_graph/passes.ts +173 -0
- package/src/scripts/code_graph/providers/angular.ts +55 -0
- package/src/scripts/code_graph/providers/aspnetcore.ts +182 -0
- package/src/scripts/code_graph/providers/catalog.ts +290 -0
- package/src/scripts/code_graph/providers/django.ts +163 -0
- package/src/scripts/code_graph/providers/express.ts +25 -0
- package/src/scripts/code_graph/providers/fastapi.ts +49 -0
- package/src/scripts/code_graph/providers/flask.ts +99 -0
- package/src/scripts/code_graph/providers/go_http.ts +121 -0
- package/src/scripts/code_graph/providers/index.ts +101 -0
- package/src/scripts/code_graph/{laravel_overlay.ts → providers/laravel.ts} +113 -7
- package/src/scripts/code_graph/providers/nestjs.ts +34 -0
- package/src/scripts/code_graph/providers/nextjs.ts +91 -0
- package/src/scripts/code_graph/providers/nuxt.ts +55 -0
- package/src/scripts/code_graph/providers/react-router.ts +58 -0
- package/src/scripts/code_graph/providers/shared/detect.ts +78 -0
- package/src/scripts/code_graph/providers/shared/extractors.ts +249 -0
- package/src/scripts/code_graph/providers/shared/js.ts +252 -0
- package/src/scripts/code_graph/providers/shared/kit.ts +210 -0
- package/src/scripts/code_graph/providers/shared/php.ts +64 -0
- package/src/scripts/code_graph/providers/shared/python.ts +223 -0
- package/src/scripts/code_graph/providers/spring.ts +138 -0
- package/src/scripts/code_graph/providers/sqlalchemy.ts +62 -0
- package/src/scripts/code_graph/providers/sveltekit.ts +59 -0
- package/src/scripts/code_graph/providers/symfony.ts +142 -0
- package/src/scripts/code_graph/providers/typeorm.ts +58 -0
- package/src/scripts/code_graph/providers/types.ts +56 -0
- package/src/scripts/code_graph/providers/vue-router.ts +21 -0
- package/src/scripts/code_graph/providers/wordpress.ts +128 -0
- package/src/scripts/code_graph/refresh_cli.ts +81 -0
- package/src/scripts/code_graph/refresh_lock.ts +193 -0
- package/src/scripts/code_graph/resolution_tiers.ts +19 -7
- package/src/scripts/code_graph/scip_reader.ts +2 -2
- package/src/scripts/code_graph/sqlite_store.ts +10 -4
- package/src/scripts/code_graph/types.ts +75 -41
- package/src/scripts/code_graph/validate.ts +6 -8
- package/src/scripts/condense.ts +9 -9
- package/src/scripts/generate_settings_reference.ts +2 -6
- package/src/scripts/hook_chunks.mjs +4 -0
- package/src/scripts/hook_manifest.json +1 -1
- package/src/scripts/hook_manifest.yaml +24 -7
- package/src/scripts/hooks/code_graph_refresh_hook.ts +160 -0
- package/src/scripts/hooks/lazy_concern.ts +4 -0
- package/src/scripts/install-hooks.sh +54 -33
- package/src/scripts/install.ts +2 -6
- package/src/scripts/lint_hook_manifest.ts +2 -0
- package/src/scripts/lint_marketplace.ts +33 -0
- package/src/scripts/lint_mcp_registry_manifest.ts +36 -0
- package/src/scripts/mcp_server/consumer_tool_catalog.json +1 -0
- package/src/scripts/mcp_server/graph_tools.ts +5 -1
- package/src/scripts/release.ts +2 -5
- package/src/scripts/release_drill.ts +4 -1
- package/src/scripts/release_env.ts +17 -0
- package/src/scripts/release_mix_taxonomy.json +3 -0
- package/src/server/schemas/settings.ts +42 -42
- package/src/shared/settingsSurface.ts +1 -1
- package/src/shared/userMd/schema.ts +1 -1
- package/src/shared/zodJsonSchema.ts +41 -0
- package/src/vendor/grammars/README.md +6 -5
- package/src/vendor/grammars/tree-sitter-c_sharp.wasm +0 -0
- package/dist/cli-delegate/chunk-XIAPY44N.js +0 -4587
- package/dist/ui/assets/index-w70Ado09.js +0 -23
|
@@ -17,8 +17,9 @@ import { fileURLToPath } from 'node:url';
|
|
|
17
17
|
|
|
18
18
|
import { write_atomic } from '../_lib/fs_atomic.js';
|
|
19
19
|
import { extractFile, type FileExtract, type RawEdge } from './extract.js';
|
|
20
|
-
import { buildLaravelOverlay } from './laravel_overlay.js';
|
|
21
20
|
import { grammarWasmPath } from './loader.js';
|
|
21
|
+
import { type PassReport, runPasses } from './passes.js';
|
|
22
|
+
import { EMPTY_PROBE, PROBE_MARKERS, type ProjectProbe } from './providers/index.js';
|
|
22
23
|
import {
|
|
23
24
|
ALIAS_VIA,
|
|
24
25
|
aliasRulesFrom,
|
|
@@ -30,8 +31,9 @@ import {
|
|
|
30
31
|
resolveAlias,
|
|
31
32
|
resolvePsr4,
|
|
32
33
|
} from './resolution_tiers.js';
|
|
33
|
-
import {
|
|
34
|
-
import {
|
|
34
|
+
import { templateBase } from './conventions_default.js';
|
|
35
|
+
import { DOC_EXTS } from './docs_layer.js';
|
|
36
|
+
import { readScipIndex, type ScipDocument } from './scip_reader.js';
|
|
35
37
|
import { emitSqliteTwin } from './sqlite_store.js';
|
|
36
38
|
import {
|
|
37
39
|
type CodeEdge,
|
|
@@ -78,6 +80,10 @@ const CODE_EXTS: ReadonlySet<string> = new Set(Object.keys(EXT_LANG));
|
|
|
78
80
|
const GENERATED_DIRS: ReadonlySet<string> = new Set(['agents/map', 'agents/runtime']);
|
|
79
81
|
|
|
80
82
|
export function discoverFiles(root: string, exts: ReadonlySet<string> = CODE_EXTS): string[] {
|
|
83
|
+
return walkFiles(root, (name) => exts.has(path.extname(name).toLowerCase()));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function walkFiles(root: string, accept: (name: string) => boolean): string[] {
|
|
81
87
|
const abs = path.resolve(root);
|
|
82
88
|
// Resolve the root itself first: a repo checked out under a symlinked
|
|
83
89
|
// path (e.g. macOS /tmp → /private/tmp) must still confine correctly —
|
|
@@ -106,8 +112,7 @@ export function discoverFiles(root: string, exts: ReadonlySet<string> = CODE_EXT
|
|
|
106
112
|
continue;
|
|
107
113
|
}
|
|
108
114
|
if (!e.isFile()) continue;
|
|
109
|
-
|
|
110
|
-
if (!exts.has(ext)) continue;
|
|
115
|
+
if (!accept(e.name)) continue;
|
|
111
116
|
let real: string;
|
|
112
117
|
try {
|
|
113
118
|
real = fs.realpathSync(full);
|
|
@@ -122,6 +127,33 @@ export function discoverFiles(root: string, exts: ReadonlySet<string> = CODE_EXT
|
|
|
122
127
|
return out.sort();
|
|
123
128
|
}
|
|
124
129
|
|
|
130
|
+
/** 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)$/;
|
|
132
|
+
|
|
133
|
+
/** The project's manifests and marker files, for provider detection (ADR-298). */
|
|
134
|
+
export function readProjectProbe(root: string): ProjectProbe {
|
|
135
|
+
const abs = path.resolve(root);
|
|
136
|
+
const manifests = new Map<string, string>();
|
|
137
|
+
const markers = new Set<string>();
|
|
138
|
+
const paths: string[] = [];
|
|
139
|
+
for (const full of walkFiles(abs, () => true)) {
|
|
140
|
+
const rel = toPosixRel(abs, full);
|
|
141
|
+
paths.push(rel);
|
|
142
|
+
const name = path.basename(full);
|
|
143
|
+
if (!MANIFEST_NAME.test(name) && !PROBE_MARKERS.has(name)) continue;
|
|
144
|
+
if (PROBE_MARKERS.has(name)) {
|
|
145
|
+
markers.add(rel);
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
try {
|
|
149
|
+
manifests.set(rel, fs.readFileSync(full, 'utf-8'));
|
|
150
|
+
} catch {
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return { manifests, markers, paths };
|
|
155
|
+
}
|
|
156
|
+
|
|
125
157
|
function toPosixRel(root: string, full: string): string {
|
|
126
158
|
return path.relative(root, full).split(path.sep).join('/');
|
|
127
159
|
}
|
|
@@ -234,10 +266,19 @@ function resolveSpecifier(fromFile: string, spec: string, fileIds: ReadonlySet<s
|
|
|
234
266
|
* go `example.com/x/mod` → every non-test `.go` file of the most specific
|
|
235
267
|
* directory whose path ends the import path — a Go import names a
|
|
236
268
|
* package, and a package is all of its files.
|
|
269
|
+
* csharp `using A.B` → the one file declaring namespace `A.B` (`namespaces`).
|
|
270
|
+
* A namespace declared by several files is left to name lookup: it
|
|
271
|
+
* binds no file, since a `using` names no single one.
|
|
237
272
|
*
|
|
238
273
|
* A suffix that matches two files is refused rather than guessed.
|
|
239
274
|
*/
|
|
240
|
-
export function resolveLangModule(
|
|
275
|
+
export function resolveLangModule(
|
|
276
|
+
lang: Lang,
|
|
277
|
+
fromFile: string,
|
|
278
|
+
spec: string,
|
|
279
|
+
fileIds: readonly string[],
|
|
280
|
+
namespaces: ReadonlyMap<string, readonly string[]> = new Map(),
|
|
281
|
+
): string[] {
|
|
241
282
|
const unique = (suffixes: string[]): string[] => {
|
|
242
283
|
const hits = fileIds.filter((f) => suffixes.some((s) => f === s || f.endsWith(`/${s}`)));
|
|
243
284
|
return hits.length === 1 ? hits : [];
|
|
@@ -264,6 +305,7 @@ export function resolveLangModule(lang: Lang, fromFile: string, spec: string, fi
|
|
|
264
305
|
}
|
|
265
306
|
return best ? goFiles.filter((f) => idDirname(f) === best).sort() : [];
|
|
266
307
|
}
|
|
308
|
+
if (lang === 'csharp') return [...(namespaces.get(spec) ?? [])];
|
|
267
309
|
return [];
|
|
268
310
|
}
|
|
269
311
|
|
|
@@ -309,6 +351,8 @@ export function buildGraph(
|
|
|
309
351
|
resolution: ResolutionConfig = EMPTY_RESOLUTION_CONFIG,
|
|
310
352
|
scip: readonly ScipDocument[] = [],
|
|
311
353
|
docs: readonly SourceFile[] = [],
|
|
354
|
+
probe: ProjectProbe = EMPTY_PROBE,
|
|
355
|
+
report?: { passes?: PassReport },
|
|
312
356
|
): CodeGraph {
|
|
313
357
|
const nodes = extracts.flatMap((e) => e.nodes);
|
|
314
358
|
const nodeIds = new Set(nodes.map((n) => n.id));
|
|
@@ -383,6 +427,8 @@ export function buildGraph(
|
|
|
383
427
|
const fileIdList = [...fileIds].sort();
|
|
384
428
|
/** Go imports name a package of several files; the binding takes the first, these carry the rest. */
|
|
385
429
|
const extraImports: Array<{ source: string; target: string }> = [];
|
|
430
|
+
const namespaceFiles = new Map<string, string[]>();
|
|
431
|
+
for (const ex of extracts) for (const ns of ex.namespaces ?? []) push(namespaceFiles, ns, ex.file);
|
|
386
432
|
for (const ex of extracts) {
|
|
387
433
|
const locals = new Map<string, string[]>();
|
|
388
434
|
for (const n of ex.nodes) if (n.kind !== 'file') push(locals, n.label, n.id);
|
|
@@ -419,8 +465,9 @@ export function buildGraph(
|
|
|
419
465
|
if (hit) imports.set(local, { target: hit, exact: false, via: 'name-lookup' });
|
|
420
466
|
continue;
|
|
421
467
|
}
|
|
422
|
-
if (ex.lang === '
|
|
423
|
-
|
|
468
|
+
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') {
|
|
470
|
+
const hits = resolveLangModule(ex.lang, ex.file, spec, fileIdList, namespaceFiles);
|
|
424
471
|
const first = hits[0];
|
|
425
472
|
if (first === undefined) {
|
|
426
473
|
imports.set(local, {
|
|
@@ -754,39 +801,9 @@ export function buildGraph(
|
|
|
754
801
|
});
|
|
755
802
|
}
|
|
756
803
|
|
|
757
|
-
//
|
|
758
|
-
|
|
759
|
-
|
|
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
|
-
}
|
|
804
|
+
// Everything after resolution: convention providers, docs, SCIP (passes.ts).
|
|
805
|
+
const passes = runPasses({ files, docs, scip, resolution, probe }, nodes, edges);
|
|
806
|
+
if (report) report.passes = passes;
|
|
790
807
|
|
|
791
808
|
// deterministic ordering
|
|
792
809
|
nodes.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
@@ -800,7 +817,9 @@ export function buildGraph(
|
|
|
800
817
|
for (const e of edges) counts[e.confidence] += 1;
|
|
801
818
|
const suppressed: CodeGraph['suppressed_edge_counts'] = {
|
|
802
819
|
dynamic_no_candidate: suppressedKeys.size,
|
|
803
|
-
...(
|
|
820
|
+
...(Object.keys(passes.suppressed).length > 0
|
|
821
|
+
? { contested_by_provider: Object.fromEntries(Object.entries(passes.suppressed).sort()) }
|
|
822
|
+
: {}),
|
|
804
823
|
};
|
|
805
824
|
|
|
806
825
|
const languages = [...new Set(extracts.map((e) => e.lang))].sort() as Lang[];
|
|
@@ -819,6 +838,18 @@ export function buildGraph(
|
|
|
819
838
|
h.update(createHash('sha256').update(d.source, 'utf-8').digest('hex'));
|
|
820
839
|
h.update('\n');
|
|
821
840
|
}
|
|
841
|
+
// Manifests and markers decide which providers run: a changed manifest is
|
|
842
|
+
// a changed input even when no source file moved.
|
|
843
|
+
for (const [p, text] of [...probe.manifests].sort((a, b) => (a[0] < b[0] ? -1 : 1))) {
|
|
844
|
+
h.update(`manifest\0${p}\0`);
|
|
845
|
+
h.update(createHash('sha256').update(text, 'utf-8').digest('hex'));
|
|
846
|
+
h.update('\n');
|
|
847
|
+
}
|
|
848
|
+
for (const p of [...probe.markers].sort()) h.update(`marker\0${p}\n`);
|
|
849
|
+
// Template files are no source, yet their presence decides `renders` and
|
|
850
|
+
// default-pass edges: they are inputs too. Other non-code paths (caches,
|
|
851
|
+
// logs, assets) are not read and stay out of the identity.
|
|
852
|
+
for (const p of probe.paths) if (templateBase(p) !== null) h.update(`template\0${p}\n`);
|
|
822
853
|
// The SCIP index is an input like any source file: a new index must not
|
|
823
854
|
// read as the same graph.
|
|
824
855
|
for (const d of [...scip].sort((a, b) => (a.path < b.path ? -1 : 1))) {
|
|
@@ -833,6 +864,7 @@ export function buildGraph(
|
|
|
833
864
|
grammar_abi: EXPECTED_GRAMMAR_ABI,
|
|
834
865
|
edge_confidence_counts: counts,
|
|
835
866
|
suppressed_edge_counts: suppressed,
|
|
867
|
+
...(passes.providers.length > 0 ? { providers: passes.providers } : {}),
|
|
836
868
|
nodes,
|
|
837
869
|
edges,
|
|
838
870
|
};
|
|
@@ -908,6 +940,7 @@ export function serializeGraph(g: CodeGraph): string {
|
|
|
908
940
|
'grammar_abi',
|
|
909
941
|
'edge_confidence_counts',
|
|
910
942
|
'suppressed_edge_counts',
|
|
943
|
+
'providers',
|
|
911
944
|
'nodes',
|
|
912
945
|
'edges',
|
|
913
946
|
];
|
|
@@ -936,13 +969,14 @@ export function serializeGraph(g: CodeGraph): string {
|
|
|
936
969
|
// byte what it was before this field existed.
|
|
937
970
|
if (e.ambiguity_reason) base['ambiguity_reason'] = e.ambiguity_reason;
|
|
938
971
|
if (e.candidates) base['candidates'] = e.candidates;
|
|
972
|
+
if (e.provider_id) base['provider_id'] = e.provider_id;
|
|
939
973
|
return base;
|
|
940
974
|
};
|
|
941
975
|
const ordered: Record<string, unknown> = {};
|
|
942
976
|
for (const k of key_order) {
|
|
943
977
|
if (k === 'nodes') ordered[k] = g.nodes.map(node);
|
|
944
978
|
else if (k === 'edges') ordered[k] = g.edges.map(edge);
|
|
945
|
-
else ordered[k] = g[k];
|
|
979
|
+
else if (g[k] !== undefined) ordered[k] = g[k];
|
|
946
980
|
}
|
|
947
981
|
return JSON.stringify(ordered, null, 2) + '\n';
|
|
948
982
|
}
|
|
@@ -1086,7 +1120,9 @@ export async function buildFromRepo(
|
|
|
1086
1120
|
continue;
|
|
1087
1121
|
}
|
|
1088
1122
|
}
|
|
1089
|
-
const
|
|
1123
|
+
const report: { passes?: PassReport } = {};
|
|
1124
|
+
const graph = buildGraph(files, extracts, readResolutionConfig(root), scip, docs, readProjectProbe(abs), report);
|
|
1125
|
+
for (const f of report.passes?.failures ?? []) process.stderr.write(`code-graph: pass ${f.pass} failed — ${f.reason}\n`);
|
|
1090
1126
|
const json = serializeGraph(graph);
|
|
1091
1127
|
if (cachePath) {
|
|
1092
1128
|
write_atomic(cachePath, json);
|
|
@@ -6,8 +6,12 @@
|
|
|
6
6
|
* validate [--graph P] schema-gate a graph.json
|
|
7
7
|
* detect [--root P] [--format json] three-state freshness verdict
|
|
8
8
|
* refresh [--root P] [--out P] [--budget-seconds N]
|
|
9
|
-
*
|
|
10
|
-
* (FRESH) / full build (ABSENT)
|
|
9
|
+
* [--single-flight] [--map] build --update (STALE) / no-op
|
|
10
|
+
* (FRESH) / full build (ABSENT);
|
|
11
|
+
* a result built while HEAD moved
|
|
12
|
+
* is discarded and retried once
|
|
13
|
+
* auto-refresh on|off|status [--root P] opt this project in to the
|
|
14
|
+
* session-start background refresh
|
|
11
15
|
*
|
|
12
16
|
* `detect --format json` (Phase 2, road-to-reachable-code-memory) emits a
|
|
13
17
|
* single-line, stable-key-order JSON verdict on stdout — no timestamps, safe
|
|
@@ -62,7 +66,7 @@
|
|
|
62
66
|
* graph.
|
|
63
67
|
*/
|
|
64
68
|
import { execFileSync, spawnSync } from 'node:child_process';
|
|
65
|
-
import { randomBytes } from 'node:crypto';
|
|
69
|
+
import { createHash, randomBytes } from 'node:crypto';
|
|
66
70
|
import * as fs from 'node:fs';
|
|
67
71
|
import * as path from 'node:path';
|
|
68
72
|
import process from 'node:process';
|
|
@@ -70,7 +74,8 @@ import { fileURLToPath } from 'node:url';
|
|
|
70
74
|
|
|
71
75
|
import { resolve_project_root } from '../_lib/agent_settings.js';
|
|
72
76
|
import { hardenedSpawnEnv } from '../_lib/spawn_env.js';
|
|
73
|
-
import { buildFromRepo, isTestFile, sidecarPath } from './build.js';
|
|
77
|
+
import { buildFromRepo, discoverFiles, isTestFile, sidecarPath } from './build.js';
|
|
78
|
+
import { DOC_EXTS } from './docs_layer.js';
|
|
74
79
|
import {
|
|
75
80
|
computeVerdict,
|
|
76
81
|
detectSources,
|
|
@@ -80,7 +85,16 @@ import {
|
|
|
80
85
|
pickSource,
|
|
81
86
|
} from './detect.js';
|
|
82
87
|
import { suggestVerb } from './intent.js';
|
|
83
|
-
import { buildMap, recordOutcome } from './map.js';
|
|
88
|
+
import { buildMap, graphGeneration, mapGeneration, recordOutcome } from './map.js';
|
|
89
|
+
import { cliPackage, usableCli } from './refresh_cli.js';
|
|
90
|
+
import {
|
|
91
|
+
AUTO_REFRESH_REL,
|
|
92
|
+
LOCK_REL,
|
|
93
|
+
acquire as acquireRefreshLock,
|
|
94
|
+
readRefreshRecords,
|
|
95
|
+
reclaimMutexPath,
|
|
96
|
+
recordRefresh,
|
|
97
|
+
} from './refresh_lock.js';
|
|
84
98
|
import { verifyMap } from './map_verify.js';
|
|
85
99
|
import {
|
|
86
100
|
affected,
|
|
@@ -655,6 +669,64 @@ function cleanupTemp(tmpOut: string): void {
|
|
|
655
669
|
* itself uses. Any other outcome (timeout, non-zero exit, missing output)
|
|
656
670
|
* cleans up the temp files and leaves `out` untouched.
|
|
657
671
|
*/
|
|
672
|
+
function git(root: string, ...args: string[]): string | null {
|
|
673
|
+
try {
|
|
674
|
+
return execFileSync('git', ['-C', root, ...args], { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'], env: hardenedSpawnEnv() }).trim();
|
|
675
|
+
} catch {
|
|
676
|
+
return null;
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
/**
|
|
681
|
+
* HEAD; `''` when no ancestor of the CANONICAL root holds `.git`; `null` when
|
|
682
|
+
* one does and HEAD cannot be read, or when the root or an ancestor cannot be
|
|
683
|
+
* inspected. "Not a repository" is read from the filesystem, never from a git
|
|
684
|
+
* probe, and is walked from the realpath so a symlinked root cannot hide the
|
|
685
|
+
* repository git itself would find.
|
|
686
|
+
*/
|
|
687
|
+
function headOf(root: string): string | null {
|
|
688
|
+
let dir: string;
|
|
689
|
+
try {
|
|
690
|
+
dir = fs.realpathSync(root);
|
|
691
|
+
} catch {
|
|
692
|
+
return null;
|
|
693
|
+
}
|
|
694
|
+
for (;;) {
|
|
695
|
+
try {
|
|
696
|
+
fs.lstatSync(path.join(dir, '.git'));
|
|
697
|
+
return git(dir, 'rev-parse', 'HEAD');
|
|
698
|
+
} catch (e) {
|
|
699
|
+
if ((e as NodeJS.ErrnoException).code !== 'ENOENT') return null;
|
|
700
|
+
}
|
|
701
|
+
if (path.dirname(dir) === dir) return '';
|
|
702
|
+
dir = path.dirname(dir);
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
/**
|
|
707
|
+
* A stamp of the files a build reads — path, size and modification time of every
|
|
708
|
+
* source and document file. HEAD alone misses an edit to the working tree made
|
|
709
|
+
* while the build ran (a second agent saving a file), which leaves a graph of
|
|
710
|
+
* content that never existed together. Best-effort, not definitive: an edit
|
|
711
|
+
* that keeps the size and lands within the filesystem's mtime resolution (or
|
|
712
|
+
* restores the mtime) is not seen; hashing every file's content would be.
|
|
713
|
+
*/
|
|
714
|
+
function treeStamp(root: string): string {
|
|
715
|
+
const h = createHash('sha256');
|
|
716
|
+
for (const full of [...discoverFiles(root), ...discoverFiles(root, DOC_EXTS)]) {
|
|
717
|
+
try {
|
|
718
|
+
const st = fs.statSync(full);
|
|
719
|
+
h.update(`${full}\0${st.size}\0${st.mtimeMs}\n`);
|
|
720
|
+
} catch {
|
|
721
|
+
h.update(`${full}\0gone\n`);
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
return h.digest('hex');
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
/** Returned by {@link runBudgetedBuild} when HEAD or the working tree moved while the child built. */
|
|
728
|
+
const HEAD_MOVED = -1;
|
|
729
|
+
|
|
658
730
|
function runBudgetedBuild(root: string, out: string, update: boolean, budgetSeconds: number): number {
|
|
659
731
|
if (!(budgetSeconds > 0)) {
|
|
660
732
|
// Node's own `timeout` option treats 0 as "no limit" — a genuinely
|
|
@@ -682,6 +754,8 @@ function runBudgetedBuild(root: string, out: string, update: boolean, budgetSeco
|
|
|
682
754
|
const [cmd, cmdArgs] = fs.existsSync(TSX_BIN)
|
|
683
755
|
? [TSX_BIN, [_HERE, ...args]]
|
|
684
756
|
: [process.execPath, [...process.execArgv, _HERE, ...args]];
|
|
757
|
+
const headBefore = headOf(root);
|
|
758
|
+
const treeBefore = treeStamp(root);
|
|
685
759
|
const res = spawnSync(cmd, cmdArgs, {
|
|
686
760
|
env: hardenedSpawnEnv(),
|
|
687
761
|
stdio: 'ignore',
|
|
@@ -705,6 +779,19 @@ function runBudgetedBuild(root: string, out: string, update: boolean, budgetSeco
|
|
|
705
779
|
process.stderr.write('code-graph: refresh produced no output — old cache kept\n');
|
|
706
780
|
return 3;
|
|
707
781
|
}
|
|
782
|
+
// A checkout or rebase that lands mid-build leaves a graph of a tree that
|
|
783
|
+
// existed only in passing; publishing it would label mixed content fresh.
|
|
784
|
+
// An unreadable HEAD is not evidence it held still, so it fails the same way.
|
|
785
|
+
if (headBefore === null || headOf(root) !== headBefore) {
|
|
786
|
+
cleanupTemp(tmpOut);
|
|
787
|
+
process.stderr.write('code-graph: HEAD moved (or could not be read) during the refresh — result discarded\n');
|
|
788
|
+
return HEAD_MOVED;
|
|
789
|
+
}
|
|
790
|
+
if (treeStamp(root) !== treeBefore) {
|
|
791
|
+
cleanupTemp(tmpOut);
|
|
792
|
+
process.stderr.write('code-graph: files changed during the refresh — result discarded\n');
|
|
793
|
+
return HEAD_MOVED;
|
|
794
|
+
}
|
|
708
795
|
fs.renameSync(tmpOut, out); // same-dir → atomic; real cache only ever changes here
|
|
709
796
|
const tmpSidecar = sidecarPath(tmpOut);
|
|
710
797
|
if (fs.existsSync(tmpSidecar)) fs.renameSync(tmpSidecar, sidecarPath(out));
|
|
@@ -712,7 +799,12 @@ function runBudgetedBuild(root: string, out: string, update: boolean, budgetSeco
|
|
|
712
799
|
return 0;
|
|
713
800
|
}
|
|
714
801
|
|
|
715
|
-
|
|
802
|
+
/**
|
|
803
|
+
* `--single-flight` takes the lock the git-hook refresh block takes and exits 0
|
|
804
|
+
* when another refresh holds it. `--map` redraws `agents/map/` afterwards, only
|
|
805
|
+
* when the project already has one and it is older than the cache.
|
|
806
|
+
*/
|
|
807
|
+
async function cmdRefresh(argv: string[]): Promise<number> {
|
|
716
808
|
const root = resolveRoot(argv);
|
|
717
809
|
const out = flag(argv, '--out') ?? cacheFor(root);
|
|
718
810
|
const budgetSeconds = budgetSecondsOf(argv);
|
|
@@ -720,12 +812,126 @@ function cmdRefresh(argv: string[]): number {
|
|
|
720
812
|
process.stderr.write(`code-graph: root not found: ${root}\n`);
|
|
721
813
|
return 1;
|
|
722
814
|
}
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
815
|
+
// `--map` writes the same files a manual `map` does, so it is never drawn unlocked.
|
|
816
|
+
const locked = argv.includes('--single-flight') || argv.includes('--map');
|
|
817
|
+
const release = locked ? acquireRefreshLock(root) : () => {};
|
|
818
|
+
const record = (outcome: string, detail?: string): void => {
|
|
819
|
+
if (locked) recordRefresh(root, { by: 'refresh', outcome, ...(detail ? { detail } : {}) });
|
|
820
|
+
};
|
|
821
|
+
if (release === null) {
|
|
822
|
+
process.stdout.write('another refresh is running — nothing to do\n');
|
|
823
|
+
record('locked');
|
|
824
|
+
return 0;
|
|
825
|
+
}
|
|
826
|
+
try {
|
|
827
|
+
let rc = HEAD_MOVED;
|
|
828
|
+
// One retry: a second move inside one build window means the tree is
|
|
829
|
+
// still in motion, and the next hook or session start will refresh it.
|
|
830
|
+
for (let attempt = 0; attempt < 2 && rc === HEAD_MOVED; attempt++) {
|
|
831
|
+
const v = computeVerdict(root, out);
|
|
832
|
+
if (v.verdict === 'FRESH') {
|
|
833
|
+
process.stdout.write('fresh — nothing to do\n');
|
|
834
|
+
rc = 0;
|
|
835
|
+
break;
|
|
836
|
+
}
|
|
837
|
+
rc = runBudgetedBuild(root, out, v.verdict === 'STALE', budgetSeconds);
|
|
838
|
+
}
|
|
839
|
+
if (rc === HEAD_MOVED) {
|
|
840
|
+
record('tree-moved', 'HEAD or files changed during both attempts; the next refresh retries');
|
|
841
|
+
return 1;
|
|
842
|
+
}
|
|
843
|
+
if (rc !== 0) {
|
|
844
|
+
record('failed', `build exited ${rc}`);
|
|
845
|
+
return rc;
|
|
846
|
+
}
|
|
847
|
+
if (argv.includes('--map') && mapIsBehind(root, out)) {
|
|
848
|
+
const mrc = await drawMap(['--root', root, '--no-build', '--no-verify']);
|
|
849
|
+
record(mrc === 0 ? 'refreshed-with-map' : 'map-failed', mrc === 0 ? undefined : `map exited ${mrc}`);
|
|
850
|
+
return mrc;
|
|
851
|
+
}
|
|
852
|
+
record('refreshed');
|
|
853
|
+
return rc;
|
|
854
|
+
} finally {
|
|
855
|
+
release();
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
/**
|
|
860
|
+
* `auto-refresh on|off|status` — the explicit, local, per-project opt-in the
|
|
861
|
+
* `code-graph-refresh` session concern requires before it starts anything
|
|
862
|
+
* (ratification round of 2026-10-23: an existing cache is not consent).
|
|
863
|
+
*/
|
|
864
|
+
function cmdAutoRefresh(argv: string[]): number {
|
|
865
|
+
const root = resolveRoot(argv);
|
|
866
|
+
const action = argv.find((a) => !a.startsWith('--') && !isFlagValue(argv, a)) ?? 'status';
|
|
867
|
+
const marker = path.join(root, AUTO_REFRESH_REL);
|
|
868
|
+
if (action === 'on') {
|
|
869
|
+
fs.mkdirSync(path.dirname(marker), { recursive: true });
|
|
870
|
+
fs.writeFileSync(marker, `${JSON.stringify({ enabled_at: new Date().toISOString() })}\n`);
|
|
871
|
+
const cli = usableCli(root);
|
|
872
|
+
process.stdout.write(
|
|
873
|
+
'auto-refresh on — each agent session start refreshes the graph, and agents/map/ when it exists,\n' +
|
|
874
|
+
'in the background (POSIX hosts). `code-graph auto-refresh off` stops it.\n' +
|
|
875
|
+
'The opt-in is per working tree: run it again in each new git worktree.\n' +
|
|
876
|
+
(fs.existsSync(cacheFor(root)) ? '' : 'No graph yet: `code-graph build` or `code-graph map` creates one.\n'),
|
|
877
|
+
);
|
|
878
|
+
if ('reason' in cli) {
|
|
879
|
+
process.stderr.write(
|
|
880
|
+
cli.reason === 'no-cli'
|
|
881
|
+
? 'warning: no agent-config outside this repository is on PATH, so nothing will run yet — install it globally (npm i -g @event4u/agent-config).\n'
|
|
882
|
+
: `warning: ${cli.cli} (version ${cliPackage(cli.cli)?.version ?? 'unknown'}) is too old for the single-flight refresh, so nothing will run yet — update it (npm i -g @event4u/agent-config).\n`,
|
|
883
|
+
);
|
|
884
|
+
}
|
|
726
885
|
return 0;
|
|
727
886
|
}
|
|
728
|
-
|
|
887
|
+
if (action === 'off') {
|
|
888
|
+
fs.rmSync(marker, { force: true });
|
|
889
|
+
process.stdout.write('auto-refresh off\n');
|
|
890
|
+
return 0;
|
|
891
|
+
}
|
|
892
|
+
if (action !== 'status') {
|
|
893
|
+
process.stderr.write('usage: code_graph/cli.ts auto-refresh on|off|status [--root P]\n');
|
|
894
|
+
return 2;
|
|
895
|
+
}
|
|
896
|
+
const cli = usableCli(root);
|
|
897
|
+
const last = readRefreshRecords(root);
|
|
898
|
+
const line = (r: (typeof last)['refresh']): string => (r ? `${r.outcome} at ${r.at}${r.detail ? ` (${r.detail})` : ''}` : 'never');
|
|
899
|
+
const mapDir = path.join(root, 'agents', 'map');
|
|
900
|
+
const gen = graphGeneration(cacheFor(root));
|
|
901
|
+
const drawnFrom = mapGeneration(mapDir);
|
|
902
|
+
const mutex = reclaimMutexPath(path.join(root, LOCK_REL));
|
|
903
|
+
process.stdout.write(
|
|
904
|
+
`auto-refresh: ${fs.existsSync(marker) ? 'on' : 'off'}\n` +
|
|
905
|
+
`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` +
|
|
906
|
+
`last session start: ${line(last['session-start'])}\n` +
|
|
907
|
+
`last refresh: ${line(last.refresh)}\n` +
|
|
908
|
+
`graph: ${computeVerdict(root, cacheFor(root)).verdict}\n` +
|
|
909
|
+
`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
|
+
(fs.existsSync(mutex) && mutexAgeMs(mutex) > 60_000
|
|
911
|
+
? `reclaim mutex: ${path.relative(root, mutex)} left by a crash — stale locks stay held until it is removed by hand\n`
|
|
912
|
+
: ''),
|
|
913
|
+
);
|
|
914
|
+
return 0;
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
function mutexAgeMs(p: string): number {
|
|
918
|
+
try {
|
|
919
|
+
return Date.now() - fs.statSync(p).mtimeMs;
|
|
920
|
+
} catch {
|
|
921
|
+
return 0;
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
/**
|
|
926
|
+
* Whether the map was drawn from a different graph than the cache now holds —
|
|
927
|
+
* decided by the generation both record, never by modification times, which a
|
|
928
|
+
* copied or restored file carries wrongly. No map → never draw one unbidden.
|
|
929
|
+
*/
|
|
930
|
+
function mapIsBehind(root: string, cache: string): boolean {
|
|
931
|
+
const mapDir = path.join(root, 'agents', 'map');
|
|
932
|
+
if (!fs.existsSync(mapDir)) return false;
|
|
933
|
+
const gen = graphGeneration(cache);
|
|
934
|
+
return gen !== null && mapGeneration(mapDir) !== gen;
|
|
729
935
|
}
|
|
730
936
|
|
|
731
937
|
/**
|
|
@@ -739,6 +945,33 @@ async function cmdMap(argv: string[]): Promise<number> {
|
|
|
739
945
|
process.stderr.write(`code-graph: root not found: ${root}\n`);
|
|
740
946
|
return 1;
|
|
741
947
|
}
|
|
948
|
+
// A background refresh may be redrawing the same files; two writers would
|
|
949
|
+
// interleave them. Wait for it — a session start is exactly when someone asks
|
|
950
|
+
// for the map — but not beyond one refresh budget plus a redraw.
|
|
951
|
+
let release = acquireRefreshLock(root);
|
|
952
|
+
const deadline = Date.now() + MAP_LOCK_WAIT_MS;
|
|
953
|
+
if (release === null) process.stderr.write('code-graph: waiting for a background refresh to finish…\n');
|
|
954
|
+
while (release === null && Date.now() < deadline) {
|
|
955
|
+
await new Promise((r) => setTimeout(r, 250));
|
|
956
|
+
release = acquireRefreshLock(root);
|
|
957
|
+
}
|
|
958
|
+
if (release === null) {
|
|
959
|
+
process.stderr.write('code-graph: a refresh is still redrawing the map — try again in a moment\n');
|
|
960
|
+
return 1;
|
|
961
|
+
}
|
|
962
|
+
try {
|
|
963
|
+
return await drawMap(argv);
|
|
964
|
+
} finally {
|
|
965
|
+
release();
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
/** How long `map` waits for a background refresh to release the lock. */
|
|
970
|
+
const MAP_LOCK_WAIT_MS = Number(process.env['AGENT_CONFIG_MAP_LOCK_WAIT_MS'] ?? 90 * 1000);
|
|
971
|
+
|
|
972
|
+
/** The map itself; callers hold the refresh lock. */
|
|
973
|
+
async function drawMap(argv: string[]): Promise<number> {
|
|
974
|
+
const root = resolveRoot(argv);
|
|
742
975
|
const out = flag(argv, '--out');
|
|
743
976
|
const diffRev = flag(argv, '--diff');
|
|
744
977
|
const r = await buildMap(root, {
|
|
@@ -756,15 +989,25 @@ async function cmdMap(argv: string[]): Promise<number> {
|
|
|
756
989
|
(r.model.diff
|
|
757
990
|
? ` 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`
|
|
758
991
|
: '') +
|
|
759
|
-
` wrote: ${r.written.length} files under ${rel}/ — open ${path.join(rel, 'index.html')}\n
|
|
992
|
+
` wrote: ${r.written.length} files under ${rel}/ — open ${path.join(rel, 'index.html')}\n` +
|
|
993
|
+
(fs.existsSync(path.join(root, AUTO_REFRESH_REL))
|
|
994
|
+
? ''
|
|
995
|
+
: ' keep it current at every agent session start: agent-config code-graph auto-refresh on\n'),
|
|
760
996
|
);
|
|
761
997
|
let rc = 0;
|
|
998
|
+
const renderFile = path.join(r.outDir, 'map-render.json');
|
|
762
999
|
if (argv.includes('--no-verify')) {
|
|
763
|
-
// A check
|
|
764
|
-
|
|
1000
|
+
// A check that names the generation it checked can stay beside a newer
|
|
1001
|
+
// map without being read as about it; one that names none cannot.
|
|
1002
|
+
try {
|
|
1003
|
+
const prev = JSON.parse(fs.readFileSync(renderFile, 'utf-8')) as { graph_generation?: unknown };
|
|
1004
|
+
if (typeof prev.graph_generation !== 'string') fs.rmSync(renderFile, { force: true });
|
|
1005
|
+
} catch {
|
|
1006
|
+
fs.rmSync(renderFile, { force: true });
|
|
1007
|
+
}
|
|
765
1008
|
} else {
|
|
766
1009
|
const check = await verifyMap(path.join(r.outDir, 'index.html'));
|
|
767
|
-
fs.writeFileSync(
|
|
1010
|
+
fs.writeFileSync(renderFile, `${JSON.stringify({ ...check, graph_generation: r.meta.graph_generation }, null, 2)}\n`);
|
|
768
1011
|
const problems = check.viewports
|
|
769
1012
|
.flatMap((v) => [
|
|
770
1013
|
...(v.pageErrors ? [`${v.name}: ${v.pageErrors} page error(s)`] : []),
|
|
@@ -831,7 +1074,7 @@ export async function main(argv: string[] = process.argv.slice(2)): Promise<numb
|
|
|
831
1074
|
if (sub === 'build') return await cmdBuild(argv.slice(1));
|
|
832
1075
|
if (sub === 'validate') return cmdValidate(argv.slice(1));
|
|
833
1076
|
if (sub === 'detect') return cmdDetect(argv.slice(1));
|
|
834
|
-
if (sub === 'refresh') return cmdRefresh(argv.slice(1));
|
|
1077
|
+
if (sub === 'refresh') return await cmdRefresh(argv.slice(1));
|
|
835
1078
|
if (sub === 'query' || sub === 'explain' || sub === 'affected' || sub === 'path')
|
|
836
1079
|
return cmdQuery(sub, argv.slice(1));
|
|
837
1080
|
if (sub === 'impact') return cmdImpact(argv.slice(1));
|
|
@@ -841,9 +1084,10 @@ export async function main(argv: string[] = process.argv.slice(2)): Promise<numb
|
|
|
841
1084
|
if (sub === 'suggest-verb') return cmdSuggestVerb(argv.slice(1));
|
|
842
1085
|
if (sub === 'map') return await cmdMap(argv.slice(1));
|
|
843
1086
|
if (sub === 'outcome') return cmdOutcome(argv.slice(1));
|
|
1087
|
+
if (sub === 'auto-refresh') return cmdAutoRefresh(argv.slice(1));
|
|
844
1088
|
process.stderr.write(
|
|
845
1089
|
'usage: code_graph/cli.ts <build|validate|detect|refresh|query|explain|affected|path|' +
|
|
846
|
-
'impact|tests-for|untested|dead|suggest-verb|map|outcome> [options]\n',
|
|
1090
|
+
'impact|tests-for|untested|dead|suggest-verb|map|outcome|auto-refresh> [options]\n',
|
|
847
1091
|
);
|
|
848
1092
|
return 2;
|
|
849
1093
|
} catch (e) {
|