@event4u/agent-config 17.2.0 → 17.3.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 (152) 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 +174 -0
  6. package/dist/agent-src/skills/code-intelligence/SKILL.md +13 -12
  7. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  8. package/dist/agent-src/templates/scripts/check_memory.ts +1 -1
  9. package/dist/cli/commands/recordTriggerEval.js +1 -1
  10. package/dist/cli-delegate/{chunk-KR5ZIFPO.js → chunk-DHSSP7V3.js} +1 -1
  11. package/dist/cli-delegate/chunk-E7ACTJTJ.js +6792 -0
  12. package/dist/cli-delegate/{chunk-BZJ4Y3CI.js → chunk-MT2WOOCY.js} +1 -1
  13. package/dist/cli-delegate/{chunk-CMZEKSNN.js → chunk-ORJPBER2.js} +2 -2
  14. package/dist/cli-delegate/{chunk-VYZGJLCA.js → chunk-RKNT4LIK.js} +22 -1286
  15. package/dist/cli-delegate/cmd_conformance.js +20 -14
  16. package/dist/cli-delegate/cmd_doctor.js +3 -3
  17. package/dist/cli-delegate/cmd_doctor_neighbours.js +2 -2
  18. package/dist/cli-delegate/cmd_explain.js +1 -1
  19. package/dist/cli-delegate/cmd_export.js +2 -2
  20. package/dist/cli-delegate/cmd_prune.js +2 -2
  21. package/dist/cli-delegate/cmd_settings_migrate.js +2 -2
  22. package/dist/cli-delegate/cmd_settings_set.js +8 -4
  23. package/dist/cli-delegate/cmd_sync.js +2 -2
  24. package/dist/cli-delegate/cmd_uninstall.js +2 -2
  25. package/dist/cli-delegate/cmd_validate.js +2 -2
  26. package/dist/discovery/deprecation-report.md +1 -1
  27. package/dist/discovery/discovery-manifest.json +3 -3
  28. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  29. package/dist/discovery/discovery-manifest.summary.md +1 -1
  30. package/dist/discovery/dormancy-report.md +1 -1
  31. package/dist/discovery/orphan-report.md +1 -1
  32. package/dist/discovery/packs.json +2 -2
  33. package/dist/discovery/trust-report.md +1 -1
  34. package/dist/discovery/workspaces.json +2 -2
  35. package/dist/hooks/code_graph_refresh.chunk.cjs +414 -0
  36. package/dist/hooks/code_graph_refresh.chunk.cjs.sha256 +1 -0
  37. package/dist/hooks/dispatch.js +21 -11
  38. package/dist/hooks/dispatch.sha256 +1 -1
  39. package/dist/hooks/egress_engine.chunk.cjs +1 -1
  40. package/dist/hooks/egress_engine.chunk.cjs.sha256 +1 -1
  41. package/dist/install/install.mjs +6974 -6037
  42. package/dist/mcp/mcp-cloudflare-catalogue.json +1 -0
  43. package/dist/mcp/registry-manifest.json +3 -2
  44. package/dist/mcp/server.json +13 -4
  45. package/dist/mcp/server.mjs +5561 -2463
  46. package/dist/scripts/_lib/egress_rules.js +8 -2
  47. package/dist/scripts/condense.js +9 -9
  48. package/dist/server/routes/discovery.js +1 -1
  49. package/dist/server/routes/install.js +1 -1
  50. package/dist/server/routes/ping.js +1 -1
  51. package/dist/server/routes/schema.js +4 -4
  52. package/dist/server/routes/settings.js +2 -6
  53. package/dist/server/routes/wizard.js +6 -6
  54. package/dist/server/schemas/settings.js +42 -42
  55. package/dist/shared/settingsSurface.js +1 -1
  56. package/dist/shared/userMd/schema.js +1 -1
  57. package/dist/shared/zodJsonSchema.js +47 -0
  58. package/dist/ui/assets/index-B0bcIgi0.js +77 -0
  59. package/dist/ui/index.html +1 -1
  60. package/package.json +3 -3
  61. package/src/agent-src/templates/scripts/check_memory.ts +1 -1
  62. package/src/config/ci-local-parity.yml +11 -0
  63. package/src/config/continuity-surface.json +9 -2
  64. package/src/config/gate-violation-baselines.json +3 -3
  65. package/src/config/gitignore-block.txt +6 -5
  66. package/src/config/hook-bundle-budget.json +16 -1
  67. package/src/config/pack-size-budget.json +12 -1
  68. package/src/config/packed-binary-manifest.json +9 -0
  69. package/src/scripts/_cli/cmd_settings_set.ts +3 -2
  70. package/src/scripts/_dispatch.bash +1 -1
  71. package/src/scripts/_lib/egress_rules.ts +8 -2
  72. package/src/scripts/_lib/listing_manifests.ts +85 -0
  73. package/src/scripts/_lib/neighbour_census.ts +1 -0
  74. package/src/scripts/_lib/release_settings_surface.ts +2 -6
  75. package/src/scripts/adoption_status.ts +231 -27
  76. package/src/scripts/build_mcp_registry_manifest.ts +23 -12
  77. package/src/scripts/check_dependency_floors.ts +7 -0
  78. package/src/scripts/check_release_manifest_content.ts +17 -2
  79. package/src/scripts/check_release_pr_shape.ts +5 -0
  80. package/src/scripts/code_graph/build.ts +80 -44
  81. package/src/scripts/code_graph/cli.ts +260 -16
  82. package/src/scripts/code_graph/conventions_default.ts +228 -0
  83. package/src/scripts/code_graph/extract.ts +5 -2
  84. package/src/scripts/code_graph/extract_more.ts +105 -1
  85. package/src/scripts/code_graph/loader.ts +10 -2
  86. package/src/scripts/code_graph/map.ts +174 -41
  87. package/src/scripts/code_graph/map_model.ts +53 -15
  88. package/src/scripts/code_graph/map_verify.ts +12 -0
  89. package/src/scripts/code_graph/map_viewer/viewer.css +6 -0
  90. package/src/scripts/code_graph/map_viewer/viewer.js +114 -13
  91. package/src/scripts/code_graph/passes.ts +173 -0
  92. package/src/scripts/code_graph/providers/angular.ts +55 -0
  93. package/src/scripts/code_graph/providers/aspnetcore.ts +182 -0
  94. package/src/scripts/code_graph/providers/catalog.ts +290 -0
  95. package/src/scripts/code_graph/providers/django.ts +163 -0
  96. package/src/scripts/code_graph/providers/express.ts +25 -0
  97. package/src/scripts/code_graph/providers/fastapi.ts +49 -0
  98. package/src/scripts/code_graph/providers/flask.ts +99 -0
  99. package/src/scripts/code_graph/providers/go_http.ts +121 -0
  100. package/src/scripts/code_graph/providers/index.ts +101 -0
  101. package/src/scripts/code_graph/{laravel_overlay.ts → providers/laravel.ts} +113 -7
  102. package/src/scripts/code_graph/providers/nestjs.ts +34 -0
  103. package/src/scripts/code_graph/providers/nextjs.ts +91 -0
  104. package/src/scripts/code_graph/providers/nuxt.ts +55 -0
  105. package/src/scripts/code_graph/providers/react-router.ts +58 -0
  106. package/src/scripts/code_graph/providers/shared/detect.ts +78 -0
  107. package/src/scripts/code_graph/providers/shared/extractors.ts +249 -0
  108. package/src/scripts/code_graph/providers/shared/js.ts +252 -0
  109. package/src/scripts/code_graph/providers/shared/kit.ts +210 -0
  110. package/src/scripts/code_graph/providers/shared/php.ts +64 -0
  111. package/src/scripts/code_graph/providers/shared/python.ts +223 -0
  112. package/src/scripts/code_graph/providers/spring.ts +138 -0
  113. package/src/scripts/code_graph/providers/sqlalchemy.ts +62 -0
  114. package/src/scripts/code_graph/providers/sveltekit.ts +59 -0
  115. package/src/scripts/code_graph/providers/symfony.ts +142 -0
  116. package/src/scripts/code_graph/providers/typeorm.ts +58 -0
  117. package/src/scripts/code_graph/providers/types.ts +56 -0
  118. package/src/scripts/code_graph/providers/vue-router.ts +21 -0
  119. package/src/scripts/code_graph/providers/wordpress.ts +128 -0
  120. package/src/scripts/code_graph/refresh_cli.ts +81 -0
  121. package/src/scripts/code_graph/refresh_lock.ts +193 -0
  122. package/src/scripts/code_graph/resolution_tiers.ts +19 -7
  123. package/src/scripts/code_graph/scip_reader.ts +2 -2
  124. package/src/scripts/code_graph/sqlite_store.ts +10 -4
  125. package/src/scripts/code_graph/types.ts +75 -41
  126. package/src/scripts/code_graph/validate.ts +6 -8
  127. package/src/scripts/condense.ts +9 -9
  128. package/src/scripts/generate_settings_reference.ts +2 -6
  129. package/src/scripts/hook_chunks.mjs +4 -0
  130. package/src/scripts/hook_manifest.json +1 -1
  131. package/src/scripts/hook_manifest.yaml +24 -7
  132. package/src/scripts/hooks/code_graph_refresh_hook.ts +160 -0
  133. package/src/scripts/hooks/lazy_concern.ts +4 -0
  134. package/src/scripts/install-hooks.sh +54 -33
  135. package/src/scripts/install.ts +2 -6
  136. package/src/scripts/lint_hook_manifest.ts +2 -0
  137. package/src/scripts/lint_marketplace.ts +33 -0
  138. package/src/scripts/lint_mcp_registry_manifest.ts +36 -0
  139. package/src/scripts/mcp_server/consumer_tool_catalog.json +1 -0
  140. package/src/scripts/mcp_server/graph_tools.ts +5 -1
  141. package/src/scripts/release.ts +2 -5
  142. package/src/scripts/release_drill.ts +4 -1
  143. package/src/scripts/release_env.ts +17 -0
  144. package/src/scripts/release_mix_taxonomy.json +3 -0
  145. package/src/server/schemas/settings.ts +42 -42
  146. package/src/shared/settingsSurface.ts +1 -1
  147. package/src/shared/userMd/schema.ts +1 -1
  148. package/src/shared/zodJsonSchema.ts +41 -0
  149. package/src/vendor/grammars/README.md +6 -5
  150. package/src/vendor/grammars/tree-sitter-c_sharp.wasm +0 -0
  151. package/dist/cli-delegate/chunk-XIAPY44N.js +0 -4587
  152. package/dist/ui/assets/index-w70Ado09.js +0 -23
