@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
@@ -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,23 +151,59 @@ 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')];
178
+ // The orb, as it opens and turned half a revolution: the far side's labels
179
+ // must stay hidden and the near side's must still fit and not overlap.
180
+ const orb = page.locator('#dim');
181
+ if ((await orb.count()) > 0) {
182
+ await orb.click();
183
+ views.push(await read('overview-orb'));
184
+ await page.evaluate(() => {
185
+ for (let i = 0; i < 21; i++) document.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowRight' }));
186
+ });
187
+ views.push(await read('overview-orb-turned'));
188
+ await orb.click();
189
+ }
124
190
  const firstArea = page.locator('#panel .plist button').first();
125
191
  if ((await firstArea.count()) > 0) {
126
192
  await firstArea.click();
127
193
  views.push(await read('largest-area'));
128
194
  }
129
195
  const horizontalScroll = await page.evaluate(() => document.documentElement.scrollWidth > window.innerWidth);
130
- out.viewports.push({ name: vp.name, pageErrors, externalRequests, horizontalScroll, views });
131
- 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
+ ) {
132
207
  out.verification = 'failed';
133
208
  }
134
209
  await page.close();
@@ -12,6 +12,8 @@
12
12
  --change: #d1495b;
13
13
  --good: #2e8b57;
14
14
  --contested: #d98b1d;
15
+ --orb-light: rgba(255, 255, 255, 0.95);
16
+ --orb-dark: rgba(214, 220, 232, 0.55);
15
17
  }
16
18
  @media (prefers-color-scheme: dark) {
17
19
  :root:not([data-theme='light']) {
@@ -28,6 +30,8 @@
28
30
  --change: #ff7a8a;
29
31
  --good: #5fd39a;
30
32
  --contested: #ffb347;
33
+ --orb-light: rgba(64, 72, 90, 0.55);
34
+ --orb-dark: rgba(20, 22, 26, 0.2);
31
35
  }
32
36
  }
33
37
  :root[data-theme='dark'] {
@@ -44,6 +48,8 @@
44
48
  --change: #ff7a8a;
45
49
  --good: #5fd39a;
46
50
  --contested: #ffb347;
51
+ --orb-light: rgba(64, 72, 90, 0.55);
52
+ --orb-dark: rgba(20, 22, 26, 0.2);
47
53
  }
