@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
|
@@ -36,6 +36,8 @@ export type FileRole =
|
|
|
36
36
|
|
|
37
37
|
/** Roles whose files take part in clustering. */
|
|
38
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;
|
|
39
41
|
|
|
40
42
|
/** Compact symbol: [name, kind, line]. */
|
|
41
43
|
export type MapSymbol = [string, string, number];
|
|
@@ -103,6 +105,8 @@ export interface RequestPath {
|
|
|
103
105
|
views: number[];
|
|
104
106
|
models: number[];
|
|
105
107
|
tables: string[];
|
|
108
|
+
/** Tables reached only through a call bound by name alone (a `dynamic` hop with one candidate). */
|
|
109
|
+
inferredTables: string[];
|
|
106
110
|
}
|
|
107
111
|
|
|
108
112
|
export interface MapThresholds {
|
|
@@ -339,9 +343,29 @@ export function buildMapModel(graph: CodeGraph, inputs: MapInputs = {}): MapMode
|
|
|
339
343
|
if ((inFiles[i] as number) > infraInDegree) roles[i] = 'infra';
|
|
340
344
|
else if (roles[i] === 'code' && inFiles[i] === 0 && outFiles[i] === 0) roles[i] = 'isolated';
|
|
341
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
|
+
}
|
|
342
366
|
|
|
343
367
|
// 4. Areas: seeded Louvain over the coupled roles.
|
|
344
|
-
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);
|
|
345
369
|
const undirected = new Map<string, number>();
|
|
346
370
|
for (const [s, t, w] of edges) {
|
|
347
371
|
if (!clusterable(s) || !clusterable(t)) continue;
|
|
@@ -415,6 +439,15 @@ export function buildMapModel(graph: CodeGraph, inputs: MapInputs = {}): MapMode
|
|
|
415
439
|
drafts.forEach((d, ai) => {
|
|
416
440
|
for (const f of d.files) areaOf[f] = ai;
|
|
417
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
|
+
}
|
|
418
451
|
|
|
419
452
|
// 6. Area metrics, area→area weights, layouts.
|
|
420
453
|
const areaWeights = new Map<string, number>();
|
|
@@ -662,6 +695,9 @@ export function buildMapModel(graph: CodeGraph, inputs: MapInputs = {}): MapMode
|
|
|
662
695
|
* tables. Read off the overlay's edges only; a route whose handler did not
|
|
663
696
|
* resolve keeps `handler: -1` rather than a guess.
|
|
664
697
|
*/
|
|
698
|
+
/** How many calls deep a request path follows the handler: handler → service → repository → model. */
|
|
699
|
+
const REQUEST_HOPS = 3;
|
|
700
|
+
|
|
665
701
|
function collectRequestPaths(graph: CodeGraph, idx: ReadonlyMap<string, number>): RequestPath[] {
|
|
666
702
|
const nodeById = new Map(graph.nodes.map((n) => [n.id, n] as const));
|
|
667
703
|
const out = new Map<string, CodeGraph['edges']>();
|
|
@@ -687,19 +723,37 @@ function collectRequestPaths(graph: CodeGraph, idx: ReadonlyMap<string, number>)
|
|
|
687
723
|
const views = new Set<number>();
|
|
688
724
|
const models = new Set<number>();
|
|
689
725
|
const tables = new Set<string>();
|
|
726
|
+
const inferred = new Set<string>();
|
|
690
727
|
if (target) {
|
|
691
|
-
//
|
|
692
|
-
//
|
|
693
|
-
//
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
for (const
|
|
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
|
+
}
|
|
702
755
|
}
|
|
756
|
+
frontier = next;
|
|
703
757
|
}
|
|
704
758
|
}
|
|
705
759
|
paths.push({
|
|
@@ -710,6 +764,7 @@ function collectRequestPaths(graph: CodeGraph, idx: ReadonlyMap<string, number>)
|
|
|
710
764
|
views: [...views].filter((v) => v >= 0).sort((a, b) => a - b),
|
|
711
765
|
models: [...models].filter((v) => v >= 0).sort((a, b) => a - b),
|
|
712
766
|
tables: [...tables].sort(),
|
|
767
|
+
inferredTables: [...inferred].filter((t) => !tables.has(t)).sort(),
|
|
713
768
|
});
|
|
714
769
|
}
|
|
715
770
|
return paths.sort((a, b) => (a.route < b.route ? -1 : a.route > b.route ? 1 : a.file - b.file));
|
|
@@ -54,10 +54,49 @@ export interface MapRenderCheck {
|
|
|
54
54
|
pageErrors: number;
|
|
55
55
|
externalRequests: number;
|
|
56
56
|
horizontalScroll: boolean;
|
|
57
|
-
views: Array<{ view: string; labels: number; outside: number; overlapping: number }>;
|
|
57
|
+
views: Array<{ view: string; labels: number; outside: number; overlapping: number; orb?: OrbSpread }>;
|
|
58
|
+
/** Area-list entries whose name is cut off, or whose colour dot sits on a line of its own. */
|
|
59
|
+
sidebar: { entries: number; clipped: number; orphanedDot: number };
|
|
58
60
|
}>;
|
|
59
61
|
}
|
|
60
62
|
|
|
63
|
+
/**
|
|
64
|
+
* How the area bubbles of one orb view sit: overlapping near-side pairs, and how
|
|
65
|
+
* far they bunch into one cap — the length of the mean of their unit vectors on
|
|
66
|
+
* the sphere, 0 for an even spread and 1 for every bubble on one point.
|
|
67
|
+
*/
|
|
68
|
+
export interface OrbSpread {
|
|
69
|
+
bubbles: number;
|
|
70
|
+
concentration: number;
|
|
71
|
+
overlapping: number;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Near-side bubbles overlapping by more than a sliver count; far-side ones are seen through the near side. */
|
|
75
|
+
export function orbSpread(nodes: ReadonlyArray<{ x: number; y: number; r: number; near: boolean; u?: number[] }>): OrbSpread {
|
|
76
|
+
const near = nodes.filter((n) => n.near);
|
|
77
|
+
let overlapping = 0;
|
|
78
|
+
for (let i = 0; i < near.length; i++)
|
|
79
|
+
for (let j = i + 1; j < near.length; j++) {
|
|
80
|
+
const a = near[i] as (typeof near)[number];
|
|
81
|
+
const b = near[j] as (typeof near)[number];
|
|
82
|
+
// The viewer shrinks a crowded bubble down to a dot (ORB_DOT px): two dots touching
|
|
83
|
+
// hide nothing, so only a pair where both still read as bubbles counts.
|
|
84
|
+
if (a.r <= ORB_DOT || b.r <= ORB_DOT) continue;
|
|
85
|
+
if (Math.hypot(a.x - b.x, a.y - b.y) < (a.r + b.r) * ORB_OVERLAP_TOLERANCE) overlapping += 1;
|
|
86
|
+
}
|
|
87
|
+
const sum = [0, 0, 0];
|
|
88
|
+
for (const n of nodes) for (let k = 0; k < 3; k++) sum[k] = (sum[k] as number) + (n.u?.[k] ?? 0);
|
|
89
|
+
const concentration = nodes.length ? Math.hypot(...(sum as [number, number, number])) / nodes.length : 0;
|
|
90
|
+
return { bubbles: nodes.length, concentration: Math.round(concentration * 100) / 100, overlapping };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** The smallest radius the viewer shrinks a crowded orb bubble to (`viewer.js`). */
|
|
94
|
+
const ORB_DOT = 2;
|
|
95
|
+
/** Two bubbles overlap when their centres are closer than this share of their summed radii. */
|
|
96
|
+
const ORB_OVERLAP_TOLERANCE = 0.9;
|
|
97
|
+
/** Above this, the bubbles bunch into one cap. An even spread of 30 sits near 0.1; one hemisphere at 0.5. */
|
|
98
|
+
export const ORB_MAX_CONCENTRATION = 0.35;
|
|
99
|
+
|
|
61
100
|
interface Box {
|
|
62
101
|
x: number;
|
|
63
102
|
y: number;
|
|
@@ -112,14 +151,29 @@ export async function verifyMap(htmlPath: string): Promise<MapRenderCheck> {
|
|
|
112
151
|
});
|
|
113
152
|
await page.goto(url);
|
|
114
153
|
await page.waitForFunction(() => (window as unknown as { __mapLabelBoxes?: unknown }).__mapLabelBoxes !== undefined);
|
|
115
|
-
const read = async (view: string): Promise<{ view: string; labels: number; outside: number; overlapping: number }> => {
|
|
154
|
+
const read = async (view: string): Promise<{ view: string; labels: number; outside: number; overlapping: number; orb?: OrbSpread }> => {
|
|
116
155
|
const r = (await page.evaluate(() => (window as unknown as { __mapLabelBoxes: { boxes: Box[]; width: number; height: number } }).__mapLabelBoxes)) as {
|
|
117
156
|
boxes: Box[];
|
|
118
157
|
width: number;
|
|
119
158
|
height: number;
|
|
120
159
|
};
|
|
121
|
-
|
|
160
|
+
const bubbles = await page.evaluate(
|
|
161
|
+
() => (window as unknown as { __mapOrbNodes?: Array<{ x: number; y: number; r: number; near: boolean; u?: number[] }> }).__mapOrbNodes ?? [],
|
|
162
|
+
);
|
|
163
|
+
return { view, labels: r.boxes.length, ...labelProblems(r.boxes, r.width, r.height), ...(bubbles.length ? { orb: orbSpread(bubbles) } : {}) };
|
|
122
164
|
};
|
|
165
|
+
const sidebar = await page.evaluate(() => {
|
|
166
|
+
const entries = [...document.querySelectorAll('#panel .plist button')].filter((b) => b.querySelector('.swatch'));
|
|
167
|
+
let clipped = 0;
|
|
168
|
+
let orphanedDot = 0;
|
|
169
|
+
for (const b of entries) {
|
|
170
|
+
const name = (b.querySelector('.fname') ?? b) as HTMLElement | null;
|
|
171
|
+
const dot = b.querySelector('.swatch') as HTMLElement | null;
|
|
172
|
+
if (name && name.scrollWidth > name.clientWidth + 1) clipped += 1;
|
|
173
|
+
if (name && dot && dot.getBoundingClientRect().top > name.getBoundingClientRect().bottom) orphanedDot += 1;
|
|
174
|
+
}
|
|
175
|
+
return { entries: entries.length, clipped, orphanedDot };
|
|
176
|
+
});
|
|
123
177
|
const views = [await read('overview')];
|
|
124
178
|
// The orb, as it opens and turned half a revolution: the far side's labels
|
|
125
179
|
// must stay hidden and the near side's must still fit and not overlap.
|
|
@@ -139,8 +193,17 @@ export async function verifyMap(htmlPath: string): Promise<MapRenderCheck> {
|
|
|
139
193
|
views.push(await read('largest-area'));
|
|
140
194
|
}
|
|
141
195
|
const horizontalScroll = await page.evaluate(() => document.documentElement.scrollWidth > window.innerWidth);
|
|
142
|
-
out.viewports.push({ name: vp.name, pageErrors, externalRequests, horizontalScroll, views });
|
|
143
|
-
|
|
196
|
+
out.viewports.push({ name: vp.name, pageErrors, externalRequests, horizontalScroll, views, sidebar });
|
|
197
|
+
const orbBad = (o: OrbSpread | undefined): boolean =>
|
|
198
|
+
!!o && (o.overlapping > 0 || (o.bubbles >= 6 && o.concentration > ORB_MAX_CONCENTRATION));
|
|
199
|
+
if (
|
|
200
|
+
pageErrors ||
|
|
201
|
+
externalRequests ||
|
|
202
|
+
horizontalScroll ||
|
|
203
|
+
sidebar.clipped ||
|
|
204
|
+
sidebar.orphanedDot ||
|
|
205
|
+
views.some((v) => v.outside || v.overlapping || orbBad(v.orb))
|
|
206
|
+
) {
|
|
144
207
|
out.verification = 'failed';
|
|
145
208
|
}
|
|
146
209
|
await page.close();
|
|
@@ -209,6 +209,18 @@ aside h3 {
|
|
|
209
209
|
background: transparent;
|
|
210
210
|
padding: 2px 4px;
|
|
211
211
|
}
|
|
212
|
+
.plist button.aentry {
|
|
213
|
+
display: grid;
|
|
214
|
+
grid-template-columns: auto minmax(0, 1fr);
|
|
215
|
+
column-gap: 6px;
|
|
216
|
+
align-items: start;
|
|
217
|
+
}
|
|
218
|
+
.aentry .swatch {
|
|
219
|
+
margin: 4px 0 0;
|
|
220
|
+
}
|
|
221
|
+
.alabel {
|
|
222
|
+
min-width: 0;
|
|
223
|
+
}
|
|
212
224
|
.plist button:hover {
|
|
213
225
|
background: var(--bg);
|
|
214
226
|
}
|
|
@@ -215,15 +215,18 @@
|
|
|
215
215
|
}
|
|
216
216
|
|
|
217
217
|
// The orb: the precomputed 2D layout wrapped onto a sphere with the inverse
|
|
218
|
-
// Lambert azimuthal equal-area projection
|
|
219
|
-
//
|
|
220
|
-
//
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
218
|
+
// Lambert azimuthal equal-area projection — the layout's centre faces the
|
|
219
|
+
// viewer, its rim wraps to the back. A node's distance from the centre and
|
|
220
|
+
// its direction are both replaced by their RANK (`share`, `turn`, 0..1),
|
|
221
|
+
// order kept, so equal shares of nodes cover equal shares of the sphere.
|
|
222
|
+
// Raw coordinates put most areas on one cap: a force layout is dense in the
|
|
223
|
+
// middle and lopsided around it.
|
|
224
|
+
function orbPoint(share, turn) {
|
|
225
|
+
var rho = 2 * Math.sqrt(share) * 0.97;
|
|
226
|
+
var phi = turn * Math.PI * 2;
|
|
224
227
|
var f = Math.sqrt(Math.max(0, 1 - (rho * rho) / 4));
|
|
225
|
-
var px = (
|
|
226
|
-
var py = (
|
|
228
|
+
var px = Math.cos(phi) * rho * f;
|
|
229
|
+
var py = Math.sin(phi) * rho * f;
|
|
227
230
|
var pz = 1 - (rho * rho) / 2;
|
|
228
231
|
var cy = Math.cos(state.rot.yaw);
|
|
229
232
|
var sy = Math.sin(state.rot.yaw);
|
|
@@ -269,21 +272,39 @@
|
|
|
269
272
|
var nodes = currentNodes();
|
|
270
273
|
var pos = Object.create(null);
|
|
271
274
|
if (state.orb) {
|
|
272
|
-
var
|
|
273
|
-
|
|
274
|
-
|
|
275
|
+
var byReach = nodes.slice().sort(function (a, b) {
|
|
276
|
+
return (a.x * a.x + a.y * a.y) - (b.x * b.x + b.y * b.y) || a.idx - b.idx;
|
|
277
|
+
});
|
|
278
|
+
var share = Object.create(null);
|
|
279
|
+
byReach.forEach(function (n, i) { share[n.kind + n.idx] = (i + 0.5) / byReach.length; });
|
|
280
|
+
var turn = Object.create(null);
|
|
281
|
+
nodes.slice().sort(function (a, b) { return Math.atan2(a.y, a.x) - Math.atan2(b.y, b.x) || a.idx - b.idx; })
|
|
282
|
+
.forEach(function (n, i) { turn[n.kind + n.idx] = i / nodes.length; });
|
|
275
283
|
var k = orbRadius();
|
|
284
|
+
// Bubble sizes are tuned for a 300 px orb; a phone's smaller orb shrinks them with it.
|
|
285
|
+
var fit = Math.min(1, k / 300);
|
|
276
286
|
var cx = s.w / 2 + state.view.x;
|
|
277
287
|
var cyy = s.h / 2 + state.view.y;
|
|
278
288
|
nodes.forEach(function (n) {
|
|
279
|
-
var p = orbPoint(n.
|
|
289
|
+
var p = orbPoint(share[n.kind + n.idx], turn[n.kind + n.idx]);
|
|
290
|
+
n.u = p;
|
|
280
291
|
n.sx = cx + p[0] * k;
|
|
281
292
|
n.sy = cyy + p[1] * k;
|
|
282
293
|
n.depth = p[2];
|
|
283
294
|
// Nearer is larger and opaque; the far side recedes.
|
|
284
|
-
n.r = n.r * (0.6 + 0.4 * (p[2] + 1) / 2);
|
|
295
|
+
n.r = n.r * fit * (0.6 + 0.4 * (p[2] + 1) / 2);
|
|
285
296
|
pos[n.kind + n.idx] = n;
|
|
286
297
|
});
|
|
298
|
+
// No two near-side bubbles may cover each other: each is capped at a
|
|
299
|
+
// share of the gap to its nearest near-side neighbour.
|
|
300
|
+
var nearNodes = nodes.filter(function (n) { return n.depth > 0; });
|
|
301
|
+
if (nearNodes.length <= 400) {
|
|
302
|
+
nearNodes.forEach(function (n) {
|
|
303
|
+
var gap = Infinity;
|
|
304
|
+
nearNodes.forEach(function (o) { if (o !== n) gap = Math.min(gap, Math.hypot(o.sx - n.sx, o.sy - n.sy)); });
|
|
305
|
+
if (gap < Infinity) n.r = Math.max(2, Math.min(n.r, gap * 0.45));
|
|
306
|
+
});
|
|
307
|
+
}
|
|
287
308
|
nodes.sort(function (a, b) { return a.depth - b.depth || a.idx - b.idx; });
|
|
288
309
|
var glow = ctx.createRadialGradient(cx - k * 0.35, cyy - k * 0.35, k * 0.1, cx, cyy, k);
|
|
289
310
|
glow.addColorStop(0, cssVar('--orb-light'));
|
|
@@ -398,6 +419,10 @@
|
|
|
398
419
|
});
|
|
399
420
|
|
|
400
421
|
drawLabels(state.orb ? nodes.filter(function (n) { return n.depth > 0.15; }) : nodes, s);
|
|
422
|
+
// For the render check: the bubbles as drawn, with their side of the orb.
|
|
423
|
+
window.__mapOrbNodes = state.orb
|
|
424
|
+
? nodes.map(function (n) { return { x: n.sx, y: n.sy, r: n.r, near: n.depth > 0, u: n.u }; })
|
|
425
|
+
: [];
|
|
401
426
|
}
|
|
402
427
|
|
|
403
428
|
// Labels: biggest first, placed only where they fit, flipped near the right
|
|
@@ -477,6 +502,7 @@
|
|
|
477
502
|
r.views.forEach(function (v) { parts.push(base(files[v].path)); });
|
|
478
503
|
r.models.forEach(function (v) { parts.push(base(files[v].path)); });
|
|
479
504
|
r.tables.forEach(function (tb) { parts.push('⊞ ' + tb); });
|
|
505
|
+
(r.inferredTables || []).forEach(function (tb) { parts.push('⊞ ' + tb + ' ?'); });
|
|
480
506
|
li.appendChild(el('span', { class: 'fdir' }, parts.length ? '→ ' + parts.join(' → ') : '→ ' + t('none')));
|
|
481
507
|
ul.appendChild(li);
|
|
482
508
|
});
|
|
@@ -582,9 +608,13 @@
|
|
|
582
608
|
panel.appendChild(el('h3', null, t('areas') + ' (' + areas.length + ')'));
|
|
583
609
|
var list = el('ul', { class: 'plist' });
|
|
584
610
|
areas.forEach(function (a, i) {
|
|
585
|
-
var b = el('button', { type: 'button' });
|
|
611
|
+
var b = el('button', { type: 'button', class: 'aentry', title: a.name + ' · ' + a.files.length });
|
|
586
612
|
b.appendChild(swatch(i));
|
|
587
|
-
|
|
613
|
+
var label = el('span', { class: 'alabel' });
|
|
614
|
+
var path = a.kind === 'small-parts' ? null : a.name;
|
|
615
|
+
label.appendChild(el('span', { class: 'fname' }, (path ? base(path) : a.name) + ' · ' + a.files.length));
|
|
616
|
+
if (path && dir(path) !== '.') label.appendChild(el('span', { class: 'fdir' }, dir(path)));
|
|
617
|
+
b.appendChild(label);
|
|
588
618
|
b.addEventListener('click', function () { openArea(i); });
|
|
589
619
|
var li = el('li');
|
|
590
620
|
li.appendChild(b);
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turn a `project` argument — a repo id, a registered name or a registered
|
|
3
|
+
* path — into the root a graph verb runs against (ADR-299, step 5.1). Only a
|
|
4
|
+
* registered project resolves: an MCP caller cannot point a tool at an
|
|
5
|
+
* arbitrary directory, only at one the owner already worked in. A name two
|
|
6
|
+
* registered projects share is refused with both candidates, never guessed.
|
|
7
|
+
*/
|
|
8
|
+
import { findProjects, readRegistry } from '../_lib/project_registry.js';
|
|
9
|
+
|
|
10
|
+
export type ProjectRef = { root: string } | { error: string; candidates: string[] };
|
|
11
|
+
|
|
12
|
+
export function resolveProjectRef(ref: string): ProjectRef {
|
|
13
|
+
const reg = readRegistry();
|
|
14
|
+
const hits = findProjects(reg, ref);
|
|
15
|
+
if (hits.length === 1) return { root: (hits[0] as [string, { root: string }])[1].root };
|
|
16
|
+
if (hits.length > 1) {
|
|
17
|
+
return { error: `'${ref}' names ${hits.length} registered projects — pass an id`, candidates: hits.map(([id, e]) => `${id} ${e.root}`) };
|
|
18
|
+
}
|
|
19
|
+
return {
|
|
20
|
+
error: `no registered project matches '${ref}' — \`code-graph projects list\` shows them`,
|
|
21
|
+
candidates: Object.entries(reg.projects)
|
|
22
|
+
.slice(0, 10)
|
|
23
|
+
.map(([id, e]) => `${id} ${e.root}`),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `code-graph projects …` — the registry of projects this machine works in
|
|
3
|
+
* (ADR-299). A project is added by use (a build or refresh calls
|
|
4
|
+
* {@link registerUse}) or by hand with `add`; nothing here walks a directory
|
|
5
|
+
* tree looking for repositories.
|
|
6
|
+
*
|
|
7
|
+
* projects list every entry; a root missing for 30 days is flagged
|
|
8
|
+
* projects add <path> register a project by hand
|
|
9
|
+
* projects remove <id|name|path> drop one entry and its user-global graph homes
|
|
10
|
+
* projects prune drop the flagged entries
|
|
11
|
+
* projects index on|off|status the estate index switch (council O1, default off)
|
|
12
|
+
* projects budget [MB] show or set the graph-home budget (default 2048)
|
|
13
|
+
* projects pin|unpin <id|name|path> keep a project's graph homes out of eviction
|
|
14
|
+
* projects evict evict least recently queried homes down to the budget
|
|
15
|
+
*/
|
|
16
|
+
import * as fs from 'node:fs';
|
|
17
|
+
import * as path from 'node:path';
|
|
18
|
+
|
|
19
|
+
import { findProjects, mainRoot, type ProjectEntry, readRegistry, recordProject, repoIdOf, updateRegistry } from '../_lib/project_registry.js';
|
|
20
|
+
import { dropProjectRows, indexAfterRefresh, indexPath } from './estate_index.js';
|
|
21
|
+
import { enforceBudget, homeUsage } from './graph_budget.js';
|
|
22
|
+
import { graphHome, graphsRoot } from './graph_home.js';
|
|
23
|
+
import type { CodeGraph } from './types.js';
|
|
24
|
+
|
|
25
|
+
/** A root missing this long is flagged by `list` and removed by `prune` (6.2). */
|
|
26
|
+
export const STALE_ROOT_DAYS = 30;
|
|
27
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
28
|
+
|
|
29
|
+
export type RootState = 'present' | 'missing' | 'stale';
|
|
30
|
+
|
|
31
|
+
export function rootState(e: ProjectEntry, now: Date = new Date()): RootState {
|
|
32
|
+
if (fs.existsSync(e.root)) return 'present';
|
|
33
|
+
return now.getTime() - Date.parse(e.last_seen) >= STALE_ROOT_DAYS * DAY_MS ? 'stale' : 'missing';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Record that `root`'s graph was just built or refreshed. Best-effort: the
|
|
38
|
+
* registry is bookkeeping, and a failure to write it never fails a build.
|
|
39
|
+
*
|
|
40
|
+
* A linked worktree shares its main checkout's repo id, so it records only its
|
|
41
|
+
* own home (for eviction and removal) and never the entry's graph home, commit,
|
|
42
|
+
* languages or estate row — a feature-branch build must not replace what the
|
|
43
|
+
* main checkout recorded.
|
|
44
|
+
*/
|
|
45
|
+
export function registerUse(root: string, graph: CodeGraph | null, commit: string | null): string | null {
|
|
46
|
+
try {
|
|
47
|
+
const home = graphHome(root);
|
|
48
|
+
const real = fs.realpathSync(root);
|
|
49
|
+
const worktree = mainRoot(real) !== real;
|
|
50
|
+
const usedHome = home.kind === 'global' ? { usedHome: home.dir } : {};
|
|
51
|
+
const id = recordProject(
|
|
52
|
+
root,
|
|
53
|
+
worktree
|
|
54
|
+
? usedHome
|
|
55
|
+
: {
|
|
56
|
+
graph_home: home.dir,
|
|
57
|
+
...(commit ? { graph_commit: commit } : {}),
|
|
58
|
+
...(graph ? { languages: [...graph.languages], providers: (graph.providers ?? []).map((p) => p.id) } : {}),
|
|
59
|
+
...usedHome,
|
|
60
|
+
},
|
|
61
|
+
);
|
|
62
|
+
if (!worktree) {
|
|
63
|
+
// A row the pre-checkout registry wrote under the bare repo id goes once that entry was re-keyed.
|
|
64
|
+
if (readRegistry().projects[repoIdOf(id)] === undefined) dropProjectRows(repoIdOf(id));
|
|
65
|
+
indexAfterRefresh(id, real, home.cachePath, graph);
|
|
66
|
+
}
|
|
67
|
+
if (home.kind === 'global') enforceBudget(home.dir);
|
|
68
|
+
return id;
|
|
69
|
+
} catch {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** User-global homes are removable caches; a home under a repository's own `agents/` never is. */
|
|
75
|
+
function removeGlobalHomes(e: ProjectEntry): string[] {
|
|
76
|
+
const base = graphsRoot();
|
|
77
|
+
const removed: string[] = [];
|
|
78
|
+
for (const dir of new Set([...Object.keys(e.homes), ...(e.graph_home ? [e.graph_home] : [])])) {
|
|
79
|
+
const rel = path.relative(base, dir);
|
|
80
|
+
if (rel === '' || rel.startsWith('..') || path.isAbsolute(rel)) continue;
|
|
81
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
82
|
+
removed.push(dir);
|
|
83
|
+
}
|
|
84
|
+
return removed;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function cmdList(): number {
|
|
88
|
+
const reg = readRegistry();
|
|
89
|
+
const rows = Object.entries(reg.projects);
|
|
90
|
+
if (!rows.length) {
|
|
91
|
+
process.stdout.write('no registered projects — a `code-graph build` or `code-graph projects add <path>` registers one\n');
|
|
92
|
+
return 0;
|
|
93
|
+
}
|
|
94
|
+
for (const [id, e] of rows) {
|
|
95
|
+
const state = rootState(e);
|
|
96
|
+
const flag = state === 'stale' ? ` · STALE (missing ${STALE_ROOT_DAYS}+ days — \`projects prune\` removes it)` : state === 'missing' ? ' · missing' : '';
|
|
97
|
+
process.stdout.write(
|
|
98
|
+
`${id} ${e.root}${e.pinned ? ' · pinned' : ''}${flag}\n` +
|
|
99
|
+
` last seen ${e.last_seen}${e.graph_commit ? ` · commit ${e.graph_commit.slice(0, 12)}` : ''}` +
|
|
100
|
+
`${e.languages.length ? ` · ${e.languages.join(', ')}` : ''}${e.providers.length ? ` · providers ${e.providers.join(', ')}` : ''}\n`,
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
process.stdout.write(`estate index: ${reg.index ? 'on' : 'off'} · budget ${reg.budget_mb} MB\n`);
|
|
104
|
+
return 0;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function cmdAdd(target: string | undefined): number {
|
|
108
|
+
if (!target) {
|
|
109
|
+
process.stderr.write('usage: code-graph projects add <path>\n');
|
|
110
|
+
return 2;
|
|
111
|
+
}
|
|
112
|
+
const abs = path.resolve(target);
|
|
113
|
+
if (!fs.existsSync(abs) || !fs.statSync(abs).isDirectory()) {
|
|
114
|
+
process.stderr.write(`code-graph: not a directory: ${abs}\n`);
|
|
115
|
+
return 1;
|
|
116
|
+
}
|
|
117
|
+
const id = recordProject(abs);
|
|
118
|
+
process.stdout.write(`registered ${id} ${fs.realpathSync(abs)}\n`);
|
|
119
|
+
return 0;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function cmdRemove(ref: string | undefined): number {
|
|
123
|
+
if (!ref) {
|
|
124
|
+
process.stderr.write('usage: code-graph projects remove <id|name|path>\n');
|
|
125
|
+
return 2;
|
|
126
|
+
}
|
|
127
|
+
const hits = findProjects(readRegistry(), ref);
|
|
128
|
+
if (hits.length === 0) {
|
|
129
|
+
process.stderr.write(`code-graph: no registered project matches '${ref}'\n`);
|
|
130
|
+
return 1;
|
|
131
|
+
}
|
|
132
|
+
if (hits.length > 1) {
|
|
133
|
+
process.stderr.write(`code-graph: '${ref}' names ${hits.length} projects — use an id:\n`);
|
|
134
|
+
for (const [id, e] of hits) process.stderr.write(` ${id} ${e.root}\n`);
|
|
135
|
+
return 1;
|
|
136
|
+
}
|
|
137
|
+
const [id, entry] = hits[0] as [string, ProjectEntry];
|
|
138
|
+
updateRegistry((reg) => {
|
|
139
|
+
delete reg.projects[id];
|
|
140
|
+
});
|
|
141
|
+
const removed = removeGlobalHomes(entry);
|
|
142
|
+
dropProjectRows(id);
|
|
143
|
+
process.stdout.write(`removed ${id} ${entry.root}${removed.length ? ` · ${removed.length} graph home(s) deleted` : ''}\n`);
|
|
144
|
+
return 0;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function cmdPrune(): number {
|
|
148
|
+
const now = new Date();
|
|
149
|
+
const dropped = updateRegistry((reg) => {
|
|
150
|
+
const out: Array<[string, ProjectEntry]> = [];
|
|
151
|
+
for (const [id, e] of Object.entries(reg.projects)) {
|
|
152
|
+
if (rootState(e, now) !== 'stale') continue;
|
|
153
|
+
out.push([id, e]);
|
|
154
|
+
delete reg.projects[id];
|
|
155
|
+
}
|
|
156
|
+
return out;
|
|
157
|
+
});
|
|
158
|
+
for (const [id, e] of dropped) {
|
|
159
|
+
removeGlobalHomes(e);
|
|
160
|
+
dropProjectRows(id);
|
|
161
|
+
process.stdout.write(`pruned ${id} ${e.root}\n`);
|
|
162
|
+
}
|
|
163
|
+
process.stdout.write(`${dropped.length} pruned\n`);
|
|
164
|
+
return 0;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function cmdIndex(action: string | undefined): number {
|
|
168
|
+
if (action === 'on' || action === 'off') {
|
|
169
|
+
updateRegistry((reg) => {
|
|
170
|
+
reg.index = action === 'on';
|
|
171
|
+
});
|
|
172
|
+
if (action === 'off') fs.rmSync(indexPath(), { force: true });
|
|
173
|
+
process.stdout.write(
|
|
174
|
+
action === 'on'
|
|
175
|
+
? 'estate index on — each registered project is indexed on its next build or refresh. It names client projects by path and never leaves this machine.\n'
|
|
176
|
+
: 'estate index off — the index file is deleted and nothing is indexed.\n',
|
|
177
|
+
);
|
|
178
|
+
return 0;
|
|
179
|
+
}
|
|
180
|
+
if (action !== undefined && action !== 'status') {
|
|
181
|
+
process.stderr.write('usage: code-graph projects index on|off|status\n');
|
|
182
|
+
return 2;
|
|
183
|
+
}
|
|
184
|
+
const reg = readRegistry();
|
|
185
|
+
process.stdout.write(`estate index: ${reg.index ? 'on' : 'off'} · ${fs.existsSync(indexPath()) ? indexPath() : 'no index file'}\n`);
|
|
186
|
+
return 0;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function one(ref: string | undefined, verb: string): [string, ProjectEntry] | null {
|
|
190
|
+
if (!ref) {
|
|
191
|
+
process.stderr.write(`usage: code-graph projects ${verb} <id|name|path>\n`);
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
const hits = findProjects(readRegistry(), ref);
|
|
195
|
+
if (hits.length !== 1) {
|
|
196
|
+
process.stderr.write(`code-graph: '${ref}' matches ${hits.length} registered projects — use an id from \`projects list\`\n`);
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
return hits[0] as [string, ProjectEntry];
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function cmdPin(ref: string | undefined, pinned: boolean): number {
|
|
203
|
+
const hit = one(ref, pinned ? 'pin' : 'unpin');
|
|
204
|
+
if (!hit) return 1;
|
|
205
|
+
updateRegistry((reg) => {
|
|
206
|
+
const e = reg.projects[hit[0]];
|
|
207
|
+
if (e) e.pinned = pinned;
|
|
208
|
+
});
|
|
209
|
+
process.stdout.write(`${pinned ? 'pinned' : 'unpinned'} ${hit[0]} ${hit[1].root}\n`);
|
|
210
|
+
return 0;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function cmdBudget(value: string | undefined): number {
|
|
214
|
+
if (value !== undefined) {
|
|
215
|
+
const mb = Number(value);
|
|
216
|
+
if (!Number.isFinite(mb) || mb <= 0) {
|
|
217
|
+
process.stderr.write('usage: code-graph projects budget [MB] — a positive number\n');
|
|
218
|
+
return 2;
|
|
219
|
+
}
|
|
220
|
+
updateRegistry((reg) => {
|
|
221
|
+
reg.budget_mb = mb;
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
const used = homeUsage().reduce((n, h) => n + h.bytes, 0);
|
|
225
|
+
process.stdout.write(`graph-home budget: ${readRegistry().budget_mb} MB · in use ${(used / (1024 * 1024)).toFixed(1)} MB under ${graphsRoot()}\n`);
|
|
226
|
+
return 0;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function cmdEvict(): number {
|
|
230
|
+
const r = enforceBudget();
|
|
231
|
+
for (const h of r.evicted) process.stdout.write(`evicted ${h.dir} (${(h.bytes / (1024 * 1024)).toFixed(1)} MB)\n`);
|
|
232
|
+
const over = r.total > r.budget;
|
|
233
|
+
process.stdout.write(
|
|
234
|
+
`${r.evicted.length} evicted · ${(r.total / (1024 * 1024)).toFixed(1)} of ${(r.budget / (1024 * 1024)).toFixed(1)} MB` +
|
|
235
|
+
(over ? ' — still over: the rest is pinned, lock-held or being written' : '') +
|
|
236
|
+
'\n',
|
|
237
|
+
);
|
|
238
|
+
return 0;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export function projectsMain(argv: string[]): number {
|
|
242
|
+
const [verb, ...rest] = argv;
|
|
243
|
+
const positional = rest.filter((a) => !a.startsWith('--'));
|
|
244
|
+
if (verb === undefined || verb === 'list') return cmdList();
|
|
245
|
+
if (verb === 'add') return cmdAdd(positional[0]);
|
|
246
|
+
if (verb === 'remove') return cmdRemove(positional[0]);
|
|
247
|
+
if (verb === 'prune') return cmdPrune();
|
|
248
|
+
if (verb === 'index') return cmdIndex(positional[0]);
|
|
249
|
+
if (verb === 'budget') return cmdBudget(positional[0]);
|
|
250
|
+
if (verb === 'pin' || verb === 'unpin') return cmdPin(positional[0], verb === 'pin');
|
|
251
|
+
if (verb === 'evict') return cmdEvict();
|
|
252
|
+
process.stderr.write('usage: code-graph projects list|add|remove|prune|index|budget|pin|unpin|evict\n');
|
|
253
|
+
return 2;
|
|
254
|
+
}
|