@@ -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,31 @@
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, 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;
224
+ 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;
227
+ var pz = 1 - (rho * rho) / 2;
228
+ var cy = Math.cos(state.rot.yaw);
229
+ var sy = Math.sin(state.rot.yaw);
230
+ var x1 = px * cy + pz * sy;
231
+ var z1 = -px * sy + pz * cy;
232
+ var cp = Math.cos(state.rot.pitch);
233
+ var sp = Math.sin(state.rot.pitch);
234
+ return [x1, py * cp - z1 * sp, py * sp + z1 * cp];
235
+ }
236
+
237
+ function orbRadius() {
238
+ var s = size();
239
+ return (Math.min(s.w, s.h) / 2 - 48) * state.view.scale;
240
+ }
241
+
205
242
  // Visible nodes for the current level.
206
243
  function currentNodes() {
207
244
  var out = [];
@@ -231,12 +268,40 @@
231
268
  ctx.clearRect(0, 0, s.w, s.h);
232
269
  var nodes = currentNodes();
233
270
  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
- });
271
+ 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 k = orbRadius();
276
+ var cx = s.w / 2 + state.view.x;
277
+ var cyy = s.h / 2 + state.view.y;
278
+ nodes.forEach(function (n) {
279
+ var p = orbPoint(n.x, n.y, reach);
280
+ n.sx = cx + p[0] * k;
281
+ n.sy = cyy + p[1] * k;
282
+ n.depth = p[2];
283
+ // Nearer is larger and opaque; the far side recedes.
284
+ n.r = n.r * (0.6 + 0.4 * (p[2] + 1) / 2);
285
+ pos[n.kind + n.idx] = n;
286
+ });
287
+ nodes.sort(function (a, b) { return a.depth - b.depth || a.idx - b.idx; });
288
+ var glow = ctx.createRadialGradient(cx - k * 0.35, cyy - k * 0.35, k * 0.1, cx, cyy, k);
289
+ glow.addColorStop(0, cssVar('--orb-light'));
290
+ glow.addColorStop(1, cssVar('--orb-dark'));
291
+ ctx.fillStyle = glow;
292
+ ctx.beginPath();
293
+ ctx.arc(cx, cyy, k, 0, Math.PI * 2);
294
+ ctx.fill();
295
+ } else {
296
+ nodes.forEach(function (n) {
297
+ var p = toScreen(n.x, n.y);
298
+ n.sx = p[0];
299
+ n.sy = p[1];
300
+ pos[n.kind + n.idx] = n;
301
+ });
302
+ }
303
+ // 1 on the near side and in 2D, fading to 0.15 on the far side of the orb.
304
+ function nearness(a) { return state.orb ? 0.15 + 0.85 * (a.depth + 1) / 2 : 1; }
240
305
 
