@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
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detection by manifest, per package root. Every helper returns the sorted
|
|
3
|
+
* roots (`''` for the repository root) whose manifest declares one of the
|
|
4
|
+
* named packages — never a guess from file suffixes.
|
|
5
|
+
*/
|
|
6
|
+
import * as path from 'node:path';
|
|
7
|
+
|
|
8
|
+
import type { ProjectProbe } from '../types.js';
|
|
9
|
+
|
|
10
|
+
function rootOf(p: string): string {
|
|
11
|
+
const d = path.posix.dirname(p);
|
|
12
|
+
return d === '.' ? '' : d;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function rootsWhere(probe: ProjectProbe, basename: RegExp, declares: (text: string) => boolean): string[] {
|
|
16
|
+
const roots = new Set<string>();
|
|
17
|
+
for (const [p, text] of probe.manifests) if (basename.test(path.posix.basename(p)) && declares(text)) roots.add(rootOf(p));
|
|
18
|
+
return [...roots].sort();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function jsonDeps(text: string, keys: readonly string[]): string[] {
|
|
22
|
+
try {
|
|
23
|
+
const j = JSON.parse(text) as Record<string, Record<string, string> | undefined>;
|
|
24
|
+
return keys.flatMap((k) => Object.keys(j[k] ?? {}));
|
|
25
|
+
} catch {
|
|
26
|
+
return [];
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const matches = (names: readonly (string | RegExp)[], dep: string): boolean =>
|
|
31
|
+
names.some((n) => (typeof n === 'string' ? n === dep : n.test(dep)));
|
|
32
|
+
|
|
33
|
+
/** `package.json` dependencies, devDependencies or peerDependencies. */
|
|
34
|
+
export function npmRoots(probe: ProjectProbe, names: readonly (string | RegExp)[]): string[] {
|
|
35
|
+
return rootsWhere(probe, /^package\.json$/, (t) =>
|
|
36
|
+
jsonDeps(t, ['dependencies', 'devDependencies', 'peerDependencies']).some((d) => matches(names, d)),
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** `composer.json` require or require-dev. */
|
|
41
|
+
export function composerRoots(probe: ProjectProbe, names: readonly (string | RegExp)[]): string[] {
|
|
42
|
+
return rootsWhere(probe, /^composer\.json$/, (t) => jsonDeps(t, ['require', 'require-dev']).some((d) => matches(names, d)));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** `pyproject.toml` or a `requirements*.txt` naming the distribution. */
|
|
46
|
+
export function pythonRoots(probe: ProjectProbe, names: readonly string[]): string[] {
|
|
47
|
+
const re = new RegExp(`(^|["'\\s\\[,])(${names.map((n) => n.replace(/[-_.]/g, '[-_.]')).join('|')})(?=[\\s"'<>=~!;\\[,]|$)`, 'im');
|
|
48
|
+
return rootsWhere(probe, /^(pyproject\.toml|requirements[\w.-]*\.txt)$/, (t) => re.test(t));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** A Maven `pom.xml` or Gradle build file naming the artifact. */
|
|
52
|
+
export function jvmRoots(probe: ProjectProbe, artifact: RegExp): string[] {
|
|
53
|
+
return rootsWhere(probe, /^(pom\.xml|build\.gradle(\.kts)?)$/, (t) => artifact.test(t));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** A `go.mod` requiring the module. */
|
|
57
|
+
export function goRoots(probe: ProjectProbe, modules: readonly string[]): string[] {
|
|
58
|
+
return rootsWhere(probe, /^go\.mod$/, (t) => modules.some((m) => t.includes(m)));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** A `*.csproj` referencing the SDK or package. */
|
|
62
|
+
export function dotnetRoots(probe: ProjectProbe, marker: RegExp): string[] {
|
|
63
|
+
return rootsWhere(probe, /\.csproj$/, (t) => marker.test(t));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** A `Gemfile` declaring the gem. */
|
|
67
|
+
export function gemRoots(probe: ProjectProbe, gem: string): string[] {
|
|
68
|
+
return rootsWhere(probe, /^Gemfile$/, (t) => new RegExp(`^\\s*gem\\s+['"]${gem}['"]`, 'm').test(t));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Marker files present in the probe, by basename, as roots. */
|
|
72
|
+
export function markerRoots(probe: ProjectProbe, basename: string): string[] {
|
|
73
|
+
return [...new Set([...probe.markers].filter((p) => path.posix.basename(p) === basename).map(rootOf))].sort();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function union(...lists: string[][]): string[] {
|
|
77
|
+
return [...new Set(lists.flat())].sort();
|
|
78
|
+
}
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The five shared mechanisms most providers are a parameter table for
|
|
3
|
+
* (road-to-graph-providers-for-every-stack 4.1):
|
|
4
|
+
*
|
|
5
|
+
* annotationRoutes @Get('/x') · @app.get("/x") · #[Route('/x')] · [HttpGet("x")] route → the method below it
|
|
6
|
+
* callRoutes app.get('/x', h) · path('x/', views.h) · r.GET("/x", h) route → the handler named
|
|
7
|
+
* fsRoutes a page module whose path is its route route → the module
|
|
8
|
+
* tableMapping @Table(name = "t") · __tablename__ · $table · conventions model → table, migration → table
|
|
9
|
+
* templateRoots render('a/b.html') · render_template('x.html') · View("Name") code → the template file
|
|
10
|
+
*
|
|
11
|
+
* Every edge is emitted only when its target exists; what a rule cannot settle
|
|
12
|
+
* (a closure, a name built at runtime) gets no edge rather than a guess.
|
|
13
|
+
*/
|
|
14
|
+
import * as path from 'node:path';
|
|
15
|
+
|
|
16
|
+
import type { SourceFile } from '../../build.js';
|
|
17
|
+
import type { CodeNode } from '../../types.js';
|
|
18
|
+
import { type Kit, lineIndex } from './kit.js';
|
|
19
|
+
|
|
20
|
+
export function joinRoute(...parts: Array<string | undefined>): string {
|
|
21
|
+
const joined = parts
|
|
22
|
+
.filter((p): p is string => typeof p === 'string' && p.length > 0)
|
|
23
|
+
.map((p) => p.replace(/^\/+|\/+$/g, ''))
|
|
24
|
+
.filter(Boolean)
|
|
25
|
+
.join('/');
|
|
26
|
+
return `/${joined}`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface AnnotationRouteSpec {
|
|
30
|
+
exts: readonly string[];
|
|
31
|
+
/** A route decorator; named groups `verb` (optional) and `path` (optional). */
|
|
32
|
+
route: RegExp;
|
|
33
|
+
/** HTTP verb when the decorator carries it in its name; `ANY` otherwise. */
|
|
34
|
+
verb?: (m: RegExpMatchArray) => string;
|
|
35
|
+
/** A class-level prefix decorator just above the class; named group `path`. */
|
|
36
|
+
prefix?: RegExp;
|
|
37
|
+
/** How far above a class its prefix decorator may sit, in lines. */
|
|
38
|
+
prefixReach?: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function annotationRoutes(kit: Kit, spec: AnnotationRouteSpec): number {
|
|
42
|
+
let count = 0;
|
|
43
|
+
for (const f of kit.filesWithExt(...spec.exts)) {
|
|
44
|
+
const at = lineIndex(f.source);
|
|
45
|
+
const prefixes = spec.prefix
|
|
46
|
+
? [...f.source.matchAll(spec.prefix)].map((m) => ({ line: at(m.index as number), path: m.groups?.['path'] ?? '' }))
|
|
47
|
+
: [];
|
|
48
|
+
for (const m of f.source.matchAll(spec.route)) {
|
|
49
|
+
const index = m.index as number;
|
|
50
|
+
// A decorator inside a line comment is prose, not a route. A docblock
|
|
51
|
+
// line (` * @Route`) is not skipped: annotation-routed frameworks read it.
|
|
52
|
+
const lineStart = f.source.lastIndexOf('\n', index - 1) + 1;
|
|
53
|
+
if (/\/\/|^\s*#(?!\[)/.test(f.source.slice(lineStart, index))) continue;
|
|
54
|
+
const line = at(index);
|
|
55
|
+
const handler = kit.nextSymbol(f.path, line, ['method', 'function']);
|
|
56
|
+
const cls = handler ? kit.classAt(f.path, handler.source_location[0] as number) : null;
|
|
57
|
+
const reach = spec.prefixReach ?? 6;
|
|
58
|
+
const clsLine = cls ? (cls.source_location[0] as number) : -1;
|
|
59
|
+
const prefix = cls ? prefixes.filter((p) => p.line <= clsLine && p.line >= clsLine - reach).pop()?.path : undefined;
|
|
60
|
+
const verb = (spec.verb ? spec.verb(m) : (m.groups?.['verb'] ?? 'ANY')).toUpperCase();
|
|
61
|
+
const id = kit.route(f.path, verb, joinRoute(prefix, m.groups?.['path']), line);
|
|
62
|
+
if (handler) kit.emit(id, handler.id, 'routes-to', 'route-table');
|
|
63
|
+
count += 1;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return count;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface CallRouteSpec {
|
|
70
|
+
exts: readonly string[];
|
|
71
|
+
/**
|
|
72
|
+
* A route registration; named groups `verb` (optional), `path`, `rest` (the
|
|
73
|
+
* text after the path). Capture `rest` inside a lookahead —
|
|
74
|
+
* `(?=(?<rest>[\s\S]{0,300}))` — or it swallows the next registration.
|
|
75
|
+
*/
|
|
76
|
+
call: RegExp;
|
|
77
|
+
verb?: (m: RegExpMatchArray) => string;
|
|
78
|
+
/** Resolve the handler from `rest`; the default reads the last argument as a name or `obj.member`. */
|
|
79
|
+
handler?: (kit: Kit, file: string, rest: string) => string | null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** The last argument of a call's remaining text, if it is a plain name or `obj.member`. */
|
|
83
|
+
export function lastArgName(rest: string): string | null {
|
|
84
|
+
let depth = 0;
|
|
85
|
+
let end = rest.length;
|
|
86
|
+
for (let i = 0; i < rest.length; i++) {
|
|
87
|
+
const c = rest[i];
|
|
88
|
+
if (c === '(' || c === '[' || c === '{') depth += 1;
|
|
89
|
+
else if (c === ')' || c === ']' || c === '}') {
|
|
90
|
+
if (depth === 0) {
|
|
91
|
+
end = i;
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
depth -= 1;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const args = rest.slice(0, end).split(',').map((a) => a.trim()).filter(Boolean);
|
|
98
|
+
const last = args[args.length - 1];
|
|
99
|
+
return last && /^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)?$/.test(last) ? last : null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function defaultHandler(kit: Kit, file: string, rest: string): string | null {
|
|
103
|
+
const name = lastArgName(rest);
|
|
104
|
+
if (!name) return null;
|
|
105
|
+
const [head, member] = name.split('.') as [string, string | undefined];
|
|
106
|
+
const resolved = kit.resolveName(file, head);
|
|
107
|
+
if (!member) return resolved;
|
|
108
|
+
if (resolved) return kit.methodOf(resolved, member) ?? kit.resolveName(path.posix.join(path.posix.dirname(file), head), member);
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function callRoutes(kit: Kit, spec: CallRouteSpec): number {
|
|
113
|
+
let count = 0;
|
|
114
|
+
for (const f of kit.filesWithExt(...spec.exts)) {
|
|
115
|
+
const at = lineIndex(f.source);
|
|
116
|
+
for (const m of f.source.matchAll(spec.call)) {
|
|
117
|
+
const line = at(m.index as number);
|
|
118
|
+
const verb = (spec.verb ? spec.verb(m) : (m.groups?.['verb'] ?? 'ANY')).toUpperCase();
|
|
119
|
+
const id = kit.route(f.path, verb, joinRoute(m.groups?.['path']), line);
|
|
120
|
+
const target = (spec.handler ?? defaultHandler)(kit, f.path, m.groups?.['rest'] ?? '');
|
|
121
|
+
if (target) kit.emit(id, target, 'routes-to', 'route-table');
|
|
122
|
+
count += 1;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return count;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export interface FsRouteSpec {
|
|
129
|
+
/** The route of a path, or null when it is no page module. */
|
|
130
|
+
route: (p: string) => { verb: string; path: string } | null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function fsRoutes(kit: Kit, spec: FsRouteSpec): number {
|
|
134
|
+
let count = 0;
|
|
135
|
+
for (const p of kit.paths) {
|
|
136
|
+
const r = spec.route(p);
|
|
137
|
+
if (!r) continue;
|
|
138
|
+
kit.fileNode(p);
|
|
139
|
+
const id = kit.route(p, r.verb, r.path, 1);
|
|
140
|
+
kit.emit(id, p, 'routes-to', 'route-table');
|
|
141
|
+
count += 1;
|
|
142
|
+
}
|
|
143
|
+
return count;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export interface TableMappingSpec {
|
|
147
|
+
exts: readonly string[];
|
|
148
|
+
/** Marks the class it precedes or opens as mapped to a table. */
|
|
149
|
+
entity: RegExp;
|
|
150
|
+
/** The explicit table name, searched in the entity's declaration region; named group `name`. */
|
|
151
|
+
tableName?: RegExp;
|
|
152
|
+
/** The table a mapped class gets when nothing names one. */
|
|
153
|
+
convention?: (className: string) => string;
|
|
154
|
+
/** A migration or schema statement creating a table; named group `name`. */
|
|
155
|
+
migration?: { files: (p: string) => boolean; create: RegExp };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function snakePlural(name: string): string {
|
|
159
|
+
const snake = name.replace(/([a-z0-9])([A-Z])/g, '$1_$2').toLowerCase();
|
|
160
|
+
if (/[^aeiou]y$/.test(snake)) return `${snake.slice(0, -1)}ies`;
|
|
161
|
+
if (/(s|x|z|ch|sh)$/.test(snake)) return `${snake}es`;
|
|
162
|
+
return `${snake}s`;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function tableMapping(kit: Kit, spec: TableMappingSpec): number {
|
|
166
|
+
let count = 0;
|
|
167
|
+
if (spec.migration) {
|
|
168
|
+
for (const f of kit.files.filter((x) => spec.migration?.files(x.path))) {
|
|
169
|
+
const at = lineIndex(f.source);
|
|
170
|
+
for (const m of f.source.matchAll(spec.migration.create)) {
|
|
171
|
+
const name = m.groups?.['name'];
|
|
172
|
+
if (!name) continue;
|
|
173
|
+
kit.emit(f.path, kit.table(name, f.path, at(m.index as number)), 'migrates', 'schema-call');
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
for (const f of kit.filesWithExt(...spec.exts)) {
|
|
178
|
+
const at = lineIndex(f.source);
|
|
179
|
+
const lines = f.source.split('\n');
|
|
180
|
+
for (const m of f.source.matchAll(spec.entity)) {
|
|
181
|
+
const line = at(m.index as number);
|
|
182
|
+
const cls = kit.classAt(f.path, line) ?? kit.nextSymbol(f.path, line, ['class']);
|
|
183
|
+
if (!cls || cls.kind !== 'class') continue;
|
|
184
|
+
const start = cls.source_location[0] as number;
|
|
185
|
+
const end = (cls.source_location[2] ?? start) as number;
|
|
186
|
+
// Up to four lines of decorators above the class — but never past the
|
|
187
|
+
// end of the class before it, whose own table name would be read.
|
|
188
|
+
let floor = 0;
|
|
189
|
+
for (const n of kit.nodesInFile(f.path)) {
|
|
190
|
+
if (n.kind !== 'class' || n.id === cls.id) continue;
|
|
191
|
+
const nEnd = (n.source_location[2] ?? n.source_location[0]) as number;
|
|
192
|
+
if (nEnd < start && nEnd > floor) floor = nEnd;
|
|
193
|
+
}
|
|
194
|
+
const region = lines.slice(Math.max(floor, Math.min(line, start) - 1 - 4), end).join('\n');
|
|
195
|
+
const explicit = spec.tableName ? spec.tableName.exec(region)?.groups?.['name'] : undefined;
|
|
196
|
+
const name = explicit ?? spec.convention?.(cls.label);
|
|
197
|
+
if (!name) continue;
|
|
198
|
+
kit.emit(cls.id, kit.table(name, f.path, start), 'persists', 'schema-call');
|
|
199
|
+
count += 1;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return count;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export interface TemplateRootSpec {
|
|
206
|
+
exts: readonly string[];
|
|
207
|
+
/** A render call; named group `name` is the template as written. */
|
|
208
|
+
call: RegExp;
|
|
209
|
+
/** Directories templates live under, repo- or package-root-relative. */
|
|
210
|
+
roots: readonly string[];
|
|
211
|
+
/** Extensions tried when the name carries none. */
|
|
212
|
+
extensions: readonly string[];
|
|
213
|
+
/** Rewrite a name before lookup (`a.b` → `a/b`). */
|
|
214
|
+
normalize?: (name: string) => string;
|
|
215
|
+
/** Further template directories for one file, repo-relative (a folder its own source declares). */
|
|
216
|
+
rootsFor?: (file: SourceFile) => readonly string[];
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export function templateRoots(kit: Kit, spec: TemplateRootSpec): number {
|
|
220
|
+
const inventory = new Set(kit.paths);
|
|
221
|
+
let count = 0;
|
|
222
|
+
for (const f of kit.filesWithExt(...spec.exts)) {
|
|
223
|
+
const at = lineIndex(f.source);
|
|
224
|
+
for (const m of f.source.matchAll(spec.call)) {
|
|
225
|
+
const raw = m.groups?.['name'];
|
|
226
|
+
if (!raw) continue;
|
|
227
|
+
const name = spec.normalize ? spec.normalize(raw) : raw;
|
|
228
|
+
const hasExt = /\.[A-Za-z0-9]+$/.test(path.posix.basename(name));
|
|
229
|
+
const hits = new Set<string>();
|
|
230
|
+
const dirs = [...kit.roots.flatMap((r) => spec.roots.map((t) => path.posix.join(r, t))), ...(spec.rootsFor?.(f) ?? [])];
|
|
231
|
+
for (const root of new Set(dirs)) {
|
|
232
|
+
for (const c of hasExt ? [path.posix.join(root, name)] : spec.extensions.map((x) => path.posix.join(root, `${name}${x}`))) {
|
|
233
|
+
if (inventory.has(c)) hits.add(c);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
if (hits.size !== 1) continue;
|
|
237
|
+
const target = [...hits][0] as string;
|
|
238
|
+
kit.fileNode(target);
|
|
239
|
+
kit.emit(kit.enclosing(f.path, at(m.index as number)), target, 'renders', 'directive-scan');
|
|
240
|
+
count += 1;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return count;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** The class a node belongs to, for a method id `<file>#<Class>::<m>`. */
|
|
247
|
+
export function classOf(n: CodeNode): string {
|
|
248
|
+
return n.id.split('::')[0] as string;
|
|
249
|
+
}
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mechanics the JavaScript providers share and the five extractors do not
|
|
3
|
+
* carry: resolving a module specifier and a default import, reading a route
|
|
4
|
+
* object literal, constructor injection, and the verb functions a route module
|
|
5
|
+
* exports.
|
|
6
|
+
*
|
|
7
|
+
* Same contract as the extractors: an edge only when its target exists; a
|
|
8
|
+
* specifier that is not relative, or a value built at runtime, gets no edge.
|
|
9
|
+
*/
|
|
10
|
+
import * as path from 'node:path';
|
|
11
|
+
|
|
12
|
+
import type { CodeNode } from '../../types.js';
|
|
13
|
+
import { joinRoute } from './extractors.js';
|
|
14
|
+
import { type Kit, lineIndex } from './kit.js';
|
|
15
|
+
|
|
16
|
+
const MODULE_EXTS = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.vue', '.svelte'];
|
|
17
|
+
|
|
18
|
+
/** A kit that sees only the code files `keep` accepts (one ORM among several sharing a decorator name). */
|
|
19
|
+
export function scopeKit(kit: Kit, keep: (source: string) => boolean): Kit {
|
|
20
|
+
const files = kit.files.filter((f) => keep(f.source));
|
|
21
|
+
return { ...kit, files, filesWithExt: (...exts) => files.filter((f) => exts.some((x) => f.path.endsWith(x))) };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** The project file a relative specifier written in `file` names; null for a package or alias specifier. */
|
|
25
|
+
export function resolveModule(kit: Kit, file: string, spec: string): string | null {
|
|
26
|
+
if (!spec.startsWith('./') && !spec.startsWith('../')) return null;
|
|
27
|
+
const base = path.posix.join(path.posix.dirname(file), spec);
|
|
28
|
+
if (base.startsWith('..')) return null;
|
|
29
|
+
const known = (p: string): boolean => kit.fileIds.has(p) || kit.nodeById.has(p) || kit.hasPath(p);
|
|
30
|
+
if (/\.[A-Za-z0-9]+$/.test(path.posix.basename(base)) && known(base)) return base;
|
|
31
|
+
for (const c of [...MODULE_EXTS.map((x) => `${base}${x}`), ...MODULE_EXTS.map((x) => `${base}/index${x}`)]) if (known(c)) return c;
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function symbolIn(kit: Kit, file: string, name: string): string | null {
|
|
36
|
+
for (const n of kit.nodesInFile(file)) if (n.label === name && n.kind !== 'method' && n.kind !== 'file') return n.id;
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** A module target: the named symbol inside it when it declares one, else the module file. */
|
|
41
|
+
export function moduleTarget(kit: Kit, file: string, spec: string, member?: string): string | null {
|
|
42
|
+
const mod = resolveModule(kit, file, spec);
|
|
43
|
+
if (!mod) return null;
|
|
44
|
+
const sym = member ? symbolIn(kit, mod, member) : null;
|
|
45
|
+
if (sym) return sym;
|
|
46
|
+
kit.fileNode(mod);
|
|
47
|
+
return mod;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* A component name as written in `file`: a same-file or named-import symbol
|
|
52
|
+
* (`kit.resolveName`), else a default import — the symbol of that name in the
|
|
53
|
+
* imported module, or the module file itself.
|
|
54
|
+
*/
|
|
55
|
+
export function resolveComponent(kit: Kit, file: string, name: string): string | null {
|
|
56
|
+
const named = kit.resolveName(file, name);
|
|
57
|
+
if (named) return named;
|
|
58
|
+
const src = kit.files.find((f) => f.path === file)?.source ?? '';
|
|
59
|
+
const esc = name.replace(/\$/g, '\\$');
|
|
60
|
+
const m = new RegExp(`\\bimport\\s+${esc}\\s*(?:,\\s*\\{[^}]*\\})?\\s+from\\s+(['"])([^'"]+)\\1`).exec(src);
|
|
61
|
+
return m ? moduleTarget(kit, file, m[2] as string, name) : null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The object literal around `index`: its bounds, and its own top-level text with
|
|
66
|
+
* every nested `{}` and `[]` blanked (parentheses kept, so `() => import('./x')`
|
|
67
|
+
* stays readable). Null when `index` sits inside parentheses or a bracket first.
|
|
68
|
+
*/
|
|
69
|
+
export function objectAt(src: string, index: number): { start: number; end: number; flat: string } | null {
|
|
70
|
+
let depth = 0;
|
|
71
|
+
let start = -1;
|
|
72
|
+
for (let i = index - 1; i >= 0; i--) {
|
|
73
|
+
const c = src[i];
|
|
74
|
+
if (c === ')' || c === ']' || c === '}') depth += 1;
|
|
75
|
+
else if (c === '(' || c === '[' || c === '{') {
|
|
76
|
+
if (depth === 0) {
|
|
77
|
+
if (c !== '{') return null;
|
|
78
|
+
start = i;
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
depth -= 1;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (start < 0) return null;
|
|
85
|
+
const out: string[] = [];
|
|
86
|
+
const stack: string[] = [];
|
|
87
|
+
for (let i = start + 1; i < src.length; i++) {
|
|
88
|
+
const c = src[i] as string;
|
|
89
|
+
const nested = (): boolean => stack.some((s) => s !== '(');
|
|
90
|
+
if (c === '{' || c === '[' || c === '(') {
|
|
91
|
+
const blank = nested() || c !== '(';
|
|
92
|
+
stack.push(c);
|
|
93
|
+
out.push(blank ? ' ' : c);
|
|
94
|
+
} else if (c === '}' || c === ']' || c === ')') {
|
|
95
|
+
if (stack.length === 0) return { start, end: i, flat: out.join('') };
|
|
96
|
+
stack.pop();
|
|
97
|
+
out.push(nested() || c !== ')' ? ' ' : c);
|
|
98
|
+
} else out.push(nested() ? ' ' : c);
|
|
99
|
+
}
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** The text of `key: <value>` in a flat object, up to the next top-level comma. */
|
|
104
|
+
export function valueOf(flat: string, key: string): string | null {
|
|
105
|
+
const m = new RegExp(`(?:^|[\\s,])${key}\\s*:`).exec(flat);
|
|
106
|
+
if (!m) return null;
|
|
107
|
+
let depth = 0;
|
|
108
|
+
const from = m.index + m[0].length;
|
|
109
|
+
for (let i = from; i < flat.length; i++) {
|
|
110
|
+
const c = flat[i];
|
|
111
|
+
if (c === '(') depth += 1;
|
|
112
|
+
else if (c === ')') depth -= 1;
|
|
113
|
+
else if (c === ',' && depth === 0) return flat.slice(from, i).trim();
|
|
114
|
+
}
|
|
115
|
+
return flat.slice(from).trim();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** What a component value names: a dynamic import (with an optional `.then(m => m.X)`), a JSX element, or a name. */
|
|
119
|
+
export function componentTarget(kit: Kit, file: string, value: string): string | null {
|
|
120
|
+
const imp = /\bimport\(\s*(['"`])([^'"`]+)\1\s*\)(?:\s*\.then\(\s*\(?\s*(\w+)\s*\)?\s*=>\s*\3\.(\w+)\s*\))?/.exec(value);
|
|
121
|
+
if (imp) return moduleTarget(kit, file, imp[2] as string, imp[4]);
|
|
122
|
+
const jsx = /^<\s*([A-Z][\w$]*)/.exec(value);
|
|
123
|
+
if (jsx) return resolveComponent(kit, file, jsx[1] as string);
|
|
124
|
+
if (/^[A-Za-z_$][\w$]*$/.test(value)) return resolveComponent(kit, file, value);
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export interface RouteObjectSpec {
|
|
129
|
+
exts: readonly string[];
|
|
130
|
+
/** Keys whose value is the component a route renders; a `path` object with none of them is no route. */
|
|
131
|
+
componentKeys: readonly string[];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** `{ path: '/x', component: X }` route objects → a PAGE route --routes-to--> the component or lazily imported module. */
|
|
135
|
+
export function routeObjects(kit: Kit, spec: RouteObjectSpec): number {
|
|
136
|
+
let count = 0;
|
|
137
|
+
for (const f of kit.filesWithExt(...spec.exts)) {
|
|
138
|
+
const at = lineIndex(f.source);
|
|
139
|
+
for (const m of f.source.matchAll(/(?:^|[\s,{])path\s*:\s*(['"`])(?<path>[^'"`$]*)\1/g)) {
|
|
140
|
+
const obj = objectAt(f.source, m.index as number);
|
|
141
|
+
if (!obj) continue;
|
|
142
|
+
const values = spec.componentKeys.map((k) => valueOf(obj.flat, k)).filter((v): v is string => v !== null);
|
|
143
|
+
if (values.length === 0) continue;
|
|
144
|
+
const id = kit.route(f.path, 'PAGE', joinRoute(m.groups?.['path']), at(m.index as number));
|
|
145
|
+
for (const v of values) {
|
|
146
|
+
const target = componentTarget(kit, f.path, v);
|
|
147
|
+
if (target) kit.emit(id, target, 'routes-to', 'route-table');
|
|
148
|
+
}
|
|
149
|
+
count += 1;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return count;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function symbolsOf(kit: Kit, file: string, kind: CodeNode['kind']): CodeNode[] {
|
|
156
|
+
return kit.nodesInFile(file).filter((n) => n.kind === kind);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** The parameter list of the first `constructor(` in `text`, balanced. */
|
|
160
|
+
function constructorParams(text: string): string[] {
|
|
161
|
+
const m = /\bconstructor\s*\(/.exec(text);
|
|
162
|
+
if (!m) return [];
|
|
163
|
+
let depth = 0;
|
|
164
|
+
const from = m.index + m[0].length;
|
|
165
|
+
const parts: string[] = [];
|
|
166
|
+
let cur = '';
|
|
167
|
+
for (let i = from; i < text.length; i++) {
|
|
168
|
+
const c = text[i] as string;
|
|
169
|
+
if (c === '(' || c === '<' || c === '[' || c === '{') depth += 1;
|
|
170
|
+
else if (c === ')' || c === '>' || c === ']' || c === '}') {
|
|
171
|
+
if (depth === 0) break;
|
|
172
|
+
depth -= 1;
|
|
173
|
+
}
|
|
174
|
+
if (c === ',' && depth === 0) {
|
|
175
|
+
parts.push(cur);
|
|
176
|
+
cur = '';
|
|
177
|
+
} else cur += c;
|
|
178
|
+
}
|
|
179
|
+
if (cur.trim()) parts.push(cur);
|
|
180
|
+
return parts;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export interface InjectionSpec {
|
|
184
|
+
exts: readonly string[];
|
|
185
|
+
/** A class decorator marking the class as container-managed, searched just above the class. */
|
|
186
|
+
classDecorator: RegExp;
|
|
187
|
+
/** How far above the class its decorator may sit, in lines. */
|
|
188
|
+
reach?: number;
|
|
189
|
+
/** A field initializer naming the dependency (`inject(X)`); group 1 is the type. */
|
|
190
|
+
injectCall?: RegExp;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** `constructor(private readonly s: UsersService)` in a decorated class → class --injects--> UsersService. */
|
|
194
|
+
export function constructorInjection(kit: Kit, spec: InjectionSpec): number {
|
|
195
|
+
let count = 0;
|
|
196
|
+
for (const f of kit.filesWithExt(...spec.exts)) {
|
|
197
|
+
const lines = f.source.split('\n');
|
|
198
|
+
for (const cls of symbolsOf(kit, f.path, 'class')) {
|
|
199
|
+
const start = cls.source_location[0] as number;
|
|
200
|
+
const end = (cls.source_location[2] ?? start) as number;
|
|
201
|
+
const above = lines.slice(Math.max(0, start - 1 - (spec.reach ?? 6)), start).join('\n');
|
|
202
|
+
if (!spec.classDecorator.test(above)) continue;
|
|
203
|
+
const body = lines.slice(start - 1, end).join('\n');
|
|
204
|
+
const types: string[] = [];
|
|
205
|
+
for (const p of constructorParams(body)) {
|
|
206
|
+
const t = /^\s*(?:@\w+\([^)]*\)\s*)*(?:(?:public|private|protected|readonly)\s+)*[A-Za-z_$][\w$]*\??\s*:\s*([A-Za-z_$][\w$]*)/.exec(p);
|
|
207
|
+
if (t) types.push(t[1] as string);
|
|
208
|
+
}
|
|
209
|
+
if (spec.injectCall) for (const m of body.matchAll(spec.injectCall)) types.push(m[1] as string);
|
|
210
|
+
for (const t of types) {
|
|
211
|
+
const target = kit.resolveName(f.path, t);
|
|
212
|
+
if (!target || target === cls.id || kit.nodeById.get(target)?.kind !== 'class') continue;
|
|
213
|
+
kit.emit(cls.id, target, 'injects', 'di-binding');
|
|
214
|
+
count += 1;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return count;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export const HTTP_VERBS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'] as const;
|
|
222
|
+
|
|
223
|
+
/** `export async function GET()` · `export const POST = …` in a route module → one route per verb → that function. */
|
|
224
|
+
export function exportedVerbRoutes(kit: Kit, file: string, routePath: string): number {
|
|
225
|
+
const src = kit.files.find((f) => f.path === file)?.source;
|
|
226
|
+
if (src === undefined) return 0;
|
|
227
|
+
const at = lineIndex(src);
|
|
228
|
+
let count = 0;
|
|
229
|
+
const re = new RegExp(`\\bexport\\s+(?:async\\s+function|function|const|let)\\s+(${HTTP_VERBS.join('|')})\\b`, 'g');
|
|
230
|
+
for (const m of src.matchAll(re)) {
|
|
231
|
+
const verb = m[1] as string;
|
|
232
|
+
const id = kit.route(file, verb, routePath, at(m.index as number));
|
|
233
|
+
const target = kit.resolveName(file, verb);
|
|
234
|
+
if (target) kit.emit(id, target, 'routes-to', 'route-table');
|
|
235
|
+
count += 1;
|
|
236
|
+
}
|
|
237
|
+
return count;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** `p` relative to the deepest of `roots` it lies under, with that root; null when under none. */
|
|
241
|
+
export function underRoot(roots: readonly string[], p: string): { root: string; rel: string } | null {
|
|
242
|
+
for (const root of [...roots].sort((a, b) => b.length - a.length)) {
|
|
243
|
+
if (root === '') return { root, rel: p };
|
|
244
|
+
if (p.startsWith(`${root}/`)) return { root, rel: p.slice(root.length + 1) };
|
|
245
|
+
}
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** Route segments as the default pass writes them: groups, slots and `index` dropped. */
|
|
250
|
+
export function cleanRoute(parts: readonly string[]): string {
|
|
251
|
+
return `/${parts.filter((s) => !/^\(.*\)$/.test(s) && !s.startsWith('@') && s !== 'index').join('/')}`.replace(/\/$/, '') || '/';
|
|
252
|
+
}
|