@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
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The storage budget for user-global graph homes (ADR-299, council O4).
|
|
3
|
+
*
|
|
4
|
+
* Only directories under `runtime/graphs/` are ever evicted — a graph under a
|
|
5
|
+
* repository's own `agents/` is the project's, not this machine's cache.
|
|
6
|
+
* Eviction is least-recently-QUERIED first: a query touches its home's
|
|
7
|
+
* `.last-query` marker, a build or refresh records it in the registry, and a
|
|
8
|
+
* registry scan changes neither. A pinned project's homes, a home whose
|
|
9
|
+
* refresh lock is held, and a home written to within a stale-lock window or
|
|
10
|
+
* holding a temp output are never evicted, so a budget can stay exceeded. The
|
|
11
|
+
* last guard exists because a plain `code-graph build` takes no lock.
|
|
12
|
+
*/
|
|
13
|
+
import * as fs from 'node:fs';
|
|
14
|
+
import * as path from 'node:path';
|
|
15
|
+
|
|
16
|
+
import { readRegistry, updateRegistry } from '../_lib/project_registry.js';
|
|
17
|
+
import { graphHome, graphsRoot } from './graph_home.js';
|
|
18
|
+
import { isStale, STALE_LOCK_MS } from './refresh_lock.js';
|
|
19
|
+
|
|
20
|
+
const MB = 1024 * 1024;
|
|
21
|
+
const LAST_QUERY = '.last-query';
|
|
22
|
+
|
|
23
|
+
/** Record that a query just read `root`'s graph. Only user-global homes keep recency. */
|
|
24
|
+
export function touchHome(root: string): void {
|
|
25
|
+
try {
|
|
26
|
+
const home = graphHome(root);
|
|
27
|
+
if (home.kind !== 'global' || !fs.existsSync(home.dir)) return;
|
|
28
|
+
const marker = path.join(home.dir, LAST_QUERY);
|
|
29
|
+
const now = new Date();
|
|
30
|
+
try {
|
|
31
|
+
fs.utimesSync(marker, now, now);
|
|
32
|
+
} catch {
|
|
33
|
+
fs.writeFileSync(marker, '');
|
|
34
|
+
}
|
|
35
|
+
} catch {
|
|
36
|
+
/* recency is a hint; a query never fails on it */
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface Walk {
|
|
41
|
+
bytes: number;
|
|
42
|
+
/** Newest mtime of anything below the home except the query marker. */
|
|
43
|
+
newest: number;
|
|
44
|
+
/** A build's or refresh's temp output is present. */
|
|
45
|
+
temp: boolean;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const TEMP_RE = /\.refresh-tmp-|\.tmp-/;
|
|
49
|
+
|
|
50
|
+
function walk(dir: string, top: boolean, acc: Walk = { bytes: 0, newest: 0, temp: false }): Walk {
|
|
51
|
+
let entries: fs.Dirent[];
|
|
52
|
+
try {
|
|
53
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
54
|
+
} catch {
|
|
55
|
+
return acc;
|
|
56
|
+
}
|
|
57
|
+
for (const e of entries) {
|
|
58
|
+
const p = path.join(dir, e.name);
|
|
59
|
+
if (TEMP_RE.test(e.name)) acc.temp = true;
|
|
60
|
+
let st: fs.Stats;
|
|
61
|
+
try {
|
|
62
|
+
st = fs.lstatSync(p);
|
|
63
|
+
} catch {
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (!(top && e.name === LAST_QUERY)) acc.newest = Math.max(acc.newest, st.mtimeMs);
|
|
67
|
+
if (e.isDirectory()) walk(p, false, acc);
|
|
68
|
+
else acc.bytes += st.size;
|
|
69
|
+
}
|
|
70
|
+
return acc;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface HomeUsage {
|
|
74
|
+
dir: string;
|
|
75
|
+
bytes: number;
|
|
76
|
+
/** Last query, else last build or refresh, else the directory's own mtime. */
|
|
77
|
+
used_at: number;
|
|
78
|
+
pinned: boolean;
|
|
79
|
+
locked: boolean;
|
|
80
|
+
/** Written within the last {@link STALE_LOCK_MS}, or holding a temp output: a build may be mid-write. */
|
|
81
|
+
busy: boolean;
|
|
82
|
+
project: string | null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Every user-global graph home with its size and why it may or may not go. */
|
|
86
|
+
export function homeUsage(): HomeUsage[] {
|
|
87
|
+
const base = graphsRoot();
|
|
88
|
+
let names: string[];
|
|
89
|
+
try {
|
|
90
|
+
names = fs.readdirSync(base);
|
|
91
|
+
} catch {
|
|
92
|
+
return [];
|
|
93
|
+
}
|
|
94
|
+
const reg = readRegistry();
|
|
95
|
+
const owner = new Map<string, { id: string; pinned: boolean; at: number }>();
|
|
96
|
+
for (const [id, e] of Object.entries(reg.projects)) {
|
|
97
|
+
for (const [dir, at] of Object.entries(e.homes)) owner.set(dir, { id, pinned: e.pinned, at: Date.parse(at) || 0 });
|
|
98
|
+
}
|
|
99
|
+
const out: HomeUsage[] = [];
|
|
100
|
+
const now = Date.now();
|
|
101
|
+
for (const name of names) {
|
|
102
|
+
const dir = path.join(base, name);
|
|
103
|
+
let st: fs.Stats;
|
|
104
|
+
try {
|
|
105
|
+
st = fs.statSync(dir);
|
|
106
|
+
} catch {
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
if (!st.isDirectory()) continue;
|
|
110
|
+
let queried = 0;
|
|
111
|
+
try {
|
|
112
|
+
queried = fs.statSync(path.join(dir, LAST_QUERY)).mtimeMs;
|
|
113
|
+
} catch {
|
|
114
|
+
queried = 0;
|
|
115
|
+
}
|
|
116
|
+
const o = owner.get(dir);
|
|
117
|
+
const lock = path.join(dir, 'state', 'code-graph-refresh.lock');
|
|
118
|
+
const w = walk(dir, true);
|
|
119
|
+
out.push({
|
|
120
|
+
dir,
|
|
121
|
+
bytes: w.bytes,
|
|
122
|
+
used_at: Math.max(queried, o?.at ?? 0) || st.mtimeMs,
|
|
123
|
+
pinned: o?.pinned ?? false,
|
|
124
|
+
locked: fs.existsSync(lock) && !isStale(lock),
|
|
125
|
+
busy: w.temp || now - w.newest < STALE_LOCK_MS,
|
|
126
|
+
project: o?.id ?? null,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
return out;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Evict the least recently used homes until the total fits `budget_mb`;
|
|
134
|
+
* returns what went. `keep` is the home a build just wrote, which is never its
|
|
135
|
+
* own victim.
|
|
136
|
+
*/
|
|
137
|
+
export function enforceBudget(keep?: string): { evicted: HomeUsage[]; total: number; budget: number } {
|
|
138
|
+
const budget = readRegistry().budget_mb * MB;
|
|
139
|
+
const homes = homeUsage();
|
|
140
|
+
let total = homes.reduce((n, h) => n + h.bytes, 0);
|
|
141
|
+
const evicted: HomeUsage[] = [];
|
|
142
|
+
for (const h of [...homes].sort((a, b) => a.used_at - b.used_at)) {
|
|
143
|
+
if (total <= budget) break;
|
|
144
|
+
if (h.pinned || h.locked || h.busy || h.dir === keep) continue;
|
|
145
|
+
fs.rmSync(h.dir, { recursive: true, force: true });
|
|
146
|
+
total -= h.bytes;
|
|
147
|
+
evicted.push(h);
|
|
148
|
+
}
|
|
149
|
+
if (evicted.length) {
|
|
150
|
+
const gone = new Set(evicted.map((h) => h.dir));
|
|
151
|
+
updateRegistry((reg) => {
|
|
152
|
+
for (const e of Object.values(reg.projects)) {
|
|
153
|
+
for (const dir of Object.keys(e.homes)) if (gone.has(dir)) delete e.homes[dir];
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
return { evicted, total, budget };
|
|
158
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where a root's graph, map and refresh lock live (ADR-299).
|
|
3
|
+
*
|
|
4
|
+
* A root that has `agents/` or a project install keeps today's paths under
|
|
5
|
+
* `<root>/agents/`, byte-identical, so a map shared through the repository is
|
|
6
|
+
* unchanged. Every other root gets a home under the user-global
|
|
7
|
+
* `runtime/graphs/<repo id>-<checkout | instance>/`, so building a graph of
|
|
8
|
+
* somebody else's repository writes nothing into it, and two clones of one
|
|
9
|
+
* origin never share a cache, lock or budget slot. Resolving never creates `agents/`.
|
|
10
|
+
*/
|
|
11
|
+
import * as fs from 'node:fs';
|
|
12
|
+
import * as path from 'node:path';
|
|
13
|
+
|
|
14
|
+
import { type EnvMap, event4u_root } from '../_lib/user_global_paths.js';
|
|
15
|
+
import { repoIdentity } from './repo_identity.js';
|
|
16
|
+
|
|
17
|
+
export const STATE_DIR_REL = path.join('agents', 'runtime', 'state');
|
|
18
|
+
export const NATIVE_CACHE_REL = path.join(STATE_DIR_REL, 'code-graph-v1.json');
|
|
19
|
+
export const MAP_DIR_REL = path.join('agents', 'map');
|
|
20
|
+
export const LOCK_REL = path.join(STATE_DIR_REL, 'code-graph-refresh.lock');
|
|
21
|
+
/** Written by `code-graph auto-refresh on`; the session concern acts only while it exists. */
|
|
22
|
+
export const AUTO_REFRESH_REL = path.join(STATE_DIR_REL, 'code-graph-auto-refresh.json');
|
|
23
|
+
|
|
24
|
+
export interface GraphHome {
|
|
25
|
+
kind: 'project' | 'global';
|
|
26
|
+
root: string;
|
|
27
|
+
/** `<root>/agents` for a project home, the per-repository directory otherwise. */
|
|
28
|
+
dir: string;
|
|
29
|
+
stateDir: string;
|
|
30
|
+
cachePath: string;
|
|
31
|
+
mapDir: string;
|
|
32
|
+
lockPath: string;
|
|
33
|
+
autoRefreshPath: string;
|
|
34
|
+
repoId: string | null;
|
|
35
|
+
instance: string | null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function graphsRoot(env?: EnvMap | null): string {
|
|
39
|
+
return path.join(event4u_root(env), 'runtime', 'graphs');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** `agents/` exists, or the installer's settings file marks a project install. */
|
|
43
|
+
export function hasProjectHome(root: string): boolean {
|
|
44
|
+
return fs.existsSync(path.join(root, 'agents')) || fs.existsSync(path.join(root, '.agent-settings.yml'));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Today's in-repository paths; callers that serve project homes only use this alone. */
|
|
48
|
+
export function projectHome(root: string): GraphHome {
|
|
49
|
+
return {
|
|
50
|
+
kind: 'project',
|
|
51
|
+
root,
|
|
52
|
+
dir: path.join(root, 'agents'),
|
|
53
|
+
stateDir: path.join(root, STATE_DIR_REL),
|
|
54
|
+
cachePath: path.join(root, NATIVE_CACHE_REL),
|
|
55
|
+
mapDir: path.join(root, MAP_DIR_REL),
|
|
56
|
+
lockPath: path.join(root, LOCK_REL),
|
|
57
|
+
autoRefreshPath: path.join(root, AUTO_REFRESH_REL),
|
|
58
|
+
repoId: null,
|
|
59
|
+
instance: null,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function graphHome(root: string, env?: EnvMap | null): GraphHome {
|
|
64
|
+
if (hasProjectHome(root)) return projectHome(root);
|
|
65
|
+
const id = repoIdentity(root, env);
|
|
66
|
+
const dir = path.join(graphsRoot(env), `${id.repoId}-${id.instance ?? id.checkout}`);
|
|
67
|
+
const stateDir = path.join(dir, 'state');
|
|
68
|
+
return {
|
|
69
|
+
kind: 'global',
|
|
70
|
+
root,
|
|
71
|
+
dir,
|
|
72
|
+
stateDir,
|
|
73
|
+
cachePath: path.join(stateDir, path.basename(NATIVE_CACHE_REL)),
|
|
74
|
+
mapDir: path.join(dir, 'map'),
|
|
75
|
+
lockPath: path.join(stateDir, path.basename(LOCK_REL)),
|
|
76
|
+
autoRefreshPath: path.join(stateDir, path.basename(AUTO_REFRESH_REL)),
|
|
77
|
+
repoId: id.repoId,
|
|
78
|
+
instance: id.instance,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `code-graph lineage <verb>` — where harvested mechanisms came from, and what is still missing.
|
|
3
|
+
*
|
|
4
|
+
* Two ledgers answer, never one (council O5): the tracked, source-free decision ledger
|
|
5
|
+
* `provenance/mechanisms.jsonl` of the repo at `--root`, and the local source ledger under
|
|
6
|
+
* the user-global `runtime/lineage/` (`_lib/lineage_ledger.ts`). A verb that needs the
|
|
7
|
+
* local side and the key refuses without them instead of answering from half the data.
|
|
8
|
+
*
|
|
9
|
+
* Exit codes: 0 ok · 1 findings · 2 usage or refused.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import * as fs from 'node:fs';
|
|
13
|
+
import * as path from 'node:path';
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
assertOutsideWorkTree,
|
|
17
|
+
checkLedger,
|
|
18
|
+
hasLedger,
|
|
19
|
+
LineageError,
|
|
20
|
+
lineageDir,
|
|
21
|
+
mergeIntoLedger,
|
|
22
|
+
readLedger,
|
|
23
|
+
writeLedger,
|
|
24
|
+
writePrivateFile,
|
|
25
|
+
} from '../_lib/lineage_ledger.js';
|
|
26
|
+
import { BACKFILL_DIR_REL, isIgnored, scanTree, writeBackfill } from '../_lib/lineage_backfill.js';
|
|
27
|
+
import { LINEAGE_VIEW_REL, renderSourceLineage, renderTrackedLineage } from '../_lib/lineage_map.js';
|
|
28
|
+
import { refuseUnderCi, renderWatchReport, renderWatchStub, runWatch, systemGit } from '../_lib/lineage_watch.js';
|
|
29
|
+
import { gaps, lineageFrom, lineageOf, readDecisions, rejected } from '../_lib/lineage_query.js';
|
|
30
|
+
import { buildLocalImport } from '../_lib/lineage_seed.js';
|
|
31
|
+
import { resolve_keys } from '../_lib/link_crypto.js';
|
|
32
|
+
import type { EnvMap } from '../_lib/user_global_paths.js';
|
|
33
|
+
|
|
34
|
+
export interface LineageDeps {
|
|
35
|
+
readonly env?: EnvMap;
|
|
36
|
+
/** Link keys; resolved from settings when absent. Tests pass them explicitly. */
|
|
37
|
+
readonly keys?: readonly string[];
|
|
38
|
+
readonly now?: () => Date;
|
|
39
|
+
readonly stdout?: (s: string) => void;
|
|
40
|
+
readonly stderr?: (s: string) => void;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface Io {
|
|
44
|
+
readonly env: EnvMap;
|
|
45
|
+
readonly root: string;
|
|
46
|
+
readonly keys: () => readonly string[];
|
|
47
|
+
readonly now: () => Date;
|
|
48
|
+
readonly out: (s: string) => void;
|
|
49
|
+
readonly err: (s: string) => void;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function flag(argv: readonly string[], name: string): string | undefined {
|
|
53
|
+
const i = argv.indexOf(name);
|
|
54
|
+
return i === -1 ? undefined : argv[i + 1];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function cmdCheck(io: Io): number {
|
|
58
|
+
const dir = lineageDir(io.env);
|
|
59
|
+
if (!hasLedger(dir)) {
|
|
60
|
+
io.err(`code-graph lineage: no local lineage ledger at ${dir} — run \`code-graph lineage import\` first\n`);
|
|
61
|
+
return 2;
|
|
62
|
+
}
|
|
63
|
+
const findings = checkLedger(dir, io.keys());
|
|
64
|
+
const { sources, edges } = readLedger(dir);
|
|
65
|
+
if (findings.length > 0) {
|
|
66
|
+
for (const f of findings) io.err(`${f.file}:${String(f.line)} ${f.message}\n`);
|
|
67
|
+
io.err(`code-graph lineage check: ${String(findings.length)} finding(s)\n`);
|
|
68
|
+
return 1;
|
|
69
|
+
}
|
|
70
|
+
io.out(`code-graph lineage check: ${String(sources.length)} source(s), ${String(edges.length)} edge(s) OK at ${dir}\n`);
|
|
71
|
+
return 0;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Refuse, naming the setting, when no link key is configured. */
|
|
75
|
+
export function needKeys(io: Io, verb: string): readonly string[] | null {
|
|
76
|
+
const keys = io.keys();
|
|
77
|
+
if (keys.length > 0) return keys;
|
|
78
|
+
io.err(
|
|
79
|
+
`code-graph lineage ${verb}: no link_encryption_key configured (secrets.link_encryption_key) — ` +
|
|
80
|
+
'the local ledger is sealed with it, so nothing is read or written without it\n',
|
|
81
|
+
);
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function cmdImport(io: Io): number {
|
|
86
|
+
const keys = needKeys(io, 'import');
|
|
87
|
+
if (keys === null) return 2;
|
|
88
|
+
const { ledger, refused } = buildLocalImport(io.root, keys);
|
|
89
|
+
const dir = lineageDir(io.env);
|
|
90
|
+
const added = mergeIntoLedger(dir, ledger, keys);
|
|
91
|
+
for (const r of refused) io.err(`code-graph lineage import: skipped ${r}\n`);
|
|
92
|
+
io.out(
|
|
93
|
+
`code-graph lineage import: ${String(added.sources)} new source(s), ${String(added.edges)} new edge(s) into ${dir}` +
|
|
94
|
+
`${refused.length > 0 ? ` · ${String(refused.length)} skipped` : ''}\n`,
|
|
95
|
+
);
|
|
96
|
+
return refused.length > 0 ? 1 : 0;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** The graph's node lookup for `root`, or null when no graph has been built. */
|
|
100
|
+
async function graphLookup(io: Io, explicit: string | undefined): Promise<{ has(id: string): boolean } | null> {
|
|
101
|
+
// Dynamic: cli.ts dispatches into this module, and the default cache path is its to own.
|
|
102
|
+
const graphPath = explicit ?? (await import('./cli.js')).cacheFor(io.root);
|
|
103
|
+
if (!fs.existsSync(graphPath)) return null;
|
|
104
|
+
const { loadGraph } = await import('./query.js');
|
|
105
|
+
return loadGraph(graphPath).byId;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function cmdOf(io: Io, query: string | undefined, graphFlag: string | undefined): Promise<number> {
|
|
109
|
+
if (query === undefined || query.startsWith('--')) {
|
|
110
|
+
io.err('usage: code-graph lineage of <mechanism-id|path> [--root P] [--graph P]\n');
|
|
111
|
+
return 2;
|
|
112
|
+
}
|
|
113
|
+
const entries = lineageOf(io.root, query, await graphLookup(io, graphFlag));
|
|
114
|
+
if (entries.length === 0) {
|
|
115
|
+
io.err(`code-graph lineage of: no mechanism matches ${query} in provenance/mechanisms.jsonl\n`);
|
|
116
|
+
return 1;
|
|
117
|
+
}
|
|
118
|
+
let broken = 0;
|
|
119
|
+
for (const e of entries) {
|
|
120
|
+
io.out(`${e.mechanism_id} ${e.verdict} ${e.decided_at} (${String(e.decisions)} decision(s))\n`);
|
|
121
|
+
for (const r of e.realised) {
|
|
122
|
+
if (r.status === 'broken') broken += 1;
|
|
123
|
+
const g = r.in_graph === null ? 'no graph built' : r.in_graph ? 'indexed' : 'not indexed';
|
|
124
|
+
io.out(` realised_in ${r.path} ${r.status === 'ok' ? 'ok' : 'BROKEN'} graph: ${g}\n`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (broken > 0) io.err(`code-graph lineage of: ${String(broken)} broken edge(s) — the file moved or is gone\n`);
|
|
128
|
+
return broken > 0 ? 1 : 0;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function cmdGaps(io: Io): number {
|
|
132
|
+
const view = gaps(readDecisions(io.root));
|
|
133
|
+
for (const g of view) {
|
|
134
|
+
io.out(`${g.mechanism_id} ${g.verdict} ${g.owner}${g.reopen_if ? ` reopen if: ${g.reopen_if}` : ''}\n`);
|
|
135
|
+
}
|
|
136
|
+
io.out(`code-graph lineage gaps: ${String(view.length)} open mechanism(s)\n`);
|
|
137
|
+
return 0;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function cmdRejected(io: Io): number {
|
|
141
|
+
const view = rejected(readDecisions(io.root));
|
|
142
|
+
for (const r of view) {
|
|
143
|
+
io.out(`${r.mechanism_id} ${r.reason ?? ''}${r.reopen_if ? ` reopen if: ${r.reopen_if}` : ''}\n`);
|
|
144
|
+
}
|
|
145
|
+
io.out(`code-graph lineage rejected: ${String(view.length)} rejected mechanism(s)\n`);
|
|
146
|
+
return 0;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function cmdFrom(io: Io, query: string | undefined): number {
|
|
150
|
+
if (query === undefined || query.startsWith('--')) {
|
|
151
|
+
io.err('usage: code-graph lineage from <source-id|identity> [--root P]\n');
|
|
152
|
+
return 2;
|
|
153
|
+
}
|
|
154
|
+
const dir = lineageDir(io.env);
|
|
155
|
+
if (!hasLedger(dir)) {
|
|
156
|
+
io.err(`code-graph lineage from: no local lineage ledger at ${dir} — the source side exists only on the owner's machine\n`);
|
|
157
|
+
return 2;
|
|
158
|
+
}
|
|
159
|
+
const keys = needKeys(io, 'from');
|
|
160
|
+
if (keys === null) return 2;
|
|
161
|
+
const found = lineageFrom(readLedger(dir), query, keys, readDecisions(io.root));
|
|
162
|
+
if (found === null) {
|
|
163
|
+
io.err('code-graph lineage from: no such source in the local ledger\n');
|
|
164
|
+
return 1;
|
|
165
|
+
}
|
|
166
|
+
io.out(`${found.source.source_id} ${found.source.kind} ${found.identity} license ${found.source.license} watch ${found.source.watch ? 'on' : 'off'}\n`);
|
|
167
|
+
for (const m of found.mechanisms) {
|
|
168
|
+
io.out(` ${m.mechanism_id} ${m.verdict ?? '(no decision)'} pin ${m.pin}${m.locator ? ` at ${m.locator}` : ''}\n`);
|
|
169
|
+
}
|
|
170
|
+
return 0;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* `map --lineage` (tracked view, verdicts only) or `map --lineage --sources` (local view).
|
|
175
|
+
* The tracked view is written only into a repo that already has `agents/`, so a foreign
|
|
176
|
+
* repository never gains a directory from it.
|
|
177
|
+
*/
|
|
178
|
+
function cmdMap(io: Io, argv: readonly string[]): number {
|
|
179
|
+
const outFlag = flag(argv, '--out');
|
|
180
|
+
if (argv.includes('--sources')) {
|
|
181
|
+
const out = path.resolve(outFlag ?? path.join(lineageDir(io.env), 'map', 'sources.md'));
|
|
182
|
+
assertOutsideWorkTree(out);
|
|
183
|
+
const dir = lineageDir(io.env);
|
|
184
|
+
if (!hasLedger(dir)) {
|
|
185
|
+
io.err(`code-graph lineage map --sources: no local lineage ledger at ${dir}\n`);
|
|
186
|
+
return 2;
|
|
187
|
+
}
|
|
188
|
+
const keys = needKeys(io, 'map --sources');
|
|
189
|
+
if (keys === null) return 2;
|
|
190
|
+
writePrivateFile(out, renderSourceLineage(readLedger(dir), keys, readDecisions(io.root)));
|
|
191
|
+
io.out(`code-graph lineage map: source view written to ${out}\n`);
|
|
192
|
+
return 0;
|
|
193
|
+
}
|
|
194
|
+
const out = path.resolve(outFlag ?? path.join(io.root, LINEAGE_VIEW_REL));
|
|
195
|
+
if (outFlag === undefined && !fs.existsSync(path.join(io.root, 'agents'))) {
|
|
196
|
+
io.err(`code-graph lineage map: ${io.root} has no agents/ directory — pass --out to choose where the view goes\n`);
|
|
197
|
+
return 2;
|
|
198
|
+
}
|
|
199
|
+
fs.mkdirSync(path.dirname(out), { recursive: true });
|
|
200
|
+
fs.writeFileSync(out, renderTrackedLineage(readDecisions(io.root)));
|
|
201
|
+
io.out(`code-graph lineage map: tracked view written to ${out}\n`);
|
|
202
|
+
return 0;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function cmdBackfill(io: Io, argv: readonly string[]): number {
|
|
206
|
+
if (!argv.includes('--draft')) {
|
|
207
|
+
io.err('code-graph lineage backfill: only --draft exists — the backfill proposes drafts and never writes a ledger row\n');
|
|
208
|
+
return 2;
|
|
209
|
+
}
|
|
210
|
+
if (!isIgnored(io.root, BACKFILL_DIR_REL)) {
|
|
211
|
+
io.err(`code-graph lineage backfill: ${BACKFILL_DIR_REL}/ is not ignored by git in ${io.root} — decrypted identities would become trackable\n`);
|
|
212
|
+
return 2;
|
|
213
|
+
}
|
|
214
|
+
const keys = needKeys(io, 'backfill');
|
|
215
|
+
if (keys === null) return 2;
|
|
216
|
+
const result = scanTree(io.root, keys);
|
|
217
|
+
const out = writeBackfill(io.root, result);
|
|
218
|
+
for (const u of result.unclear) io.out(`unclear ${u}\n`);
|
|
219
|
+
for (const u of result.undecryptable) io.out(`undecryptable ${u}\n`);
|
|
220
|
+
io.out(
|
|
221
|
+
`code-graph lineage backfill: ${String(result.drafts.length)} draft(s), ${String(result.unclear.length)} unclear, ` +
|
|
222
|
+
`${String(result.undecryptable.length)} undecryptable — written to ${out}; no ledger row written\n`,
|
|
223
|
+
);
|
|
224
|
+
return 0;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function cmdWatchFlag(io: Io, on: boolean, id: string | undefined): number {
|
|
228
|
+
const dir = lineageDir(io.env);
|
|
229
|
+
if (id === undefined || !hasLedger(dir)) {
|
|
230
|
+
io.err(`usage: code-graph lineage watch ${on ? 'enable' : 'disable'} <source-id> (needs the local ledger at ${dir})\n`);
|
|
231
|
+
return 2;
|
|
232
|
+
}
|
|
233
|
+
const ledger = readLedger(dir);
|
|
234
|
+
const hit = ledger.sources.find((s) => s.source_id === id);
|
|
235
|
+
if (hit === undefined) {
|
|
236
|
+
io.err(`code-graph lineage watch: no source ${id} in the local ledger\n`);
|
|
237
|
+
return 1;
|
|
238
|
+
}
|
|
239
|
+
writeLedger(dir, { sources: ledger.sources.map((s) => (s.source_id === id ? { ...s, watch: on } : s)), edges: ledger.edges });
|
|
240
|
+
io.out(`code-graph lineage watch: ${id} ${on ? 'on' : 'off'}\n`);
|
|
241
|
+
return 0;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function cmdWatch(io: Io, argv: readonly string[]): number {
|
|
245
|
+
if (argv[1] === 'enable' || argv[1] === 'disable') return cmdWatchFlag(io, argv[1] === 'enable', argv[2]);
|
|
246
|
+
refuseUnderCi(io.env);
|
|
247
|
+
const dir = lineageDir(io.env);
|
|
248
|
+
if (!hasLedger(dir)) {
|
|
249
|
+
io.err(`code-graph lineage watch: no local lineage ledger at ${dir}\n`);
|
|
250
|
+
return 2;
|
|
251
|
+
}
|
|
252
|
+
const keys = needKeys(io, 'watch');
|
|
253
|
+
if (keys === null) return 2;
|
|
254
|
+
const reports = runWatch({ dir, keys, rows: readDecisions(io.root), git: systemGit, env: io.env });
|
|
255
|
+
for (const r of reports) {
|
|
256
|
+
io.out(`${r.source_id} ${r.status}${r.note ? ` ${r.note}` : ''}\n`);
|
|
257
|
+
for (const f of r.findings) io.out(` ${f.class} ${f.mechanism_id ?? '(no mechanism)'} ${f.locator}\n`);
|
|
258
|
+
}
|
|
259
|
+
io.out(`code-graph lineage watch: ${String(reports.length)} watched source(s)\n`);
|
|
260
|
+
if (argv.includes('--dry-run')) return 0;
|
|
261
|
+
|
|
262
|
+
const date = io.now().toISOString().slice(0, 10);
|
|
263
|
+
if (reports.length > 0) {
|
|
264
|
+
const reportPath = path.join(dir, 'watch', `report-${date}.md`);
|
|
265
|
+
writePrivateFile(reportPath, renderWatchReport(reports, date));
|
|
266
|
+
io.out(`code-graph lineage watch: report written to ${reportPath}\n`);
|
|
267
|
+
}
|
|
268
|
+
const stub = renderWatchStub(reports, date, keys[0] as string);
|
|
269
|
+
if (stub === null) return 0;
|
|
270
|
+
const stubDir = path.join(io.root, 'agents', 'roadmaps', 'stubs');
|
|
271
|
+
if (!fs.existsSync(stubDir)) {
|
|
272
|
+
io.err(`code-graph lineage watch: ${io.root} has no agents/roadmaps/stubs/ — the stub was not written\n`);
|
|
273
|
+
return 2;
|
|
274
|
+
}
|
|
275
|
+
const stubPath = path.join(stubDir, `road-to-lineage-delta-${date}.md`);
|
|
276
|
+
fs.writeFileSync(stubPath, stub);
|
|
277
|
+
io.out(`code-graph lineage watch: stub written to ${stubPath}\n`);
|
|
278
|
+
return 0;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const USAGE = 'usage: code-graph lineage <check|import|of|gaps|rejected|from|map|backfill|watch> [args] [--root P]\n';
|
|
282
|
+
|
|
283
|
+
export async function main(argv: readonly string[], deps: LineageDeps = {}): Promise<number> {
|
|
284
|
+
const env = deps.env ?? process.env;
|
|
285
|
+
const root = path.resolve(flag(argv, '--root') ?? process.cwd());
|
|
286
|
+
let cached: readonly string[] | null = null;
|
|
287
|
+
const io: Io = {
|
|
288
|
+
env,
|
|
289
|
+
root,
|
|
290
|
+
keys: () => (cached ??= deps.keys ?? resolve_keys(root)),
|
|
291
|
+
now: deps.now ?? (() => new Date()),
|
|
292
|
+
out: deps.stdout ?? ((s) => void process.stdout.write(s)),
|
|
293
|
+
err: deps.stderr ?? ((s) => void process.stderr.write(s)),
|
|
294
|
+
};
|
|
295
|
+
const verb = argv[0];
|
|
296
|
+
try {
|
|
297
|
+
if (verb === 'check') return cmdCheck(io);
|
|
298
|
+
if (verb === 'import') return cmdImport(io);
|
|
299
|
+
if (verb === 'of') return await cmdOf(io, argv[1], flag(argv, '--graph'));
|
|
300
|
+
if (verb === 'gaps') return cmdGaps(io);
|
|
301
|
+
if (verb === 'rejected') return cmdRejected(io);
|
|
302
|
+
if (verb === 'from') return cmdFrom(io, argv[1]);
|
|
303
|
+
if (verb === 'map') return cmdMap(io, argv);
|
|
304
|
+
if (verb === 'backfill') return cmdBackfill(io, argv);
|
|
305
|
+
if (verb === 'watch') return cmdWatch(io, argv);
|
|
306
|
+
} catch (e) {
|
|
307
|
+
if (e instanceof LineageError) {
|
|
308
|
+
io.err(`code-graph lineage: ${e.message}\n`);
|
|
309
|
+
return 2;
|
|
310
|
+
}
|
|
311
|
+
throw e;
|
|
312
|
+
}
|
|
313
|
+
io.err(USAGE);
|
|
314
|
+
return 2;
|
|
315
|
+
}
|
|
@@ -23,7 +23,8 @@ import { write_atomic } from '../_lib/fs_atomic.js';
|
|
|
23
23
|
import { hardenedSpawnEnv } from '../_lib/spawn_env.js';
|
|
24
24
|
import { buildFromRepo, readProjectProbe } from './build.js';
|
|
25
25
|
import { acquire as acquireRefreshLock, heldByThisProcess } from './refresh_lock.js';
|
|
26
|
-
import { computeVerdict, graphState
|
|
26
|
+
import { computeVerdict, graphState } from './detect.js';
|
|
27
|
+
import { graphHome } from './graph_home.js';
|
|
27
28
|
import {
|
|
28
29
|
areaDrift,
|
|
29
30
|
buildMapModel,
|
|
@@ -39,7 +40,6 @@ import type { CodeGraph } from './types.js';
|
|
|
39
40
|
const _HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
40
41
|
const VIEWER_DIR = path.join(_HERE, 'map_viewer');
|
|
41
42
|
|
|
42
|
-
export const MAP_DIR_REL = path.join('agents', 'map');
|
|
43
43
|
/** Rough characters-per-token used to hold MAP.md to its budget. */
|
|
44
44
|
const CHARS_PER_TOKEN = 4;
|
|
45
45
|
export const MAP_TOKEN_BUDGET = 2000;
|
|
@@ -233,12 +233,14 @@ export function diffFiles(root: string, rev: string): string[] | null {
|
|
|
233
233
|
}
|
|
234
234
|
|
|
235
235
|
/** Where `code-graph outcome` records which files a session found useful — local, never sent anywhere. */
|
|
236
|
-
export
|
|
236
|
+
export function outcomesPath(root: string): string {
|
|
237
|
+
return path.join(graphHome(root).stateDir, 'map-outcomes.jsonl');
|
|
238
|
+
}
|
|
237
239
|
|
|
238
240
|
export function readOutcomes(root: string): OutcomeEvent[] {
|
|
239
241
|
let text: string;
|
|
240
242
|
try {
|
|
241
|
-
text = fs.readFileSync(
|
|
243
|
+
text = fs.readFileSync(outcomesPath(root), 'utf-8');
|
|
242
244
|
} catch {
|
|
243
245
|
return [];
|
|
244
246
|
}
|
|
@@ -259,7 +261,7 @@ export function readOutcomes(root: string): OutcomeEvent[] {
|
|
|
259
261
|
}
|
|
260
262
|
|
|
261
263
|
export function recordOutcome(root: string, file: string, outcome: OutcomeEvent['outcome'], at: Date = new Date()): void {
|
|
262
|
-
const p =
|
|
264
|
+
const p = outcomesPath(root);
|
|
263
265
|
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
264
266
|
const rel = path.relative(fs.realpathSync(root), fs.realpathSync(path.resolve(file)));
|
|
265
267
|
if (rel === '' || rel.startsWith('..') || path.isAbsolute(rel)) throw new Error(`not a file inside ${root}: ${file}`);
|
|
@@ -310,7 +312,8 @@ export async function buildMap(
|
|
|
310
312
|
let drawing = false;
|
|
311
313
|
|
|
312
314
|
async function drawGeneration(root: string, opts: Parameters<typeof buildMap>[1] = {}): Promise<MapBuildResult> {
|
|
313
|
-
const
|
|
315
|
+
const home = graphHome(root);
|
|
316
|
+
const outDir = opts.outDir ?? home.mapDir;
|
|
314
317
|
// A chosen --out directory is only ever written into when it is empty or
|
|
315
318
|
// already holds a map: the writer clears stale area pages, and that must
|
|
316
319
|
// never reach a directory of someone else's Markdown.
|
|
@@ -322,7 +325,7 @@ async function drawGeneration(root: string, opts: Parameters<typeof buildMap>[1]
|
|
|
322
325
|
) {
|
|
323
326
|
throw new Error(`refusing to write a map into ${outDir}: it is not empty and holds no map.json`);
|
|
324
327
|
}
|
|
325
|
-
const cachePath = opts.cachePath ??
|
|
328
|
+
const cachePath = opts.cachePath ?? home.cachePath;
|
|
326
329
|
const { graph, built } = await loadOrBuildGraph(root, cachePath, opts.noBuild === true);
|
|
327
330
|
let diff: { rev: string; changed: string[] } | undefined;
|
|
328
331
|
if (opts.diffRev !== undefined) {
|
|
@@ -364,7 +367,7 @@ async function drawGeneration(root: string, opts: Parameters<typeof buildMap>[1]
|
|
|
364
367
|
// The default map stages under the gitignored runtime dir, so a killed redraw
|
|
365
368
|
// leaves nothing in agents/ itself; a chosen --out stages beside itself.
|
|
366
369
|
const suffix = `${process.pid}-${randomBytes(4).toString('hex')}`;
|
|
367
|
-
const stage = opts.outDir === undefined ?
|
|
370
|
+
const stage = opts.outDir === undefined ? home.stateDir : path.dirname(outDir);
|
|
368
371
|
fs.mkdirSync(stage, { recursive: true });
|
|
369
372
|
const next = path.join(stage, `.map-next-${suffix}`);
|
|
370
373
|
const prev = path.join(stage, `.map-prev-${suffix}`);
|
|
@@ -633,7 +636,7 @@ export function renderMapMd(m: MapModel, meta: MapMeta): string {
|
|
|
633
636
|
const rank = (r: (typeof rp)[number]): number =>
|
|
634
637
|
(r.route.startsWith('GET ') ? 0 : 2) + (r.views.length > 0 ? 0 : 1);
|
|
635
638
|
const withChain = rp
|
|
636
|
-
.filter((r) => r.views.length + r.tables.length > 0)
|
|
639
|
+
.filter((r) => r.views.length + r.tables.length + (r.inferredTables?.length ?? 0) > 0)
|
|
637
640
|
.sort((a, b) => rank(a) - rank(b) || (a.route < b.route ? -1 : a.route > b.route ? 1 : 0));
|
|
638
641
|
sections.push([
|
|
639
642
|
'Request paths',
|
|
@@ -641,7 +644,8 @@ export function renderMapMd(m: MapModel, meta: MapMeta): string {
|
|
|
641
644
|
'## Request paths',
|
|
642
645
|
'',
|
|
643
646
|
`${m.analyses.requestPathsTotal} declared routes; ${m.analyses.requestPathsResolved} reach a handler in this tree. ` +
|
|
644
|
-
'Route → handler → views it renders · tables its models persist
|
|
647
|
+
'Route → handler → views it renders · tables its models persist, up to three calls deep ' +
|
|
648
|
+
'(inferred: reached through a call bound by name alone):',
|
|
645
649
|
'',
|
|
646
650
|
...(withChain.length ? withChain : rp).slice(0, 10).map((r) => {
|
|
647
651
|
const handlerFile = path.posix.basename(f[r.handler]?.path ?? '');
|
|
@@ -649,6 +653,7 @@ export function renderMapMd(m: MapModel, meta: MapMeta): string {
|
|
|
649
653
|
handlerFile.replace(/\.php$/, '') === r.handlerSymbol ? handlerFile : `${handlerFile}::${r.handlerSymbol}`,
|
|
650
654
|
...r.views.map((v) => path.posix.basename(f[v]?.path ?? '')),
|
|
651
655
|
...r.tables.map((t) => `table ${code(t)}`),
|
|
656
|
+
...(r.inferredTables ?? []).map((t) => `table ${code(t)} (inferred)`),
|
|
652
657
|
];
|
|
653
658
|
return `- ${code(r.route)} → ${chain.map((c) => (c.startsWith('table') ? c : `${code(c)}`)).join(' → ')}`;
|
|
654
659
|
}),
|
|
@@ -872,7 +877,7 @@ export function renderHtml(m: MapModel, meta: MapMeta): string {
|
|
|
872
877
|
|
|
873
878
|
/** The map a previous `code-graph map` wrote, or null when there is none. */
|
|
874
879
|
export function readMap(root: string): { meta: MapMeta; model: MapModel } | null {
|
|
875
|
-
return readMapAt(
|
|
880
|
+
return readMapAt(graphHome(root).mapDir);
|
|
876
881
|
}
|
|
877
882
|
|
|
878
883
|
function readMapAt(dir: string): { meta: MapMeta; model: MapModel } | null {
|
|
@@ -918,7 +923,11 @@ export interface AreaReport {
|
|
|
918
923
|
* One area, found by id (`a3`), by exact name, or by a file it contains —
|
|
919
924
|
* exact matches only, so the answer is always about the area that was named.
|
|
920
925
|
*/
|
|
921
|
-
export function areaReport(
|
|
926
|
+
export function areaReport(
|
|
927
|
+
m: MapModel,
|
|
928
|
+
query: string,
|
|
929
|
+
pageDir: string = path.posix.join('agents', 'map'),
|
|
930
|
+
): AreaReport | { refusal: string; candidates: string[] } {
|
|
922
931
|
let i = m.areas.findIndex((a) => a.id === query || a.name === query);
|
|
923
932
|
if (i < 0) {
|
|
924
933
|
const f = m.files.findIndex((x) => x.path === query);
|
|
@@ -974,6 +983,6 @@ export function areaReport(m: MapModel, query: string): AreaReport | { refusal:
|
|
|
974
983
|
.sort((x, y) => y[1] - x[1] || x[0] - y[0])
|
|
975
984
|
.slice(0, cap)
|
|
976
985
|
.map(([ai, w]) => ({ area: `${m.areas[ai]?.id ?? ''} ${m.areas[ai]?.name ?? ''}`, links: w })),
|
|
977
|
-
page: a.files.length >= AREA_PAGE_MIN_FILES ? path.posix.join(
|
|
986
|
+
page: a.files.length >= AREA_PAGE_MIN_FILES ? path.posix.join(pageDir, 'areas', `${a.id}.md`) : null,
|
|
978
987
|
};
|
|
979
988
|
}
|