241
306
  // Edges first.
242
307
  var edge = cssVar('--edge');
@@ -252,11 +317,13 @@
252
317
  var hot = state.area === e[0] || state.area === e[1];
253
318
  ctx.strokeStyle = hot ? strong : edge;
254
319
  ctx.lineWidth = 0.5 + 4 * Math.sqrt(e[2] / maxW);
320
+ ctx.globalAlpha = Math.min(nearness(a), nearness(b));
255
321
  ctx.beginPath();
256
322
  ctx.moveTo(a.sx, a.sy);
257
323
  ctx.lineTo(b.sx, b.sy);
258
324
  ctx.stroke();
259
325
  });
326
+ ctx.globalAlpha = 1;
260
327
  } else {
261
328
  var drawEdges = function (list, dashed) {
262
329
  ctx.setLineDash(dashed ? [4, 4] : []);
@@ -267,11 +334,13 @@
267
334
  var hot = state.file === e[0] || state.file === e[1];
268
335
  ctx.strokeStyle = hot ? strong : edge;
269
336
  ctx.lineWidth = hot ? 1.8 : 0.8;
337
+ ctx.globalAlpha = Math.min(nearness(a), nearness(b));
270
338
  ctx.beginPath();
271
339
  ctx.moveTo(a.sx, a.sy);
272
340
  ctx.lineTo(b.sx, b.sy);
273
341
  ctx.stroke();
274
342
  });
