@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.
Files changed (137) 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/CHANGELOG.md +164 -342
  5. package/CREDITS.md +2 -2
  6. package/README.md +1 -1
  7. package/dist/agent-src/skills/code-intelligence/SKILL.md +3 -0
  8. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  9. package/dist/cli/registry.js +2 -2
  10. package/dist/cli-delegate/{chunk-DHSSP7V3.js → chunk-5AYJGUA3.js} +0 -2
  11. package/dist/cli-delegate/chunk-DIFZBP4G.js +1336 -0
  12. package/dist/cli-delegate/{chunk-MT2WOOCY.js → chunk-WY5ZZPGL.js} +2 -2
  13. package/dist/cli-delegate/{chunk-ORJPBER2.js → chunk-X6LZ7W5A.js} +73 -10
  14. package/dist/cli-delegate/cmd_conformance.js +175 -183
  15. package/dist/cli-delegate/cmd_doctor.js +5 -5
  16. package/dist/cli-delegate/cmd_doctor_neighbours.js +3 -3
  17. package/dist/cli-delegate/cmd_explain.js +1 -1
  18. package/dist/cli-delegate/cmd_handoff.js +2 -2
  19. package/dist/cli-delegate/cmd_memory_get.js +1 -1
  20. package/dist/cli-delegate/cmd_route_audit.js +1 -1
  21. package/dist/cli-delegate/cmd_session_recycle.js +2 -2
  22. package/dist/cli-delegate/cmd_update.js +24 -13
  23. package/dist/cli-delegate/cmd_upgrade.js +2 -2
  24. package/dist/cli-delegate/dist-25AZAHOP.js +3240 -0
  25. package/dist/cli-delegate/dist-P7MFPMF3.js +411 -0
  26. package/dist/cli-delegate/dist-XAS5ZLQB.js +6 -0
  27. package/dist/cli-delegate/{upgrade_unattended-VVL6JLSX.js → upgrade_unattended-QCWOKTUN.js} +178 -163
  28. package/dist/discovery/deprecation-report.md +1 -1
  29. package/dist/discovery/discovery-manifest.json +3 -3
  30. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  31. package/dist/discovery/discovery-manifest.summary.md +1 -1
  32. package/dist/discovery/dormancy-report.md +1 -1
  33. package/dist/discovery/orphan-report.md +1 -1
  34. package/dist/discovery/packs.json +2 -2
  35. package/dist/discovery/trust-report.md +1 -1
  36. package/dist/discovery/workspaces.json +2 -2
  37. package/dist/hooks/code_graph_refresh.chunk.cjs +56 -27
  38. package/dist/hooks/code_graph_refresh.chunk.cjs.sha256 +1 -1
  39. package/dist/hooks/dispatch.js +982 -929
  40. package/dist/hooks/dispatch.sha256 +1 -1
  41. package/dist/hooks/egress_engine.chunk.cjs +56 -6
  42. package/dist/hooks/egress_engine.chunk.cjs.sha256 +1 -1
  43. package/dist/hooks/update_notice.chunk.cjs +914 -0
  44. package/dist/hooks/update_notice.chunk.cjs.sha256 +1 -0
  45. package/dist/mcp/registry-manifest.json +1 -1
  46. package/dist/mcp/server.json +2 -2
  47. package/dist/mcp/server.mjs +71259 -66437
  48. package/dist/scripts/condense.js +9 -6
  49. package/package.json +4 -1
  50. package/src/config/evaluator-budgets.json +9 -8
  51. package/src/config/gate-violation-baselines.json +2 -2
  52. package/src/config/hook-bundle-budget.json +31 -6
  53. package/src/config/packed-binary-manifest.json +9 -0
  54. package/src/config/sigstore/npm_keys.json +50 -0
  55. package/src/config/sigstore/trust-material.json +9 -0
  56. package/src/config/sigstore/trusted_root.json +126 -0
  57. package/src/config/skill-body-budget.json +7 -1
  58. package/src/scripts/_cli/cmd_update.ts +48 -20
  59. package/src/scripts/_cli/cmd_upgrade.ts +7 -1
  60. package/src/scripts/_cli/upgrade_unattended.ts +90 -76
  61. package/src/scripts/_dispatch.bash +3 -2
  62. package/src/scripts/_lib/confusables.ts +0 -19
  63. package/src/scripts/_lib/egress_normalize.ts +53 -23
  64. package/src/scripts/_lib/egress_text.ts +5 -3
  65. package/src/scripts/_lib/egress_token.ts +7 -5
  66. package/src/scripts/_lib/gate_collector_experimental.ts +633 -0
  67. package/src/scripts/_lib/git_common_dir.ts +10 -0
  68. package/src/scripts/_lib/graph_feeder_record.ts +3 -2
  69. package/src/scripts/_lib/install_reach_checks.ts +32 -2
  70. package/src/scripts/_lib/lineage_backfill.ts +136 -0
  71. package/src/scripts/_lib/lineage_ledger.ts +211 -0
  72. package/src/scripts/_lib/lineage_map.ts +55 -0
  73. package/src/scripts/_lib/lineage_query.ts +140 -0
  74. package/src/scripts/_lib/lineage_seed.ts +116 -0
  75. package/src/scripts/_lib/lineage_watch.ts +249 -0
  76. package/src/scripts/_lib/link_crypto.ts +8 -4
  77. package/src/scripts/_lib/production_reach.ts +3 -3
  78. package/src/scripts/_lib/project_registry.ts +227 -0
  79. package/src/scripts/_lib/provenance_verify.ts +14 -242
  80. package/src/scripts/_lib/sigstore_provenance.ts +177 -0
  81. package/src/scripts/_lib/source_id.ts +48 -0
  82. package/src/scripts/_lib/source_shape.ts +4 -0
  83. package/src/scripts/_lib/unattended_gates.ts +6 -1
  84. package/src/scripts/advisories.ts +26 -10
  85. package/src/scripts/ai_council/council_egress.ts +5 -6
  86. package/src/scripts/bench_graph_feeder_latency.ts +10 -3
  87. package/src/scripts/check_dependency_floors.ts +7 -0
  88. package/src/scripts/check_egress_mutations.ts +1 -0
  89. package/src/scripts/code_graph/build.ts +67 -3
  90. package/src/scripts/code_graph/cli.ts +131 -28
  91. package/src/scripts/code_graph/conventions_default.ts +20 -1
  92. package/src/scripts/code_graph/detect.ts +5 -5
  93. package/src/scripts/code_graph/estate_cli.ts +135 -0
  94. package/src/scripts/code_graph/estate_index.ts +394 -0
  95. package/src/scripts/code_graph/estate_manifests.ts +223 -0
  96. package/src/scripts/code_graph/estate_map.ts +141 -0
  97. package/src/scripts/code_graph/extract.ts +4 -1
  98. package/src/scripts/code_graph/extract_more.ts +137 -2
  99. package/src/scripts/code_graph/graph_budget.ts +158 -0
  100. package/src/scripts/code_graph/graph_home.ts +80 -0
  101. package/src/scripts/code_graph/lineage_cli.ts +315 -0
  102. package/src/scripts/code_graph/map.ts +22 -13
  103. package/src/scripts/code_graph/map_model.ts +67 -12
  104. package/src/scripts/code_graph/map_verify.ts +68 -5
  105. package/src/scripts/code_graph/map_viewer/viewer.css +12 -0
  106. package/src/scripts/code_graph/map_viewer/viewer.js +45 -15
  107. package/src/scripts/code_graph/project_ref.ts +25 -0
  108. package/src/scripts/code_graph/projects_cli.ts +254 -0
  109. package/src/scripts/code_graph/providers/catalog.ts +14 -5
  110. package/src/scripts/code_graph/providers/django.ts +52 -9
  111. package/src/scripts/code_graph/providers/index.ts +2 -0
  112. package/src/scripts/code_graph/providers/rails.ts +204 -0
  113. package/src/scripts/code_graph/providers/spring.ts +38 -1
  114. package/src/scripts/code_graph/query.ts +9 -9
  115. package/src/scripts/code_graph/refresh_lock.ts +12 -8
  116. package/src/scripts/code_graph/repo_identity.ts +169 -0
  117. package/src/scripts/code_graph/sqlite_store.ts +22 -25
  118. package/src/scripts/code_graph/types.ts +6 -2
  119. package/src/scripts/condense.ts +6 -6
  120. package/src/scripts/hook_chunks.mjs +7 -0
  121. package/src/scripts/hook_manifest.json +1 -1
  122. package/src/scripts/hook_manifest.yaml +20 -17
  123. package/src/scripts/hooks/code_graph_refresh_hook.ts +10 -7
  124. package/src/scripts/hooks/dispatch_hook.ts +6 -7
  125. package/src/scripts/hooks/host_semantics.ts +72 -0
  126. package/src/scripts/hooks/lazy_concern.ts +4 -0
  127. package/src/scripts/hooks/user_notice.ts +105 -0
  128. package/src/scripts/lint_harvest_provenance.ts +137 -11
  129. package/src/scripts/mcp_server/consumer_tool_catalog.json +69 -15
  130. package/src/scripts/mcp_server/graph_tools.ts +161 -41
  131. package/src/scripts/network_sources.ts +24 -6
  132. package/src/scripts/schemas/provenance-mechanism.schema.json +46 -0
  133. package/src/scripts/update_notice_hook.ts +75 -7
  134. package/src/vendor/grammars/README.md +7 -6
  135. package/src/vendor/grammars/tree-sitter-ruby.wasm +0 -0
  136. /package/dist/cli-delegate/{chunk-FKYZOWVD.js → chunk-QBAFP67O.js} +0 -0
  137. /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