48
54
  * {
49
55
  box-sizing: border-box;
@@ -203,6 +209,18 @@ aside h3 {
203
209
  background: transparent;
204
210
  padding: 2px 4px;
205
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
+ }
206
224
  .plist button:hover {
207
225
  background: var(--bg);
208
226
  }
@@ -29,6 +29,7 @@
29
29
  search: 'Search files and areas',
30
30
  files: function (n) { return n === 1 ? '1 file' : n + ' files'; },
31
31
  role: 'Role',
32
+ linkedBy: 'Linked by provider',
32
33
  area: 'Area',
33
34
  usedBy: 'Used by',
34
35
  uses: 'Uses',
@@ -56,6 +57,8 @@
56
57
  zoomIn: 'Zoom in',
57
58
  zoomOut: 'Zoom out',
58
59
  reset: 'Fit',
60
+ dim3d: '3D',
61
+ dimHint: 'Show the map as an orb. Drag to turn it, Shift+drag to move it, arrow keys turn it too.',
59
62
  none: 'none',
60
63
  hint: 'Click an area to open it. Click a file to inspect it. Esc goes back.',
61
64
  roles: {
@@ -75,6 +78,7 @@
75
78
  search: 'Dateien und Bereiche suchen',
76
79
  files: function (n) { return n === 1 ? '1 Datei' : n + ' Dateien'; },
77
80
  role: 'Rolle',
81
+ linkedBy: 'Verbunden durch Provider',
78
82
  area: 'Bereich',
79
83
  usedBy: 'Verwendet von',
80
84
  uses: 'Verwendet',
@@ -102,6 +106,8 @@
102
106
  zoomIn: 'Vergrößern',
103
107
  zoomOut: 'Verkleinern',
104
108
  reset: 'Einpassen',
109
+ dim3d: '3D',
110
+ dimHint: 'Karte als Kugel zeigen. Ziehen dreht sie, Shift+Ziehen verschiebt sie, die Pfeiltasten drehen ebenfalls.',
105
111
  none: 'keine',
106
112
  hint: 'Bereich anklicken zum Öffnen, Datei anklicken zum Ansehen. Esc geht zurück.',
107
113
  roles: {
@@ -168,6 +174,10 @@
168
174
  var searchInput = document.getElementById('search');
169
175
  var results = document.getElementById('results');
170
176
 
177
+ // The orb starts turned a little, so it reads as a sphere and not as a disc.
178
+ var ORB_YAW = 0.35;
179
+ var ORB_PITCH = -0.3;
180
+
171
181
  var state = {
172
182
  level: 'overview', // overview | area
173
183
  area: -1,
@@ -176,7 +186,9 @@
176
186
  showGuesses: false,
177
187
  showTests: true,
178
188
  lens: 'areas', // areas | folders
179
- view: { scale: 1, x: 0, y: 0 }
189
+ view: { scale: 1, x: 0, y: 0 },
190
+ orb: false,
191
+ rot: { yaw: ORB_YAW, pitch: ORB_PITCH }
180
192
  };
181
193
  var drawn = []; // {kind, idx, x, y, r} in screen space, for hit tests
182
194
  window.__mapLabelBoxes = { boxes: [], width: 0, height: 0 };
@@ -202,6 +214,34 @@
202
214
  return [s.w / 2 + (x * k) + state.view.x, s.h / 2 + (y * k) + state.view.y];
203
215
  }
204
216
 
217
+ // The orb: the precomputed 2D layout wrapped onto a sphere with the inverse
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;
227
+ var f = Math.sqrt(Math.max(0, 1 - (rho * rho) / 4));
228
+ var px = Math.cos(phi) * rho * f;
229
+ var py = Math.sin(phi) * rho * f;
230
+ var pz = 1 - (rho * rho) / 2;
231
+ var cy = Math.cos(state.rot.yaw);
232
+ var sy = Math.sin(state.rot.yaw);
233
+ var x1 = px * cy + pz * sy;
234
+ var z1 = -px * sy + pz * cy;
235
+ var cp = Math.cos(state.rot.pitch);
236
+ var sp = Math.sin(state.rot.pitch);
237
+ return [x1, py * cp - z1 * sp, py * sp + z1 * cp];
238
+ }
239
+
240
+ function orbRadius() {
241
+ var s = size();
242
+ return (Math.min(s.w, s.h) / 2 - 48) * state.view.scale;
243
+ }
244
+
205
245
  // Visible nodes for the current level.
206
246
  function currentNodes() {
207
247
  var out = [];
@@ -231,12 +271,58 @@
231
271
  ctx.clearRect(0, 0, s.w, s.h);
232
272
  var nodes = currentNodes();
233
273
  var pos = Object.create(null);
234
- nodes.forEach(function (n) {
235
- var p = toScreen(n.x, n.y);
236
- n.sx = p[0];
237
- n.sy = p[1];
238
- pos[n.kind + n.idx] = n;
239
- });
274
+ if (state.orb) {
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; });
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);
286
+ var cx = s.w / 2 + state.view.x;
287
+ var cyy = s.h / 2 + state.view.y;
288
+ nodes.forEach(function (n) {
289
+ var p = orbPoint(share[n.kind + n.idx], turn[n.kind + n.idx]);
290
+ n.u = p;
291
+ n.sx = cx + p[0] * k;
292
+ n.sy = cyy + p[1] * k;
293
+ n.depth = p[2];
294
+ // Nearer is larger and opaque; the far side recedes.
295
+ n.r = n.r * fit * (0.6 + 0.4 * (p[2] + 1) / 2);
296
+ pos[n.kind + n.idx] = n;
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
+ }
308
+ nodes.sort(function (a, b) { return a.depth - b.depth || a.idx - b.idx; });
309
+ var glow = ctx.createRadialGradient(cx - k * 0.35, cyy - k * 0.35, k * 0.1, cx, cyy, k);
310
+ glow.addColorStop(0, cssVar('--orb-light'));
311
+ glow.addColorStop(1, cssVar('--orb-dark'));
312
+ ctx.fillStyle = glow;
313
+ ctx.beginPath();
314
+ ctx.arc(cx, cyy, k, 0, Math.PI * 2);
315
+ ctx.fill();
316
+ } else {
317
+ nodes.forEach(function (n) {
318
+ var p = toScreen(n.x, n.y);
319
+ n.sx = p[0];
320
+ n.sy = p[1];
321
+ pos[n.kind + n.idx] = n;
322
+ });
323
+ }
324
+ // 1 on the near side and in 2D, fading to 0.15 on the far side of the orb.
325
+ function nearness(a) { return state.orb ? 0.15 + 0.85 * (a.depth + 1) / 2 : 1; }
240
326
 
