@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
|
@@ -14,15 +14,17 @@
|
|
|
14
14
|
* host path or anything else that differs between two builds of one commit.
|
|
15
15
|
*/
|
|
16
16
|
import { execFileSync } from 'node:child_process';
|
|
17
|
-
import { createHash } from 'node:crypto';
|
|
17
|
+
import { createHash, randomBytes } from 'node:crypto';
|
|
18
18
|
import * as fs from 'node:fs';
|
|
19
19
|
import * as path from 'node:path';
|
|
20
20
|
import { fileURLToPath } from 'node:url';
|
|
21
21
|
|
|
22
22
|
import { write_atomic } from '../_lib/fs_atomic.js';
|
|
23
23
|
import { hardenedSpawnEnv } from '../_lib/spawn_env.js';
|
|
24
|
-
import { buildFromRepo } from './build.js';
|
|
25
|
-
import {
|
|
24
|
+
import { buildFromRepo, readProjectProbe } from './build.js';
|
|
25
|
+
import { acquire as acquireRefreshLock, heldByThisProcess } from './refresh_lock.js';
|
|
26
|
+
import { computeVerdict, graphState } from './detect.js';
|
|
27
|
+
import { graphHome } from './graph_home.js';
|
|
26
28
|
import {
|
|
27
29
|
areaDrift,
|
|
28
30
|
buildMapModel,
|
|
@@ -38,7 +40,6 @@ import type { CodeGraph } from './types.js';
|
|
|
38
40
|
const _HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
39
41
|
const VIEWER_DIR = path.join(_HERE, 'map_viewer');
|
|
40
42
|
|
|
41
|
-
export const MAP_DIR_REL = path.join('agents', 'map');
|
|
42
43
|
/** Rough characters-per-token used to hold MAP.md to its budget. */
|
|
43
44
|
const CHARS_PER_TOKEN = 4;
|
|
44
45
|
export const MAP_TOKEN_BUDGET = 2000;
|
|
@@ -49,6 +50,33 @@ export interface MapMeta {
|
|
|
49
50
|
commit: string;
|
|
50
51
|
state: string;
|
|
51
52
|
summary: string;
|
|
53
|
+
/**
|
|
54
|
+
* The graph this map was drawn from: the first 16 hex of the SHA-256 of the
|
|
55
|
+
* graph cache's bytes (or of the serialized graph when there is no cache).
|
|
56
|
+
* `graph.json`, `map.json` and `index.html` agree exactly when this matches
|
|
57
|
+
* {@link graphGeneration} of the current cache — the test a reader runs
|
|
58
|
+
* instead of comparing modification times.
|
|
59
|
+
*/
|
|
60
|
+
graph_generation?: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** The generation of a graph cache: the first 16 hex of the SHA-256 of its bytes; null when unreadable. */
|
|
64
|
+
export function graphGeneration(cachePath: string): string | null {
|
|
65
|
+
try {
|
|
66
|
+
return createHash('sha256').update(fs.readFileSync(cachePath)).digest('hex').slice(0, 16);
|
|
67
|
+
} catch {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** The generation a drawn map records, or null when it records none (an older map) or cannot be read. */
|
|
73
|
+
export function mapGeneration(outDir: string): string | null {
|
|
74
|
+
try {
|
|
75
|
+
const doc = JSON.parse(fs.readFileSync(path.join(outDir, 'map.json'), 'utf-8')) as { meta?: { graph_generation?: unknown } };
|
|
76
|
+
return typeof doc.meta?.graph_generation === 'string' ? doc.meta.graph_generation : null;
|
|
77
|
+
} catch {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
52
80
|
}
|
|
53
81
|
|
|
54
82
|
export interface MapBuildResult {
|
|
@@ -72,32 +100,48 @@ function gitHead(root: string): string {
|
|
|
72
100
|
}
|
|
73
101
|
|
|
74
102
|
/**
|
|
75
|
-
* Entry points
|
|
76
|
-
* `
|
|
77
|
-
*
|
|
103
|
+
* Entry points the project's manifests declare, in every package root:
|
|
104
|
+
* `package.json` `bin` / `main` / `module` / `exports`, `composer.json` `bin`,
|
|
105
|
+
* `pyproject.toml` `[project.scripts]`, a Maven `<mainClass>`. Framework entry
|
|
106
|
+
* files are a provider's role (`roleOf`); this only adds what a manifest names.
|
|
78
107
|
*/
|
|
79
108
|
export function declaredEntryFiles(root: string): string[] {
|
|
80
109
|
const out = new Set<string>();
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
110
|
+
for (const [rel, text] of readProjectProbe(root).manifests) {
|
|
111
|
+
const dir = path.posix.dirname(rel);
|
|
112
|
+
const add = (v: unknown): void => {
|
|
113
|
+
if (typeof v !== 'string' || v.includes('*') || /^[a-z]+:/i.test(v)) return;
|
|
114
|
+
out.add(path.posix.normalize(path.posix.join(dir, v.replace(/^\.\//, ''))));
|
|
115
|
+
};
|
|
116
|
+
const base = path.posix.basename(rel);
|
|
117
|
+
try {
|
|
118
|
+
if (base === 'package.json') {
|
|
119
|
+
const pkg = JSON.parse(text) as Record<string, unknown>;
|
|
120
|
+
const leaves = (v: unknown): void => {
|
|
121
|
+
if (typeof v === 'string') add(v);
|
|
122
|
+
else if (v && typeof v === 'object') for (const x of Object.values(v)) leaves(x);
|
|
123
|
+
};
|
|
124
|
+
leaves(pkg['bin']);
|
|
125
|
+
add(pkg['main']);
|
|
126
|
+
add(pkg['module']);
|
|
127
|
+
leaves(pkg['exports']);
|
|
128
|
+
} else if (base === 'composer.json') {
|
|
129
|
+
const bin = (JSON.parse(text) as Record<string, unknown>)['bin'];
|
|
130
|
+
if (Array.isArray(bin)) for (const v of bin) add(v);
|
|
131
|
+
} else if (base === 'pyproject.toml') {
|
|
132
|
+
const section = /^\[project\.scripts\]\s*$([\s\S]*?)(?=^\[|(?![\s\S]))/m.exec(text)?.[1] ?? '';
|
|
133
|
+
for (const m of section.matchAll(/=\s*["']([\w.]+):[\w.]+["']/g)) {
|
|
134
|
+
const mod = (m[1] as string).replace(/\./g, '/');
|
|
135
|
+
add(`${mod}.py`);
|
|
136
|
+
add(`src/${mod}.py`);
|
|
137
|
+
}
|
|
138
|
+
} else if (base === 'pom.xml') {
|
|
139
|
+
const main = /<mainClass>\s*([\w.]+)\s*<\/mainClass>/.exec(text)?.[1];
|
|
140
|
+
if (main) add(`src/main/java/${main.replace(/\./g, '/')}.java`);
|
|
141
|
+
}
|
|
142
|
+
} catch {
|
|
143
|
+
/* an unreadable manifest declares nothing */
|
|
144
|
+
}
|
|
101
145
|
}
|
|
102
146
|
return [...out].sort();
|
|
103
147
|
}
|
|
@@ -189,12 +233,14 @@ export function diffFiles(root: string, rev: string): string[] | null {
|
|
|
189
233
|
}
|
|
190
234
|
|
|
191
235
|
/** Where `code-graph outcome` records which files a session found useful — local, never sent anywhere. */
|
|
192
|
-
export
|
|
236
|
+
export function outcomesPath(root: string): string {
|
|
237
|
+
return path.join(graphHome(root).stateDir, 'map-outcomes.jsonl');
|
|
238
|
+
}
|
|
193
239
|
|
|
194
240
|
export function readOutcomes(root: string): OutcomeEvent[] {
|
|
195
241
|
let text: string;
|
|
196
242
|
try {
|
|
197
|
-
text = fs.readFileSync(
|
|
243
|
+
text = fs.readFileSync(outcomesPath(root), 'utf-8');
|
|
198
244
|
} catch {
|
|
199
245
|
return [];
|
|
200
246
|
}
|
|
@@ -215,7 +261,7 @@ export function readOutcomes(root: string): OutcomeEvent[] {
|
|
|
215
261
|
}
|
|
216
262
|
|
|
217
263
|
export function recordOutcome(root: string, file: string, outcome: OutcomeEvent['outcome'], at: Date = new Date()): void {
|
|
218
|
-
const p =
|
|
264
|
+
const p = outcomesPath(root);
|
|
219
265
|
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
220
266
|
const rel = path.relative(fs.realpathSync(root), fs.realpathSync(path.resolve(file)));
|
|
221
267
|
if (rel === '' || rel.startsWith('..') || path.isAbsolute(rel)) throw new Error(`not a file inside ${root}: ${file}`);
|
|
@@ -239,9 +285,35 @@ export function openPrs(root: string): Array<{ number: number; title: string; fi
|
|
|
239
285
|
|
|
240
286
|
export async function buildMap(
|
|
241
287
|
root: string,
|
|
242
|
-
opts: {
|
|
288
|
+
opts: {
|
|
289
|
+
outDir?: string;
|
|
290
|
+
cachePath?: string;
|
|
291
|
+
diffRev?: string;
|
|
292
|
+
prs?: boolean;
|
|
293
|
+
noBuild?: boolean;
|
|
294
|
+
/** Test seam: called after each artefact lands in the new generation. */
|
|
295
|
+
_afterWrite?: (rel: string) => void;
|
|
296
|
+
} = {},
|
|
243
297
|
): Promise<MapBuildResult> {
|
|
244
|
-
|
|
298
|
+
// Single writer, enforced here rather than trusted to callers: one redraw per
|
|
299
|
+
// process at a time, and none while another process holds the refresh lock.
|
|
300
|
+
if (drawing) throw new Error('a map redraw is already running in this process');
|
|
301
|
+
const release = heldByThisProcess(root) ? (): void => {} : acquireRefreshLock(root);
|
|
302
|
+
if (release === null) throw new Error('another process holds the code-graph refresh lock');
|
|
303
|
+
drawing = true;
|
|
304
|
+
try {
|
|
305
|
+
return await drawGeneration(root, opts);
|
|
306
|
+
} finally {
|
|
307
|
+
drawing = false;
|
|
308
|
+
release();
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
let drawing = false;
|
|
313
|
+
|
|
314
|
+
async function drawGeneration(root: string, opts: Parameters<typeof buildMap>[1] = {}): Promise<MapBuildResult> {
|
|
315
|
+
const home = graphHome(root);
|
|
316
|
+
const outDir = opts.outDir ?? home.mapDir;
|
|
245
317
|
// A chosen --out directory is only ever written into when it is empty or
|
|
246
318
|
// already holds a map: the writer clears stale area pages, and that must
|
|
247
319
|
// never reach a directory of someone else's Markdown.
|
|
@@ -253,7 +325,7 @@ export async function buildMap(
|
|
|
253
325
|
) {
|
|
254
326
|
throw new Error(`refusing to write a map into ${outDir}: it is not empty and holds no map.json`);
|
|
255
327
|
}
|
|
256
|
-
const cachePath = opts.cachePath ??
|
|
328
|
+
const cachePath = opts.cachePath ?? home.cachePath;
|
|
257
329
|
const { graph, built } = await loadOrBuildGraph(root, cachePath, opts.noBuild === true);
|
|
258
330
|
let diff: { rev: string; changed: string[] } | undefined;
|
|
259
331
|
if (opts.diffRev !== undefined) {
|
|
@@ -283,31 +355,88 @@ export async function buildMap(
|
|
|
283
355
|
commit: gitHead(root),
|
|
284
356
|
state: graphState(root, cachePath),
|
|
285
357
|
summary: '',
|
|
358
|
+
graph_generation: graphGeneration(cachePath) ?? createHash('sha256').update(JSON.stringify(graph)).digest('hex').slice(0, 16),
|
|
286
359
|
};
|
|
287
360
|
meta.summary =
|
|
288
361
|
`${model.files.length} files · ${model.areas.length} areas · ` +
|
|
289
362
|
`${model.analyses.trustedEdges} trusted links · guess share ${pct(model.analyses.guessShare)}`;
|
|
290
363
|
|
|
364
|
+
// One generation at a time: everything is written into a sibling directory,
|
|
365
|
+
// which then replaces the live one, so a reader never sees `map.json` of one
|
|
366
|
+
// build beside pages of another, and a failed redraw leaves the last one whole.
|
|
367
|
+
// The default map stages under the gitignored runtime dir, so a killed redraw
|
|
368
|
+
// leaves nothing in agents/ itself; a chosen --out stages beside itself.
|
|
369
|
+
const suffix = `${process.pid}-${randomBytes(4).toString('hex')}`;
|
|
370
|
+
const stage = opts.outDir === undefined ? home.stateDir : path.dirname(outDir);
|
|
371
|
+
fs.mkdirSync(stage, { recursive: true });
|
|
372
|
+
const next = path.join(stage, `.map-next-${suffix}`);
|
|
373
|
+
const prev = path.join(stage, `.map-prev-${suffix}`);
|
|
291
374
|
const written: string[] = [];
|
|
292
|
-
|
|
293
|
-
const
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
375
|
+
try {
|
|
376
|
+
const write = (rel: string, content: string): void => {
|
|
377
|
+
const p = path.join(next, rel);
|
|
378
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
379
|
+
write_atomic(p, content);
|
|
380
|
+
written.push(rel);
|
|
381
|
+
opts._afterWrite?.(rel);
|
|
382
|
+
};
|
|
383
|
+
write('map.json', `${JSON.stringify({ meta, model })}\n`);
|
|
384
|
+
write('MAP.md', renderMapMd(model, meta));
|
|
385
|
+
for (const [i, a] of model.areas.entries()) {
|
|
386
|
+
if (a.files.length >= AREA_PAGE_MIN_FILES) write(path.join('areas', `${a.id}.md`), renderAreaMd(model, i, meta));
|
|
387
|
+
}
|
|
388
|
+
write('index.html', renderHtml(model, meta));
|
|
389
|
+
carryForeignEntries(outDir, next);
|
|
390
|
+
swapIn(next, outDir, prev);
|
|
391
|
+
} finally {
|
|
392
|
+
fs.rmSync(next, { recursive: true, force: true });
|
|
306
393
|
}
|
|
307
|
-
write('index.html', renderHtml(model, meta));
|
|
308
394
|
return { outDir, model, meta, written, graphBuilt: built };
|
|
309
395
|
}
|
|
310
396
|
|
|
397
|
+
/**
|
|
398
|
+
* What a map generation consists of; anything else in the directory is someone else's and is
|
|
399
|
+
* carried over. `map-render.json` belongs to a generation although `buildMap` does not write it:
|
|
400
|
+
* it is `code-graph map`'s render check OF that generation, written after the swap, so the
|
|
401
|
+
* previous one's is dropped rather than left describing pages it never saw.
|
|
402
|
+
*/
|
|
403
|
+
const GENERATION_ENTRIES = new Set(['map.json', 'MAP.md', 'index.html', 'areas', 'map-render.json']);
|
|
404
|
+
|
|
405
|
+
/** Copy entries the writer does not produce into the new generation, so the swap drops nothing. */
|
|
406
|
+
function carryForeignEntries(live: string, next: string): void {
|
|
407
|
+
if (!fs.existsSync(live)) return;
|
|
408
|
+
for (const entry of fs.readdirSync(live)) {
|
|
409
|
+
if (GENERATION_ENTRIES.has(entry)) continue;
|
|
410
|
+
fs.cpSync(path.join(live, entry), path.join(next, entry), { recursive: true, verbatimSymlinks: true });
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Replace `live` by `next` with two renames; on a failed second rename the old generation goes
|
|
416
|
+
* back. Consistent, not atomic: between the renames `live` is briefly absent, and a process
|
|
417
|
+
* killed there leaves the old generation at `prev` (under the gitignored runtime dir for the
|
|
418
|
+
* default map) — a reader sees no map, never a mixed one.
|
|
419
|
+
*/
|
|
420
|
+
export function swapIn(next: string, live: string, prev: string, rename: typeof fs.renameSync = fs.renameSync): void {
|
|
421
|
+
fs.mkdirSync(path.dirname(live), { recursive: true });
|
|
422
|
+
const hadLive = fs.existsSync(live);
|
|
423
|
+
if (hadLive) rename(live, prev);
|
|
424
|
+
try {
|
|
425
|
+
rename(next, live);
|
|
426
|
+
} catch (e) {
|
|
427
|
+
if (!hadLive) throw e;
|
|
428
|
+
try {
|
|
429
|
+
rename(prev, live);
|
|
430
|
+
} catch {
|
|
431
|
+
throw new Error(`map swap failed and the restore failed too; the previous generation is at ${prev}`, {
|
|
432
|
+
cause: e,
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
throw e;
|
|
436
|
+
}
|
|
437
|
+
if (hadLive) fs.rmSync(prev, { recursive: true, force: true });
|
|
438
|
+
}
|
|
439
|
+
|
|
311
440
|
function pct(x: number): string {
|
|
312
441
|
return `${Math.round(x * 1000) / 10} %`;
|
|
313
442
|
}
|
|
@@ -363,6 +492,12 @@ export function renderMapMd(m: MapModel, meta: MapMeta): string {
|
|
|
363
492
|
`areas under ${m.thresholds.smallArea} files fold into "small parts" · an area above ` +
|
|
364
493
|
`${Math.round(m.thresholds.maxAreaShare * 100)} % is split once · lists capped at ${m.thresholds.listCap}.`,
|
|
365
494
|
'',
|
|
495
|
+
m.providers && m.providers.length > 0
|
|
496
|
+
? `Convention providers: ${m.providers
|
|
497
|
+
.map((p) => `${md(p.name)} (${p.status}${p.roots.some((r) => r !== '') ? `, ${p.roots.map((r) => code(r || '.')).join(', ')}` : ''})`)
|
|
498
|
+
.join(' · ')} — plus the framework-neutral default.`
|
|
499
|
+
: 'Convention providers: none detected — only the framework-neutral default ran.',
|
|
500
|
+
'',
|
|
366
501
|
'',
|
|
367
502
|
].join('\n');
|
|
368
503
|
|
|
@@ -501,7 +636,7 @@ export function renderMapMd(m: MapModel, meta: MapMeta): string {
|
|
|
501
636
|
const rank = (r: (typeof rp)[number]): number =>
|
|
502
637
|
(r.route.startsWith('GET ') ? 0 : 2) + (r.views.length > 0 ? 0 : 1);
|
|
503
638
|
const withChain = rp
|
|
504
|
-
.filter((r) => r.views.length + r.tables.length > 0)
|
|
639
|
+
.filter((r) => r.views.length + r.tables.length + (r.inferredTables?.length ?? 0) > 0)
|
|
505
640
|
.sort((a, b) => rank(a) - rank(b) || (a.route < b.route ? -1 : a.route > b.route ? 1 : 0));
|
|
506
641
|
sections.push([
|
|
507
642
|
'Request paths',
|
|
@@ -509,7 +644,8 @@ export function renderMapMd(m: MapModel, meta: MapMeta): string {
|
|
|
509
644
|
'## Request paths',
|
|
510
645
|
'',
|
|
511
646
|
`${m.analyses.requestPathsTotal} declared routes; ${m.analyses.requestPathsResolved} reach a handler in this tree. ` +
|
|
512
|
-
'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):',
|
|
513
649
|
'',
|
|
514
650
|
...(withChain.length ? withChain : rp).slice(0, 10).map((r) => {
|
|
515
651
|
const handlerFile = path.posix.basename(f[r.handler]?.path ?? '');
|
|
@@ -517,6 +653,7 @@ export function renderMapMd(m: MapModel, meta: MapMeta): string {
|
|
|
517
653
|
handlerFile.replace(/\.php$/, '') === r.handlerSymbol ? handlerFile : `${handlerFile}::${r.handlerSymbol}`,
|
|
518
654
|
...r.views.map((v) => path.posix.basename(f[v]?.path ?? '')),
|
|
519
655
|
...r.tables.map((t) => `table ${code(t)}`),
|
|
656
|
+
...(r.inferredTables ?? []).map((t) => `table ${code(t)} (inferred)`),
|
|
520
657
|
];
|
|
521
658
|
return `- ${code(r.route)} → ${chain.map((c) => (c.startsWith('table') ? c : `${code(c)}`)).join(' → ')}`;
|
|
522
659
|
}),
|
|
@@ -721,6 +858,7 @@ export function renderHtml(m: MapModel, meta: MapMeta): string {
|
|
|
721
858
|
'<button type="button" id="z-out">−</button>',
|
|
722
859
|
'<button type="button" id="z-in">+</button>',
|
|
723
860
|
'<button type="button" id="z-fit"></button>',
|
|
861
|
+
'<button type="button" id="dim" aria-pressed="false"></button>',
|
|
724
862
|
'<button type="button" id="theme"></button>',
|
|
725
863
|
'<button type="button" id="lang"></button>',
|
|
726
864
|
'</div>',
|
|
@@ -739,7 +877,7 @@ export function renderHtml(m: MapModel, meta: MapMeta): string {
|
|
|
739
877
|
|
|
740
878
|
/** The map a previous `code-graph map` wrote, or null when there is none. */
|
|
741
879
|
export function readMap(root: string): { meta: MapMeta; model: MapModel } | null {
|
|
742
|
-
return readMapAt(
|
|
880
|
+
return readMapAt(graphHome(root).mapDir);
|
|
743
881
|
}
|
|
744
882
|
|
|
745
883
|
function readMapAt(dir: string): { meta: MapMeta; model: MapModel } | null {
|
|
@@ -785,7 +923,11 @@ export interface AreaReport {
|
|
|
785
923
|
* One area, found by id (`a3`), by exact name, or by a file it contains —
|
|
786
924
|
* exact matches only, so the answer is always about the area that was named.
|
|
787
925
|
*/
|
|
788
|
-
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[] } {
|
|
789
931
|
let i = m.areas.findIndex((a) => a.id === query || a.name === query);
|
|
790
932
|
if (i < 0) {
|
|
791
933
|
const f = m.files.findIndex((x) => x.path === query);
|
|
@@ -841,6 +983,6 @@ export function areaReport(m: MapModel, query: string): AreaReport | { refusal:
|
|
|
841
983
|
.sort((x, y) => y[1] - x[1] || x[0] - y[0])
|
|
842
984
|
.slice(0, cap)
|
|
843
985
|
.map(([ai, w]) => ({ area: `${m.areas[ai]?.id ?? ''} ${m.areas[ai]?.name ?? ''}`, links: w })),
|
|
844
|
-
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,
|
|
845
987
|
};
|
|
846
988
|
}
|
|
@@ -16,7 +16,8 @@ import Graph from 'graphology';
|
|
|
16
16
|
import louvain from 'graphology-communities-louvain';
|
|
17
17
|
import forceAtlas2 from 'graphology-layout-forceatlas2';
|
|
18
18
|
|
|
19
|
-
import
|
|
19
|
+
import { catalogRow, type CoverageStatus, providerRole } from './providers/index.js';
|
|
20
|
+
import type { ActiveProviderRecord, CodeGraph, ResolvedVia } from './types.js';
|
|
20
21
|
import { isStatedResolution } from './types.js';
|
|
21
22
|
|
|
22
23
|
export const MAP_MODEL_VERSION = 1;
|
|
@@ -35,6 +36,8 @@ export type FileRole =
|
|
|
35
36
|
|
|
36
37
|
/** Roles whose files take part in clustering. */
|
|
37
38
|
const CLUSTER_ROLES: ReadonlySet<FileRole> = new Set(['code', 'test', 'entry', 'view']);
|
|
39
|
+
/** Directories of including views at which a view counts as a shared partial. */
|
|
40
|
+
const SHARED_PARTIAL_DIRS = 3;
|
|
38
41
|
|
|
39
42
|
/** Compact symbol: [name, kind, line]. */
|
|
40
43
|
export type MapSymbol = [string, string, number];
|
|
@@ -57,6 +60,16 @@ export interface MapFile {
|
|
|
57
60
|
externals: string[];
|
|
58
61
|
/** Test files that cover this file (`tests` relation). */
|
|
59
62
|
tests: number[];
|
|
63
|
+
/** Convention providers that drew an edge into or out of this file; absent when none did. */
|
|
64
|
+
linkedBy?: string[];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** A convention provider active in the graph, with its catalog status. */
|
|
68
|
+
export interface MapProvider {
|
|
69
|
+
id: string;
|
|
70
|
+
name: string;
|
|
71
|
+
status: CoverageStatus | 'uncatalogued';
|
|
72
|
+
roots: string[];
|
|
60
73
|
}
|
|
61
74
|
|
|
62
75
|
export interface MapArea {
|
|
@@ -81,7 +94,7 @@ export interface MapArea {
|
|
|
81
94
|
neighbours: number[];
|
|
82
95
|
}
|
|
83
96
|
|
|
84
|
-
/** One declared route and what its handler reaches (
|
|
97
|
+
/** One declared route and what its handler reaches (a convention provider). */
|
|
85
98
|
export interface RequestPath {
|
|
86
99
|
route: string;
|
|
87
100
|
/** Index of the routes file. */
|
|
@@ -92,6 +105,8 @@ export interface RequestPath {
|
|
|
92
105
|
views: number[];
|
|
93
106
|
models: number[];
|
|
94
107
|
tables: string[];
|
|
108
|
+
/** Tables reached only through a call bound by name alone (a `dynamic` hop with one candidate). */
|
|
109
|
+
inferredTables: string[];
|
|
95
110
|
}
|
|
96
111
|
|
|
97
112
|
export interface MapThresholds {
|
|
@@ -118,6 +133,8 @@ export interface MapModel {
|
|
|
118
133
|
areaEdges: Array<[number, number, number]>;
|
|
119
134
|
diff?: MapDiff;
|
|
120
135
|
artefacts?: MapArtefacts;
|
|
136
|
+
/** Convention providers active in the graph; absent when none was. */
|
|
137
|
+
providers?: MapProvider[];
|
|
121
138
|
/** Set by the map writer, not by `buildMapModel`: they need IO. */
|
|
122
139
|
drift?: AreaDrift;
|
|
123
140
|
prOverlap?: PrOverlap;
|
|
@@ -208,16 +225,19 @@ const CONFIG_BASENAME =
|
|
|
208
225
|
* The role a path has before any edge is looked at. `infra` and `isolated`
|
|
209
226
|
* depend on edges and are decided later; everything here is a name fact.
|
|
210
227
|
*/
|
|
211
|
-
export function staticRole(p: string, entries: ReadonlySet<string
|
|
228
|
+
export function staticRole(p: string, entries: ReadonlySet<string>, active?: readonly ActiveProviderRecord[]): FileRole {
|
|
212
229
|
if (/\.mdx?$/i.test(p)) return 'doc';
|
|
213
230
|
if (isTestPath(p)) return 'test';
|
|
214
231
|
const base = path.posix.basename(p);
|
|
215
|
-
|
|
232
|
+
// A provider's view role outranks the neutral rules below; any other role it
|
|
233
|
+
// gives sits where an entry would.
|
|
234
|
+
const byProvider = providerRole(p, active);
|
|
235
|
+
if (byProvider === 'view') return 'view';
|
|
216
236
|
if (/(^|\/)(database\/)?migrations\//.test(p)) return 'migration';
|
|
217
237
|
if (/(^|\/)(resources\/)?lang\//.test(p)) return 'translation';
|
|
218
238
|
if (p.startsWith('config/') || CONFIG_BASENAME.test(base)) return 'config';
|
|
219
|
-
if (entries.has(p)
|
|
220
|
-
return 'code';
|
|
239
|
+
if (entries.has(p)) return 'entry';
|
|
240
|
+
return byProvider ?? 'code';
|
|
221
241
|
}
|
|
222
242
|
|
|
223
243
|
|
|
@@ -307,7 +327,8 @@ export function buildMapModel(graph: CodeGraph, inputs: MapInputs = {}): MapMode
|
|
|
307
327
|
}
|
|
308
328
|
|
|
309
329
|
// 3. Roles: names first, then the two edge-dependent ones.
|
|
310
|
-
const
|
|
330
|
+
const active = graph.providers ?? [];
|
|
331
|
+
const roles: FileRole[] = paths.map((p) => staticRole(p, entries, active));
|
|
311
332
|
// A layout every view extends couples views exactly the way a utility
|
|
312
333
|
// couples code, so views are eligible for the hold-out too.
|
|
313
334
|
const holdable = (r: FileRole | undefined): boolean => r === 'code' || r === 'view';
|
|
@@ -322,9 +343,29 @@ export function buildMapModel(graph: CodeGraph, inputs: MapInputs = {}): MapMode
|
|
|
322
343
|
if ((inFiles[i] as number) > infraInDegree) roles[i] = 'infra';
|
|
323
344
|
else if (roles[i] === 'code' && inFiles[i] === 0 && outFiles[i] === 0) roles[i] = 'isolated';
|
|
324
345
|
}
|
|
346
|
+
// A partial that views of SHARED_PARTIAL_DIRS directories include is shared
|
|
347
|
+
// the way a utility is: kept in, it glues every page that shows it into one
|
|
348
|
+
// area and names that area after itself. Counted by including directory,
|
|
349
|
+
// not by in-degree, because a project's code dwarfs its views in the p99.
|
|
350
|
+
const includers = new Map<number, Set<string>>();
|
|
351
|
+
for (const [s, t] of edges) {
|
|
352
|
+
if (roles[s] !== 'view' || roles[t] !== 'view') continue;
|
|
353
|
+
const dirs = includers.get(t) ?? new Set<string>();
|
|
354
|
+
dirs.add(path.posix.dirname(paths[s] as string));
|
|
355
|
+
includers.set(t, dirs);
|
|
356
|
+
}
|
|
357
|
+
for (const [t, dirs] of includers) if (dirs.size >= SHARED_PARTIAL_DIRS) roles[t] = 'infra';
|
|
358
|
+
// A routes file links to every handler it routes, so in the clustering it
|
|
359
|
+
// is the hub of whatever it touches. It sits out the clustering and then
|
|
360
|
+
// joins the area most of its handlers landed in.
|
|
361
|
+
const routeFiles = new Set<number>();
|
|
362
|
+
for (const n of graph.nodes) {
|
|
363
|
+
const i = n.kind === 'route' ? idx.get(n.source_file) : undefined;
|
|
364
|
+
if (i !== undefined && CLUSTER_ROLES.has(roles[i] as FileRole)) routeFiles.add(i);
|
|
365
|
+
}
|
|
325
366
|
|
|
326
367
|
// 4. Areas: seeded Louvain over the coupled roles.
|
|
327
|
-
const clusterable = (i: number): boolean => CLUSTER_ROLES.has(roles[i] as FileRole);
|
|
368
|
+
const clusterable = (i: number): boolean => CLUSTER_ROLES.has(roles[i] as FileRole) && !routeFiles.has(i);
|
|
328
369
|
const undirected = new Map<string, number>();
|
|
329
370
|
for (const [s, t, w] of edges) {
|
|
330
371
|
if (!clusterable(s) || !clusterable(t)) continue;
|
|
@@ -398,6 +439,15 @@ export function buildMapModel(graph: CodeGraph, inputs: MapInputs = {}): MapMode
|
|
|
398
439
|
drafts.forEach((d, ai) => {
|
|
399
440
|
for (const f of d.files) areaOf[f] = ai;
|
|
400
441
|
});
|
|
442
|
+
for (const rf of [...routeFiles].sort((a, b) => a - b)) {
|
|
443
|
+
const pull = new Map<number, number>();
|
|
444
|
+
for (const [s, t, w] of edges) if (s === rf && (areaOf[t] as number) >= 0) pull.set(areaOf[t] as number, (pull.get(areaOf[t] as number) ?? 0) + w);
|
|
445
|
+
const best = [...pull.entries()].sort((a, b) => b[1] - a[1] || a[0] - b[0])[0]?.[0];
|
|
446
|
+
const ai = best ?? drafts.push({ files: [], kind: 'small-parts', hub: rf, name: `small parts · ${topDir(paths[rf] as string)}` }) - 1;
|
|
447
|
+
const d = drafts[ai] as Draft;
|
|
448
|
+
d.files = [...d.files, rf].sort((a, b) => byPath(paths[a] as string, paths[b] as string));
|
|
449
|
+
areaOf[rf] = ai;
|
|
450
|
+
}
|
|
401
451
|
|
|
402
452
|
// 6. Area metrics, area→area weights, layouts.
|
|
403
453
|
const areaWeights = new Map<string, number>();
|
|
@@ -506,6 +556,22 @@ export function buildMapModel(graph: CodeGraph, inputs: MapInputs = {}): MapMode
|
|
|
506
556
|
const trustedTotal = edges.reduce((n, e) => n + e[2], 0);
|
|
507
557
|
const guessTotal = guessEdges.reduce((n, e) => n + e[2], 0);
|
|
508
558
|
|
|
559
|
+
const linkedBy = new Map<number, Set<string>>();
|
|
560
|
+
const sourceFileOf = new Map(graph.nodes.map((n) => [n.id, n.source_file] as const));
|
|
561
|
+
const fileIndexOf = (id: string): number | undefined => {
|
|
562
|
+
const f = sourceFileOf.get(id);
|
|
563
|
+
return f === undefined ? undefined : idx.get(f);
|
|
564
|
+
};
|
|
565
|
+
for (const e of graph.edges) {
|
|
566
|
+
if (!e.provider_id) continue;
|
|
567
|
+
for (const i of [fileIndexOf(e.source), fileIndexOf(e.target)]) {
|
|
568
|
+
if (i === undefined) continue;
|
|
569
|
+
const set = linkedBy.get(i) ?? new Set<string>();
|
|
570
|
+
set.add(e.provider_id);
|
|
571
|
+
linkedBy.set(i, set);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
509
575
|
const symbolsPerFile = 40;
|
|
510
576
|
const files: MapFile[] = paths.map((p, i) => {
|
|
511
577
|
const syms = (symbolsOf.get(p) ?? []).sort((a, b) => a[2] - b[2] || byPath(a[0], b[0]));
|
|
@@ -520,8 +586,13 @@ export function buildMapModel(graph: CodeGraph, inputs: MapInputs = {}): MapMode
|
|
|
520
586
|
symbolsOmitted: Math.max(0, syms.length - symbolsPerFile),
|
|
521
587
|
externals: [...(externals.get(i) ?? [])].sort(byPath),
|
|
522
588
|
tests: [...(testsOf.get(i) ?? [])].sort((a, b) => a - b),
|
|
589
|
+
...(linkedBy.has(i) ? { linkedBy: [...(linkedBy.get(i) as Set<string>)].sort() } : {}),
|
|
523
590
|
};
|
|
524
591
|
});
|
|
592
|
+
const providers: MapProvider[] = (graph.providers ?? []).map((p) => {
|
|
593
|
+
const row = catalogRow(p.id);
|
|
594
|
+
return { id: p.id, name: row?.name ?? p.id, status: row?.status ?? 'uncatalogued', roots: p.roots };
|
|
595
|
+
});
|
|
525
596
|
|
|
526
597
|
let diff: MapDiff | undefined;
|
|
527
598
|
if (inputs.diff) {
|
|
@@ -599,6 +670,7 @@ export function buildMapModel(graph: CodeGraph, inputs: MapInputs = {}): MapMode
|
|
|
599
670
|
guessEdges,
|
|
600
671
|
areas,
|
|
601
672
|
areaEdges,
|
|
673
|
+
...(providers.length > 0 ? { providers } : {}),
|
|
602
674
|
...(diff ? { diff } : {}),
|
|
603
675
|
...(artefacts ? { artefacts } : {}),
|
|
604
676
|
analyses: {
|
|
@@ -623,6 +695,9 @@ export function buildMapModel(graph: CodeGraph, inputs: MapInputs = {}): MapMode
|
|
|
623
695
|
* tables. Read off the overlay's edges only; a route whose handler did not
|
|
624
696
|
* resolve keeps `handler: -1` rather than a guess.
|
|
625
697
|
*/
|
|
698
|
+
/** How many calls deep a request path follows the handler: handler → service → repository → model. */
|
|
699
|
+
const REQUEST_HOPS = 3;
|
|
700
|
+
|
|
626
701
|
function collectRequestPaths(graph: CodeGraph, idx: ReadonlyMap<string, number>): RequestPath[] {
|
|
627
702
|
const nodeById = new Map(graph.nodes.map((n) => [n.id, n] as const));
|
|
628
703
|
const out = new Map<string, CodeGraph['edges']>();
|
|
@@ -648,20 +723,37 @@ function collectRequestPaths(graph: CodeGraph, idx: ReadonlyMap<string, number>)
|
|
|
648
723
|
const views = new Set<number>();
|
|
649
724
|
const models = new Set<number>();
|
|
650
725
|
const tables = new Set<string>();
|
|
726
|
+
const inferred = new Set<string>();
|
|
651
727
|
if (target) {
|
|
652
|
-
//
|
|
653
|
-
// a
|
|
654
|
-
//
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
const
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
728
|
+
// What the handler's own code reaches, up to REQUEST_HOPS calls
|
|
729
|
+
// deep: a service, its repository, the model. Only functions and
|
|
730
|
+
// methods are expanded — a class's links would hand every model it
|
|
731
|
+
// knows to every route it serves. A chain is stated when every hop
|
|
732
|
+
// is; one hop bound by name alone makes what it reaches inferred.
|
|
733
|
+
const best = new Map<string, boolean>([[target, true]]);
|
|
734
|
+
let frontier: Array<[string, boolean]> = [[target, true]];
|
|
735
|
+
for (let hop = 0; hop < REQUEST_HOPS && frontier.length > 0; hop++) {
|
|
736
|
+
const next: Array<[string, boolean]> = [];
|
|
737
|
+
for (const [id, stated] of frontier) {
|
|
738
|
+
for (const e of out.get(id) ?? []) {
|
|
739
|
+
const sure = isStatedResolution(e.resolved_via as ResolvedVia);
|
|
740
|
+
if (!sure && !(e.resolved_via === 'dynamic' && nodeById.has(e.target))) continue;
|
|
741
|
+
const chain = stated && sure;
|
|
742
|
+
if (e.relation === 'renders' && chain) views.add(fileIdx(e.target));
|
|
743
|
+
const model = tableOf.has(e.target) ? e.target : e.target.split('::')[0];
|
|
744
|
+
const modelTables = model ? tableOf.get(model) : undefined;
|
|
745
|
+
if (modelTables) {
|
|
746
|
+
if (chain) models.add(fileIdx(model as string));
|
|
747
|
+
for (const t of modelTables) (chain ? tables : inferred).add(t);
|
|
748
|
+
}
|
|
749
|
+
const kind = nodeById.get(e.target)?.kind;
|
|
750
|
+
if (e.relation !== 'calls' || (kind !== 'method' && kind !== 'function')) continue;
|
|
751
|
+
if (best.get(e.target) === true || best.get(e.target) === chain) continue;
|
|
752
|
+
best.set(e.target, chain);
|
|
753
|
+
next.push([e.target, chain]);
|
|
754
|
+
}
|
|
664
755
|
}
|
|
756
|
+
frontier = next;
|
|
665
757
|
}
|
|
666
758
|
}
|
|
667
759
|
paths.push({
|
|
@@ -672,6 +764,7 @@ function collectRequestPaths(graph: CodeGraph, idx: ReadonlyMap<string, number>)
|
|
|
672
764
|
views: [...views].filter((v) => v >= 0).sort((a, b) => a - b),
|
|
673
765
|
models: [...models].filter((v) => v >= 0).sort((a, b) => a - b),
|
|
674
766
|
tables: [...tables].sort(),
|
|
767
|
+
inferredTables: [...inferred].filter((t) => !tables.has(t)).sort(),
|
|
675
768
|
});
|
|
676
769
|
}
|
|
677
770
|
return paths.sort((a, b) => (a.route < b.route ? -1 : a.route > b.route ? 1 : a.file - b.file));
|