@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.
- package/.augment-plugin/marketplace.json +3 -3
- package/.augment-plugin/plugin.json +1 -1
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/skills/install-agent-config/SKILL.md +9 -9
- package/CHANGELOG.md +174 -0
- package/dist/agent-src/skills/code-intelligence/SKILL.md +13 -12
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/scripts/check_memory.ts +1 -1
- package/dist/cli/commands/recordTriggerEval.js +1 -1
- package/dist/cli-delegate/{chunk-KR5ZIFPO.js → chunk-DHSSP7V3.js} +1 -1
- package/dist/cli-delegate/chunk-E7ACTJTJ.js +6792 -0
- package/dist/cli-delegate/{chunk-BZJ4Y3CI.js → chunk-MT2WOOCY.js} +1 -1
- package/dist/cli-delegate/{chunk-CMZEKSNN.js → chunk-ORJPBER2.js} +2 -2
- package/dist/cli-delegate/{chunk-VYZGJLCA.js → chunk-RKNT4LIK.js} +22 -1286
- package/dist/cli-delegate/cmd_conformance.js +20 -14
- package/dist/cli-delegate/cmd_doctor.js +3 -3
- package/dist/cli-delegate/cmd_doctor_neighbours.js +2 -2
- package/dist/cli-delegate/cmd_explain.js +1 -1
- package/dist/cli-delegate/cmd_export.js +2 -2
- package/dist/cli-delegate/cmd_prune.js +2 -2
- package/dist/cli-delegate/cmd_settings_migrate.js +2 -2
- package/dist/cli-delegate/cmd_settings_set.js +8 -4
- package/dist/cli-delegate/cmd_sync.js +2 -2
- package/dist/cli-delegate/cmd_uninstall.js +2 -2
- package/dist/cli-delegate/cmd_validate.js +2 -2
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +3 -3
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +1 -1
- package/dist/discovery/dormancy-report.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +2 -2
- package/dist/discovery/trust-report.md +1 -1
- package/dist/discovery/workspaces.json +2 -2
- package/dist/hooks/code_graph_refresh.chunk.cjs +414 -0
- package/dist/hooks/code_graph_refresh.chunk.cjs.sha256 +1 -0
- package/dist/hooks/dispatch.js +21 -11
- package/dist/hooks/dispatch.sha256 +1 -1
- package/dist/hooks/egress_engine.chunk.cjs +1 -1
- package/dist/hooks/egress_engine.chunk.cjs.sha256 +1 -1
- package/dist/install/install.mjs +6974 -6037
- package/dist/mcp/mcp-cloudflare-catalogue.json +1 -0
- package/dist/mcp/registry-manifest.json +3 -2
- package/dist/mcp/server.json +13 -4
- package/dist/mcp/server.mjs +5561 -2463
- package/dist/scripts/_lib/egress_rules.js +8 -2
- package/dist/scripts/condense.js +9 -9
- package/dist/server/routes/discovery.js +1 -1
- package/dist/server/routes/install.js +1 -1
- package/dist/server/routes/ping.js +1 -1
- package/dist/server/routes/schema.js +4 -4
- package/dist/server/routes/settings.js +2 -6
- package/dist/server/routes/wizard.js +6 -6
- package/dist/server/schemas/settings.js +42 -42
- package/dist/shared/settingsSurface.js +1 -1
- package/dist/shared/userMd/schema.js +1 -1
- package/dist/shared/zodJsonSchema.js +47 -0
- package/dist/ui/assets/index-B0bcIgi0.js +77 -0
- package/dist/ui/index.html +1 -1
- package/package.json +3 -3
- package/src/agent-src/templates/scripts/check_memory.ts +1 -1
- package/src/config/ci-local-parity.yml +11 -0
- package/src/config/continuity-surface.json +9 -2
- package/src/config/gate-violation-baselines.json +3 -3
- package/src/config/gitignore-block.txt +6 -5
- package/src/config/hook-bundle-budget.json +16 -1
- package/src/config/pack-size-budget.json +12 -1
- package/src/config/packed-binary-manifest.json +9 -0
- package/src/scripts/_cli/cmd_settings_set.ts +3 -2
- package/src/scripts/_dispatch.bash +1 -1
- package/src/scripts/_lib/egress_rules.ts +8 -2
- package/src/scripts/_lib/listing_manifests.ts +85 -0
- package/src/scripts/_lib/neighbour_census.ts +1 -0
- package/src/scripts/_lib/release_settings_surface.ts +2 -6
- package/src/scripts/adoption_status.ts +231 -27
- package/src/scripts/build_mcp_registry_manifest.ts +23 -12
- package/src/scripts/check_dependency_floors.ts +7 -0
- package/src/scripts/check_release_manifest_content.ts +17 -2
- package/src/scripts/check_release_pr_shape.ts +5 -0
- package/src/scripts/code_graph/build.ts +80 -44
- package/src/scripts/code_graph/cli.ts +260 -16
- package/src/scripts/code_graph/conventions_default.ts +228 -0
- package/src/scripts/code_graph/extract.ts +5 -2
- package/src/scripts/code_graph/extract_more.ts +105 -1
- package/src/scripts/code_graph/loader.ts +10 -2
- package/src/scripts/code_graph/map.ts +174 -41
- package/src/scripts/code_graph/map_model.ts +53 -15
- package/src/scripts/code_graph/map_verify.ts +12 -0
- package/src/scripts/code_graph/map_viewer/viewer.css +6 -0
- package/src/scripts/code_graph/map_viewer/viewer.js +114 -13
- package/src/scripts/code_graph/passes.ts +173 -0
- package/src/scripts/code_graph/providers/angular.ts +55 -0
- package/src/scripts/code_graph/providers/aspnetcore.ts +182 -0
- package/src/scripts/code_graph/providers/catalog.ts +290 -0
- package/src/scripts/code_graph/providers/django.ts +163 -0
- package/src/scripts/code_graph/providers/express.ts +25 -0
- package/src/scripts/code_graph/providers/fastapi.ts +49 -0
- package/src/scripts/code_graph/providers/flask.ts +99 -0
- package/src/scripts/code_graph/providers/go_http.ts +121 -0
- package/src/scripts/code_graph/providers/index.ts +101 -0
- package/src/scripts/code_graph/{laravel_overlay.ts → providers/laravel.ts} +113 -7
- package/src/scripts/code_graph/providers/nestjs.ts +34 -0
- package/src/scripts/code_graph/providers/nextjs.ts +91 -0
- package/src/scripts/code_graph/providers/nuxt.ts +55 -0
- package/src/scripts/code_graph/providers/react-router.ts +58 -0
- package/src/scripts/code_graph/providers/shared/detect.ts +78 -0
- package/src/scripts/code_graph/providers/shared/extractors.ts +249 -0
- package/src/scripts/code_graph/providers/shared/js.ts +252 -0
- package/src/scripts/code_graph/providers/shared/kit.ts +210 -0
- package/src/scripts/code_graph/providers/shared/php.ts +64 -0
- package/src/scripts/code_graph/providers/shared/python.ts +223 -0
- package/src/scripts/code_graph/providers/spring.ts +138 -0
- package/src/scripts/code_graph/providers/sqlalchemy.ts +62 -0
- package/src/scripts/code_graph/providers/sveltekit.ts +59 -0
- package/src/scripts/code_graph/providers/symfony.ts +142 -0
- package/src/scripts/code_graph/providers/typeorm.ts +58 -0
- package/src/scripts/code_graph/providers/types.ts +56 -0
- package/src/scripts/code_graph/providers/vue-router.ts +21 -0
- package/src/scripts/code_graph/providers/wordpress.ts +128 -0
- package/src/scripts/code_graph/refresh_cli.ts +81 -0
- package/src/scripts/code_graph/refresh_lock.ts +193 -0
- package/src/scripts/code_graph/resolution_tiers.ts +19 -7
- package/src/scripts/code_graph/scip_reader.ts +2 -2
- package/src/scripts/code_graph/sqlite_store.ts +10 -4
- package/src/scripts/code_graph/types.ts +75 -41
- package/src/scripts/code_graph/validate.ts +6 -8
- package/src/scripts/condense.ts +9 -9
- package/src/scripts/generate_settings_reference.ts +2 -6
- package/src/scripts/hook_chunks.mjs +4 -0
- package/src/scripts/hook_manifest.json +1 -1
- package/src/scripts/hook_manifest.yaml +24 -7
- package/src/scripts/hooks/code_graph_refresh_hook.ts +160 -0
- package/src/scripts/hooks/lazy_concern.ts +4 -0
- package/src/scripts/install-hooks.sh +54 -33
- package/src/scripts/install.ts +2 -6
- package/src/scripts/lint_hook_manifest.ts +2 -0
- package/src/scripts/lint_marketplace.ts +33 -0
- package/src/scripts/lint_mcp_registry_manifest.ts +36 -0
- package/src/scripts/mcp_server/consumer_tool_catalog.json +1 -0
- package/src/scripts/mcp_server/graph_tools.ts +5 -1
- package/src/scripts/release.ts +2 -5
- package/src/scripts/release_drill.ts +4 -1
- package/src/scripts/release_env.ts +17 -0
- package/src/scripts/release_mix_taxonomy.json +3 -0
- package/src/server/schemas/settings.ts +42 -42
- package/src/shared/settingsSurface.ts +1 -1
- package/src/shared/userMd/schema.ts +1 -1
- package/src/shared/zodJsonSchema.ts +41 -0
- package/src/vendor/grammars/README.md +6 -5
- package/src/vendor/grammars/tree-sitter-c_sharp.wasm +0 -0
- package/dist/cli-delegate/chunk-XIAPY44N.js +0 -4587
- 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
|
-
|
|
235
|
-
var
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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
|
-
|
|
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() {
|
|
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 = {
|
|
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)
|
|
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
|
+
};
|