241
327
  // Edges first.
242
328
  var edge = cssVar('--edge');
@@ -252,11 +338,13 @@
252
338
  var hot = state.area === e[0] || state.area === e[1];
253
339
  ctx.strokeStyle = hot ? strong : edge;
254
340
  ctx.lineWidth = 0.5 + 4 * Math.sqrt(e[2] / maxW);
341
+ ctx.globalAlpha = Math.min(nearness(a), nearness(b));
255
342
  ctx.beginPath();
256
343
  ctx.moveTo(a.sx, a.sy);
257
344
  ctx.lineTo(b.sx, b.sy);
258
345
  ctx.stroke();
259
346
  });
347
+ ctx.globalAlpha = 1;
260
348
  } else {
261
349
  var drawEdges = function (list, dashed) {
262
350
  ctx.setLineDash(dashed ? [4, 4] : []);
@@ -267,11 +355,13 @@
267
355
  var hot = state.file === e[0] || state.file === e[1];
268
356
  ctx.strokeStyle = hot ? strong : edge;
269
357
  ctx.lineWidth = hot ? 1.8 : 0.8;
358
+ ctx.globalAlpha = Math.min(nearness(a), nearness(b));
270
359
  ctx.beginPath();
271
360
  ctx.moveTo(a.sx, a.sy);
272
361
  ctx.lineTo(b.sx, b.sy);
273
362
  ctx.stroke();
274
363
  });
364
+ ctx.globalAlpha = 1;
275
365
  ctx.setLineDash([]);
276
366
  };
277
367
  drawEdges(model.edges, false);
@@ -299,7 +389,7 @@
299
389
  var f = files[n.idx];
300
390
  color = f.role === 'test' ? areaColor(state.area, 0.45) : f.role === 'infra' ? grey : areaColor(state.area, 1);
301
391
  }
302
- ctx.globalAlpha = dim ? 0.35 : 1;
392
+ ctx.globalAlpha = (dim ? 0.35 : 1) * nearness(n);
303
393
  ctx.fillStyle = color;
304
394
  ctx.beginPath();
305
395
  ctx.arc(n.sx, n.sy, r, 0, Math.PI * 2);
@@ -324,10 +414,15 @@
324
414
  ctx.setLineDash([]);
325
415
  }
326
416
  ctx.globalAlpha = 1;
327
- drawn.push({ kind: n.kind, idx: n.idx, x: n.sx, y: n.sy, r: Math.max(r, 8) });
417
+ // The far side of the orb is seen through the near side: never a click target.
418
+ if (!state.orb || n.depth > 0) drawn.push({ kind: n.kind, idx: n.idx, x: n.sx, y: n.sy, r: Math.max(r, 8) });
328
419
  });
329
420
 
330
- drawLabels(nodes, s);
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
+ : [];
331
426
  }
332
427
 
333
428
  // Labels: biggest first, placed only where they fit, flipped near the right
@@ -407,6 +502,7 @@
407
502
  r.views.forEach(function (v) { parts.push(base(files[v].path)); });
408
503
  r.models.forEach(function (v) { parts.push(base(files[v].path)); });
409
504
  r.tables.forEach(function (tb) { parts.push('⊞ ' + tb); });
505
+ (r.inferredTables || []).forEach(function (tb) { parts.push('⊞ ' + tb + ' ?'); });
410
506
  li.appendChild(el('span', { class: 'fdir' }, parts.length ? '→ ' + parts.join(' → ') : '→ ' + t('none')));
411
507
  ul.appendChild(li);
412
508
  });
