@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
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `code-graph map --estate` — one offline page of the registered projects and
|
|
3
|
+
* the edges the estate index derived between them (ADR-299, step 5.4).
|
|
4
|
+
*
|
|
5
|
+
* Projects are nodes; a `package` edge (a declared dependency on a package
|
|
6
|
+
* another project publishes) is drawn solid, the two guesses (`unique-name`,
|
|
7
|
+
* `mention`) dashed. The page names client projects, so it is written under
|
|
8
|
+
* the user-global home and refused anywhere inside a git work tree.
|
|
9
|
+
*
|
|
10
|
+
* The page exposes `window.__mapLabelBoxes` and a `#panel .plist` list in the
|
|
11
|
+
* shape `map_verify` reads, so the same three-width render check applies.
|
|
12
|
+
*/
|
|
13
|
+
import * as fs from 'node:fs';
|
|
14
|
+
import * as path from 'node:path';
|
|
15
|
+
|
|
16
|
+
import { enclosingWorkTree } from '../_lib/git_common_dir.js';
|
|
17
|
+
import { write_atomic } from '../_lib/fs_atomic.js';
|
|
18
|
+
import { event4u_root } from '../_lib/user_global_paths.js';
|
|
19
|
+
import { openEstate, type XEdge } from './estate_index.js';
|
|
20
|
+
import { verifyMap } from './map_verify.js';
|
|
21
|
+
|
|
22
|
+
export interface EstateMapData {
|
|
23
|
+
projects: Array<{ id: string; name: string; indexed_at: string }>;
|
|
24
|
+
edges: XEdge[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function estateMapDir(): string {
|
|
28
|
+
return path.join(event4u_root(), 'runtime', 'estate', 'map');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** JSON safe inside a `<script>` element: no `</script>`, no `<!--`. */
|
|
32
|
+
function scriptJson(v: unknown): string {
|
|
33
|
+
return JSON.stringify(v).replace(/</g, '\\u003c').replace(/\u2028/g, '\\u2028').replace(/\u2029/g, '\\u2029');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const CSS = `
|
|
37
|
+
:root{--bg:#fbfbf9;--fg:#1d1d1b;--muted:#6b6b66;--line:#3c6e8f;--guess:#b0703a;--node:#ffffff;--ring:#3c6e8f;--hl:#d9e8f2}
|
|
38
|
+
@media (prefers-color-scheme:dark){:root{--bg:#161615;--fg:#ecece8;--muted:#a3a39c;--line:#7fb2d4;--guess:#e0a46b;--node:#22221f;--ring:#7fb2d4;--hl:#25343e}}
|
|
39
|
+
*{box-sizing:border-box}html,body{margin:0;background:var(--bg);color:var(--fg);font:14px/1.45 system-ui,-apple-system,"Segoe UI",sans-serif}
|
|
40
|
+
header{padding:16px 16px 4px}h1{font-size:18px;margin:0 0 4px}p{margin:0;color:var(--muted);max-width:60ch}
|
|
41
|
+
main{display:flex;flex-wrap:wrap;gap:16px;padding:8px 16px 16px}.stage{flex:1 1 320px;min-width:0}svg{display:block;width:100%;height:auto}
|
|
42
|
+
#panel{flex:0 1 260px;min-width:0}.plist{list-style:none;margin:0;padding:0}.plist button{display:flex;align-items:center;gap:8px;width:100%;
|
|
43
|
+
border:0;background:none;color:inherit;font:inherit;text-align:left;padding:6px 4px;cursor:pointer;border-radius:6px}
|
|
44
|
+
.plist button[aria-pressed=true]{background:var(--hl)}.swatch{flex:0 0 10px;height:10px;border-radius:50%;background:var(--ring)}
|
|
45
|
+
.fname{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0}.legend{display:flex;flex-wrap:wrap;gap:12px;margin-top:8px;color:var(--muted);font-size:12px}
|
|
46
|
+
.k{display:inline-block;width:24px;border-top:2px solid var(--line);vertical-align:middle;margin-right:4px}.k.g{border-top:2px dashed var(--guess)}
|
|
47
|
+
line.package{stroke:var(--line);stroke-width:2}line.guess{stroke:var(--guess);stroke-width:1.5;stroke-dasharray:5 4}
|
|
48
|
+
line.dim{opacity:.15}circle{fill:var(--node);stroke:var(--ring);stroke-width:2}text{fill:var(--fg);font-size:12px}
|
|
49
|
+
`;
|
|
50
|
+
|
|
51
|
+
const JS = `
|
|
52
|
+
(function(){
|
|
53
|
+
var data=JSON.parse(document.getElementById('estate-data').textContent);
|
|
54
|
+
var svg=document.getElementById('stage');var NS='http://www.w3.org/2000/svg';
|
|
55
|
+
var list=document.getElementById('plist');var selected=null;
|
|
56
|
+
function short(s){return s.length>18?s.slice(0,17)+'\\u2026':s}
|
|
57
|
+
function draw(){
|
|
58
|
+
while(svg.firstChild)svg.removeChild(svg.firstChild);
|
|
59
|
+
var w=Math.max(240,svg.parentNode.clientWidth),h=Math.min(w,520);
|
|
60
|
+
svg.setAttribute('viewBox','0 0 '+w+' '+h);svg.setAttribute('width',w);svg.setAttribute('height',h);
|
|
61
|
+
var n=data.projects.length,texts=[],maxw=0;
|
|
62
|
+
var gl=document.createElementNS(NS,'g'),gn=document.createElementNS(NS,'g');svg.appendChild(gl);svg.appendChild(gn);
|
|
63
|
+
data.projects.forEach(function(p){var t=document.createElementNS(NS,'text');t.textContent=short(p.name);t.setAttribute('text-anchor','middle');gn.appendChild(t);texts.push(t);maxw=Math.max(maxw,t.getComputedTextLength())});
|
|
64
|
+
var r=n<2?0:Math.max(24,Math.min(w/2-maxw/2-12,h/2-40));var cx=w/2,cy=h/2-8,pos={};
|
|
65
|
+
data.projects.forEach(function(p,i){var a=-Math.PI/2+2*Math.PI*i/Math.max(1,n);pos[p.id]={x:cx+r*Math.cos(a),y:cy+r*Math.sin(a)}});
|
|
66
|
+
data.edges.forEach(function(e){var s=pos[e.source],t=pos[e.target];if(!s||!t)return;var l=document.createElementNS(NS,'line');
|
|
67
|
+
l.setAttribute('x1',s.x);l.setAttribute('y1',s.y);l.setAttribute('x2',t.x);l.setAttribute('y2',t.y);
|
|
68
|
+
var cls=e.via==='package'?'package':'guess';if(selected&&e.source!==selected&&e.target!==selected)cls+=' dim';l.setAttribute('class',cls);
|
|
69
|
+
var tt=document.createElementNS(NS,'title');tt.textContent=e.via+': '+e.detail;l.appendChild(tt);gl.appendChild(l)});
|
|
70
|
+
var boxes=[];
|
|
71
|
+
data.projects.forEach(function(p,i){var c=document.createElementNS(NS,'circle');var q=pos[p.id];c.setAttribute('cx',q.x);c.setAttribute('cy',q.y);c.setAttribute('r',p.id===selected?9:7);
|
|
72
|
+
gn.insertBefore(c,texts[0]);var t=texts[i];t.setAttribute('x',q.x);t.setAttribute('y',q.y+22);var b=t.getBBox();boxes.push({x:b.x,y:b.y,w:b.width,h:b.height})});
|
|
73
|
+
window.__mapLabelBoxes={boxes:boxes,width:w,height:h};
|
|
74
|
+
}
|
|
75
|
+
data.projects.forEach(function(p){var li=document.createElement('li');var b=document.createElement('button');b.type='button';b.setAttribute('aria-pressed','false');
|
|
76
|
+
var s=document.createElement('span');s.className='swatch';var f=document.createElement('span');f.className='fname';f.textContent=p.name;f.title=p.id;
|
|
77
|
+
b.appendChild(s);b.appendChild(f);b.addEventListener('click',function(){selected=selected===p.id?null:p.id;
|
|
78
|
+
Array.prototype.forEach.call(list.querySelectorAll('button'),function(x){x.setAttribute('aria-pressed','false')});if(selected)b.setAttribute('aria-pressed','true');draw()});
|
|
79
|
+
li.appendChild(b);list.appendChild(li)});
|
|
80
|
+
draw();window.addEventListener('resize',draw);
|
|
81
|
+
})();
|
|
82
|
+
`;
|
|
83
|
+
|
|
84
|
+
export function renderEstateHtml(data: EstateMapData): string {
|
|
85
|
+
const packages = data.edges.filter((e) => e.via === 'package').length;
|
|
86
|
+
const guesses = data.edges.length - packages;
|
|
87
|
+
return [
|
|
88
|
+
'<!doctype html>',
|
|
89
|
+
'<html lang="en">',
|
|
90
|
+
'<head>',
|
|
91
|
+
'<meta charset="utf-8">',
|
|
92
|
+
'<meta name="viewport" content="width=device-width, initial-scale=1">',
|
|
93
|
+
'<title>Project estate</title>',
|
|
94
|
+
`<style>${CSS}</style>`,
|
|
95
|
+
'</head>',
|
|
96
|
+
'<body>',
|
|
97
|
+
'<header>',
|
|
98
|
+
'<h1>Project estate</h1>',
|
|
99
|
+
`<p>${data.projects.length} registered projects · ${packages} package links · ${guesses} guessed links. Local to this machine; never committed.</p>`,
|
|
100
|
+
'<div class="legend"><span><span class="k"></span>declared dependency</span><span><span class="k g"></span>guess (unique name or mention)</span></div>',
|
|
101
|
+
'</header>',
|
|
102
|
+
'<main>',
|
|
103
|
+
'<div class="stage"><svg id="stage" role="img" aria-label="Projects and the links between them"></svg></div>',
|
|
104
|
+
'<aside id="panel"><ul class="plist" id="plist"></ul></aside>',
|
|
105
|
+
'</main>',
|
|
106
|
+
`<script type="application/json" id="estate-data">${scriptJson(data)}</script>`,
|
|
107
|
+
`<script>${JS}</script>`,
|
|
108
|
+
'</body>',
|
|
109
|
+
'</html>',
|
|
110
|
+
'',
|
|
111
|
+
].join('\n');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export async function estateMapMain(argv: string[]): Promise<number> {
|
|
115
|
+
const i = argv.indexOf('--out');
|
|
116
|
+
const outDir = i >= 0 && i + 1 < argv.length ? path.resolve(argv[i + 1] as string) : estateMapDir();
|
|
117
|
+
const tree = enclosingWorkTree(outDir);
|
|
118
|
+
if (tree !== null) {
|
|
119
|
+
process.stderr.write(`code-graph: refusing to write the estate map inside a git work tree (${tree}): it names client projects\n`);
|
|
120
|
+
return 1;
|
|
121
|
+
}
|
|
122
|
+
const db = openEstate();
|
|
123
|
+
if (!db) {
|
|
124
|
+
process.stderr.write('code-graph: no estate index — `code-graph projects index on`, then build or refresh each project\n');
|
|
125
|
+
return 1;
|
|
126
|
+
}
|
|
127
|
+
let data: EstateMapData;
|
|
128
|
+
try {
|
|
129
|
+
data = { projects: db.projects().map(({ id, name, indexed_at }) => ({ id, name, indexed_at })), edges: db.xedges() };
|
|
130
|
+
} finally {
|
|
131
|
+
db.close();
|
|
132
|
+
}
|
|
133
|
+
const page = path.join(outDir, 'index.html');
|
|
134
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
135
|
+
write_atomic(page, renderEstateHtml(data));
|
|
136
|
+
process.stdout.write(`✅ estate map — ${data.projects.length} projects · ${data.edges.length} links\n wrote ${page}\n`);
|
|
137
|
+
if (argv.includes('--no-verify')) return 0;
|
|
138
|
+
const check = await verifyMap(page);
|
|
139
|
+
process.stdout.write(` render check: ${check.verification}${check.reason ? ` — ${check.reason}` : ''}\n`);
|
|
140
|
+
return check.verification === 'failed' ? 1 : 0;
|
|
141
|
+
}
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* receivers resolved against the parsed class hierarchy.
|
|
12
12
|
* AMBIGUOUS — dynamic method calls, facades, scoped calls to an unresolved
|
|
13
13
|
* name: kept with candidates, never dropped. This is the
|
|
14
|
-
* expected majority for dynamic-dispatch code (
|
|
14
|
+
* expected majority for dynamic-dispatch code (PHP frameworks etc.).
|
|
15
15
|
*/
|
|
16
16
|
import {
|
|
17
17
|
type CodeNode,
|
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
MAX_FILE_BYTES,
|
|
21
21
|
type Relation,
|
|
22
22
|
} from './types.js';
|
|
23
|
-
import { extractGo, extractJava, extractPython } from './extract_more.js';
|
|
23
|
+
import { extractCSharp, extractGo, extractJava, extractPython, extractRuby } from './extract_more.js';
|
|
24
24
|
import { getParser, nodeHasError, type TsNode } from './loader.js';
|
|
25
25
|
|
|
26
26
|
export type CallStyle = 'new' | 'free' | 'this' | 'self' | 'static' | 'parent' | 'scoped' | 'dynamic';
|
|
@@ -88,6 +88,10 @@ export interface FileExtract {
|
|
|
88
88
|
/** local class id → its declared parent/interface names (2nd-pass inherits). */
|
|
89
89
|
inherits: Array<{ classId: string; parentName: string }>;
|
|
90
90
|
parseError: boolean;
|
|
91
|
+
/** C#: the namespaces the file declares, for `using` resolution in the build pass. */
|
|
92
|
+
namespaces?: string[];
|
|
93
|
+
/** C#: the namespace each declared type sits in, by node id, for namespace-scope resolution. */
|
|
94
|
+
typeNamespaces?: Record<string, string>;
|
|
91
95
|
}
|
|
92
96
|
|
|
93
97
|
export function loc(n: TsNode): number[] {
|
|
@@ -700,6 +704,8 @@ export async function extractFile(relPath: string, source: string, lang: Lang):
|
|
|
700
704
|
else if (lang === 'python') extractPython(tree.rootNode, relPath, out);
|
|
701
705
|
else if (lang === 'java') extractJava(tree.rootNode, relPath, out);
|
|
702
706
|
else if (lang === 'go') extractGo(tree.rootNode, relPath, out);
|
|
707
|
+
else if (lang === 'csharp') extractCSharp(tree.rootNode, relPath, out);
|
|
708
|
+
else if (lang === 'ruby') extractRuby(tree.rootNode, relPath, out);
|
|
703
709
|
else extractTsJs(tree.rootNode, relPath, out);
|
|
704
710
|
} finally {
|
|
705
711
|
tree.delete(); // delete the tree, never the cached parser (loader.ts)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Python, Go and
|
|
2
|
+
* Python, Go, Java, C# and Ruby extraction — the same shape `extract.ts` emits for
|
|
3
3
|
* PHP / TS / JS, so the build pass resolves them with no language branch
|
|
4
4
|
* beyond module lookup.
|
|
5
5
|
*
|
|
@@ -113,11 +113,18 @@ export function extractPython(root: TsNode, file: string, out: FileExtract): voi
|
|
|
113
113
|
case 'call': {
|
|
114
114
|
const fn = n.childForFieldName('function');
|
|
115
115
|
if (fn?.type === 'identifier') {
|
|
116
|
-
|
|
116
|
+
// `Order(...)` instantiates: a CapWords name is a class by Python convention.
|
|
117
|
+
const isNew = /^[A-Z]/.test(fn.text);
|
|
118
|
+
out.rawEdges.push({ sourceId: scopeId, relation: 'calls', targetName: fn.text, confidenceHint: 'EXTRACTED', callStyle: isNew ? 'new' : 'free' });
|
|
117
119
|
} else if (fn?.type === 'attribute') {
|
|
118
120
|
const obj = fn.childForFieldName('object');
|
|
119
121
|
const attr = text(fn.childForFieldName('attribute'));
|
|
120
122
|
const isSelf = obj?.type === 'identifier' && (obj.text === 'self' || obj.text === 'cls');
|
|
123
|
+
let base = obj;
|
|
124
|
+
while (base?.type === 'attribute') base = base.childForFieldName('object');
|
|
125
|
+
// `Post.objects.count()` / `Service.run()` name a class at the root of the chain.
|
|
126
|
+
const classRoot = base?.type === 'identifier' && /^[A-Z]/.test(base.text) ? base.text : null;
|
|
127
|
+
if (classRoot) out.rawEdges.push({ sourceId: scopeId, relation: 'uses', targetName: classRoot, confidenceHint: 'EXTRACTED' });
|
|
121
128
|
out.rawEdges.push({
|
|
122
129
|
sourceId: scopeId,
|
|
123
130
|
relation: 'calls',
|
|
@@ -125,6 +132,7 @@ export function extractPython(root: TsNode, file: string, out: FileExtract): voi
|
|
|
125
132
|
confidenceHint: isSelf && classId ? 'INFERRED' : 'AMBIGUOUS',
|
|
126
133
|
callStyle: isSelf ? 'this' : 'dynamic',
|
|
127
134
|
...(classId ? { enclosingClassId: classId } : {}),
|
|
135
|
+
...(classRoot && obj === base ? { scopeName: classRoot } : {}),
|
|
128
136
|
});
|
|
129
137
|
}
|
|
130
138
|
break;
|
|
@@ -312,3 +320,234 @@ export function extractGo(root: TsNode, file: string, out: FileExtract): void {
|
|
|
312
320
|
};
|
|
313
321
|
walk(root, file);
|
|
314
322
|
}
|
|
323
|
+
|
|
324
|
+
/** The name a C# type expression names: `List<User>` → `List`, `A.B` → `B`. */
|
|
325
|
+
function csType(n: TsNode): string {
|
|
326
|
+
if (n.type === 'generic_name') return text(namedChildren(n).find((c) => c.type === 'identifier'));
|
|
327
|
+
if (n.type === 'qualified_name') return lastSegment(n.text.replace(/<.*$/, ''));
|
|
328
|
+
return n.text.replace(/<.*$/, '');
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
export function extractCSharp(root: TsNode, file: string, out: FileExtract): void {
|
|
332
|
+
out.nodes.push({ id: file, label: file, kind: 'file', source_file: file, source_location: [] });
|
|
333
|
+
const TYPES: Record<string, 'class' | 'interface' | 'enum'> = {
|
|
334
|
+
class_declaration: 'class',
|
|
335
|
+
record_declaration: 'class',
|
|
336
|
+
struct_declaration: 'class',
|
|
337
|
+
interface_declaration: 'interface',
|
|
338
|
+
enum_declaration: 'enum',
|
|
339
|
+
};
|
|
340
|
+
const namespaces: string[] = [];
|
|
341
|
+
const typeNamespaces: Record<string, string> = {};
|
|
342
|
+
/** Every project-type name a type expression mentions: `List<Order>` names `List` and `Order`; `int` names none. */
|
|
343
|
+
const typeNames = (t: TsNode | null, acc: string[] = []): string[] => {
|
|
344
|
+
if (!t || t.type === 'predefined_type') return acc;
|
|
345
|
+
if (t.type === 'identifier' || t.type === 'qualified_name') return [...acc, csType(t)];
|
|
346
|
+
if (t.type === 'generic_name') acc.push(csType(t));
|
|
347
|
+
for (const c of namedChildren(t)) if (t.type !== 'generic_name' || c.type === 'type_argument_list') acc = typeNames(c, acc);
|
|
348
|
+
return acc;
|
|
349
|
+
};
|
|
350
|
+
const usesTypes = (sourceId: string, t: TsNode | null): void => {
|
|
351
|
+
for (const nm of new Set(typeNames(t))) out.rawEdges.push({ sourceId, relation: 'uses', targetName: nm, confidenceHint: 'EXTRACTED' });
|
|
352
|
+
};
|
|
353
|
+
const walk = (n: TsNode, classId: string | null, scopeId: string, ns = ''): void => {
|
|
354
|
+
const kind = TYPES[n.type];
|
|
355
|
+
if (kind) {
|
|
356
|
+
const nm = nameField(n) ?? '<anon>';
|
|
357
|
+
const id = uniq(out, classId ? `${classId}::${nm}` : `${file}#${nm}`);
|
|
358
|
+
out.nodes.push({ id, label: nm, kind, source_file: file, source_location: loc(n) });
|
|
359
|
+
if (!classId && ns) typeNamespaces[id] = ns;
|
|
360
|
+
out.rawEdges.push({ sourceId: classId ?? file, relation: 'member', targetName: nm, confidenceHint: 'EXTRACTED' });
|
|
361
|
+
for (const c of namedChildren(n)) {
|
|
362
|
+
if (c.type !== 'base_list') continue;
|
|
363
|
+
for (const t of namedChildren(c)) {
|
|
364
|
+
if (t.type === 'identifier' || t.type === 'generic_name' || t.type === 'qualified_name') {
|
|
365
|
+
out.inherits.push({ classId: id, parentName: csType(t) });
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
for (const c of namedChildren(n)) if (c.type === 'declaration_list') for (const d of namedChildren(c)) walk(d, id, id, ns);
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
switch (n.type) {
|
|
373
|
+
case 'namespace_declaration':
|
|
374
|
+
case 'file_scoped_namespace_declaration': {
|
|
375
|
+
const name = n.childForFieldName('name');
|
|
376
|
+
const inner = ns ? `${ns}.${text(name)}` : text(name);
|
|
377
|
+
if (name && !namespaces.includes(inner)) namespaces.push(inner);
|
|
378
|
+
const body = n.childForFieldName('body') ?? n;
|
|
379
|
+
for (const c of namedChildren(body)) if (!same(c, name)) walk(c, classId, scopeId, inner);
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
case 'using_directive': {
|
|
383
|
+
const parts = namedChildren(n);
|
|
384
|
+
const spec = parts.find((c) => c.type === 'qualified_name' || c.type === 'identifier');
|
|
385
|
+
const target = parts.filter((c) => c.type === 'qualified_name' || c.type === 'identifier').pop();
|
|
386
|
+
if (spec && target) {
|
|
387
|
+
const aliased = parts.some((c) => c.type === 'name_equals') || n.text.includes('=');
|
|
388
|
+
out.rawEdges.push({
|
|
389
|
+
sourceId: file,
|
|
390
|
+
relation: 'imports',
|
|
391
|
+
targetName: lastSegment(target.text),
|
|
392
|
+
confidenceHint: 'EXTRACTED',
|
|
393
|
+
moduleSpecifier: target.text,
|
|
394
|
+
importKind: 'namespace',
|
|
395
|
+
...(aliased && spec !== target ? { localName: spec.text } : {}),
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
case 'field_declaration':
|
|
401
|
+
case 'property_declaration': {
|
|
402
|
+
const decl = n.type === 'field_declaration' ? namedChildren(n).find((c) => c.type === 'variable_declaration') : n;
|
|
403
|
+
if (classId) usesTypes(classId, decl?.childForFieldName('type') ?? null);
|
|
404
|
+
break;
|
|
405
|
+
}
|
|
406
|
+
case 'method_declaration':
|
|
407
|
+
case 'constructor_declaration': {
|
|
408
|
+
const nm = nameField(n) ?? '<anon>';
|
|
409
|
+
const id = uniq(out, classId ? `${classId}::${nm}` : `${file}#${nm}`);
|
|
410
|
+
out.nodes.push({ id, label: nm, kind: 'method', source_file: file, source_location: loc(n) });
|
|
411
|
+
out.rawEdges.push({ sourceId: classId ?? file, relation: 'member', targetName: nm, confidenceHint: 'EXTRACTED' });
|
|
412
|
+
usesTypes(id, n.childForFieldName('returns') ?? n.childForFieldName('type'));
|
|
413
|
+
for (const c of namedChildren(n))
|
|
414
|
+
if (c.type === 'parameter_list') for (const p of namedChildren(c)) if (p.type === 'parameter') usesTypes(id, p.childForFieldName('type'));
|
|
415
|
+
for (const c of namedChildren(n)) if (c.type === 'block' || c.type === 'arrow_expression_clause') walk(c, classId, id, ns);
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
case 'invocation_expression': {
|
|
419
|
+
const fn = n.childForFieldName('function');
|
|
420
|
+
const ctx = classId ? { enclosingClassId: classId } : {};
|
|
421
|
+
if (fn && (fn.type === 'identifier' || fn.type === 'generic_name')) {
|
|
422
|
+
out.rawEdges.push({ sourceId: scopeId, relation: 'calls', targetName: csType(fn), confidenceHint: classId ? 'INFERRED' : 'AMBIGUOUS', callStyle: 'this', ...ctx });
|
|
423
|
+
} else if (fn && fn.type === 'member_access_expression') {
|
|
424
|
+
const obj = fn.childForFieldName('expression');
|
|
425
|
+
const nm = csType(fn.childForFieldName('name') ?? fn);
|
|
426
|
+
if (obj && (obj.type === 'this_expression' || obj.type === 'base_expression')) {
|
|
427
|
+
out.rawEdges.push({ sourceId: scopeId, relation: 'calls', targetName: nm, confidenceHint: classId ? 'INFERRED' : 'AMBIGUOUS', callStyle: obj.type === 'base_expression' ? 'parent' : 'this', ...ctx });
|
|
428
|
+
} else if (obj && obj.type === 'identifier' && /^[A-Z]/.test(obj.text)) {
|
|
429
|
+
out.rawEdges.push({ sourceId: scopeId, relation: 'calls', targetName: nm, confidenceHint: 'AMBIGUOUS', callStyle: 'scoped', scopeName: obj.text, ...ctx });
|
|
430
|
+
} else {
|
|
431
|
+
out.rawEdges.push({ sourceId: scopeId, relation: 'calls', targetName: nm, confidenceHint: 'AMBIGUOUS', callStyle: 'dynamic', ...ctx });
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
break;
|
|
435
|
+
}
|
|
436
|
+
case 'object_creation_expression': {
|
|
437
|
+
const t = n.childForFieldName('type');
|
|
438
|
+
if (t) out.rawEdges.push({ sourceId: scopeId, relation: 'calls', targetName: csType(t), confidenceHint: 'EXTRACTED', callStyle: 'new' });
|
|
439
|
+
break;
|
|
440
|
+
}
|
|
441
|
+
default:
|
|
442
|
+
break;
|
|
443
|
+
}
|
|
444
|
+
for (const c of namedChildren(n)) walk(c, classId, scopeId, ns);
|
|
445
|
+
};
|
|
446
|
+
walk(root, null, file);
|
|
447
|
+
out.namespaces = namespaces;
|
|
448
|
+
out.typeNamespaces = typeNamespaces;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/** The last segment of a Ruby constant path: `Admin::User` → `User`. */
|
|
452
|
+
function rbConst(n: TsNode | null): string {
|
|
453
|
+
return n ? (n.text.split('::').pop() ?? n.text).trim() : '';
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Ruby: classes, modules (as `trait` — a mixin is what PHP calls a trait),
|
|
458
|
+
* instance and singleton methods, `require` / `require_relative`, the
|
|
459
|
+
* superclass and every `include` / `extend` / `prepend` as `inherits`, and
|
|
460
|
+
* calls: implicit-self and `self.` calls on the enclosing class, `Foo.new`,
|
|
461
|
+
* `Foo.bar` scoped calls (plus a `uses` of `Foo`), and dynamic receivers.
|
|
462
|
+
*/
|
|
463
|
+
export function extractRuby(root: TsNode, file: string, out: FileExtract): void {
|
|
464
|
+
out.nodes.push({ id: file, label: file, kind: 'file', source_file: file, source_location: [] });
|
|
465
|
+
const MIXINS = new Set(['include', 'extend', 'prepend']);
|
|
466
|
+
const walk = (n: TsNode, classId: string | null, scopeId: string): void => {
|
|
467
|
+
switch (n.type) {
|
|
468
|
+
case 'class':
|
|
469
|
+
case 'module': {
|
|
470
|
+
const nm = rbConst(n.childForFieldName('name')) || '<anon>';
|
|
471
|
+
const id = uniq(out, classId ? `${classId}::${nm}` : `${file}#${nm}`);
|
|
472
|
+
out.nodes.push({ id, label: nm, kind: n.type === 'class' ? 'class' : 'trait', source_file: file, source_location: loc(n) });
|
|
473
|
+
out.rawEdges.push({ sourceId: classId ?? file, relation: 'member', targetName: nm, confidenceHint: 'EXTRACTED' });
|
|
474
|
+
const sup = n.childForFieldName('superclass');
|
|
475
|
+
const parent = sup ? namedChildren(sup)[0] : undefined;
|
|
476
|
+
if (parent && (parent.type === 'constant' || parent.type === 'scope_resolution')) out.inherits.push({ classId: id, parentName: rbConst(parent) });
|
|
477
|
+
const body = n.childForFieldName('body');
|
|
478
|
+
if (body) for (const c of namedChildren(body)) walk(c, id, id);
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
case 'method':
|
|
482
|
+
case 'singleton_method': {
|
|
483
|
+
const nm = nameField(n) ?? '<anon>';
|
|
484
|
+
const id = uniq(out, classId ? `${classId}::${nm}` : `${file}#${nm}`);
|
|
485
|
+
out.nodes.push({ id, label: nm, kind: classId ? 'method' : 'function', source_file: file, source_location: loc(n) });
|
|
486
|
+
out.rawEdges.push({ sourceId: classId ?? file, relation: 'member', targetName: nm, confidenceHint: 'EXTRACTED' });
|
|
487
|
+
const body = n.childForFieldName('body');
|
|
488
|
+
for (const c of body ? namedChildren(body) : []) {
|
|
489
|
+
// A bare identifier statement is an argument-less implicit-self
|
|
490
|
+
// call (`audit`); a local read looks the same and resolves to
|
|
491
|
+
// nothing, which is what an unresolved call records.
|
|
492
|
+
if (c.type === 'identifier' && classId)
|
|
493
|
+
out.rawEdges.push({ sourceId: id, relation: 'calls', targetName: c.text, confidenceHint: 'INFERRED', callStyle: 'this', enclosingClassId: classId });
|
|
494
|
+
else walk(c, classId, id);
|
|
495
|
+
}
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
case 'call': {
|
|
499
|
+
const recv = n.childForFieldName('receiver');
|
|
500
|
+
const nm = text(n.childForFieldName('method'));
|
|
501
|
+
const args = n.childForFieldName('arguments');
|
|
502
|
+
const first = args ? namedChildren(args)[0] : undefined;
|
|
503
|
+
if (!recv && (nm === 'require' || nm === 'require_relative') && first?.type === 'string') {
|
|
504
|
+
const raw = text(namedChildren(first).find((c) => c.type === 'string_content'));
|
|
505
|
+
if (raw) {
|
|
506
|
+
const spec = nm === 'require_relative' && !raw.startsWith('.') ? `./${raw}` : raw;
|
|
507
|
+
out.rawEdges.push({
|
|
508
|
+
sourceId: file,
|
|
509
|
+
relation: 'imports',
|
|
510
|
+
targetName: lastSegment(raw),
|
|
511
|
+
confidenceHint: 'EXTRACTED',
|
|
512
|
+
moduleSpecifier: spec,
|
|
513
|
+
importKind: 'namespace',
|
|
514
|
+
// A require binds no name; a local that cannot be written keeps it from shadowing one.
|
|
515
|
+
localName: `require:${spec}`,
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
if (!recv && classId && MIXINS.has(nm) && args) {
|
|
521
|
+
for (const c of namedChildren(args))
|
|
522
|
+
if (c.type === 'constant' || c.type === 'scope_resolution') out.inherits.push({ classId, parentName: rbConst(c) });
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
const ctx = classId ? { enclosingClassId: classId } : {};
|
|
526
|
+
if (recv && (recv.type === 'constant' || recv.type === 'scope_resolution')) {
|
|
527
|
+
const cls = rbConst(recv);
|
|
528
|
+
if (nm === 'new') out.rawEdges.push({ sourceId: scopeId, relation: 'calls', targetName: cls, confidenceHint: 'EXTRACTED', callStyle: 'new' });
|
|
529
|
+
else {
|
|
530
|
+
out.rawEdges.push({ sourceId: scopeId, relation: 'uses', targetName: cls, confidenceHint: 'EXTRACTED' });
|
|
531
|
+
out.rawEdges.push({ sourceId: scopeId, relation: 'calls', targetName: nm, confidenceHint: 'AMBIGUOUS', callStyle: 'scoped', scopeName: cls, ...ctx });
|
|
532
|
+
}
|
|
533
|
+
} else if (!recv || recv.type === 'self') {
|
|
534
|
+
out.rawEdges.push({
|
|
535
|
+
sourceId: scopeId,
|
|
536
|
+
relation: 'calls',
|
|
537
|
+
targetName: nm,
|
|
538
|
+
confidenceHint: classId ? 'INFERRED' : 'AMBIGUOUS',
|
|
539
|
+
callStyle: classId ? 'this' : 'free',
|
|
540
|
+
...ctx,
|
|
541
|
+
});
|
|
542
|
+
} else if (nm) {
|
|
543
|
+
out.rawEdges.push({ sourceId: scopeId, relation: 'calls', targetName: nm, confidenceHint: 'AMBIGUOUS', callStyle: 'dynamic', ...ctx });
|
|
544
|
+
}
|
|
545
|
+
break;
|
|
546
|
+
}
|
|
547
|
+
default:
|
|
548
|
+
break;
|
|
549
|
+
}
|
|
550
|
+
for (const c of namedChildren(n)) walk(c, classId, scopeId);
|
|
551
|
+
};
|
|
552
|
+
walk(root, null, file);
|
|
553
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The storage budget for user-global graph homes (ADR-299, council O4).
|
|
3
|
+
*
|
|
4
|
+
* Only directories under `runtime/graphs/` are ever evicted — a graph under a
|
|
5
|
+
* repository's own `agents/` is the project's, not this machine's cache.
|
|
6
|
+
* Eviction is least-recently-QUERIED first: a query touches its home's
|
|
7
|
+
* `.last-query` marker, a build or refresh records it in the registry, and a
|
|
8
|
+
* registry scan changes neither. A pinned project's homes, a home whose
|
|
9
|
+
* refresh lock is held, and a home written to within a stale-lock window or
|
|
10
|
+
* holding a temp output are never evicted, so a budget can stay exceeded. The
|
|
11
|
+
* last guard exists because a plain `code-graph build` takes no lock.
|
|
12
|
+
*/
|
|
13
|
+
import * as fs from 'node:fs';
|
|
14
|
+
import * as path from 'node:path';
|
|
15
|
+
|
|
16
|
+
import { readRegistry, updateRegistry } from '../_lib/project_registry.js';
|
|
17
|
+
import { graphHome, graphsRoot } from './graph_home.js';
|
|
18
|
+
import { isStale, STALE_LOCK_MS } from './refresh_lock.js';
|
|
19
|
+
|
|
20
|
+
const MB = 1024 * 1024;
|
|
21
|
+
const LAST_QUERY = '.last-query';
|
|
22
|
+
|
|
23
|
+
/** Record that a query just read `root`'s graph. Only user-global homes keep recency. */
|
|
24
|
+
export function touchHome(root: string): void {
|
|
25
|
+
try {
|
|
26
|
+
const home = graphHome(root);
|
|
27
|
+
if (home.kind !== 'global' || !fs.existsSync(home.dir)) return;
|
|
28
|
+
const marker = path.join(home.dir, LAST_QUERY);
|
|
29
|
+
const now = new Date();
|
|
30
|
+
try {
|
|
31
|
+
fs.utimesSync(marker, now, now);
|
|
32
|
+
} catch {
|
|
33
|
+
fs.writeFileSync(marker, '');
|
|
34
|
+
}
|
|
35
|
+
} catch {
|
|
36
|
+
/* recency is a hint; a query never fails on it */
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface Walk {
|
|
41
|
+
bytes: number;
|
|
42
|
+
/** Newest mtime of anything below the home except the query marker. */
|
|
43
|
+
newest: number;
|
|
44
|
+
/** A build's or refresh's temp output is present. */
|
|
45
|
+
temp: boolean;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const TEMP_RE = /\.refresh-tmp-|\.tmp-/;
|
|
49
|
+
|
|
50
|
+
function walk(dir: string, top: boolean, acc: Walk = { bytes: 0, newest: 0, temp: false }): Walk {
|
|
51
|
+
let entries: fs.Dirent[];
|
|
52
|
+
try {
|
|
53
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
54
|
+
} catch {
|
|
55
|
+
return acc;
|
|
56
|
+
}
|
|
57
|
+
for (const e of entries) {
|
|
58
|
+
const p = path.join(dir, e.name);
|
|
59
|
+
if (TEMP_RE.test(e.name)) acc.temp = true;
|
|
60
|
+
let st: fs.Stats;
|
|
61
|
+
try {
|
|
62
|
+
st = fs.lstatSync(p);
|
|
63
|
+
} catch {
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (!(top && e.name === LAST_QUERY)) acc.newest = Math.max(acc.newest, st.mtimeMs);
|
|
67
|
+
if (e.isDirectory()) walk(p, false, acc);
|
|
68
|
+
else acc.bytes += st.size;
|
|
69
|
+
}
|
|
70
|
+
return acc;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface HomeUsage {
|
|
74
|
+
dir: string;
|
|
75
|
+
bytes: number;
|
|
76
|
+
/** Last query, else last build or refresh, else the directory's own mtime. */
|
|
77
|
+
used_at: number;
|
|
78
|
+
pinned: boolean;
|
|
79
|
+
locked: boolean;
|
|
80
|
+
/** Written within the last {@link STALE_LOCK_MS}, or holding a temp output: a build may be mid-write. */
|
|
81
|
+
busy: boolean;
|
|
82
|
+
project: string | null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Every user-global graph home with its size and why it may or may not go. */
|
|
86
|
+
export function homeUsage(): HomeUsage[] {
|
|
87
|
+
const base = graphsRoot();
|
|
88
|
+
let names: string[];
|
|
89
|
+
try {
|
|
90
|
+
names = fs.readdirSync(base);
|
|
91
|
+
} catch {
|
|
92
|
+
return [];
|
|
93
|
+
}
|
|
94
|
+
const reg = readRegistry();
|
|
95
|
+
const owner = new Map<string, { id: string; pinned: boolean; at: number }>();
|
|
96
|
+
for (const [id, e] of Object.entries(reg.projects)) {
|
|
97
|
+
for (const [dir, at] of Object.entries(e.homes)) owner.set(dir, { id, pinned: e.pinned, at: Date.parse(at) || 0 });
|
|
98
|
+
}
|
|
99
|
+
const out: HomeUsage[] = [];
|
|
100
|
+
const now = Date.now();
|
|
101
|
+
for (const name of names) {
|
|
102
|
+
const dir = path.join(base, name);
|
|
103
|
+
let st: fs.Stats;
|
|
104
|
+
try {
|
|
105
|
+
st = fs.statSync(dir);
|
|
106
|
+
} catch {
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
if (!st.isDirectory()) continue;
|
|
110
|
+
let queried = 0;
|
|
111
|
+
try {
|
|
112
|
+
queried = fs.statSync(path.join(dir, LAST_QUERY)).mtimeMs;
|
|
113
|
+
} catch {
|
|
114
|
+
queried = 0;
|
|
115
|
+
}
|
|
116
|
+
const o = owner.get(dir);
|
|
117
|
+
const lock = path.join(dir, 'state', 'code-graph-refresh.lock');
|
|
118
|
+
const w = walk(dir, true);
|
|
119
|
+
out.push({
|
|
120
|
+
dir,
|
|
121
|
+
bytes: w.bytes,
|
|
122
|
+
used_at: Math.max(queried, o?.at ?? 0) || st.mtimeMs,
|
|
123
|
+
pinned: o?.pinned ?? false,
|
|
124
|
+
locked: fs.existsSync(lock) && !isStale(lock),
|
|
125
|
+
busy: w.temp || now - w.newest < STALE_LOCK_MS,
|
|
126
|
+
project: o?.id ?? null,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
return out;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Evict the least recently used homes until the total fits `budget_mb`;
|
|
134
|
+
* returns what went. `keep` is the home a build just wrote, which is never its
|
|
135
|
+
* own victim.
|
|
136
|
+
*/
|
|
137
|
+
export function enforceBudget(keep?: string): { evicted: HomeUsage[]; total: number; budget: number } {
|
|
138
|
+
const budget = readRegistry().budget_mb * MB;
|
|
139
|
+
const homes = homeUsage();
|
|
140
|
+
let total = homes.reduce((n, h) => n + h.bytes, 0);
|
|
141
|
+
const evicted: HomeUsage[] = [];
|
|
142
|
+
for (const h of [...homes].sort((a, b) => a.used_at - b.used_at)) {
|
|
143
|
+
if (total <= budget) break;
|
|
144
|
+
if (h.pinned || h.locked || h.busy || h.dir === keep) continue;
|
|
145
|
+
fs.rmSync(h.dir, { recursive: true, force: true });
|
|
146
|
+
total -= h.bytes;
|
|
147
|
+
evicted.push(h);
|
|
148
|
+
}
|
|
149
|
+
if (evicted.length) {
|
|
150
|
+
const gone = new Set(evicted.map((h) => h.dir));
|
|
151
|
+
updateRegistry((reg) => {
|
|
152
|
+
for (const e of Object.values(reg.projects)) {
|
|
153
|
+
for (const dir of Object.keys(e.homes)) if (gone.has(dir)) delete e.homes[dir];
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
return { evicted, total, budget };
|
|
158
|
+
}
|