343
+ ctx.globalAlpha = 1;
275
344
  ctx.setLineDash([]);
276
345
  };
277
346
  drawEdges(model.edges, false);
@@ -299,7 +368,7 @@
299
368
  var f = files[n.idx];
300
369
  color = f.role === 'test' ? areaColor(state.area, 0.45) : f.role === 'infra' ? grey : areaColor(state.area, 1);
301
370
  }
302
- ctx.globalAlpha = dim ? 0.35 : 1;
371
+ ctx.globalAlpha = (dim ? 0.35 : 1) * nearness(n);
303
372
  ctx.fillStyle = color;
304
373
  ctx.beginPath();
305
374
  ctx.arc(n.sx, n.sy, r, 0, Math.PI * 2);
@@ -324,10 +393,11 @@
324
393
  ctx.setLineDash([]);
325
394
  }
326
395
  ctx.globalAlpha = 1;
327
- drawn.push({ kind: n.kind, idx: n.idx, x: n.sx, y: n.sy, r: Math.max(r, 8) });
396
+ // The far side of the orb is seen through the near side: never a click target.
397
+ 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
398
  });
329
399
 
330
- drawLabels(nodes, s);
400
+ drawLabels(state.orb ? nodes.filter(function (n) { return n.depth > 0.15; }) : nodes, s);
331
401
  }
332
402
 
333
403
  // Labels: biggest first, placed only where they fit, flipped near the right
