@event4u/agent-config 17.2.1 → 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/.claude-plugin/skills/install-agent-config/SKILL.md +9 -9
- package/CHANGELOG.md +167 -193
- package/CREDITS.md +2 -2
- package/README.md +1 -1
- package/dist/agent-src/skills/code-intelligence/SKILL.md +16 -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/registry.js +2 -2
- package/dist/cli-delegate/{chunk-KR5ZIFPO.js → chunk-5AYJGUA3.js} +1 -3
- package/dist/cli-delegate/chunk-DIFZBP4G.js +1336 -0
- package/dist/cli-delegate/chunk-E7ACTJTJ.js +6792 -0
- package/dist/cli-delegate/{chunk-VYZGJLCA.js → chunk-RKNT4LIK.js} +22 -1286
- package/dist/cli-delegate/{chunk-BZJ4Y3CI.js → chunk-WY5ZZPGL.js} +2 -2
- package/dist/cli-delegate/{chunk-CMZEKSNN.js → chunk-X6LZ7W5A.js} +73 -10
- package/dist/cli-delegate/cmd_conformance.js +190 -192
- 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_export.js +2 -2
- package/dist/cli-delegate/cmd_handoff.js +2 -2
- package/dist/cli-delegate/cmd_memory_get.js +1 -1
- package/dist/cli-delegate/cmd_prune.js +2 -2
- 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_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_update.js +24 -13
- package/dist/cli-delegate/cmd_upgrade.js +2 -2
- package/dist/cli-delegate/cmd_validate.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 +443 -0
- package/dist/hooks/code_graph_refresh.chunk.cjs.sha256 +1 -0
- package/dist/hooks/dispatch.js +1001 -938
- package/dist/hooks/dispatch.sha256 +1 -1
- package/dist/hooks/egress_engine.chunk.cjs +57 -7
- 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/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 +71202 -63282
- package/dist/scripts/_lib/egress_rules.js +8 -2
- package/dist/scripts/condense.js +18 -15
- 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 +6 -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/evaluator-budgets.json +9 -8
- package/src/config/gate-violation-baselines.json +3 -3
- package/src/config/gitignore-block.txt +6 -5
- package/src/config/hook-bundle-budget.json +46 -6
- package/src/config/pack-size-budget.json +12 -1
- package/src/config/packed-binary-manifest.json +18 -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_settings_set.ts +3 -2
- 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 +4 -3
- package/src/scripts/_lib/confusables.ts +0 -19
- package/src/scripts/_lib/egress_normalize.ts +53 -23
- package/src/scripts/_lib/egress_rules.ts +8 -2
- 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/listing_manifests.ts +85 -0
- package/src/scripts/_lib/neighbour_census.ts +1 -0
- 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/release_settings_surface.ts +2 -6
- 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/adoption_status.ts +231 -27
- 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/build_mcp_registry_manifest.ts +23 -12
- package/src/scripts/check_dependency_floors.ts +14 -0
- package/src/scripts/check_egress_mutations.ts +1 -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 +145 -45
- package/src/scripts/code_graph/cli.ts +382 -35
- package/src/scripts/code_graph/conventions_default.ts +247 -0
- 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 +8 -2
- package/src/scripts/code_graph/extract_more.ts +241 -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/loader.ts +10 -2
- package/src/scripts/code_graph/map.ts +195 -53
- package/src/scripts/code_graph/map_model.ts +113 -20
- package/src/scripts/code_graph/map_verify.ts +80 -5
- package/src/scripts/code_graph/map_viewer/viewer.css +18 -0
- package/src/scripts/code_graph/map_viewer/viewer.js +146 -15
- package/src/scripts/code_graph/passes.ts +173 -0
- 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/angular.ts +55 -0
- package/src/scripts/code_graph/providers/aspnetcore.ts +182 -0
- package/src/scripts/code_graph/providers/catalog.ts +299 -0
- package/src/scripts/code_graph/providers/django.ts +206 -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 +103 -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/rails.ts +204 -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 +175 -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/query.ts +9 -9
- package/src/scripts/code_graph/refresh_cli.ts +81 -0
- package/src/scripts/code_graph/refresh_lock.ts +197 -0
- package/src/scripts/code_graph/repo_identity.ts +169 -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 +31 -28
- package/src/scripts/code_graph/types.ts +79 -41
- package/src/scripts/code_graph/validate.ts +6 -8
- package/src/scripts/condense.ts +15 -15
- package/src/scripts/generate_settings_reference.ts +2 -6
- package/src/scripts/hook_chunks.mjs +11 -0
- package/src/scripts/hook_manifest.json +1 -1
- package/src/scripts/hook_manifest.yaml +43 -23
- package/src/scripts/hooks/code_graph_refresh_hook.ts +163 -0
- 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 +8 -0
- package/src/scripts/hooks/user_notice.ts +105 -0
- package/src/scripts/install-hooks.sh +54 -33
- package/src/scripts/install.ts +2 -6
- package/src/scripts/lint_harvest_provenance.ts +137 -11
- 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 +70 -15
- package/src/scripts/mcp_server/graph_tools.ts +165 -41
- package/src/scripts/network_sources.ts +24 -6
- 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/scripts/schemas/provenance-mechanism.schema.json +46 -0
- package/src/scripts/update_notice_hook.ts +75 -7
- 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 +8 -6
- package/src/vendor/grammars/tree-sitter-c_sharp.wasm +0 -0
- package/src/vendor/grammars/tree-sitter-ruby.wasm +0 -0
- package/dist/cli-delegate/chunk-XIAPY44N.js +0 -4587
- package/dist/ui/assets/index-w70Ado09.js +0 -23
- /package/dist/cli-delegate/{chunk-FKYZOWVD.js → chunk-QBAFP67O.js} +0 -0
- /package/dist/cli-delegate/{chunk-BMGUGSY6.js → chunk-TQG6UMPC.js} +0 -0
|
@@ -6,8 +6,17 @@
|
|
|
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
|
|
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`)
|
|
11
20
|
*
|
|
12
21
|
* `detect --format json` (Phase 2, road-to-reachable-code-memory) emits a
|
|
13
22
|
* single-line, stable-key-order JSON verdict on stdout — no timestamps, safe
|
|
@@ -25,8 +34,10 @@
|
|
|
25
34
|
* completed temp output — on budget-exceeded, build failure, or the child
|
|
26
35
|
* never producing output, the rename never happens and the OLD cache is left
|
|
27
36
|
* byte-for-byte untouched. The cache itself (default
|
|
28
|
-
* `agents/runtime/state/code-graph-v1.json
|
|
29
|
-
*
|
|
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.
|
|
30
41
|
*
|
|
31
42
|
* impact --diff <rev> [--depth N] callers/dependents/test files
|
|
32
43
|
* reachable from the changed
|
|
@@ -42,16 +53,24 @@
|
|
|
42
53
|
* three-state staleness — so a caller can tell "no callers" from "no callers I
|
|
43
54
|
* would trust", and "dead" from "dead as of a graph 40 commits behind".
|
|
44
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
|
+
*
|
|
45
60
|
* Query subcommands (query/path/explain/affected) are added by the Phase-3
|
|
46
61
|
* query tier. Deterministic, LLM-free, no network. Exit codes:
|
|
47
62
|
* 0 ok · 1 not-found / validation-failed / refresh-budget-exceeded ·
|
|
48
63
|
* 2 usage · 3 internal.
|
|
49
64
|
*
|
|
50
65
|
* map [--root P] [--out DIR] [--open] draw the project map into
|
|
51
|
-
* [--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
|
|
52
68
|
* [--prs] (open PRs by area, via gh)
|
|
53
69
|
* at three widths when Playwright
|
|
54
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
|
|
55
74
|
*
|
|
56
75
|
* suggest-verb "<question>" (Phase 9 D3) print the verb
|
|
57
76
|
* (query/affected/path/explain)
|
|
@@ -62,7 +81,7 @@
|
|
|
62
81
|
* graph.
|
|
63
82
|
*/
|
|
64
83
|
import { execFileSync, spawnSync } from 'node:child_process';
|
|
65
|
-
import { randomBytes } from 'node:crypto';
|
|
84
|
+
import { createHash, randomBytes } from 'node:crypto';
|
|
66
85
|
import * as fs from 'node:fs';
|
|
67
86
|
import * as path from 'node:path';
|
|
68
87
|
import process from 'node:process';
|
|
@@ -70,17 +89,32 @@ import { fileURLToPath } from 'node:url';
|
|
|
70
89
|
|
|
71
90
|
import { resolve_project_root } from '../_lib/agent_settings.js';
|
|
72
91
|
import { hardenedSpawnEnv } from '../_lib/spawn_env.js';
|
|
73
|
-
import { buildFromRepo, isTestFile, sidecarPath } from './build.js';
|
|
92
|
+
import { buildFromRepo, discoverFiles, isTestFile, sidecarPath } from './build.js';
|
|
93
|
+
import { DOC_EXTS } from './docs_layer.js';
|
|
74
94
|
import {
|
|
75
95
|
computeVerdict,
|
|
76
96
|
detectSources,
|
|
77
97
|
type GraphState,
|
|
78
98
|
graphState,
|
|
79
|
-
NATIVE_CACHE_REL,
|
|
80
99
|
pickSource,
|
|
81
100
|
} from './detect.js';
|
|
101
|
+
import { graphHome } from './graph_home.js';
|
|
82
102
|
import { suggestVerb } from './intent.js';
|
|
83
|
-
import {
|
|
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';
|
|
108
|
+
import { buildMap, graphGeneration, mapGeneration, recordOutcome } from './map.js';
|
|
109
|
+
import { cliPackage, usableCli } from './refresh_cli.js';
|
|
110
|
+
import {
|
|
111
|
+
STALE_LOCK_MS,
|
|
112
|
+
acquire as acquireRefreshLock,
|
|
113
|
+
pidAlive,
|
|
114
|
+
readRefreshRecords,
|
|
115
|
+
reclaimMutexPath,
|
|
116
|
+
recordRefresh,
|
|
117
|
+
} from './refresh_lock.js';
|
|
84
118
|
import { verifyMap } from './map_verify.js';
|
|
85
119
|
import {
|
|
86
120
|
affected,
|
|
@@ -107,24 +141,34 @@ import {
|
|
|
107
141
|
untested,
|
|
108
142
|
type UntestedResult,
|
|
109
143
|
} from './verbs.js';
|
|
144
|
+
import { main as lineageMain } from './lineage_cli.js';
|
|
110
145
|
|
|
111
146
|
const _HERE = fileURLToPath(import.meta.url);
|
|
112
147
|
export const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..', '..');
|
|
113
148
|
/**
|
|
114
|
-
* The cache belongs to the project it describes: `<root>/agents/runtime/state
|
|
115
|
-
* 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
|
|
116
152
|
* the package tree put a consumer's graph inside `node_modules`, where `npm ci`
|
|
117
153
|
* deleted it, two projects sharing an install overwrote each other, and MCP
|
|
118
154
|
* answered "no graph" right after the CLI reported a fresh one.
|
|
119
155
|
*/
|
|
120
156
|
export function cacheFor(root: string): string {
|
|
121
|
-
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;
|
|
122
163
|
}
|
|
123
164
|
/** Local `tsx` binary — `refresh` spawns `build` as a child of THIS file
|
|
124
165
|
* through it, so a budget-exceeded kill only ever terminates the child. */
|
|
125
166
|
const TSX_BIN = path.join(REPO_ROOT, 'node_modules', '.bin', process.platform === 'win32' ? 'tsx.cmd' : 'tsx');
|
|
126
167
|
const DEFAULT_BUDGET_SECONDS = 60;
|
|
127
168
|
|
|
169
|
+
/** A refusal that names what the caller asked for wrongly; exits 1, not 3. */
|
|
170
|
+
class UsageError extends Error {}
|
|
171
|
+
|
|
128
172
|
function flag(argv: string[], name: string): string | null {
|
|
129
173
|
const i = argv.indexOf(name);
|
|
130
174
|
return i >= 0 && i + 1 < argv.length ? (argv[i + 1] as string) : null;
|
|
@@ -143,8 +187,13 @@ function flag(argv: string[], name: string): string | null {
|
|
|
143
187
|
*/
|
|
144
188
|
function resolveRoot(argv: string[]): string {
|
|
145
189
|
const explicit = flag(argv, '--root');
|
|
190
|
+
const project = flag(argv, '--project');
|
|
146
191
|
let candidate: string;
|
|
147
|
-
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) {
|
|
148
197
|
candidate = explicit;
|
|
149
198
|
} else {
|
|
150
199
|
try {
|
|
@@ -183,6 +232,7 @@ async function cmdBuild(argv: string[]): Promise<number> {
|
|
|
183
232
|
}
|
|
184
233
|
const update = argv.includes('--update');
|
|
185
234
|
const { graph, fileCount, reExtracted, reused } = await buildFromRepo(root, out, { update });
|
|
235
|
+
if (out === cacheFor(root)) registerUse(root, graph, git(root, 'rev-parse', 'HEAD'));
|
|
186
236
|
const c = graph.edge_confidence_counts;
|
|
187
237
|
process.stdout.write(
|
|
188
238
|
`✅ code-graph ${update ? 'updated' : 'built'} — ${fileCount} files · ${graph.nodes.length} nodes · ${graph.edges.length} edges\n` +
|
|
@@ -190,7 +240,7 @@ async function cmdBuild(argv: string[]): Promise<number> {
|
|
|
190
240
|
` edges: EXTRACTED ${c.EXTRACTED} · INFERRED ${c.INFERRED} · AMBIGUOUS ${c.AMBIGUOUS}\n` +
|
|
191
241
|
` resolved_via: ${resolvedViaHistogram(graph)}\n` +
|
|
192
242
|
(update ? ` incremental: ${reExtracted} re-extracted · ${reused} reused\n` : '') +
|
|
193
|
-
(out ? ` cache: ${
|
|
243
|
+
(out ? ` cache: ${shown(root, out)}\n` : '') +
|
|
194
244
|
legacyCacheNotice(root),
|
|
195
245
|
);
|
|
196
246
|
return 0;
|
|
@@ -247,12 +297,12 @@ function cmdValidate(argv: string[]): number {
|
|
|
247
297
|
// content-addressed cache key, so agreement here means the twin was emitted
|
|
248
298
|
// from THIS graph and not merely from a graph.
|
|
249
299
|
const twinState = twinChecksumState(p, (parsed as CodeGraph).source_checksum);
|
|
250
|
-
const rel =
|
|
300
|
+
const rel = shown(root, p);
|
|
251
301
|
if (twinState.kind === 'mismatch') {
|
|
252
302
|
process.stderr.write(
|
|
253
303
|
`❌ SQLite twin disagrees with the canonical JSON: twin ${twinState.twin} ≠ json ${twinState.json}\n` +
|
|
254
304
|
' The twin is derived and disposable — delete it and re-run `code-graph build`.\n' +
|
|
255
|
-
` ${
|
|
305
|
+
` ${shown(root, sqliteTwinPath(p))}\n`,
|
|
256
306
|
);
|
|
257
307
|
return 1;
|
|
258
308
|
}
|
|
@@ -295,7 +345,8 @@ function resolveGraph(argv: string[]): { g: LoadedGraph; note: string } | { err:
|
|
|
295
345
|
if (!picked) return { err: 'no code-graph source found — run `code_graph build` or ship a graph.json' };
|
|
296
346
|
if (picked.kind === 'scip') return { err: `${picked.note ?? 'SCIP detected'} — run \`code-graph build\` to merge it into the native graph` };
|
|
297
347
|
const staleNote = picked.stale ? ` ⚠ index is ${picked.commits_behind ?? 'N'} commit(s) behind — rebuild before trusting` : '';
|
|
298
|
-
|
|
348
|
+
touchHome(root);
|
|
349
|
+
return { g: loadGraph(picked.path, `${picked.kind}:${shown(root, picked.path)}`), note: staleNote };
|
|
299
350
|
}
|
|
300
351
|
|
|
301
352
|
function render(r: QueryResult, staleNote: string): number {
|
|
@@ -415,7 +466,8 @@ function isFlagValue(argv: string[], token: string): boolean {
|
|
|
415
466
|
*/
|
|
416
467
|
function stateOf(argv: string[], answeredBy?: string): GraphState {
|
|
417
468
|
const explicit = flag(argv, '--graph');
|
|
418
|
-
|
|
469
|
+
const root = resolveRoot(argv);
|
|
470
|
+
return graphState(root, explicit ?? answeredBy ?? cacheFor(root));
|
|
419
471
|
}
|
|
420
472
|
|
|
421
473
|
/** The two histogram lines + the staleness line, in one place so the four
|
|
@@ -621,7 +673,7 @@ function cmdDetect(argv: string[]): number {
|
|
|
621
673
|
}
|
|
622
674
|
for (const v of verdicts) {
|
|
623
675
|
const fresh = v.stale === undefined ? '' : v.stale ? ` · STALE${v.commits_behind !== undefined ? ` (${v.commits_behind} behind)` : ''}` : ' · fresh';
|
|
624
|
-
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`);
|
|
625
677
|
}
|
|
626
678
|
const picked = pickSource(verdicts);
|
|
627
679
|
if (picked) process.stdout.write(`→ query would use: ${picked.kind}\n`);
|
|
@@ -635,10 +687,21 @@ function budgetSecondsOf(argv: string[]): number {
|
|
|
635
687
|
return Number.isFinite(n) ? n : DEFAULT_BUDGET_SECONDS;
|
|
636
688
|
}
|
|
637
689
|
|
|
638
|
-
/** Best-effort removal of a temp build output
|
|
639
|
-
*
|
|
640
|
-
|
|
641
|
-
|
|
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]) {
|
|
642
705
|
try {
|
|
643
706
|
fs.unlinkSync(p);
|
|
644
707
|
} catch {
|
|
@@ -647,6 +710,33 @@ function cleanupTemp(tmpOut: string): void {
|
|
|
647
710
|
}
|
|
648
711
|
}
|
|
649
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
|
+
|
|
650
740
|
/**
|
|
651
741
|
* Run `build` (optionally `--update`) as a CHILD process against a temp
|
|
652
742
|
* `--out` path, under a hard wall-clock budget. Only on a clean, in-budget
|
|
@@ -655,6 +745,64 @@ function cleanupTemp(tmpOut: string): void {
|
|
|
655
745
|
* itself uses. Any other outcome (timeout, non-zero exit, missing output)
|
|
656
746
|
* cleans up the temp files and leaves `out` untouched.
|
|
657
747
|
*/
|
|
748
|
+
function git(root: string, ...args: string[]): string | null {
|
|
749
|
+
try {
|
|
750
|
+
return execFileSync('git', ['-C', root, ...args], { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'], env: hardenedSpawnEnv() }).trim();
|
|
751
|
+
} catch {
|
|
752
|
+
return null;
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
/**
|
|
757
|
+
* HEAD; `''` when no ancestor of the CANONICAL root holds `.git`; `null` when
|
|
758
|
+
* one does and HEAD cannot be read, or when the root or an ancestor cannot be
|
|
759
|
+
* inspected. "Not a repository" is read from the filesystem, never from a git
|
|
760
|
+
* probe, and is walked from the realpath so a symlinked root cannot hide the
|
|
761
|
+
* repository git itself would find.
|
|
762
|
+
*/
|
|
763
|
+
function headOf(root: string): string | null {
|
|
764
|
+
let dir: string;
|
|
765
|
+
try {
|
|
766
|
+
dir = fs.realpathSync(root);
|
|
767
|
+
} catch {
|
|
768
|
+
return null;
|
|
769
|
+
}
|
|
770
|
+
for (;;) {
|
|
771
|
+
try {
|
|
772
|
+
fs.lstatSync(path.join(dir, '.git'));
|
|
773
|
+
return git(dir, 'rev-parse', 'HEAD');
|
|
774
|
+
} catch (e) {
|
|
775
|
+
if ((e as NodeJS.ErrnoException).code !== 'ENOENT') return null;
|
|
776
|
+
}
|
|
777
|
+
if (path.dirname(dir) === dir) return '';
|
|
778
|
+
dir = path.dirname(dir);
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
/**
|
|
783
|
+
* A stamp of the files a build reads — path, size and modification time of every
|
|
784
|
+
* source and document file. HEAD alone misses an edit to the working tree made
|
|
785
|
+
* while the build ran (a second agent saving a file), which leaves a graph of
|
|
786
|
+
* content that never existed together. Best-effort, not definitive: an edit
|
|
787
|
+
* that keeps the size and lands within the filesystem's mtime resolution (or
|
|
788
|
+
* restores the mtime) is not seen; hashing every file's content would be.
|
|
789
|
+
*/
|
|
790
|
+
function treeStamp(root: string): string {
|
|
791
|
+
const h = createHash('sha256');
|
|
792
|
+
for (const full of [...discoverFiles(root), ...discoverFiles(root, DOC_EXTS)]) {
|
|
793
|
+
try {
|
|
794
|
+
const st = fs.statSync(full);
|
|
795
|
+
h.update(`${full}\0${st.size}\0${st.mtimeMs}\n`);
|
|
796
|
+
} catch {
|
|
797
|
+
h.update(`${full}\0gone\n`);
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
return h.digest('hex');
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
/** Returned by {@link runBudgetedBuild} when HEAD or the working tree moved while the child built. */
|
|
804
|
+
const HEAD_MOVED = -1;
|
|
805
|
+
|
|
658
806
|
function runBudgetedBuild(root: string, out: string, update: boolean, budgetSeconds: number): number {
|
|
659
807
|
if (!(budgetSeconds > 0)) {
|
|
660
808
|
// Node's own `timeout` option treats 0 as "no limit" — a genuinely
|
|
@@ -664,6 +812,7 @@ function runBudgetedBuild(root: string, out: string, update: boolean, budgetSeco
|
|
|
664
812
|
return 1;
|
|
665
813
|
}
|
|
666
814
|
const dir = path.dirname(out);
|
|
815
|
+
sweepOrphanedTemps(out);
|
|
667
816
|
const tmpOut = path.join(dir, `.${path.basename(out)}.refresh-tmp-${process.pid}-${randomBytes(4).toString('hex')}.json`);
|
|
668
817
|
if (update) {
|
|
669
818
|
const realSidecar = sidecarPath(out);
|
|
@@ -682,6 +831,8 @@ function runBudgetedBuild(root: string, out: string, update: boolean, budgetSeco
|
|
|
682
831
|
const [cmd, cmdArgs] = fs.existsSync(TSX_BIN)
|
|
683
832
|
? [TSX_BIN, [_HERE, ...args]]
|
|
684
833
|
: [process.execPath, [...process.execArgv, _HERE, ...args]];
|
|
834
|
+
const headBefore = headOf(root);
|
|
835
|
+
const treeBefore = treeStamp(root);
|
|
685
836
|
const res = spawnSync(cmd, cmdArgs, {
|
|
686
837
|
env: hardenedSpawnEnv(),
|
|
687
838
|
stdio: 'ignore',
|
|
@@ -705,14 +856,38 @@ function runBudgetedBuild(root: string, out: string, update: boolean, budgetSeco
|
|
|
705
856
|
process.stderr.write('code-graph: refresh produced no output — old cache kept\n');
|
|
706
857
|
return 3;
|
|
707
858
|
}
|
|
859
|
+
// A checkout or rebase that lands mid-build leaves a graph of a tree that
|
|
860
|
+
// existed only in passing; publishing it would label mixed content fresh.
|
|
861
|
+
// An unreadable HEAD is not evidence it held still, so it fails the same way.
|
|
862
|
+
if (headBefore === null || headOf(root) !== headBefore) {
|
|
863
|
+
cleanupTemp(tmpOut);
|
|
864
|
+
process.stderr.write('code-graph: HEAD moved (or could not be read) during the refresh — result discarded\n');
|
|
865
|
+
return HEAD_MOVED;
|
|
866
|
+
}
|
|
867
|
+
if (treeStamp(root) !== treeBefore) {
|
|
868
|
+
cleanupTemp(tmpOut);
|
|
869
|
+
process.stderr.write('code-graph: files changed during the refresh — result discarded\n');
|
|
870
|
+
return HEAD_MOVED;
|
|
871
|
+
}
|
|
708
872
|
fs.renameSync(tmpOut, out); // same-dir → atomic; real cache only ever changes here
|
|
709
873
|
const tmpSidecar = sidecarPath(tmpOut);
|
|
710
874
|
if (fs.existsSync(tmpSidecar)) fs.renameSync(tmpSidecar, sidecarPath(out));
|
|
711
|
-
|
|
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`);
|
|
712
882
|
return 0;
|
|
713
883
|
}
|
|
714
884
|
|
|
715
|
-
|
|
885
|
+
/**
|
|
886
|
+
* `--single-flight` takes the lock the git-hook refresh block takes and exits 0
|
|
887
|
+
* when another refresh holds it. `--map` redraws `agents/map/` afterwards, only
|
|
888
|
+
* when the project already has one and it is older than the cache.
|
|
889
|
+
*/
|
|
890
|
+
async function cmdRefresh(argv: string[]): Promise<number> {
|
|
716
891
|
const root = resolveRoot(argv);
|
|
717
892
|
const out = flag(argv, '--out') ?? cacheFor(root);
|
|
718
893
|
const budgetSeconds = budgetSecondsOf(argv);
|
|
@@ -720,12 +895,137 @@ function cmdRefresh(argv: string[]): number {
|
|
|
720
895
|
process.stderr.write(`code-graph: root not found: ${root}\n`);
|
|
721
896
|
return 1;
|
|
722
897
|
}
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
898
|
+
// `--map` writes the same files a manual `map` does, so it is never drawn unlocked.
|
|
899
|
+
const locked = argv.includes('--single-flight') || argv.includes('--map');
|
|
900
|
+
const release = locked ? acquireRefreshLock(root) : () => {};
|
|
901
|
+
const record = (outcome: string, detail?: string): void => {
|
|
902
|
+
if (locked) recordRefresh(root, { by: 'refresh', outcome, ...(detail ? { detail } : {}) });
|
|
903
|
+
};
|
|
904
|
+
if (release === null) {
|
|
905
|
+
process.stdout.write('another refresh is running — nothing to do\n');
|
|
906
|
+
record('locked');
|
|
726
907
|
return 0;
|
|
727
908
|
}
|
|
728
|
-
|
|
909
|
+
try {
|
|
910
|
+
let rc = HEAD_MOVED;
|
|
911
|
+
// One retry: a second move inside one build window means the tree is
|
|
912
|
+
// still in motion, and the next hook or session start will refresh it.
|
|
913
|
+
for (let attempt = 0; attempt < 2 && rc === HEAD_MOVED; attempt++) {
|
|
914
|
+
const v = computeVerdict(root, out);
|
|
915
|
+
if (v.verdict === 'FRESH') {
|
|
916
|
+
process.stdout.write('fresh — nothing to do\n');
|
|
917
|
+
rc = 0;
|
|
918
|
+
break;
|
|
919
|
+
}
|
|
920
|
+
rc = runBudgetedBuild(root, out, v.verdict === 'STALE', budgetSeconds);
|
|
921
|
+
}
|
|
922
|
+
if (rc === HEAD_MOVED) {
|
|
923
|
+
record('tree-moved', 'HEAD or files changed during both attempts; the next refresh retries');
|
|
924
|
+
return 1;
|
|
925
|
+
}
|
|
926
|
+
if (rc !== 0) {
|
|
927
|
+
record('failed', `build exited ${rc}`);
|
|
928
|
+
return rc;
|
|
929
|
+
}
|
|
930
|
+
if (out === cacheFor(root)) registerUse(root, null, git(root, 'rev-parse', 'HEAD'));
|
|
931
|
+
if (argv.includes('--map') && mapIsBehind(root, out)) {
|
|
932
|
+
const mrc = await drawMap(['--root', root, '--no-build', '--no-verify']);
|
|
933
|
+
record(mrc === 0 ? 'refreshed-with-map' : 'map-failed', mrc === 0 ? undefined : `map exited ${mrc}`);
|
|
934
|
+
return mrc;
|
|
935
|
+
}
|
|
936
|
+
record('refreshed');
|
|
937
|
+
return rc;
|
|
938
|
+
} finally {
|
|
939
|
+
release();
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
/**
|
|
944
|
+
* `auto-refresh on|off|status` — the explicit, local, per-project opt-in the
|
|
945
|
+
* `code-graph-refresh` session concern requires before it starts anything
|
|
946
|
+
* (ratification round of 2026-10-23: an existing cache is not consent).
|
|
947
|
+
*/
|
|
948
|
+
function cmdAutoRefresh(argv: string[]): number {
|
|
949
|
+
const root = resolveRoot(argv);
|
|
950
|
+
const action = argv.find((a) => !a.startsWith('--') && !isFlagValue(argv, a)) ?? 'status';
|
|
951
|
+
const home = graphHome(root);
|
|
952
|
+
const marker = home.autoRefreshPath;
|
|
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
|
+
}
|
|
963
|
+
fs.mkdirSync(path.dirname(marker), { recursive: true });
|
|
964
|
+
fs.writeFileSync(marker, `${JSON.stringify({ enabled_at: new Date().toISOString() })}\n`);
|
|
965
|
+
const cli = usableCli(root);
|
|
966
|
+
process.stdout.write(
|
|
967
|
+
'auto-refresh on — each agent session start refreshes the graph, and agents/map/ when it exists,\n' +
|
|
968
|
+
'in the background (POSIX hosts). `code-graph auto-refresh off` stops it.\n' +
|
|
969
|
+
'The opt-in is per working tree: run it again in each new git worktree.\n' +
|
|
970
|
+
(fs.existsSync(cacheFor(root)) ? '' : 'No graph yet: `code-graph build` or `code-graph map` creates one.\n'),
|
|
971
|
+
);
|
|
972
|
+
if ('reason' in cli) {
|
|
973
|
+
process.stderr.write(
|
|
974
|
+
cli.reason === 'no-cli'
|
|
975
|
+
? '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'
|
|
976
|
+
: `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`,
|
|
977
|
+
);
|
|
978
|
+
}
|
|
979
|
+
return 0;
|
|
980
|
+
}
|
|
981
|
+
if (action === 'off') {
|
|
982
|
+
fs.rmSync(marker, { force: true });
|
|
983
|
+
process.stdout.write('auto-refresh off\n');
|
|
984
|
+
return 0;
|
|
985
|
+
}
|
|
986
|
+
if (action !== 'status') {
|
|
987
|
+
process.stderr.write('usage: code_graph/cli.ts auto-refresh on|off|status [--root P]\n');
|
|
988
|
+
return 2;
|
|
989
|
+
}
|
|
990
|
+
const cli = usableCli(root);
|
|
991
|
+
const last = readRefreshRecords(root);
|
|
992
|
+
const line = (r: (typeof last)['refresh']): string => (r ? `${r.outcome} at ${r.at}${r.detail ? ` (${r.detail})` : ''}` : 'never');
|
|
993
|
+
const mapDir = home.mapDir;
|
|
994
|
+
const gen = graphGeneration(cacheFor(root));
|
|
995
|
+
const drawnFrom = mapGeneration(mapDir);
|
|
996
|
+
const mutex = reclaimMutexPath(home.lockPath);
|
|
997
|
+
process.stdout.write(
|
|
998
|
+
`auto-refresh: ${fs.existsSync(marker) ? 'on' : 'off'}\n` +
|
|
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` +
|
|
1000
|
+
`last session start: ${line(last['session-start'])}\n` +
|
|
1001
|
+
`last refresh: ${line(last.refresh)}\n` +
|
|
1002
|
+
`graph: ${computeVerdict(root, cacheFor(root)).verdict}\n` +
|
|
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` +
|
|
1004
|
+
(fs.existsSync(mutex) && mutexAgeMs(mutex) > 60_000
|
|
1005
|
+
? `reclaim mutex: ${shown(root, mutex)} left by a crash — stale locks stay held until it is removed by hand\n`
|
|
1006
|
+
: ''),
|
|
1007
|
+
);
|
|
1008
|
+
return 0;
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
function mutexAgeMs(p: string): number {
|
|
1012
|
+
try {
|
|
1013
|
+
return Date.now() - fs.statSync(p).mtimeMs;
|
|
1014
|
+
} catch {
|
|
1015
|
+
return 0;
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
/**
|
|
1020
|
+
* Whether the map was drawn from a different graph than the cache now holds —
|
|
1021
|
+
* decided by the generation both record, never by modification times, which a
|
|
1022
|
+
* copied or restored file carries wrongly. No map → never draw one unbidden.
|
|
1023
|
+
*/
|
|
1024
|
+
function mapIsBehind(root: string, cache: string): boolean {
|
|
1025
|
+
const mapDir = graphHome(root).mapDir;
|
|
1026
|
+
if (!fs.existsSync(mapDir)) return false;
|
|
1027
|
+
const gen = graphGeneration(cache);
|
|
1028
|
+
return gen !== null && mapGeneration(mapDir) !== gen;
|
|
729
1029
|
}
|
|
730
1030
|
|
|
731
1031
|
/**
|
|
@@ -734,11 +1034,39 @@ function cmdRefresh(argv: string[]): number {
|
|
|
734
1034
|
* Builds or updates the graph first when it is absent or behind.
|
|
735
1035
|
*/
|
|
736
1036
|
async function cmdMap(argv: string[]): Promise<number> {
|
|
1037
|
+
if (argv.includes('--estate')) return estateMapMain(argv);
|
|
737
1038
|
const root = resolveRoot(argv);
|
|
738
1039
|
if (!fs.existsSync(root)) {
|
|
739
1040
|
process.stderr.write(`code-graph: root not found: ${root}\n`);
|
|
740
1041
|
return 1;
|
|
741
1042
|
}
|
|
1043
|
+
// A background refresh may be redrawing the same files; two writers would
|
|
1044
|
+
// interleave them. Wait for it — a session start is exactly when someone asks
|
|
1045
|
+
// for the map — but not beyond one refresh budget plus a redraw.
|
|
1046
|
+
let release = acquireRefreshLock(root);
|
|
1047
|
+
const deadline = Date.now() + MAP_LOCK_WAIT_MS;
|
|
1048
|
+
if (release === null) process.stderr.write('code-graph: waiting for a background refresh to finish…\n');
|
|
1049
|
+
while (release === null && Date.now() < deadline) {
|
|
1050
|
+
await new Promise((r) => setTimeout(r, 250));
|
|
1051
|
+
release = acquireRefreshLock(root);
|
|
1052
|
+
}
|
|
1053
|
+
if (release === null) {
|
|
1054
|
+
process.stderr.write('code-graph: a refresh is still redrawing the map — try again in a moment\n');
|
|
1055
|
+
return 1;
|
|
1056
|
+
}
|
|
1057
|
+
try {
|
|
1058
|
+
return await drawMap(argv);
|
|
1059
|
+
} finally {
|
|
1060
|
+
release();
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
/** How long `map` waits for a background refresh to release the lock. */
|
|
1065
|
+
const MAP_LOCK_WAIT_MS = Number(process.env['AGENT_CONFIG_MAP_LOCK_WAIT_MS'] ?? 90 * 1000);
|
|
1066
|
+
|
|
1067
|
+
/** The map itself; callers hold the refresh lock. */
|
|
1068
|
+
async function drawMap(argv: string[]): Promise<number> {
|
|
1069
|
+
const root = resolveRoot(argv);
|
|
742
1070
|
const out = flag(argv, '--out');
|
|
743
1071
|
const diffRev = flag(argv, '--diff');
|
|
744
1072
|
const r = await buildMap(root, {
|
|
@@ -756,15 +1084,25 @@ async function cmdMap(argv: string[]): Promise<number> {
|
|
|
756
1084
|
(r.model.diff
|
|
757
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`
|
|
758
1086
|
: '') +
|
|
759
|
-
` wrote: ${r.written.length} files under ${rel}/ — open ${path.join(rel, 'index.html')}\n
|
|
1087
|
+
` wrote: ${r.written.length} files under ${rel}/ — open ${path.join(rel, 'index.html')}\n` +
|
|
1088
|
+
(fs.existsSync(graphHome(root).autoRefreshPath)
|
|
1089
|
+
? ''
|
|
1090
|
+
: ' keep it current at every agent session start: agent-config code-graph auto-refresh on\n'),
|
|
760
1091
|
);
|
|
761
1092
|
let rc = 0;
|
|
1093
|
+
const renderFile = path.join(r.outDir, 'map-render.json');
|
|
762
1094
|
if (argv.includes('--no-verify')) {
|
|
763
|
-
// A check
|
|
764
|
-
|
|
1095
|
+
// A check that names the generation it checked can stay beside a newer
|
|
1096
|
+
// map without being read as about it; one that names none cannot.
|
|
1097
|
+
try {
|
|
1098
|
+
const prev = JSON.parse(fs.readFileSync(renderFile, 'utf-8')) as { graph_generation?: unknown };
|
|
1099
|
+
if (typeof prev.graph_generation !== 'string') fs.rmSync(renderFile, { force: true });
|
|
1100
|
+
} catch {
|
|
1101
|
+
fs.rmSync(renderFile, { force: true });
|
|
1102
|
+
}
|
|
765
1103
|
} else {
|
|
766
1104
|
const check = await verifyMap(path.join(r.outDir, 'index.html'));
|
|
767
|
-
fs.writeFileSync(
|
|
1105
|
+
fs.writeFileSync(renderFile, `${JSON.stringify({ ...check, graph_generation: r.meta.graph_generation }, null, 2)}\n`);
|
|
768
1106
|
const problems = check.viewports
|
|
769
1107
|
.flatMap((v) => [
|
|
770
1108
|
...(v.pageErrors ? [`${v.name}: ${v.pageErrors} page error(s)`] : []),
|
|
@@ -831,7 +1169,7 @@ export async function main(argv: string[] = process.argv.slice(2)): Promise<numb
|
|
|
831
1169
|
if (sub === 'build') return await cmdBuild(argv.slice(1));
|
|
832
1170
|
if (sub === 'validate') return cmdValidate(argv.slice(1));
|
|
833
1171
|
if (sub === 'detect') return cmdDetect(argv.slice(1));
|
|
834
|
-
if (sub === 'refresh') return cmdRefresh(argv.slice(1));
|
|
1172
|
+
if (sub === 'refresh') return await cmdRefresh(argv.slice(1));
|
|
835
1173
|
if (sub === 'query' || sub === 'explain' || sub === 'affected' || sub === 'path')
|
|
836
1174
|
return cmdQuery(sub, argv.slice(1));
|
|
837
1175
|
if (sub === 'impact') return cmdImpact(argv.slice(1));
|
|
@@ -839,14 +1177,23 @@ export async function main(argv: string[] = process.argv.slice(2)): Promise<numb
|
|
|
839
1177
|
if (sub === 'untested') return cmdUntested(argv.slice(1));
|
|
840
1178
|
if (sub === 'dead') return cmdDead(argv.slice(1));
|
|
841
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)]);
|
|
842
1182
|
if (sub === 'map') return await cmdMap(argv.slice(1));
|
|
843
1183
|
if (sub === 'outcome') return cmdOutcome(argv.slice(1));
|
|
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));
|
|
844
1187
|
process.stderr.write(
|
|
845
1188
|
'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',
|
|
1189
|
+
'impact|tests-for|untested|dead|suggest-verb|map|outcome|auto-refresh|projects|estate|lineage> [options]\n',
|
|
847
1190
|
);
|
|
848
1191
|
return 2;
|
|
849
1192
|
} catch (e) {
|
|
1193
|
+
if (e instanceof UsageError) {
|
|
1194
|
+
process.stderr.write(`code-graph: ${e.message}\n`);
|
|
1195
|
+
return 1;
|
|
1196
|
+
}
|
|
850
1197
|
process.stderr.write(`code-graph: internal error: ${(e as Error).message}\n`);
|
|
851
1198
|
return 3;
|
|
852
1199
|
}
|