@event4u/agent-config 17.2.1 → 17.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.augment-plugin/marketplace.json +3 -3
- package/.augment-plugin/plugin.json +1 -1
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/skills/install-agent-config/SKILL.md +9 -9
- package/CHANGELOG.md +152 -0
- package/dist/agent-src/skills/code-intelligence/SKILL.md +13 -12
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/scripts/check_memory.ts +1 -1
- package/dist/cli/commands/recordTriggerEval.js +1 -1
- package/dist/cli-delegate/{chunk-KR5ZIFPO.js → chunk-DHSSP7V3.js} +1 -1
- package/dist/cli-delegate/chunk-E7ACTJTJ.js +6792 -0
- package/dist/cli-delegate/{chunk-BZJ4Y3CI.js → chunk-MT2WOOCY.js} +1 -1
- package/dist/cli-delegate/{chunk-CMZEKSNN.js → chunk-ORJPBER2.js} +2 -2
- package/dist/cli-delegate/{chunk-VYZGJLCA.js → chunk-RKNT4LIK.js} +22 -1286
- package/dist/cli-delegate/cmd_conformance.js +20 -14
- package/dist/cli-delegate/cmd_doctor.js +3 -3
- package/dist/cli-delegate/cmd_doctor_neighbours.js +2 -2
- package/dist/cli-delegate/cmd_explain.js +1 -1
- package/dist/cli-delegate/cmd_export.js +2 -2
- package/dist/cli-delegate/cmd_prune.js +2 -2
- package/dist/cli-delegate/cmd_settings_migrate.js +2 -2
- package/dist/cli-delegate/cmd_settings_set.js +8 -4
- package/dist/cli-delegate/cmd_sync.js +2 -2
- package/dist/cli-delegate/cmd_uninstall.js +2 -2
- package/dist/cli-delegate/cmd_validate.js +2 -2
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +3 -3
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +1 -1
- package/dist/discovery/dormancy-report.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +2 -2
- package/dist/discovery/trust-report.md +1 -1
- package/dist/discovery/workspaces.json +2 -2
- package/dist/hooks/code_graph_refresh.chunk.cjs +414 -0
- package/dist/hooks/code_graph_refresh.chunk.cjs.sha256 +1 -0
- package/dist/hooks/dispatch.js +21 -11
- package/dist/hooks/dispatch.sha256 +1 -1
- package/dist/hooks/egress_engine.chunk.cjs +1 -1
- package/dist/hooks/egress_engine.chunk.cjs.sha256 +1 -1
- package/dist/install/install.mjs +6974 -6037
- package/dist/mcp/mcp-cloudflare-catalogue.json +1 -0
- package/dist/mcp/registry-manifest.json +3 -2
- package/dist/mcp/server.json +13 -4
- package/dist/mcp/server.mjs +5561 -2463
- package/dist/scripts/_lib/egress_rules.js +8 -2
- package/dist/scripts/condense.js +9 -9
- package/dist/server/routes/discovery.js +1 -1
- package/dist/server/routes/install.js +1 -1
- package/dist/server/routes/ping.js +1 -1
- package/dist/server/routes/schema.js +4 -4
- package/dist/server/routes/settings.js +2 -6
- package/dist/server/routes/wizard.js +6 -6
- package/dist/server/schemas/settings.js +42 -42
- package/dist/shared/settingsSurface.js +1 -1
- package/dist/shared/userMd/schema.js +1 -1
- package/dist/shared/zodJsonSchema.js +47 -0
- package/dist/ui/assets/index-B0bcIgi0.js +77 -0
- package/dist/ui/index.html +1 -1
- package/package.json +3 -3
- package/src/agent-src/templates/scripts/check_memory.ts +1 -1
- package/src/config/ci-local-parity.yml +11 -0
- package/src/config/continuity-surface.json +9 -2
- package/src/config/gate-violation-baselines.json +3 -3
- package/src/config/gitignore-block.txt +6 -5
- package/src/config/hook-bundle-budget.json +16 -1
- package/src/config/pack-size-budget.json +12 -1
- package/src/config/packed-binary-manifest.json +9 -0
- package/src/scripts/_cli/cmd_settings_set.ts +3 -2
- package/src/scripts/_dispatch.bash +1 -1
- package/src/scripts/_lib/egress_rules.ts +8 -2
- package/src/scripts/_lib/listing_manifests.ts +85 -0
- package/src/scripts/_lib/neighbour_census.ts +1 -0
- package/src/scripts/_lib/release_settings_surface.ts +2 -6
- package/src/scripts/adoption_status.ts +231 -27
- package/src/scripts/build_mcp_registry_manifest.ts +23 -12
- package/src/scripts/check_dependency_floors.ts +7 -0
- package/src/scripts/check_release_manifest_content.ts +17 -2
- package/src/scripts/check_release_pr_shape.ts +5 -0
- package/src/scripts/code_graph/build.ts +80 -44
- package/src/scripts/code_graph/cli.ts +260 -16
- package/src/scripts/code_graph/conventions_default.ts +228 -0
- package/src/scripts/code_graph/extract.ts +5 -2
- package/src/scripts/code_graph/extract_more.ts +105 -1
- package/src/scripts/code_graph/loader.ts +10 -2
- package/src/scripts/code_graph/map.ts +174 -41
- package/src/scripts/code_graph/map_model.ts +53 -15
- package/src/scripts/code_graph/map_verify.ts +12 -0
- package/src/scripts/code_graph/map_viewer/viewer.css +6 -0
- package/src/scripts/code_graph/map_viewer/viewer.js +114 -13
- package/src/scripts/code_graph/passes.ts +173 -0
- package/src/scripts/code_graph/providers/angular.ts +55 -0
- package/src/scripts/code_graph/providers/aspnetcore.ts +182 -0
- package/src/scripts/code_graph/providers/catalog.ts +290 -0
- package/src/scripts/code_graph/providers/django.ts +163 -0
- package/src/scripts/code_graph/providers/express.ts +25 -0
- package/src/scripts/code_graph/providers/fastapi.ts +49 -0
- package/src/scripts/code_graph/providers/flask.ts +99 -0
- package/src/scripts/code_graph/providers/go_http.ts +121 -0
- package/src/scripts/code_graph/providers/index.ts +101 -0
- package/src/scripts/code_graph/{laravel_overlay.ts → providers/laravel.ts} +113 -7
- package/src/scripts/code_graph/providers/nestjs.ts +34 -0
- package/src/scripts/code_graph/providers/nextjs.ts +91 -0
- package/src/scripts/code_graph/providers/nuxt.ts +55 -0
- package/src/scripts/code_graph/providers/react-router.ts +58 -0
- package/src/scripts/code_graph/providers/shared/detect.ts +78 -0
- package/src/scripts/code_graph/providers/shared/extractors.ts +249 -0
- package/src/scripts/code_graph/providers/shared/js.ts +252 -0
- package/src/scripts/code_graph/providers/shared/kit.ts +210 -0
- package/src/scripts/code_graph/providers/shared/php.ts +64 -0
- package/src/scripts/code_graph/providers/shared/python.ts +223 -0
- package/src/scripts/code_graph/providers/spring.ts +138 -0
- package/src/scripts/code_graph/providers/sqlalchemy.ts +62 -0
- package/src/scripts/code_graph/providers/sveltekit.ts +59 -0
- package/src/scripts/code_graph/providers/symfony.ts +142 -0
- package/src/scripts/code_graph/providers/typeorm.ts +58 -0
- package/src/scripts/code_graph/providers/types.ts +56 -0
- package/src/scripts/code_graph/providers/vue-router.ts +21 -0
- package/src/scripts/code_graph/providers/wordpress.ts +128 -0
- package/src/scripts/code_graph/refresh_cli.ts +81 -0
- package/src/scripts/code_graph/refresh_lock.ts +193 -0
- package/src/scripts/code_graph/resolution_tiers.ts +19 -7
- package/src/scripts/code_graph/scip_reader.ts +2 -2
- package/src/scripts/code_graph/sqlite_store.ts +10 -4
- package/src/scripts/code_graph/types.ts +75 -41
- package/src/scripts/code_graph/validate.ts +6 -8
- package/src/scripts/condense.ts +9 -9
- package/src/scripts/generate_settings_reference.ts +2 -6
- package/src/scripts/hook_chunks.mjs +4 -0
- package/src/scripts/hook_manifest.json +1 -1
- package/src/scripts/hook_manifest.yaml +24 -7
- package/src/scripts/hooks/code_graph_refresh_hook.ts +160 -0
- package/src/scripts/hooks/lazy_concern.ts +4 -0
- package/src/scripts/install-hooks.sh +54 -33
- package/src/scripts/install.ts +2 -6
- package/src/scripts/lint_hook_manifest.ts +2 -0
- package/src/scripts/lint_marketplace.ts +33 -0
- package/src/scripts/lint_mcp_registry_manifest.ts +36 -0
- package/src/scripts/mcp_server/consumer_tool_catalog.json +1 -0
- package/src/scripts/mcp_server/graph_tools.ts +5 -1
- package/src/scripts/release.ts +2 -5
- package/src/scripts/release_drill.ts +4 -1
- package/src/scripts/release_env.ts +17 -0
- package/src/scripts/release_mix_taxonomy.json +3 -0
- package/src/server/schemas/settings.ts +42 -42
- package/src/shared/settingsSurface.ts +1 -1
- package/src/shared/userMd/schema.ts +1 -1
- package/src/shared/zodJsonSchema.ts +41 -0
- package/src/vendor/grammars/README.md +6 -5
- package/src/vendor/grammars/tree-sitter-c_sharp.wasm +0 -0
- package/dist/cli-delegate/chunk-XIAPY44N.js +0 -4587
- package/dist/ui/assets/index-w70Ado09.js +0 -23
|
@@ -14,14 +14,15 @@
|
|
|
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';
|
|
24
|
+
import { buildFromRepo, readProjectProbe } from './build.js';
|
|
25
|
+
import { acquire as acquireRefreshLock, heldByThisProcess } from './refresh_lock.js';
|
|
25
26
|
import { computeVerdict, graphState, NATIVE_CACHE_REL } from './detect.js';
|
|
26
27
|
import {
|
|
27
28
|
areaDrift,
|
|
@@ -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
|
}
|
|
@@ -239,8 +283,33 @@ export function openPrs(root: string): Array<{ number: number; title: string; fi
|
|
|
239
283
|
|
|
240
284
|
export async function buildMap(
|
|
241
285
|
root: string,
|
|
242
|
-
opts: {
|
|
286
|
+
opts: {
|
|
287
|
+
outDir?: string;
|
|
288
|
+
cachePath?: string;
|
|
289
|
+
diffRev?: string;
|
|
290
|
+
prs?: boolean;
|
|
291
|
+
noBuild?: boolean;
|
|
292
|
+
/** Test seam: called after each artefact lands in the new generation. */
|
|
293
|
+
_afterWrite?: (rel: string) => void;
|
|
294
|
+
} = {},
|
|
243
295
|
): Promise<MapBuildResult> {
|
|
296
|
+
// Single writer, enforced here rather than trusted to callers: one redraw per
|
|
297
|
+
// process at a time, and none while another process holds the refresh lock.
|
|
298
|
+
if (drawing) throw new Error('a map redraw is already running in this process');
|
|
299
|
+
const release = heldByThisProcess(root) ? (): void => {} : acquireRefreshLock(root);
|
|
300
|
+
if (release === null) throw new Error('another process holds the code-graph refresh lock');
|
|
301
|
+
drawing = true;
|
|
302
|
+
try {
|
|
303
|
+
return await drawGeneration(root, opts);
|
|
304
|
+
} finally {
|
|
305
|
+
drawing = false;
|
|
306
|
+
release();
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
let drawing = false;
|
|
311
|
+
|
|
312
|
+
async function drawGeneration(root: string, opts: Parameters<typeof buildMap>[1] = {}): Promise<MapBuildResult> {
|
|
244
313
|
const outDir = opts.outDir ?? path.join(root, MAP_DIR_REL);
|
|
245
314
|
// A chosen --out directory is only ever written into when it is empty or
|
|
246
315
|
// already holds a map: the writer clears stale area pages, and that must
|
|
@@ -283,31 +352,88 @@ export async function buildMap(
|
|
|
283
352
|
commit: gitHead(root),
|
|
284
353
|
state: graphState(root, cachePath),
|
|
285
354
|
summary: '',
|
|
355
|
+
graph_generation: graphGeneration(cachePath) ?? createHash('sha256').update(JSON.stringify(graph)).digest('hex').slice(0, 16),
|
|
286
356
|
};
|
|
287
357
|
meta.summary =
|
|
288
358
|
`${model.files.length} files · ${model.areas.length} areas · ` +
|
|
289
359
|
`${model.analyses.trustedEdges} trusted links · guess share ${pct(model.analyses.guessShare)}`;
|
|
290
360
|
|
|
361
|
+
// One generation at a time: everything is written into a sibling directory,
|
|
362
|
+
// which then replaces the live one, so a reader never sees `map.json` of one
|
|
363
|
+
// build beside pages of another, and a failed redraw leaves the last one whole.
|
|
364
|
+
// The default map stages under the gitignored runtime dir, so a killed redraw
|
|
365
|
+
// leaves nothing in agents/ itself; a chosen --out stages beside itself.
|
|
366
|
+
const suffix = `${process.pid}-${randomBytes(4).toString('hex')}`;
|
|
367
|
+
const stage = opts.outDir === undefined ? path.join(root, 'agents', 'runtime', 'state') : path.dirname(outDir);
|
|
368
|
+
fs.mkdirSync(stage, { recursive: true });
|
|
369
|
+
const next = path.join(stage, `.map-next-${suffix}`);
|
|
370
|
+
const prev = path.join(stage, `.map-prev-${suffix}`);
|
|
291
371
|
const written: string[] = [];
|
|
292
|
-
|
|
293
|
-
const
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
372
|
+
try {
|
|
373
|
+
const write = (rel: string, content: string): void => {
|
|
374
|
+
const p = path.join(next, rel);
|
|
375
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
376
|
+
write_atomic(p, content);
|
|
377
|
+
written.push(rel);
|
|
378
|
+
opts._afterWrite?.(rel);
|
|
379
|
+
};
|
|
380
|
+
write('map.json', `${JSON.stringify({ meta, model })}\n`);
|
|
381
|
+
write('MAP.md', renderMapMd(model, meta));
|
|
382
|
+
for (const [i, a] of model.areas.entries()) {
|
|
383
|
+
if (a.files.length >= AREA_PAGE_MIN_FILES) write(path.join('areas', `${a.id}.md`), renderAreaMd(model, i, meta));
|
|
384
|
+
}
|
|
385
|
+
write('index.html', renderHtml(model, meta));
|
|
386
|
+
carryForeignEntries(outDir, next);
|
|
387
|
+
swapIn(next, outDir, prev);
|
|
388
|
+
} finally {
|
|
389
|
+
fs.rmSync(next, { recursive: true, force: true });
|
|
306
390
|
}
|
|
307
|
-
write('index.html', renderHtml(model, meta));
|
|
308
391
|
return { outDir, model, meta, written, graphBuilt: built };
|
|
309
392
|
}
|
|
310
393
|
|
|
394
|
+
/**
|
|
395
|
+
* What a map generation consists of; anything else in the directory is someone else's and is
|
|
396
|
+
* carried over. `map-render.json` belongs to a generation although `buildMap` does not write it:
|
|
397
|
+
* it is `code-graph map`'s render check OF that generation, written after the swap, so the
|
|
398
|
+
* previous one's is dropped rather than left describing pages it never saw.
|
|
399
|
+
*/
|
|
400
|
+
const GENERATION_ENTRIES = new Set(['map.json', 'MAP.md', 'index.html', 'areas', 'map-render.json']);
|
|
401
|
+
|
|
402
|
+
/** Copy entries the writer does not produce into the new generation, so the swap drops nothing. */
|
|
403
|
+
function carryForeignEntries(live: string, next: string): void {
|
|
404
|
+
if (!fs.existsSync(live)) return;
|
|
405
|
+
for (const entry of fs.readdirSync(live)) {
|
|
406
|
+
if (GENERATION_ENTRIES.has(entry)) continue;
|
|
407
|
+
fs.cpSync(path.join(live, entry), path.join(next, entry), { recursive: true, verbatimSymlinks: true });
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Replace `live` by `next` with two renames; on a failed second rename the old generation goes
|
|
413
|
+
* back. Consistent, not atomic: between the renames `live` is briefly absent, and a process
|
|
414
|
+
* killed there leaves the old generation at `prev` (under the gitignored runtime dir for the
|
|
415
|
+
* default map) — a reader sees no map, never a mixed one.
|
|
416
|
+
*/
|
|
417
|
+
export function swapIn(next: string, live: string, prev: string, rename: typeof fs.renameSync = fs.renameSync): void {
|
|
418
|
+
fs.mkdirSync(path.dirname(live), { recursive: true });
|
|
419
|
+
const hadLive = fs.existsSync(live);
|
|
420
|
+
if (hadLive) rename(live, prev);
|
|
421
|
+
try {
|
|
422
|
+
rename(next, live);
|
|
423
|
+
} catch (e) {
|
|
424
|
+
if (!hadLive) throw e;
|
|
425
|
+
try {
|
|
426
|
+
rename(prev, live);
|
|
427
|
+
} catch {
|
|
428
|
+
throw new Error(`map swap failed and the restore failed too; the previous generation is at ${prev}`, {
|
|
429
|
+
cause: e,
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
throw e;
|
|
433
|
+
}
|
|
434
|
+
if (hadLive) fs.rmSync(prev, { recursive: true, force: true });
|
|
435
|
+
}
|
|
436
|
+
|
|
311
437
|
function pct(x: number): string {
|
|
312
438
|
return `${Math.round(x * 1000) / 10} %`;
|
|
313
439
|
}
|
|
@@ -363,6 +489,12 @@ export function renderMapMd(m: MapModel, meta: MapMeta): string {
|
|
|
363
489
|
`areas under ${m.thresholds.smallArea} files fold into "small parts" · an area above ` +
|
|
364
490
|
`${Math.round(m.thresholds.maxAreaShare * 100)} % is split once · lists capped at ${m.thresholds.listCap}.`,
|
|
365
491
|
'',
|
|
492
|
+
m.providers && m.providers.length > 0
|
|
493
|
+
? `Convention providers: ${m.providers
|
|
494
|
+
.map((p) => `${md(p.name)} (${p.status}${p.roots.some((r) => r !== '') ? `, ${p.roots.map((r) => code(r || '.')).join(', ')}` : ''})`)
|
|
495
|
+
.join(' · ')} — plus the framework-neutral default.`
|
|
496
|
+
: 'Convention providers: none detected — only the framework-neutral default ran.',
|
|
497
|
+
'',
|
|
366
498
|
'',
|
|
367
499
|
].join('\n');
|
|
368
500
|
|
|
@@ -721,6 +853,7 @@ export function renderHtml(m: MapModel, meta: MapMeta): string {
|
|
|
721
853
|
'<button type="button" id="z-out">−</button>',
|
|
722
854
|
'<button type="button" id="z-in">+</button>',
|
|
723
855
|
'<button type="button" id="z-fit"></button>',
|
|
856
|
+
'<button type="button" id="dim" aria-pressed="false"></button>',
|
|
724
857
|
'<button type="button" id="theme"></button>',
|
|
725
858
|
'<button type="button" id="lang"></button>',
|
|
726
859
|
'</div>',
|
|
@@ -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;
|
|
@@ -57,6 +58,16 @@ export interface MapFile {
|
|
|
57
58
|
externals: string[];
|
|
58
59
|
/** Test files that cover this file (`tests` relation). */
|
|
59
60
|
tests: number[];
|
|
61
|
+
/** Convention providers that drew an edge into or out of this file; absent when none did. */
|
|
62
|
+
linkedBy?: string[];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** A convention provider active in the graph, with its catalog status. */
|
|
66
|
+
export interface MapProvider {
|
|
67
|
+
id: string;
|
|
68
|
+
name: string;
|
|
69
|
+
status: CoverageStatus | 'uncatalogued';
|
|
70
|
+
roots: string[];
|
|
60
71
|
}
|
|
61
72
|
|
|
62
73
|
export interface MapArea {
|
|
@@ -81,7 +92,7 @@ export interface MapArea {
|
|
|
81
92
|
neighbours: number[];
|
|
82
93
|
}
|
|
83
94
|
|
|
84
|
-
/** One declared route and what its handler reaches (
|
|
95
|
+
/** One declared route and what its handler reaches (a convention provider). */
|
|
85
96
|
export interface RequestPath {
|
|
86
97
|
route: string;
|
|
87
98
|
/** Index of the routes file. */
|
|
@@ -118,6 +129,8 @@ export interface MapModel {
|
|
|
118
129
|
areaEdges: Array<[number, number, number]>;
|
|
119
130
|
diff?: MapDiff;
|
|
120
131
|
artefacts?: MapArtefacts;
|
|
132
|
+
/** Convention providers active in the graph; absent when none was. */
|
|
133
|
+
providers?: MapProvider[];
|
|
121
134
|
/** Set by the map writer, not by `buildMapModel`: they need IO. */
|
|
122
135
|
drift?: AreaDrift;
|
|
123
136
|
prOverlap?: PrOverlap;
|
|
@@ -208,16 +221,19 @@ const CONFIG_BASENAME =
|
|
|
208
221
|
* The role a path has before any edge is looked at. `infra` and `isolated`
|
|
209
222
|
* depend on edges and are decided later; everything here is a name fact.
|
|
210
223
|
*/
|
|
211
|
-
export function staticRole(p: string, entries: ReadonlySet<string
|
|
224
|
+
export function staticRole(p: string, entries: ReadonlySet<string>, active?: readonly ActiveProviderRecord[]): FileRole {
|
|
212
225
|
if (/\.mdx?$/i.test(p)) return 'doc';
|
|
213
226
|
if (isTestPath(p)) return 'test';
|
|
214
227
|
const base = path.posix.basename(p);
|
|
215
|
-
|
|
228
|
+
// A provider's view role outranks the neutral rules below; any other role it
|
|
229
|
+
// gives sits where an entry would.
|
|
230
|
+
const byProvider = providerRole(p, active);
|
|
231
|
+
if (byProvider === 'view') return 'view';
|
|
216
232
|
if (/(^|\/)(database\/)?migrations\//.test(p)) return 'migration';
|
|
217
233
|
if (/(^|\/)(resources\/)?lang\//.test(p)) return 'translation';
|
|
218
234
|
if (p.startsWith('config/') || CONFIG_BASENAME.test(base)) return 'config';
|
|
219
|
-
if (entries.has(p)
|
|
220
|
-
return 'code';
|
|
235
|
+
if (entries.has(p)) return 'entry';
|
|
236
|
+
return byProvider ?? 'code';
|
|
221
237
|
}
|
|
222
238
|
|
|
223
239
|
|
|
@@ -307,7 +323,8 @@ export function buildMapModel(graph: CodeGraph, inputs: MapInputs = {}): MapMode
|
|
|
307
323
|
}
|
|
308
324
|
|
|
309
325
|
// 3. Roles: names first, then the two edge-dependent ones.
|
|
310
|
-
const
|
|
326
|
+
const active = graph.providers ?? [];
|
|
327
|
+
const roles: FileRole[] = paths.map((p) => staticRole(p, entries, active));
|
|
311
328
|
// A layout every view extends couples views exactly the way a utility
|
|
312
329
|
// couples code, so views are eligible for the hold-out too.
|
|
313
330
|
const holdable = (r: FileRole | undefined): boolean => r === 'code' || r === 'view';
|
|
@@ -506,6 +523,22 @@ export function buildMapModel(graph: CodeGraph, inputs: MapInputs = {}): MapMode
|
|
|
506
523
|
const trustedTotal = edges.reduce((n, e) => n + e[2], 0);
|
|
507
524
|
const guessTotal = guessEdges.reduce((n, e) => n + e[2], 0);
|
|
508
525
|
|
|
526
|
+
const linkedBy = new Map<number, Set<string>>();
|
|
527
|
+
const sourceFileOf = new Map(graph.nodes.map((n) => [n.id, n.source_file] as const));
|
|
528
|
+
const fileIndexOf = (id: string): number | undefined => {
|
|
529
|
+
const f = sourceFileOf.get(id);
|
|
530
|
+
return f === undefined ? undefined : idx.get(f);
|
|
531
|
+
};
|
|
532
|
+
for (const e of graph.edges) {
|
|
533
|
+
if (!e.provider_id) continue;
|
|
534
|
+
for (const i of [fileIndexOf(e.source), fileIndexOf(e.target)]) {
|
|
535
|
+
if (i === undefined) continue;
|
|
536
|
+
const set = linkedBy.get(i) ?? new Set<string>();
|
|
537
|
+
set.add(e.provider_id);
|
|
538
|
+
linkedBy.set(i, set);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
509
542
|
const symbolsPerFile = 40;
|
|
510
543
|
const files: MapFile[] = paths.map((p, i) => {
|
|
511
544
|
const syms = (symbolsOf.get(p) ?? []).sort((a, b) => a[2] - b[2] || byPath(a[0], b[0]));
|
|
@@ -520,8 +553,13 @@ export function buildMapModel(graph: CodeGraph, inputs: MapInputs = {}): MapMode
|
|
|
520
553
|
symbolsOmitted: Math.max(0, syms.length - symbolsPerFile),
|
|
521
554
|
externals: [...(externals.get(i) ?? [])].sort(byPath),
|
|
522
555
|
tests: [...(testsOf.get(i) ?? [])].sort((a, b) => a - b),
|
|
556
|
+
...(linkedBy.has(i) ? { linkedBy: [...(linkedBy.get(i) as Set<string>)].sort() } : {}),
|
|
523
557
|
};
|
|
524
558
|
});
|
|
559
|
+
const providers: MapProvider[] = (graph.providers ?? []).map((p) => {
|
|
560
|
+
const row = catalogRow(p.id);
|
|
561
|
+
return { id: p.id, name: row?.name ?? p.id, status: row?.status ?? 'uncatalogued', roots: p.roots };
|
|
562
|
+
});
|
|
525
563
|
|
|
526
564
|
let diff: MapDiff | undefined;
|
|
527
565
|
if (inputs.diff) {
|
|
@@ -599,6 +637,7 @@ export function buildMapModel(graph: CodeGraph, inputs: MapInputs = {}): MapMode
|
|
|
599
637
|
guessEdges,
|
|
600
638
|
areas,
|
|
601
639
|
areaEdges,
|
|
640
|
+
...(providers.length > 0 ? { providers } : {}),
|
|
602
641
|
...(diff ? { diff } : {}),
|
|
603
642
|
...(artefacts ? { artefacts } : {}),
|
|
604
643
|
analyses: {
|
|
@@ -649,17 +688,16 @@ function collectRequestPaths(graph: CodeGraph, idx: ReadonlyMap<string, number>)
|
|
|
649
688
|
const models = new Set<number>();
|
|
650
689
|
const tables = new Set<string>();
|
|
651
690
|
if (target) {
|
|
652
|
-
// The handler
|
|
653
|
-
//
|
|
654
|
-
//
|
|
655
|
-
const
|
|
656
|
-
const reach = [...(out.get(target) ?? []), ...(handlerFile ? (out.get(handlerFile) ?? []) : [])];
|
|
657
|
-
for (const e of reach) {
|
|
691
|
+
// The handler's own links only: what this route's code renders and
|
|
692
|
+
// names. Its file's links would hand every model the controller
|
|
693
|
+
// imports to every route the controller serves.
|
|
694
|
+
for (const e of out.get(target) ?? []) {
|
|
658
695
|
if (!isStatedResolution(e.resolved_via as ResolvedVia)) continue;
|
|
659
696
|
if (e.relation === 'renders') views.add(fileIdx(e.target));
|
|
660
|
-
const
|
|
697
|
+
const model = tableOf.has(e.target) ? e.target : e.target.split('::')[0];
|
|
698
|
+
const modelTables = model ? tableOf.get(model) : undefined;
|
|
661
699
|
if (modelTables) {
|
|
662
|
-
models.add(fileIdx(
|
|
700
|
+
models.add(fileIdx(model as string));
|
|
663
701
|
for (const t of modelTables) tables.add(t);
|
|
664
702
|
}
|
|
665
703
|
}
|
|
@@ -121,6 +121,18 @@ export async function verifyMap(htmlPath: string): Promise<MapRenderCheck> {
|
|
|
121
121
|
return { view, labels: r.boxes.length, ...labelProblems(r.boxes, r.width, r.height) };
|
|
122
122
|
};
|
|
123
123
|
const views = [await read('overview')];
|
|
124
|
+
// The orb, as it opens and turned half a revolution: the far side's labels
|
|
125
|
+
// must stay hidden and the near side's must still fit and not overlap.
|
|
126
|
+
const orb = page.locator('#dim');
|
|
127
|
+
if ((await orb.count()) > 0) {
|
|
128
|
+
await orb.click();
|
|
129
|
+
views.push(await read('overview-orb'));
|
|
130
|
+
await page.evaluate(() => {
|
|
131
|
+
for (let i = 0; i < 21; i++) document.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowRight' }));
|
|
132
|
+
});
|
|
133
|
+
views.push(await read('overview-orb-turned'));
|
|
134
|
+
await orb.click();
|
|
135
|
+
}
|
|
124
136
|
const firstArea = page.locator('#panel .plist button').first();
|
|
125
137
|
if ((await firstArea.count()) > 0) {
|
|
126
138
|
await firstArea.click();
|
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
--change: #d1495b;
|
|
13
13
|
--good: #2e8b57;
|
|
14
14
|
--contested: #d98b1d;
|
|
15
|
+
--orb-light: rgba(255, 255, 255, 0.95);
|
|
16
|
+
--orb-dark: rgba(214, 220, 232, 0.55);
|
|
15
17
|
}
|
|
16
18
|
@media (prefers-color-scheme: dark) {
|
|
17
19
|
:root:not([data-theme='light']) {
|
|
@@ -28,6 +30,8 @@
|
|
|
28
30
|
--change: #ff7a8a;
|
|
29
31
|
--good: #5fd39a;
|
|
30
32
|
--contested: #ffb347;
|
|
33
|
+
--orb-light: rgba(64, 72, 90, 0.55);
|
|
34
|
+
--orb-dark: rgba(20, 22, 26, 0.2);
|
|
31
35
|
}
|
|
32
36
|
}
|
|
33
37
|
:root[data-theme='dark'] {
|
|
@@ -44,6 +48,8 @@
|
|
|
44
48
|
--change: #ff7a8a;
|
|
45
49
|
--good: #5fd39a;
|
|
46
50
|
--contested: #ffb347;
|
|
51
|
+
--orb-light: rgba(64, 72, 90, 0.55);
|
|
52
|
+
--orb-dark: rgba(20, 22, 26, 0.2);
|
|
47
53
|
}
|
|
48
54
|
* {
|
|
49
55
|
box-sizing: border-box;
|