@@ -428,6 +524,7 @@
428
524
  kv([
429
525
  [t('area'), f.area >= 0 ? areas[f.area].name : '—'],
430
526
  [t('role'), STR[lang].roles[f.role]],
527
+ [t('linkedBy'), f.linkedBy ? f.linkedBy.join(', ') : '—'],
431
528
  [t('guesses'), String(f.guesses)],
432
529
  [t('outcome'), outcomeOf[state.file] === undefined ? '—' : String(outcomeOf[state.file])],
433
530
  [t('state'), data.meta.state]
@@ -511,9 +608,13 @@
511
608
  panel.appendChild(el('h3', null, t('areas') + ' (' + areas.length + ')'));
512
609
  var list = el('ul', { class: 'plist' });
513
610
  areas.forEach(function (a, i) {
514
- var b = el('button', { type: 'button' });
611
+ var b = el('button', { type: 'button', class: 'aentry', title: a.name + ' · ' + a.files.length });
515
612
  b.appendChild(swatch(i));
516
- 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);
517
618
  b.addEventListener('click', function () { openArea(i); });
518
619
  var li = el('li');
519
620
  li.appendChild(b);
@@ -615,7 +716,10 @@
615
716
  renderPanel();
616
717
  draw();
617
718
  }
618
- function resetView() { state.view = { scale: 1, x: 0, y: 0 }; }
719
+ function resetView() {
720
+ state.view = { scale: 1, x: 0, y: 0 };
721
+ state.rot = { yaw: ORB_YAW, pitch: ORB_PITCH };
722
+ }
619
723
  function goHome() { state.level = 'overview'; state.area = -1; state.file = -1; resetView(); refresh(); }
620
724
  function openArea(i) { state.level = 'area'; state.area = i; state.file = -1; resetView(); refresh(); }
621
725
  function openFile(i) {
@@ -632,7 +736,10 @@
632
736
  // Input.
633
737
  var drag = null;
634
738
  canvas.addEventListener('pointerdown', function (e) {
635
- drag = { x: e.clientX, y: e.clientY, vx: state.view.x, vy: state.view.y, moved: false };
739
+ drag = {
740
+ x: e.clientX, y: e.clientY, vx: state.view.x, vy: state.view.y,
741
+ yaw: state.rot.yaw, pitch: state.rot.pitch, turn: state.orb && !e.shiftKey, moved: false
742
+ };
636
743
  canvas.setPointerCapture(e.pointerId);
637
744
  });
638
745
  canvas.addEventListener('pointermove', function (e) {
@@ -640,8 +747,16 @@
640
747
  var dx = e.clientX - drag.x;
641
748
  var dy = e.clientY - drag.y;
642
749
  if (Math.abs(dx) + Math.abs(dy) > 3) drag.moved = true;
643
- if (drag.moved) { state.view.x = drag.vx + dx; state.view.y = drag.vy + dy; draw(); }
750
+ if (!drag.moved) return;
751
+ if (drag.turn) turnTo(drag.yaw + dx * 0.008, drag.pitch - dy * 0.008);
752
+ else { state.view.x = drag.vx + dx; state.view.y = drag.vy + dy; draw(); }
644
753
  });
754
+ // Pitch stops short of the poles, so "up" never flips under the pointer.
755
+ function turnTo(yaw, pitch) {
756
+ state.rot.yaw = yaw;
757
+ state.rot.pitch = Math.max(-1.4, Math.min(1.4, pitch));
758
+ draw();
759
+ }
645
760
  canvas.addEventListener('pointerup', function (e) {
646
761
  var wasDrag = drag && drag.moved;
647
762
  drag = null;
@@ -660,7 +775,14 @@
660
775
  state.view.scale = Math.min(12, Math.max(0.3, state.view.scale * f));
661
776
  draw();
662
777
  }
778
+ var TURN_KEYS = { ArrowLeft: [-0.15, 0], ArrowRight: [0.15, 0], ArrowUp: [0, 0.15], ArrowDown: [0, -0.15] };
663
779
  document.addEventListener('keydown', function (e) {
780
+ var step = TURN_KEYS[e.key];
781
+ if (step && state.orb && document.activeElement !== searchInput) {
782
+ e.preventDefault();
783
+ turnTo(state.rot.yaw + step[0], state.rot.pitch + step[1]);
784
+ return;
785
+ }
664
786
  if (e.key === 'Escape') {
665
787
  if (document.activeElement === searchInput && searchInput.value) { searchInput.value = ''; clear(results); return; }
666
788
  if (state.file >= 0) { state.file = -1; refresh(); } else if (state.area >= 0) goHome();
@@ -710,6 +832,13 @@
710
832
  document.getElementById('z-in').addEventListener('click', function () { zoom(1.25); });
711
833
  document.getElementById('z-out').addEventListener('click', function () { zoom(1 / 1.25); });
712
834
  document.getElementById('z-fit').addEventListener('click', function () { resetView(); draw(); });
835
+ var dimButton = document.getElementById('dim');
836
+ dimButton.addEventListener('click', function () {
837
+ state.orb = !state.orb;
838
+ dimButton.setAttribute('aria-pressed', String(state.orb));
839
+ resetView();
840
+ draw();
841
+ });
713
842
 
714
843
  var themes = ['system', 'light', 'dark'];
715
844
  var theme = 'system';
@@ -744,6 +873,8 @@
744
873
  document.getElementById('z-in').setAttribute('aria-label', t('zoomIn'));
745
874
  document.getElementById('z-out').setAttribute('aria-label', t('zoomOut'));
746
875
  document.getElementById('z-fit').textContent = t('reset');
876
+ document.getElementById('dim').textContent = t('dim3d');
877
+ document.getElementById('dim').setAttribute('title', t('dimHint'));
747
878
  document.getElementById('lang').textContent = t('lang');
748
879
  applyTheme();
749
880
  }
@@ -0,0 +1,173 @@
1
+ /**
2
+ * The ordered post-extraction passes (ADR-298). The build runs this list and
3
+ * nothing else after resolution; a pass that throws is reported and skipped,
4
+ * and the build goes on.
5
+ *
6
+ * Order is meaning: convention providers contest base edges they prove false
7
+ * before anything is added on top, docs add their links, and SCIP last
8
+ * supersedes the name guesses it duplicates.
9
+ */
10
+ import type { SourceFile } from './build.js';
11
+ import { conventionsDefault } from './conventions_default.js';
12
+ import { docsLayer } from './docs_layer.js';
13
+ import { activeProviders, type ProjectProbe } from './providers/index.js';
14
+ import type { ResolutionConfig } from './resolution_tiers.js';
15
+ import { type ScipDocument, scipEdges, supersededByScip } from './scip_reader.js';
16
+ import type { ActiveProviderRecord, CodeEdge, CodeNode } from './types.js';
17
+
18
+ export interface PassContext {
19
+ files: readonly SourceFile[];
20
+ docs: readonly SourceFile[];
21
+ scip: readonly ScipDocument[];
22
+ resolution: ResolutionConfig;
23
+ probe: ProjectProbe;
24
+ /** The graph so far, read-only: a pass proposes, the orchestrator applies. */
25
+ nodes: readonly CodeNode[];
26
+ edges: readonly CodeEdge[];
27
+ }
28
+
29
+ export interface PassFailure {
30
+ pass: string;
31
+ reason: string;
32
+ }
33
+
34
+ export interface Contest {
35
+ /** Suppressed-edge counter the removed edges are reported under, if any. */
36
+ key?: string;
37
+ test: (e: CodeEdge) => boolean;
38
+ }
39
+
40
+ export interface PassResult {
41
+ nodes: CodeNode[];
42
+ edges: CodeEdge[];
43
+ /** Applied to the existing edges, in order, before this pass's own are added. */
44
+ contests?: Contest[];
45
+ /** Failures inside the pass that did not stop it (one provider of several). */
46
+ failures?: PassFailure[];
47
+ /** Convention providers that applied, for the graph's record. */
48
+ providers?: ActiveProviderRecord[];
49
+ }
50
+
51
+ export interface GraphPass {
52
+ id: string;
53
+ applies(ctx: PassContext): boolean;
54
+ run(ctx: PassContext): PassResult;
55
+ }
56
+
57
+ const conventionProviders: GraphPass = {
58
+ id: 'convention-providers',
59
+ applies: (ctx) => activeProviders(ctx.probe).length > 0,
60
+ run: (ctx) => {
61
+ const out: PassResult = { nodes: [], edges: [], contests: [], failures: [], providers: [] };
62
+ const known = new Set(ctx.nodes.map((n) => n.id));
63
+ const drawn = new Set<string>();
64
+ for (const { provider, roots } of activeProviders(ctx.probe)) {
65
+ out.providers?.push({ id: provider.id, roots });
66
+ try {
67
+ const under = (p: string): boolean => roots.includes('') || roots.some((r) => p.startsWith(`${r}/`));
68
+ const files = ctx.files.filter((f) => under(f.path));
69
+ const allPaths = ctx.probe.paths.length > 0 ? ctx.probe.paths : ctx.files.map((f) => f.path);
70
+ const r = provider.build({
71
+ files,
72
+ baseNodes: ctx.nodes,
73
+ baseEdges: ctx.edges,
74
+ paths: allPaths.filter(under),
75
+ manifests: new Map([...ctx.probe.manifests].filter(([p]) => under(p))),
76
+ resolution: ctx.resolution,
77
+ roots,
78
+ });
79
+ if (r.nodes.length === 0 && r.edges.length === 0) continue;
80
+ if (r.contest) out.contests?.push({ key: provider.id, test: r.contest });
81
+ // Two providers may draw the same edge; the first keeps it.
82
+ for (const e of r.edges) {
83
+ const key = `${e.source}\0${e.relation}\0${e.target}`;
84
+ if (drawn.has(key)) continue;
85
+ drawn.add(key);
86
+ out.edges.push({ ...e, provider_id: provider.id });
87
+ }
88
+ // Two providers may declare the same table or template node; the first wins.
89
+ for (const n of r.nodes) {
90
+ if (known.has(n.id)) continue;
91
+ known.add(n.id);
92
+ out.nodes.push(n);
93
+ }
94
+ } catch (e) {
95
+ out.failures?.push({ pass: `convention-providers:${provider.id}`, reason: (e as Error).message });
96
+ }
97
+ }
98
+ return out;
99
+ },
100
+ };
101
+
102
+ const defaults: GraphPass = {
103
+ id: 'conventions-default',
104
+ applies: (ctx) => ctx.files.length > 0,
105
+ run: (ctx) => {
106
+ const paths = ctx.probe.paths.length > 0 ? ctx.probe.paths : [...ctx.files, ...ctx.docs].map((f) => f.path).sort();
107
+ return conventionsDefault({ files: ctx.files, paths, nodes: ctx.nodes, edges: ctx.edges });
108
+ },
109
+ };
110
+
111
+ const docs: GraphPass = {
112
+ id: 'docs',
113
+ applies: (ctx) => ctx.docs.length > 0,
114
+ run: (ctx) => {
115
+ const allFiles = new Set([...ctx.files.map((f) => f.path), ...ctx.docs.map((d) => d.path)]);
116
+ const layer = docsLayer(ctx.docs, allFiles);
117
+ // An earlier pass may already have drawn a document as a link target.
118
+ const known = new Set(ctx.nodes.map((n) => n.id));
119
+ return { nodes: layer.nodes.filter((n) => !known.has(n.id)), edges: layer.edges };
120
+ },
121
+ };
122
+
123
+ const scip: GraphPass = {
124
+ id: 'scip',
125
+ applies: (ctx) => ctx.scip.length > 0,
126
+ run: (ctx) => {
127
+ const stated = scipEdges(ctx.scip, ctx.nodes);
128
+ return { nodes: [], edges: stated, contests: [{ test: supersededByScip(stated, ctx.nodes) }] };
129
+ },
130
+ };
131
+
132
+ export const PASSES: readonly GraphPass[] = [conventionProviders, defaults, docs, scip];
133
+
134
+ export interface PassReport {
135
+ /** Edges removed by a keyed contest, per key (a provider id); only non-zero keys appear. */
136
+ suppressed: Record<string, number>;
137
+ failures: PassFailure[];
138
+ providers: ActiveProviderRecord[];
139
+ }
140
+
141
+ /** Run every applicable pass in order, mutating `nodes` / `edges` in place. */
142
+ export function runPasses(
143
+ base: Omit<PassContext, 'nodes' | 'edges'>,
144
+ nodes: CodeNode[],
145
+ edges: CodeEdge[],
146
+ passes: readonly GraphPass[] = PASSES,
147
+ ): PassReport {
148
+ const report: PassReport = { suppressed: {}, failures: [], providers: [] };
149
+ for (const pass of passes) {
150
+ const ctx: PassContext = { ...base, nodes, edges };
151
+ let result: PassResult;
152
+ try {
153
+ if (!pass.applies(ctx)) continue;
154
+ result = pass.run(ctx);
155
+ } catch (e) {
156
+ report.failures.push({ pass: pass.id, reason: (e as Error).message });
157
+ continue;
158
+ }
159
+ report.failures.push(...(result.failures ?? []));
160
+ report.providers.push(...(result.providers ?? []));
161
+ for (const c of result.contests ?? []) {
162
+ const kept = edges.filter((e) => !c.test(e));
163
+ const removed = edges.length - kept.length;
164
+ if (c.key && removed > 0) report.suppressed[c.key] = (report.suppressed[c.key] ?? 0) + removed;
165
+ // Element-wise: a spread of ~200k edges overflows the call stack.
166
+ edges.length = 0;
167
+ for (const e of kept) edges.push(e);
168
+ }
169
+ for (const e of result.edges) edges.push(e);
170
+ for (const n of result.nodes) nodes.push(n);
171
+ }
172
+ return report;
173
+ }
@@ -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
+ }