- // The handler's own links only: what this route's code renders and
692
- // names. Its file's links would hand every model the controller
693
- // imports to every route the controller serves.
694
- for (const e of out.get(target) ?? []) {
695
- if (!isStatedResolution(e.resolved_via as ResolvedVia)) continue;
696
- if (e.relation === 'renders') views.add(fileIdx(e.target));
697
- const model = tableOf.has(e.target) ? e.target : e.target.split('::')[0];
698
- const modelTables = model ? tableOf.get(model) : undefined;
699
- if (modelTables) {
700
- models.add(fileIdx(model as string));
701
- for (const t of modelTables) tables.add(t);
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
- return { view, labels: r.boxes.length, ...labelProblems(r.boxes, r.width, r.height) };
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
- if (pageErrors || externalRequests || horizontalScroll || views.some((v) => v.outside || v.overlapping)) {
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, so neighbours stay neighbours and
219
- // no region is squeezed — the layout's centre faces the viewer, its rim
220
- // wraps to the back. Nothing is simulated; the same layout, turned.
221
- function orbPoint(x, y, reach) {
222
- var rho = (Math.sqrt(x * x + y * y) / reach) * 1.9;
223
- var len = Math.sqrt(x * x + y * y) || 1;
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 = (x / len) * rho * f;
226
- var py = (y / len) * rho * f;
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 reach = 0;
273
- nodes.forEach(function (n) { reach = Math.max(reach, Math.sqrt(n.x * n.x + n.y * n.y)); });
274
- reach = reach || 1;
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.x, n.y, reach);
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
- b.appendChild(document.createTextNode(a.name + ' · ' + a.files.length));
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
+ }