@@ -428,6 +498,7 @@
428
498
  kv([
429
499
  [t('area'), f.area >= 0 ? areas[f.area].name : '—'],
430
500
  [t('role'), STR[lang].roles[f.role]],
501
+ [t('linkedBy'), f.linkedBy ? f.linkedBy.join(', ') : '—'],
431
502
  [t('guesses'), String(f.guesses)],
432
503
  [t('outcome'), outcomeOf[state.file] === undefined ? '—' : String(outcomeOf[state.file])],
433
504
  [t('state'), data.meta.state]
@@ -615,7 +686,10 @@
615
686
  renderPanel();
616
687
  draw();
617
688
  }
618
- function resetView() { state.view = { scale: 1, x: 0, y: 0 }; }
689
+ function resetView() {
690
+ state.view = { scale: 1, x: 0, y: 0 };
691
+ state.rot = { yaw: ORB_YAW, pitch: ORB_PITCH };
692
+ }
619
693
  function goHome() { state.level = 'overview'; state.area = -1; state.file = -1; resetView(); refresh(); }
620
694
  function openArea(i) { state.level = 'area'; state.area = i; state.file = -1; resetView(); refresh(); }
621
695
  function openFile(i) {
@@ -632,7 +706,10 @@
632
706
  // Input.
633
707
  var drag = null;
634
708
  canvas.addEventListener('pointerdown', function (e) {
635
- drag = { x: e.clientX, y: e.clientY, vx: state.view.x, vy: state.view.y, moved: false };
709
+ drag = {
710
+ x: e.clientX, y: e.clientY, vx: state.view.x, vy: state.view.y,
711
+ yaw: state.rot.yaw, pitch: state.rot.pitch, turn: state.orb && !e.shiftKey, moved: false
712
+ };
636
713
  canvas.setPointerCapture(e.pointerId);
637
714
  });
638
715
  canvas.addEventListener('pointermove', function (e) {
@@ -640,8 +717,16 @@
640
717
  var dx = e.clientX - drag.x;
641
718
  var dy = e.clientY - drag.y;
642
719
  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(); }
720
+ if (!drag.moved) return;
721
+ if (drag.turn) turnTo(drag.yaw + dx * 0.008, drag.pitch - dy * 0.008);
722
+ else { state.view.x = drag.vx + dx; state.view.y = drag.vy + dy; draw(); }
644
723
  });
724
+ // Pitch stops short of the poles, so "up" never flips under the pointer.
725
+ function turnTo(yaw, pitch) {
726
+ state.rot.yaw = yaw;
727
+ state.rot.pitch = Math.max(-1.4, Math.min(1.4, pitch));
728
+ draw();
729
+ }
645
730
  canvas.addEventListener('pointerup', function (e) {
646
731
  var wasDrag = drag && drag.moved;
647
732
  drag = null;
@@ -660,7 +745,14 @@
660
745
  state.view.scale = Math.min(12, Math.max(0.3, state.view.scale * f));
661
746
  draw();
662
747
  }
748
+ var TURN_KEYS = { ArrowLeft: [-0.15, 0], ArrowRight: [0.15, 0], ArrowUp: [0, 0.15], ArrowDown: [0, -0.15] };
663
749
  document.addEventListener('keydown', function (e) {
750
+ var step = TURN_KEYS[e.key];
751
+ if (step && state.orb && document.activeElement !== searchInput) {
752
+ e.preventDefault();
753
+ turnTo(state.rot.yaw + step[0], state.rot.pitch + step[1]);
754
+ return;
755
+ }
664
756
  if (e.key === 'Escape') {
665
757
  if (document.activeElement === searchInput && searchInput.value) { searchInput.value = ''; clear(results); return; }
666
758
  if (state.file >= 0) { state.file = -1; refresh(); } else if (state.area >= 0) goHome();
@@ -710,6 +802,13 @@
710
802
  document.getElementById('z-in').addEventListener('click', function () { zoom(1.25); });
711
803
  document.getElementById('z-out').addEventListener('click', function () { zoom(1 / 1.25); });
712
804
  document.getElementById('z-fit').addEventListener('click', function () { resetView(); draw(); });
805
+ var dimButton = document.getElementById('dim');
806
+ dimButton.addEventListener('click', function () {
807
+ state.orb = !state.orb;
808
+ dimButton.setAttribute('aria-pressed', String(state.orb));
809
+ resetView();
810
+ draw();
811
+ });
713
812
 
714
813
  var themes = ['system', 'light', 'dark'];
715
814
  var theme = 'system';
@@ -744,6 +843,8 @@
744
843
  document.getElementById('z-in').setAttribute('aria-label', t('zoomIn'));
745
844
  document.getElementById('z-out').setAttribute('aria-label', t('zoomOut'));
746
845
  document.getElementById('z-fit').textContent = t('reset');
846
+ document.getElementById('dim').textContent = t('dim3d');
847
+ document.getElementById('dim').setAttribute('title', t('dimHint'));
747
848
  document.getElementById('lang').textContent = t('lang');
748
849
  applyTheme();
749
850
  }
@@ -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,55 @@
1
+ /**
2
+ * Angular:
3
+ *
4
+ * { path: 'x', component: XComponent } PAGE /x --routes-to--> XComponent
5
+ * { path: 'x', loadComponent: () => import('./x').then((m) => m.X) } → X, else the module
6
+ * @Component({ templateUrl: './x.html' }) class --renders--> the template file
7
+ * constructor(private s: S) · s = inject(S) --injects--> S (in a decorated class)
8
+ *
9
+ * Child route paths are not joined to their parent's; `loadChildren` modules,
10
+ * inline `template:` strings and `styleUrls` are not followed.
11
+ */
12
+ import * as path from 'node:path';
13
+
14
+ import { npmRoots } from './shared/detect.js';
15
+ import { constructorInjection, routeObjects } from './shared/js.js';
16
+ import { type Kit, lineIndex, makeKit } from './shared/kit.js';
17
+ import type { ConventionProvider } from './types.js';
18
+
19
+ const EXTS = ['.ts'];
20
+
21
+ function templateUrls(kit: Kit): number {
22
+ const inventory = new Set(kit.paths);
23
+ let count = 0;
24
+ for (const f of kit.filesWithExt(...EXTS)) {
25
+ const at = lineIndex(f.source);
26
+ for (const m of f.source.matchAll(/@Component\(\s*\{(?<body>[^]*?)\}\s*\)/g)) {
27
+ const url = /\btemplateUrl\s*:\s*(['"`])([^'"`]+)\1/.exec(m.groups?.['body'] ?? '');
28
+ if (!url) continue;
29
+ const cls = kit.nextSymbol(f.path, at(m.index as number), ['class']);
30
+ const target = path.posix.join(path.posix.dirname(f.path), url[2] as string);
31
+ if (!cls || cls.kind !== 'class' || !inventory.has(target)) continue;
32
+ kit.fileNode(target);
33
+ kit.emit(cls.id, target, 'renders', 'directive-scan');
34
+ count += 1;
35
+ }
36
+ }
37
+ return count;
38
+ }
39
+
40
+ export const angularProvider: ConventionProvider = {
41
+ id: 'angular',
42
+ detect: (probe) => npmRoots(probe, ['@angular/core']),
43
+ build: (input) => {
44
+ const kit = makeKit(input);
45
+ routeObjects(kit, { exts: EXTS, componentKeys: ['component', 'loadComponent'] });
46
+ templateUrls(kit);
47
+ constructorInjection(kit, {
48
+ exts: EXTS,
49
+ classDecorator: /@(?:Component|Injectable|Directive|Pipe)\(/,
50
+ reach: 12,
51
+ injectCall: /=\s*inject\(\s*([A-Za-z_$][\w$]*)\s*\)/g,
52
+ });
53
+ return kit.result();
54
+ },
55
+ };
@@ -0,0 +1,182 @@
1
+ /**
2
+ * ASP.NET Core, read by its conventions:
3
+ *
4
+ * routing `[HttpGet("x")]` / `[HttpPost]` / `[Route("x")]` on an action,
5
+ * under a class `[Route("api/[controller]")]` → route --routes-to--> the action;
6
+ * `app.MapGet("/x", Handler)` → the handler when it is a method group
7
+ * EF Core `DbSet<User> Users` in a `DbContext` → User --persists--> table:Users
8
+ * (`[Table("x")]` on the entity wins)
9
+ * views `View()` / `View("Name")` in a controller action
10
+ * → `Views/<Controller>/<Name or action>.cshtml`, else `Views/Shared/`
11
+ * DI a constructor parameter typed as an in-repo class or interface → injects
12
+ *
13
+ * Not followed: conventional routing tables (`MapControllerRoute`), a view named
14
+ * by a variable or `View(model)`, primary constructors, and a type name two
15
+ * in-repo declarations share — each gets no edge rather than a guess.
16
+ */
17
+ import * as path from 'node:path';
18
+
19
+ import type { CodeNode } from '../types.js';
20
+ import { dotnetRoots } from './shared/detect.js';
21
+ import { callRoutes, joinRoute, lastArgName } from './shared/extractors.js';
22
+ import { escapeRe, type Kit, lineIndex, makeKit } from './shared/kit.js';
23
+ import type { ConventionProvider } from './types.js';
24
+
25
+ const ATTR =
26
+ /[[,]\s*(?<attr>Http(?:Get|Post|Put|Patch|Delete|Head|Options)|Route)(?:Attribute)?\b(?:\s*\(\s*(?:template\s*:\s*)?"(?<path>[^"]*)"[^)]*\))?/g;
27
+
28
+ /** In-repo types by simple name, kept only when the name is declared once. */
29
+ function uniqueTypes(kit: Kit): Map<string, CodeNode> {
30
+ const seen = new Map<string, CodeNode | null>();
31
+ for (const n of kit.nodeById.values()) {
32
+ if (!n.source_file.endsWith('.cs') || (n.kind !== 'class' && n.kind !== 'interface')) continue;
33
+ seen.set(n.label, seen.has(n.label) ? null : n);
34
+ }
35
+ const out = new Map<string, CodeNode>();
36
+ for (const [k, v] of seen) if (v) out.set(k, v);
37
+ return out;
38
+ }
39
+
40
+ const controllerName = (cls: CodeNode): string => cls.label.replace(/Controller$/, '');
41
+
42
+ function attributeRoutes(kit: Kit): void {
43
+ for (const f of kit.filesWithExt('.cs')) {
44
+ const at = lineIndex(f.source);
45
+ const prefixes = new Map<string, string>();
46
+ const actions = new Map<string, { node: CodeNode; line: number; verbs: Array<{ verb: string; path?: string }>; routes: string[] }>();
47
+ for (const m of f.source.matchAll(ATTR)) {
48
+ const line = at(m.index as number);
49
+ const attr = m.groups?.['attr'] as string;
50
+ const tpl = m.groups?.['path'];
51
+ const owner = kit.enclosing(f.path, line);
52
+ const method = owner === f.path ? null : kit.nodeById.get(owner);
53
+ if (!method) {
54
+ const cls = kit.classAt(f.path, line);
55
+ if (cls && attr === 'Route' && tpl !== undefined) prefixes.set(cls.id, tpl);
56
+ continue;
57
+ }
58
+ const a = actions.get(method.id) ?? { node: method, line, verbs: [], routes: [] };
59
+ if (attr === 'Route') {
60
+ if (tpl !== undefined) a.routes.push(tpl);
61
+ } else a.verbs.push({ verb: attr.slice(4).toUpperCase(), ...(tpl !== undefined ? { path: tpl } : {}) });
62
+ actions.set(method.id, a);
63
+ }
64
+ for (const a of actions.values()) {
65
+ const cls = kit.classAt(f.path, a.node.source_location[0] as number);
66
+ const prefix = cls ? prefixes.get(cls.id) : undefined;
67
+ const pairs: Array<[string, string | undefined]> = [];
68
+ for (const v of a.verbs) {
69
+ if (v.path !== undefined) pairs.push([v.verb, v.path]);
70
+ else if (a.routes.length > 0) for (const r of a.routes) pairs.push([v.verb, r]);
71
+ else pairs.push([v.verb, undefined]);
72
+ }
73
+ if (a.verbs.length === 0) for (const r of a.routes) pairs.push(['ANY', r]);
74
+ for (const [verb, tpl] of pairs) {
75
+ const absolute = tpl !== undefined && /^~?\//.test(tpl);
76
+ const raw = absolute ? joinRoute(tpl.replace(/^~/, '')) : joinRoute(prefix, tpl);
77
+ const routePath = raw
78
+ .replace(/\[controller\]/gi, cls ? controllerName(cls) : '[controller]')
79
+ .replace(/\[action\]/gi, a.node.label);
80
+ kit.emit(kit.route(f.path, verb, routePath, a.line), a.node.id, 'routes-to', 'route-table');
81
+ }
82
+ }
83
+ }
84
+ }
85
+
86
+ function minimalApi(kit: Kit, types: Map<string, CodeNode>): void {
87
+ callRoutes(kit, {
88
+ exts: ['.cs'],
89
+ call: /\b\w+\s*\.\s*Map(?<verb>Get|Post|Put|Patch|Delete)\(\s*"(?<path>[^"]*)"\s*,(?=(?<rest>[\s\S]{0,300}))/g,
90
+ handler: (_k, file, rest) => {
91
+ const name = lastArgName(rest);
92
+ if (!name) return null;
93
+ const [head, member] = name.split('.') as [string, string | undefined];
94
+ if (member) {
95
+ const cls = types.get(head);
96
+ return cls ? kit.methodOf(cls.id, member) : null;
97
+ }
98
+ const local = kit.nodesInFile(file).filter((n) => n.label === head && (n.kind === 'method' || n.kind === 'function'));
99
+ return local.length === 1 ? (local[0] as CodeNode).id : null;
100
+ },
101
+ });
102
+ }
103
+
104
+ function entities(kit: Kit, types: Map<string, CodeNode>): void {
105
+ const sourceOf = new Map(kit.files.map((f) => [f.path, f.source] as const));
106
+ for (const f of kit.filesWithExt('.cs')) {
107
+ if (!/\bclass\s+\w+[^{;]*:\s*[^{;]*\bDbContext\b/.test(f.source)) continue;
108
+ const at = lineIndex(f.source);
109
+ for (const m of f.source.matchAll(/\bDbSet<\s*(?:[\w.]+\.)?(?<entity>\w+)\s*>\s+(?<prop>\w+)/g)) {
110
+ const entity = types.get(m.groups?.['entity'] as string);
111
+ if (!entity || entity.kind !== 'class') continue;
112
+ const lines = (sourceOf.get(entity.source_file) ?? '').split('\n');
113
+ const start = entity.source_location[0] as number;
114
+ const head = lines.slice(start - 1, start + 5).join('\n').split(/\bclass\b/)[0] ?? '';
115
+ const explicit = /\[\s*Table\s*\(\s*"(?<name>[^"]+)"/.exec(head)?.groups?.['name'];
116
+ const table = kit.table(explicit ?? (m.groups?.['prop'] as string), f.path, at(m.index as number));
117
+ kit.emit(entity.id, table, 'persists', 'schema-call');
118
+ }
119
+ }
120
+ }
121
+
122
+ function views(kit: Kit): void {
123
+ const inventory = new Set(kit.paths);
124
+ for (const f of kit.filesWithExt('.cs')) {
125
+ const at = lineIndex(f.source);
126
+ for (const m of f.source.matchAll(/\b(?:Partial)?View\(\s*(?:"(?<name>[^"]+)"\s*)?[,)]/g)) {
127
+ const line = at(m.index as number);
128
+ const owner = kit.enclosing(f.path, line);
129
+ const action = owner === f.path ? null : kit.nodeById.get(owner);
130
+ const cls = action ? kit.classAt(f.path, line) : null;
131
+ if (!action || !cls || !cls.label.endsWith('Controller')) continue;
132
+ const name = m.groups?.['name'] ?? action.label;
133
+ const candidates: string[] = [];
134
+ for (const root of kit.roots) {
135
+ if (/^~?\//.test(name)) candidates.push(path.posix.join(root, name.replace(/^~?\//, '')));
136
+ else {
137
+ const file = name.endsWith('.cshtml') ? name : `${name}.cshtml`;
138
+ candidates.push(path.posix.join(root, 'Views', controllerName(cls), file), path.posix.join(root, 'Views', 'Shared', file));
139
+ }
140
+ }
141
+ const target = candidates.find((c) => inventory.has(c));
142
+ if (!target) continue;
143
+ kit.fileNode(target);
144
+ kit.emit(action.id, target, 'renders', 'directive-scan');
145
+ }
146
+ }
147
+ }
148
+
149
+ function injection(kit: Kit, types: Map<string, CodeNode>): void {
150
+ const sourceOf = new Map(kit.files.map((f) => [f.path, f.source] as const));
151
+ for (const n of kit.nodeById.values()) {
152
+ if (n.kind !== 'method' || !n.source_file.endsWith('.cs')) continue;
153
+ const clsId = n.id.split('::').slice(0, -1).join('::');
154
+ const cls = kit.nodeById.get(clsId);
155
+ if (!cls || cls.kind !== 'class' || cls.label !== n.label) continue;
156
+ const lines = (sourceOf.get(n.source_file) ?? '').split('\n');
157
+ const start = n.source_location[0] as number;
158
+ const decl = lines.slice(start - 1, (n.source_location[2] as number) ?? start).join('\n');
159
+ const params = new RegExp(`\\b${escapeRe(n.label)}\\s*\\(([^)]*)\\)`).exec(decl)?.[1] ?? '';
160
+ for (const p of params.split(',')) {
161
+ const t = /(?:^|\s)(?:[\w.]+\.)?(?<type>[A-Za-z_]\w*)\s*(?:<[^>]*>)?\??\s+\w+\s*(?:=.*)?$/.exec(p.trim())?.groups?.['type'];
162
+ const target = t ? types.get(t) : undefined;
163
+ if (target && target.id !== cls.id) kit.emit(cls.id, target.id, 'injects', 'di-binding');
164
+ }
165
+ }
166
+ }
167
+
168
+ export const aspnetcoreProvider: ConventionProvider = {
169
+ id: 'aspnetcore',
170
+ detect: (probe) => dotnetRoots(probe, /Sdk\s*=\s*"Microsoft\.NET\.Sdk\.Web"|Include\s*=\s*"Microsoft\.AspNetCore\./),
171
+ build: (input) => {
172
+ const kit = makeKit(input);
173
+ const types = uniqueTypes(kit);
174
+ attributeRoutes(kit);
175
+ minimalApi(kit, types);
176
+ entities(kit, types);
177
+ views(kit);
178
+ injection(kit, types);
179
+ return kit.result();
180
+ },
181
+ roleOf: (p) => (p.endsWith('.cshtml') ? 'view' : path.posix.basename(p) === 'Program.cs' ? 'entry' : undefined),
182
+ };