@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.
Files changed (225) hide show
  1. package/.augment-plugin/marketplace.json +3 -3
  2. package/.augment-plugin/plugin.json +1 -1
  3. package/.claude-plugin/marketplace.json +1 -1
  4. package/.claude-plugin/skills/install-agent-config/SKILL.md +9 -9
  5. package/CHANGELOG.md +167 -193
  6. package/CREDITS.md +2 -2
  7. package/README.md +1 -1
  8. package/dist/agent-src/skills/code-intelligence/SKILL.md +16 -12
  9. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  10. package/dist/agent-src/templates/scripts/check_memory.ts +1 -1
  11. package/dist/cli/commands/recordTriggerEval.js +1 -1
  12. package/dist/cli/registry.js +2 -2
  13. package/dist/cli-delegate/{chunk-KR5ZIFPO.js → chunk-5AYJGUA3.js} +1 -3
  14. package/dist/cli-delegate/chunk-DIFZBP4G.js +1336 -0
  15. package/dist/cli-delegate/chunk-E7ACTJTJ.js +6792 -0
  16. package/dist/cli-delegate/{chunk-VYZGJLCA.js → chunk-RKNT4LIK.js} +22 -1286
  17. package/dist/cli-delegate/{chunk-BZJ4Y3CI.js → chunk-WY5ZZPGL.js} +2 -2
  18. package/dist/cli-delegate/{chunk-CMZEKSNN.js → chunk-X6LZ7W5A.js} +73 -10
  19. package/dist/cli-delegate/cmd_conformance.js +190 -192
  20. package/dist/cli-delegate/cmd_doctor.js +5 -5
  21. package/dist/cli-delegate/cmd_doctor_neighbours.js +3 -3
  22. package/dist/cli-delegate/cmd_explain.js +1 -1
  23. package/dist/cli-delegate/cmd_export.js +2 -2
  24. package/dist/cli-delegate/cmd_handoff.js +2 -2
  25. package/dist/cli-delegate/cmd_memory_get.js +1 -1
  26. package/dist/cli-delegate/cmd_prune.js +2 -2
  27. package/dist/cli-delegate/cmd_route_audit.js +1 -1
  28. package/dist/cli-delegate/cmd_session_recycle.js +2 -2
  29. package/dist/cli-delegate/cmd_settings_migrate.js +2 -2
  30. package/dist/cli-delegate/cmd_settings_set.js +8 -4
  31. package/dist/cli-delegate/cmd_sync.js +2 -2
  32. package/dist/cli-delegate/cmd_uninstall.js +2 -2
  33. package/dist/cli-delegate/cmd_update.js +24 -13
  34. package/dist/cli-delegate/cmd_upgrade.js +2 -2
  35. package/dist/cli-delegate/cmd_validate.js +2 -2
  36. package/dist/cli-delegate/dist-25AZAHOP.js +3240 -0
  37. package/dist/cli-delegate/dist-P7MFPMF3.js +411 -0
  38. package/dist/cli-delegate/dist-XAS5ZLQB.js +6 -0
  39. package/dist/cli-delegate/{upgrade_unattended-VVL6JLSX.js → upgrade_unattended-QCWOKTUN.js} +178 -163
  40. package/dist/discovery/deprecation-report.md +1 -1
  41. package/dist/discovery/discovery-manifest.json +3 -3
  42. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  43. package/dist/discovery/discovery-manifest.summary.md +1 -1
  44. package/dist/discovery/dormancy-report.md +1 -1
  45. package/dist/discovery/orphan-report.md +1 -1
  46. package/dist/discovery/packs.json +2 -2
  47. package/dist/discovery/trust-report.md +1 -1
  48. package/dist/discovery/workspaces.json +2 -2
  49. package/dist/hooks/code_graph_refresh.chunk.cjs +443 -0
  50. package/dist/hooks/code_graph_refresh.chunk.cjs.sha256 +1 -0
  51. package/dist/hooks/dispatch.js +1001 -938
  52. package/dist/hooks/dispatch.sha256 +1 -1
  53. package/dist/hooks/egress_engine.chunk.cjs +57 -7
  54. package/dist/hooks/egress_engine.chunk.cjs.sha256 +1 -1
  55. package/dist/hooks/update_notice.chunk.cjs +914 -0
  56. package/dist/hooks/update_notice.chunk.cjs.sha256 +1 -0
  57. package/dist/install/install.mjs +6974 -6037
  58. package/dist/mcp/mcp-cloudflare-catalogue.json +1 -0
  59. package/dist/mcp/registry-manifest.json +3 -2
  60. package/dist/mcp/server.json +13 -4
  61. package/dist/mcp/server.mjs +71202 -63282
  62. package/dist/scripts/_lib/egress_rules.js +8 -2
  63. package/dist/scripts/condense.js +18 -15
  64. package/dist/server/routes/discovery.js +1 -1
  65. package/dist/server/routes/install.js +1 -1
  66. package/dist/server/routes/ping.js +1 -1
  67. package/dist/server/routes/schema.js +4 -4
  68. package/dist/server/routes/settings.js +2 -6
  69. package/dist/server/routes/wizard.js +6 -6
  70. package/dist/server/schemas/settings.js +42 -42
  71. package/dist/shared/settingsSurface.js +1 -1
  72. package/dist/shared/userMd/schema.js +1 -1
  73. package/dist/shared/zodJsonSchema.js +47 -0
  74. package/dist/ui/assets/index-B0bcIgi0.js +77 -0
  75. package/dist/ui/index.html +1 -1
  76. package/package.json +6 -3
  77. package/src/agent-src/templates/scripts/check_memory.ts +1 -1
  78. package/src/config/ci-local-parity.yml +11 -0
  79. package/src/config/continuity-surface.json +9 -2
  80. package/src/config/evaluator-budgets.json +9 -8
  81. package/src/config/gate-violation-baselines.json +3 -3
  82. package/src/config/gitignore-block.txt +6 -5
  83. package/src/config/hook-bundle-budget.json +46 -6
  84. package/src/config/pack-size-budget.json +12 -1
  85. package/src/config/packed-binary-manifest.json +18 -0
  86. package/src/config/sigstore/npm_keys.json +50 -0
  87. package/src/config/sigstore/trust-material.json +9 -0
  88. package/src/config/sigstore/trusted_root.json +126 -0
  89. package/src/config/skill-body-budget.json +7 -1
  90. package/src/scripts/_cli/cmd_settings_set.ts +3 -2
  91. package/src/scripts/_cli/cmd_update.ts +48 -20
  92. package/src/scripts/_cli/cmd_upgrade.ts +7 -1
  93. package/src/scripts/_cli/upgrade_unattended.ts +90 -76
  94. package/src/scripts/_dispatch.bash +4 -3
  95. package/src/scripts/_lib/confusables.ts +0 -19
  96. package/src/scripts/_lib/egress_normalize.ts +53 -23
  97. package/src/scripts/_lib/egress_rules.ts +8 -2
  98. package/src/scripts/_lib/egress_text.ts +5 -3
  99. package/src/scripts/_lib/egress_token.ts +7 -5
  100. package/src/scripts/_lib/gate_collector_experimental.ts +633 -0
  101. package/src/scripts/_lib/git_common_dir.ts +10 -0
  102. package/src/scripts/_lib/graph_feeder_record.ts +3 -2
  103. package/src/scripts/_lib/install_reach_checks.ts +32 -2
  104. package/src/scripts/_lib/lineage_backfill.ts +136 -0
  105. package/src/scripts/_lib/lineage_ledger.ts +211 -0
  106. package/src/scripts/_lib/lineage_map.ts +55 -0
  107. package/src/scripts/_lib/lineage_query.ts +140 -0
  108. package/src/scripts/_lib/lineage_seed.ts +116 -0
  109. package/src/scripts/_lib/lineage_watch.ts +249 -0
  110. package/src/scripts/_lib/link_crypto.ts +8 -4
  111. package/src/scripts/_lib/listing_manifests.ts +85 -0
  112. package/src/scripts/_lib/neighbour_census.ts +1 -0
  113. package/src/scripts/_lib/production_reach.ts +3 -3
  114. package/src/scripts/_lib/project_registry.ts +227 -0
  115. package/src/scripts/_lib/provenance_verify.ts +14 -242
  116. package/src/scripts/_lib/release_settings_surface.ts +2 -6
  117. package/src/scripts/_lib/sigstore_provenance.ts +177 -0
  118. package/src/scripts/_lib/source_id.ts +48 -0
  119. package/src/scripts/_lib/source_shape.ts +4 -0
  120. package/src/scripts/_lib/unattended_gates.ts +6 -1
  121. package/src/scripts/adoption_status.ts +231 -27
  122. package/src/scripts/advisories.ts +26 -10
  123. package/src/scripts/ai_council/council_egress.ts +5 -6
  124. package/src/scripts/bench_graph_feeder_latency.ts +10 -3
  125. package/src/scripts/build_mcp_registry_manifest.ts +23 -12
  126. package/src/scripts/check_dependency_floors.ts +14 -0
  127. package/src/scripts/check_egress_mutations.ts +1 -0
  128. package/src/scripts/check_release_manifest_content.ts +17 -2
  129. package/src/scripts/check_release_pr_shape.ts +5 -0
  130. package/src/scripts/code_graph/build.ts +145 -45
  131. package/src/scripts/code_graph/cli.ts +382 -35
  132. package/src/scripts/code_graph/conventions_default.ts +247 -0
  133. package/src/scripts/code_graph/detect.ts +5 -5
  134. package/src/scripts/code_graph/estate_cli.ts +135 -0
  135. package/src/scripts/code_graph/estate_index.ts +394 -0
  136. package/src/scripts/code_graph/estate_manifests.ts +223 -0
  137. package/src/scripts/code_graph/estate_map.ts +141 -0
  138. package/src/scripts/code_graph/extract.ts +8 -2
  139. package/src/scripts/code_graph/extract_more.ts +241 -2
  140. package/src/scripts/code_graph/graph_budget.ts +158 -0
  141. package/src/scripts/code_graph/graph_home.ts +80 -0
  142. package/src/scripts/code_graph/lineage_cli.ts +315 -0
  143. package/src/scripts/code_graph/loader.ts +10 -2
  144. package/src/scripts/code_graph/map.ts +195 -53
  145. package/src/scripts/code_graph/map_model.ts +113 -20
  146. package/src/scripts/code_graph/map_verify.ts +80 -5
  147. package/src/scripts/code_graph/map_viewer/viewer.css +18 -0
  148. package/src/scripts/code_graph/map_viewer/viewer.js +146 -15
  149. package/src/scripts/code_graph/passes.ts +173 -0
  150. package/src/scripts/code_graph/project_ref.ts +25 -0
  151. package/src/scripts/code_graph/projects_cli.ts +254 -0
  152. package/src/scripts/code_graph/providers/angular.ts +55 -0
  153. package/src/scripts/code_graph/providers/aspnetcore.ts +182 -0
  154. package/src/scripts/code_graph/providers/catalog.ts +299 -0
  155. package/src/scripts/code_graph/providers/django.ts +206 -0
  156. package/src/scripts/code_graph/providers/express.ts +25 -0
  157. package/src/scripts/code_graph/providers/fastapi.ts +49 -0
  158. package/src/scripts/code_graph/providers/flask.ts +99 -0
  159. package/src/scripts/code_graph/providers/go_http.ts +121 -0
  160. package/src/scripts/code_graph/providers/index.ts +103 -0
  161. package/src/scripts/code_graph/{laravel_overlay.ts → providers/laravel.ts} +113 -7
  162. package/src/scripts/code_graph/providers/nestjs.ts +34 -0
  163. package/src/scripts/code_graph/providers/nextjs.ts +91 -0
  164. package/src/scripts/code_graph/providers/nuxt.ts +55 -0
  165. package/src/scripts/code_graph/providers/rails.ts +204 -0
  166. package/src/scripts/code_graph/providers/react-router.ts +58 -0
  167. package/src/scripts/code_graph/providers/shared/detect.ts +78 -0
  168. package/src/scripts/code_graph/providers/shared/extractors.ts +249 -0
  169. package/src/scripts/code_graph/providers/shared/js.ts +252 -0
  170. package/src/scripts/code_graph/providers/shared/kit.ts +210 -0
  171. package/src/scripts/code_graph/providers/shared/php.ts +64 -0
  172. package/src/scripts/code_graph/providers/shared/python.ts +223 -0
  173. package/src/scripts/code_graph/providers/spring.ts +175 -0
  174. package/src/scripts/code_graph/providers/sqlalchemy.ts +62 -0
  175. package/src/scripts/code_graph/providers/sveltekit.ts +59 -0
  176. package/src/scripts/code_graph/providers/symfony.ts +142 -0
  177. package/src/scripts/code_graph/providers/typeorm.ts +58 -0
  178. package/src/scripts/code_graph/providers/types.ts +56 -0
  179. package/src/scripts/code_graph/providers/vue-router.ts +21 -0
  180. package/src/scripts/code_graph/providers/wordpress.ts +128 -0
  181. package/src/scripts/code_graph/query.ts +9 -9
  182. package/src/scripts/code_graph/refresh_cli.ts +81 -0
  183. package/src/scripts/code_graph/refresh_lock.ts +197 -0
  184. package/src/scripts/code_graph/repo_identity.ts +169 -0
  185. package/src/scripts/code_graph/resolution_tiers.ts +19 -7
  186. package/src/scripts/code_graph/scip_reader.ts +2 -2
  187. package/src/scripts/code_graph/sqlite_store.ts +31 -28
  188. package/src/scripts/code_graph/types.ts +79 -41
  189. package/src/scripts/code_graph/validate.ts +6 -8
  190. package/src/scripts/condense.ts +15 -15
  191. package/src/scripts/generate_settings_reference.ts +2 -6
  192. package/src/scripts/hook_chunks.mjs +11 -0
  193. package/src/scripts/hook_manifest.json +1 -1
  194. package/src/scripts/hook_manifest.yaml +43 -23
  195. package/src/scripts/hooks/code_graph_refresh_hook.ts +163 -0
  196. package/src/scripts/hooks/dispatch_hook.ts +6 -7
  197. package/src/scripts/hooks/host_semantics.ts +72 -0
  198. package/src/scripts/hooks/lazy_concern.ts +8 -0
  199. package/src/scripts/hooks/user_notice.ts +105 -0
  200. package/src/scripts/install-hooks.sh +54 -33
  201. package/src/scripts/install.ts +2 -6
  202. package/src/scripts/lint_harvest_provenance.ts +137 -11
  203. package/src/scripts/lint_hook_manifest.ts +2 -0
  204. package/src/scripts/lint_marketplace.ts +33 -0
  205. package/src/scripts/lint_mcp_registry_manifest.ts +36 -0
  206. package/src/scripts/mcp_server/consumer_tool_catalog.json +70 -15
  207. package/src/scripts/mcp_server/graph_tools.ts +165 -41
  208. package/src/scripts/network_sources.ts +24 -6
  209. package/src/scripts/release.ts +2 -5
  210. package/src/scripts/release_drill.ts +4 -1
  211. package/src/scripts/release_env.ts +17 -0
  212. package/src/scripts/release_mix_taxonomy.json +3 -0
  213. package/src/scripts/schemas/provenance-mechanism.schema.json +46 -0
  214. package/src/scripts/update_notice_hook.ts +75 -7
  215. package/src/server/schemas/settings.ts +42 -42
  216. package/src/shared/settingsSurface.ts +1 -1
  217. package/src/shared/userMd/schema.ts +1 -1
  218. package/src/shared/zodJsonSchema.ts +41 -0
  219. package/src/vendor/grammars/README.md +8 -6
  220. package/src/vendor/grammars/tree-sitter-c_sharp.wasm +0 -0
  221. package/src/vendor/grammars/tree-sitter-ruby.wasm +0 -0
  222. package/dist/cli-delegate/chunk-XIAPY44N.js +0 -4587
  223. package/dist/ui/assets/index-w70Ado09.js +0 -23
  224. /package/dist/cli-delegate/{chunk-FKYZOWVD.js → chunk-QBAFP67O.js} +0 -0
  225. /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 (Laravel etc.).
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 Java extraction — the same shape `extract.ts` emits for
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
- out.rawEdges.push({ sourceId: scopeId, relation: 'calls', targetName: fn.text, confidenceHint: 'EXTRACTED', callStyle: 'free' });
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
+ }