@event4u/agent-config 17.2.1 → 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 +152 -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
@@ -0,0 +1,62 @@
1
+ /**
2
+ * SQLAlchemy (and Flask-SQLAlchemy, SQLModel): a declarative class with
3
+ * `__tablename__ = "x"` --persists--> `table:x`; a SQLModel class declared with
4
+ * `table=True` and no `__tablename__` gets SQLModel's default, its lowercased
5
+ * class name. `relationship("Other")` / `relationship(Other)` inside a mapped
6
+ * class --relates--> the mapped class named: a string resolves to the one mapped
7
+ * class of that name in the project, a bare name the way the file imports it.
8
+ *
9
+ * Not drawn: imperative `Table("x", metadata, ...)` mappings, Flask-SQLAlchemy's
10
+ * implicit table names (a `db.Model` without `__tablename__`), a
11
+ * `relationship(lambda: X)` or a target named only in a `Mapped[...]` annotation,
12
+ * and SQLModel's `Relationship(...)`.
13
+ */
14
+ import type { CodeNode } from '../types.js';
15
+ import { pythonRoots } from './shared/detect.js';
16
+ import { lineIndex, makeKit } from './shared/kit.js';
17
+ import { balanced, spanText, splitArgs, stringLiteral } from './shared/python.js';
18
+ import type { ConventionProvider } from './types.js';
19
+
20
+ export const sqlalchemyProvider: ConventionProvider = {
21
+ id: 'sqlalchemy',
22
+ detect: (probe) => pythonRoots(probe, ['sqlalchemy', 'flask-sqlalchemy', 'sqlmodel']),
23
+ build: (input) => {
24
+ const kit = makeKit(input);
25
+ const mapped = new Map<string, { node: CodeNode; source: string }>();
26
+ for (const f of kit.filesWithExt('.py')) {
27
+ const at = lineIndex(f.source);
28
+ for (const m of f.source.matchAll(/^[ \t]+__tablename__\s*(?::[^=\n]+)?=\s*(['"])(\w+)\1/gm)) {
29
+ const cls = kit.classAt(f.path, at(m.index as number));
30
+ if (!cls || mapped.has(cls.id)) continue;
31
+ mapped.set(cls.id, { node: cls, source: f.source });
32
+ kit.emit(cls.id, kit.table(m[2] as string, f.path, cls.source_location[0] as number), 'persists', 'schema-call');
33
+ }
34
+ for (const n of kit.nodesInFile(f.path)) {
35
+ if (n.kind !== 'class' || mapped.has(n.id)) continue;
36
+ const head = /^\s*class\s+\w+\s*\(([^)]*)\)/.exec(spanText(f.source, n))?.[1] ?? '';
37
+ if (!/\btable\s*=\s*True\b/.test(head)) continue;
38
+ mapped.set(n.id, { node: n, source: f.source });
39
+ kit.emit(n.id, kit.table(n.label.toLowerCase(), f.path, n.source_location[0] as number), 'persists', 'schema-call');
40
+ }
41
+ }
42
+ const byLabel = (label: string): string | null => {
43
+ const hits = [...mapped.values()].filter((x) => x.node.label === label);
44
+ return hits.length === 1 ? (hits[0]?.node.id ?? null) : null;
45
+ };
46
+ for (const { node, source } of mapped.values()) {
47
+ const region = spanText(source, node);
48
+ for (const r of region.matchAll(/\brelationship\s*\(/g)) {
49
+ const first = splitArgs(balanced(region, (r.index as number) + r[0].length - 1))[0] ?? '';
50
+ const lit = stringLiteral(first);
51
+ let target: string | null = null;
52
+ if (lit !== null) target = byLabel(lit.slice(lit.lastIndexOf('.') + 1));
53
+ else if (/^\w+$/.test(first)) {
54
+ const resolved = kit.resolveName(node.source_file, first);
55
+ target = resolved && mapped.has(resolved) ? resolved : null;
56
+ }
57
+ if (target && target !== node.id) kit.emit(node.id, target, 'relates', 'orm-relation');
58
+ }
59
+ }
60
+ return kit.result();
61
+ },
62
+ };
@@ -0,0 +1,59 @@
1
+ /**
2
+ * SvelteKit file routing under `src/routes/` of a package root:
3
+ *
4
+ * +page.svelte · +page.ts PAGE /a/[slug] --routes-to--> that module
5
+ * +server.ts GET /api/x · POST … --routes-to--> the exported verb function
6
+ * +layout.svelte --includes--> every +page.svelte below its directory
7
+ *
8
+ * Route groups drop out of the path; route ids match the default pass's, so a
9
+ * page drawn here replaces the generic one. `+page.server.ts` load functions,
10
+ * `+layout@group` resets and a custom `kit.files.routes` directory are not
11
+ * followed; the default pass still adds its generic `ROUTE` node for a
12
+ * `+server.ts`.
13
+ */
14
+ import * as path from 'node:path';
15
+
16
+ import { npmRoots } from './shared/detect.js';
17
+ import { cleanRoute, exportedVerbRoutes, underRoot } from './shared/js.js';
18
+ import { makeKit } from './shared/kit.js';
19
+ import type { ConventionProvider } from './types.js';
20
+
21
+ const PAGE = /^\+page\.(svelte|ts|js)$/;
22
+ const SERVER = /^\+server\.(ts|js)$/;
23
+ const LAYOUT = /^\+layout\.svelte$/;
24
+
25
+ export const sveltekitProvider: ConventionProvider = {
26
+ id: 'sveltekit',
27
+ detect: (probe) => npmRoots(probe, ['@sveltejs/kit']),
28
+ build: (input) => {
29
+ const kit = makeKit(input);
30
+ const pages: string[] = [];
31
+ const layouts: string[] = [];
32
+ for (const p of kit.paths) {
33
+ const at = underRoot(kit.roots, p);
34
+ if (!at) continue;
35
+ const segs = at.rel.split('/');
36
+ if (segs[0] !== 'src' || segs[1] !== 'routes') continue;
37
+ const base = segs[segs.length - 1] as string;
38
+ const route = cleanRoute(segs.slice(2, -1));
39
+ if (PAGE.test(base)) {
40
+ kit.fileNode(p);
41
+ kit.emit(kit.route(p, 'PAGE', route, 1), p, 'routes-to', 'route-table');
42
+ if (base.endsWith('.svelte')) pages.push(p);
43
+ } else if (SERVER.test(base)) {
44
+ if (kit.fileIds.has(p)) exportedVerbRoutes(kit, p, route);
45
+ } else if (LAYOUT.test(base)) {
46
+ layouts.push(p);
47
+ }
48
+ }
49
+ for (const layout of layouts) {
50
+ const dir = `${path.posix.dirname(layout)}/`;
51
+ for (const page of pages) {
52
+ if (!page.startsWith(dir)) continue;
53
+ kit.fileNode(layout);
54
+ kit.emit(layout, page, 'includes', 'directive-scan');
55
+ }
56
+ }
57
+ return kit.result();
58
+ },
59
+ };
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Symfony: the links a Symfony application makes by attribute or convention.
3
+ *
4
+ * #[Route('/x', methods: ['GET'])] · @Route("/x") route --routes-to--> the method below it,
5
+ * under the class-level `#[Route('/prefix')]`
6
+ * $this->render('a/b.html.twig') method --renders--> templates/a/b.html.twig
7
+ * #[ORM\Entity] (+ #[ORM\Table(name: 't')]) entity --persists--> table:t (default: snake_case short name)
8
+ * #[ORM\ManyToOne(targetEntity: X::class)] … entity --relates--> X (else the property's type)
9
+ * migrations/*.php $this->addSql('CREATE TABLE t') migration --migrates--> table:t
10
+ * __construct(private Mailer $m) class --injects--> Mailer (an in-repo class only)
11
+ *
12
+ * Class references resolve through the file's `use` aliases and the PSR-4
13
+ * map, so a same-named class in another namespace is never the target. A route
14
+ * with several `methods` is declared once, under the first. Twig's own
15
+ * `{% include %}` / `{% extends %}` are not followed: a provider reads only
16
+ * code files, and `.twig` is not one.
17
+ */
18
+ import * as path from 'node:path';
19
+
20
+ import type { FileRole } from '../map_model.js';
21
+ import { composerRoots } from './shared/detect.js';
22
+ import { annotationRoutes, tableMapping, templateRoots } from './shared/extractors.js';
23
+ import { type Kit, lineIndex, makeKit } from './shared/kit.js';
24
+ import { classNode, matchingClose, phpScope, qualify } from './shared/php.js';
25
+ import type { ConventionProvider } from './types.js';
26
+ import type { ResolutionConfig } from '../resolution_tiers.js';
27
+
28
+ /** Arguments of an attribute or annotation, one level of nested brackets deep. */
29
+ const ARGS = String.raw`(?<args>(?:[^()\[\]{}]|\[[^\]]*\]|\{[^}]*\})*)`;
30
+ /** The next declaration after the match is a method, not a class (`Post::class` is no declaration). */
31
+ const BEFORE = (decl: string): string =>
32
+ String.raw`(?=(?:(?!(?<!::)\bclass\s+[A-Za-z_]|\bfunction\b)[\s\S]){0,800}${decl})`;
33
+ const ROUTE_HEAD = String.raw`(?:#\[|@)Route\(\s*(?:path\s*[:=]\s*)?(['"])(?<path>[^'"]*)\1${ARGS}\)`;
34
+ const METHOD_ROUTE = new RegExp(ROUTE_HEAD + BEFORE(String.raw`\bfunction\b`), 'g');
35
+ const CLASS_ROUTE = new RegExp(ROUTE_HEAD + BEFORE(String.raw`(?<!::)\bclass\s+[A-Za-z_]`), 'g');
36
+
37
+ const RELATION = /(?:#\[|@)ORM\\(?:ManyToOne|OneToMany|ManyToMany|OneToOne)\b(?<args>\((?:[^()]|\([^()]*\))*\))?/g;
38
+ const TARGET_ENTITY = /targetEntity\s*[:=]\s*(?:(?<cls>[\\\w]+)::class|(['"])(?<str>[\\\w]+)\2)/;
39
+ const PROPERTY = /(?:public|protected|private|var)\s+(?:readonly\s+)?(?:(?<type>\??[\\\w]+)\s+)?\$\w+/;
40
+
41
+ const snake = (name: string): string => name.replace(/([a-z0-9])([A-Z])/g, '$1_$2').toLowerCase();
42
+
43
+ function verbOf(m: RegExpMatchArray): string {
44
+ const args = m.groups?.['args'] ?? '';
45
+ return /methods\s*[:=]\s*[[{]\s*['"](\w+)['"]/.exec(args)?.[1] ?? 'ANY';
46
+ }
47
+
48
+ function relations(kit: Kit, resolution: ResolutionConfig): void {
49
+ for (const f of kit.filesWithExt('.php')) {
50
+ const at = lineIndex(f.source);
51
+ const scope = phpScope(f.source);
52
+ for (const m of f.source.matchAll(RELATION)) {
53
+ const line = at(m.index as number);
54
+ const cls = kit.classAt(f.path, line);
55
+ if (!cls) continue;
56
+ const t = TARGET_ENTITY.exec(m.groups?.['args'] ?? '');
57
+ let fq: string | null = null;
58
+ if (t?.groups?.['cls']) fq = qualify(t.groups['cls'], scope);
59
+ else if (t?.groups?.['str']) {
60
+ const s = t.groups['str'];
61
+ fq = s.includes('\\') ? s.replace(/^\\/, '') : qualify(s, { namespace: scope.namespace, uses: new Map() });
62
+ } else {
63
+ const type = PROPERTY.exec(f.source.slice((m.index as number) + m[0].length))?.groups?.['type'];
64
+ if (type) fq = qualify(type.replace(/^\?/, ''), scope);
65
+ }
66
+ const target = fq ? classNode(kit, fq, resolution) : null;
67
+ if (target && target !== cls.id) kit.emit(cls.id, target, 'relates', 'orm-relation');
68
+ }
69
+ }
70
+ }
71
+
72
+ /** Top-level comma split of a parameter list. */
73
+ function params(list: string): string[] {
74
+ const out: string[] = [];
75
+ let depth = 0;
76
+ let cur = '';
77
+ for (const ch of list) {
78
+ if (ch === '(' || ch === '[' || ch === '{') depth += 1;
79
+ else if (ch === ')' || ch === ']' || ch === '}') depth -= 1;
80
+ if (ch === ',' && depth === 0) {
81
+ out.push(cur);
82
+ cur = '';
83
+ } else cur += ch;
84
+ }
85
+ out.push(cur);
86
+ return out;
87
+ }
88
+
89
+ function injections(kit: Kit, resolution: ResolutionConfig): void {
90
+ for (const f of kit.filesWithExt('.php')) {
91
+ const at = lineIndex(f.source);
92
+ const scope = phpScope(f.source);
93
+ for (const m of f.source.matchAll(/\bfunction\s+__construct\s*\(/g)) {
94
+ const open = (m.index as number) + m[0].length - 1;
95
+ const cls = kit.classAt(f.path, at(m.index as number));
96
+ if (!cls) continue;
97
+ for (const p of params(f.source.slice(open + 1, matchingClose(f.source, open)))) {
98
+ const type = /^\s*(?:#\[[^\]]*\]\s*)*(?:(?:public|protected|private|readonly)\s+)*(?<type>\??[\\\w|]+)\s+&?(?:\.\.\.)?\$\w+/.exec(p)?.groups?.['type'];
99
+ for (const ref of (type ?? '').replace(/^\?/, '').split('|').filter(Boolean)) {
100
+ const target = classNode(kit, qualify(ref, scope), resolution);
101
+ if (target && target !== cls.id) kit.emit(cls.id, target, 'injects', 'di-binding');
102
+ }
103
+ }
104
+ }
105
+ }
106
+ }
107
+
108
+ function symfonyRole(p: string): FileRole | undefined {
109
+ if (p.startsWith('templates/')) return 'view';
110
+ if (p === 'public/index.php' || p === 'bin/console') return 'entry';
111
+ if (p.startsWith('migrations/')) return 'migration';
112
+ return undefined;
113
+ }
114
+
115
+ export const symfonyProvider: ConventionProvider = {
116
+ id: 'symfony',
117
+ detect: (probe) => composerRoots(probe, ['symfony/framework-bundle', 'symfony/symfony']),
118
+ build: (input) => {
119
+ const kit = makeKit(input);
120
+ annotationRoutes(kit, { exts: ['.php'], route: METHOD_ROUTE, verb: verbOf, prefix: CLASS_ROUTE, prefixReach: 12 });
121
+ templateRoots(kit, {
122
+ exts: ['.php'],
123
+ call: /\$this->render(?:View|Block)?\(\s*(['"])(?<name>[\w./@-]+)\1/g,
124
+ roots: ['templates'],
125
+ extensions: ['.html.twig', '.twig'],
126
+ });
127
+ tableMapping(kit, {
128
+ exts: ['.php'],
129
+ entity: /(?:#\[|@)ORM\\Entity\b/g,
130
+ tableName: /(?:#\[|@)ORM\\Table\(\s*(?:name\s*[:=]\s*)?(['"])(?<name>\w+)\1/,
131
+ convention: snake,
132
+ migration: {
133
+ files: (p) => /(?:^|\/)migrations\/[^/]+\.php$/.test(p),
134
+ create: /addSql\(\s*(['"])\s*(?:CREATE|ALTER)\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?[`"]?(?<name>\w+)/gi,
135
+ },
136
+ });
137
+ relations(kit, input.resolution);
138
+ injections(kit, input.resolution);
139
+ return kit.result();
140
+ },
141
+ roleOf: (p) => symfonyRole(path.posix.normalize(p)),
142
+ };
@@ -0,0 +1,58 @@
1
+ /**
2
+ * TypeORM, in files that import from `typeorm` (MikroORM shares `@Entity`):
3
+ *
4
+ * @Entity('t') · @Entity({ name: 't' }) · @Entity() class --persists--> table:t
5
+ * (bare: snake_case of the class, TypeORM's default naming)
6
+ * @ManyToOne(() => X) · @OneToMany(() => X, …) class --relates--> X
7
+ * queryRunner.createTable(new Table({ name: 't' })) migration --migrates--> table:t
8
+ *
9
+ * A custom naming strategy, `@ViewEntity`, string-named relation targets and
10
+ * `EntitySchema` definitions are not followed.
11
+ */
12
+ import { npmRoots } from './shared/detect.js';
13
+ import { scopeKit } from './shared/js.js';
14
+ import { lineIndex, makeKit } from './shared/kit.js';
15
+ import type { ConventionProvider } from './types.js';
16
+
17
+ const EXTS = ['.ts', '.js'];
18
+
19
+ /** TypeORM's DefaultNamingStrategy table name: `UserProfile` → `user_profile`, `HTTPLog` → `http_log`. */
20
+ export function typeormTableName(className: string): string {
21
+ return className
22
+ .replace(/([A-Z]+)([A-Z][a-z])/g, '$1_$2')
23
+ .replace(/([a-z0-9])([A-Z])/g, '$1_$2')
24
+ .toLowerCase();
25
+ }
26
+
27
+ export const typeormProvider: ConventionProvider = {
28
+ id: 'typeorm',
29
+ detect: (probe) => npmRoots(probe, ['typeorm']),
30
+ build: (input) => {
31
+ const kit = scopeKit(makeKit(input), (src) => /\bfrom\s+['"]typeorm['"]|require\(\s*['"]typeorm['"]\s*\)/.test(src));
32
+ for (const f of kit.filesWithExt(...EXTS)) {
33
+ const at = lineIndex(f.source);
34
+ for (const m of f.source.matchAll(/@Entity\(\s*(?:(['"`])(?<name>[^'"`]+)\1|\{(?<opts>[^}]*)\})?/g)) {
35
+ const cls = kit.nextSymbol(f.path, at(m.index as number), ['class']);
36
+ if (!cls || cls.kind !== 'class') continue;
37
+ const fromOpts = /\bname\s*:\s*(['"`])([^'"`]+)\1/.exec(m.groups?.['opts'] ?? '')?.[2];
38
+ const name = m.groups?.['name'] ?? fromOpts ?? typeormTableName(cls.label);
39
+ kit.emit(cls.id, kit.table(name, f.path, cls.source_location[0] as number), 'persists', 'schema-call');
40
+ }
41
+ const relation = /@(?:ManyToOne|OneToMany|OneToOne|ManyToMany)\(\s*(?:\(\s*\w*\s*\)|\w+)\s*=>\s*([A-Za-z_$][\w$]*)/g;
42
+ for (const m of f.source.matchAll(relation)) {
43
+ const owner = kit.classAt(f.path, at(m.index as number));
44
+ const target = kit.resolveName(f.path, m[1] as string);
45
+ if (!owner || !target || target === owner.id || kit.nodeById.get(target)?.kind !== 'class') continue;
46
+ kit.emit(owner.id, target, 'relates', 'orm-relation');
47
+ }
48
+ const create = /new\s+Table\(\s*\{\s*name\s*:\s*(['"`])(?<name>[^'"`]+)\1|CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?["`]?(?<raw>\w+)/gi;
49
+ if (/\bMigrationInterface\b/.test(f.source)) {
50
+ for (const m of f.source.matchAll(create)) {
51
+ const name = m.groups?.['name'] ?? m.groups?.['raw'];
52
+ if (name) kit.emit(f.path, kit.table(name, f.path, at(m.index as number)), 'migrates', 'schema-call');
53
+ }
54
+ }
55
+ }
56
+ return kit.result();
57
+ },
58
+ };
@@ -0,0 +1,56 @@
1
+ /**
2
+ * The provider seam (ADR-298): everything a framework links by convention
3
+ * reaches the graph through a registered provider, never through a call the
4
+ * build hard-wires. The core imports providers only through `./index.ts`.
5
+ */
6
+ import type { SourceFile } from '../build.js';
7
+ import type { FileRole } from '../map_model.js';
8
+ import type { ResolutionConfig } from '../resolution_tiers.js';
9
+ import type { CodeEdge, CodeNode } from '../types.js';
10
+
11
+ /**
12
+ * What a provider may read to decide whether it applies: manifests and marker
13
+ * files, never file suffixes — `.php` does not mean any one framework.
14
+ */
15
+ export interface ProjectProbe {
16
+ /** Manifest files by repo-relative POSIX path → text. */
17
+ manifests: ReadonlyMap<string, string>;
18
+ /** Repo-relative paths of marker files that carry no text worth reading (an entry script). */
19
+ markers: ReadonlySet<string>;
20
+ /** Every project path, sorted, code or not (templates, assets); empty when unknown. */
21
+ paths: readonly string[];
22
+ }
23
+
24
+ export const EMPTY_PROBE: ProjectProbe = { manifests: new Map(), markers: new Set(), paths: [] };
25
+
26
+ export interface ProviderInput {
27
+ files: readonly SourceFile[];
28
+ baseNodes: readonly CodeNode[];
29
+ /** The graph's edges so far, read-only (import bindings, for resolving a handler name). */
30
+ baseEdges: readonly CodeEdge[];
31
+ /** Every project path under the provider's roots, code or not (templates). */
32
+ paths: readonly string[];
33
+ /** The manifests under the provider's roots, by repo-relative path → text. */
34
+ manifests: ReadonlyMap<string, string>;
35
+ resolution: ResolutionConfig;
36
+ /** Package roots the provider was detected in; `''` is the repository root. */
37
+ roots: readonly string[];
38
+ }
39
+
40
+ export interface ProviderResult {
41
+ nodes: CodeNode[];
42
+ edges: CodeEdge[];
43
+ /** Base edges the provider proves false; applied only when it drew something. */
44
+ contest?: (e: CodeEdge) => boolean;
45
+ }
46
+
47
+ export interface ConventionProvider {
48
+ id: string;
49
+ /** Basenames of marker files whose presence the probe should record (an entry script). */
50
+ markers?: readonly string[];
51
+ /** Package roots this provider applies to; empty when it does not. */
52
+ detect(probe: ProjectProbe): string[];
53
+ build(input: ProviderInput): ProviderResult;
54
+ /** A role the map gives a file by this provider's conventions. */
55
+ roleOf?(path: string): FileRole | undefined;
56
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Vue Router: route objects `{ path: '/x', component: X }` or
3
+ * `component: () => import('./views/X.vue')` → PAGE /x --routes-to--> the
4
+ * component the file names or the single-file component it imports. Child
5
+ * paths are not joined to their parent's; `components: { default: … }` named
6
+ * views and alias specifiers (`@/views/X.vue`) are not followed.
7
+ */
8
+ import { npmRoots } from './shared/detect.js';
9
+ import { routeObjects } from './shared/js.js';
10
+ import { makeKit } from './shared/kit.js';
11
+ import type { ConventionProvider } from './types.js';
12
+
13
+ export const vueRouterProvider: ConventionProvider = {
14
+ id: 'vue-router',
15
+ detect: (probe) => npmRoots(probe, ['vue-router']),
16
+ build: (input) => {
17
+ const kit = makeKit(input);
18
+ routeObjects(kit, { exts: ['.ts', '.js', '.mjs'], componentKeys: ['component'] });
19
+ return kit.result();
20
+ },
21
+ };
@@ -0,0 +1,128 @@
1
+ /**
2
+ * WordPress: hooks and theme parts, which no import or call states.
3
+ *
4
+ * add_action('init', 'cb') · add_filter(…) route `HOOK init` --routes-to--> the callback
5
+ * add_shortcode('card', 'cb') route `SHORTCODE card` --routes-to--> the callback
6
+ * get_template_part('parts/card'[, 'wide']) caller --renders--> parts/card(-wide).php
7
+ *
8
+ * A hook name is no URL, so its route path is the name as written, without the
9
+ * leading slash an HTTP route carries. A callback resolves as a function of the
10
+ * same file, else the one function of that name in the tree; `[$this, 'm']`
11
+ * resolves to the enclosing class's method, `[X::class, 'm']` to X's method
12
+ * only when X is the class the file's namespace names. A closure, a dynamic
13
+ * hook name or an ambiguous function gets no edge. `do_action` fires a hook and
14
+ * declares nothing.
15
+ *
16
+ * Detection: a `wp-config.php` marker, a composer manifest requiring a
17
+ * WordPress core package or a wpackagist plugin/theme, or a composer manifest
18
+ * whose `type` is a WordPress plugin, theme or must-use plugin. A plugin or theme
19
+ * shipping no composer manifest carries no signal and stays undetected.
20
+ */
21
+ import * as path from 'node:path';
22
+
23
+ import type { CodeNode } from '../types.js';
24
+ import { markerRoots, union } from './shared/detect.js';
25
+ import { type Kit, lineIndex, makeKit } from './shared/kit.js';
26
+ import { classNode, phpScope, qualify, shortName } from './shared/php.js';
27
+ import type { ConventionProvider, ProjectProbe } from './types.js';
28
+ import type { ResolutionConfig } from '../resolution_tiers.js';
29
+
30
+ const CORE = /^(?:johnpbloch\/wordpress(?:-core)?|roots\/wordpress(?:-no-content)?|roots\/bedrock|wpackagist-(?:plugin|theme)\/.+)$/;
31
+ const TYPES = new Set(['wordpress-plugin', 'wordpress-theme', 'wordpress-muplugin']);
32
+
33
+ function detectWordpress(probe: ProjectProbe): string[] {
34
+ const roots: string[] = [];
35
+ for (const [p, text] of probe.manifests) {
36
+ if (path.posix.basename(p) !== 'composer.json') continue;
37
+ try {
38
+ const j = JSON.parse(text) as { name?: string; type?: string; require?: Record<string, string>; 'require-dev'?: Record<string, string> };
39
+ const deps = [...Object.keys(j.require ?? {}), ...Object.keys(j['require-dev'] ?? {})];
40
+ if (TYPES.has(j.type ?? '') || j.name === 'roots/bedrock' || deps.some((d) => CORE.test(d))) {
41
+ const d = path.posix.dirname(p);
42
+ roots.push(d === '.' ? '' : d);
43
+ }
44
+ } catch {
45
+ continue;
46
+ }
47
+ }
48
+ return union(roots, markerRoots(probe, 'wp-config.php'));
49
+ }
50
+
51
+ const REGISTRATION = /\b(?<fn>add_action|add_filter|add_shortcode)\(\s*(['"])(?<name>[\w\-/.:]+)\2\s*,\s*/g;
52
+
53
+ function callback(kit: Kit, file: string, src: string, at: number, line: number, functions: Map<string, CodeNode[]>, resolution: ResolutionConfig): string | null {
54
+ const rest = src.slice(at, at + 300);
55
+ const fn = /^(['"])\\?(?:[\w\\]+\\)?(?<name>\w+)\1/.exec(rest)?.groups?.['name'];
56
+ if (fn) {
57
+ const local = (functions.get(fn) ?? []).filter((n) => n.source_file === file);
58
+ if (local.length === 1) return (local[0] as CodeNode).id;
59
+ const all = functions.get(fn) ?? [];
60
+ return all.length === 1 ? (all[0] as CodeNode).id : null;
61
+ }
62
+ const pair = /^(?:\[|array\()\s*(?<recv>\$this|self::class|static::class|__CLASS__|[\\\w]+::class|(['"])[\\\w]+\2)\s*,\s*(['"])(?<member>\w+)\3/.exec(rest);
63
+ if (!pair?.groups) return null;
64
+ const recv = pair.groups['recv'] as string;
65
+ const member = pair.groups['member'] as string;
66
+ let cls: string | null = null;
67
+ if (/^(?:\$this|self::class|static::class|__CLASS__)$/.test(recv)) cls = kit.classAt(file, line)?.id ?? null;
68
+ else {
69
+ const ref = recv.replace(/::class$/, '').replace(/^['"]|['"]$/g, '');
70
+ const fq = /^['"]/.test(recv) ? ref.replace(/^\\/, '') : qualify(ref, phpScope(src));
71
+ cls = classNode(kit, fq, resolution) ?? classByNamespace(kit, fq);
72
+ }
73
+ return cls ? kit.methodOf(cls, member) : null;
74
+ }
75
+
76
+ /** The one class node whose short name and declaring file's namespace spell `fq`. */
77
+ function classByNamespace(kit: Kit, fq: string): string | null {
78
+ const short = shortName(fq);
79
+ const hits = kit.nodesNamed(short).filter((n) => {
80
+ if (n.kind !== 'class') return false;
81
+ const f = kit.files.find((x) => x.path === n.source_file);
82
+ const ns = f ? phpScope(f.source).namespace : '';
83
+ return (ns ? `${ns}\\${short}` : short) === fq;
84
+ });
85
+ return hits.length === 1 ? (hits[0] as CodeNode).id : null;
86
+ }
87
+
88
+ /** `slug[-name].php`, preferring a part under a directory that holds the calling file (its theme). */
89
+ function templatePart(kit: Kit, caller: string, slug: string, name: string | undefined): string | null {
90
+ for (const rel of name ? [`${slug}-${name}.php`, `${slug}.php`] : [`${slug}.php`]) {
91
+ const hits = kit.paths.filter((p) => p === rel || p.endsWith(`/${rel}`));
92
+ const own = hits.filter((p) => caller.startsWith(p.slice(0, p.length - rel.length)));
93
+ if (own.length === 1) return own[0] as string;
94
+ if (own.length === 0 && hits.length === 1) return hits[0] as string;
95
+ }
96
+ return null;
97
+ }
98
+
99
+ export const wordpressProvider: ConventionProvider = {
100
+ id: 'wordpress',
101
+ markers: ['wp-config.php'],
102
+ detect: detectWordpress,
103
+ build: (input) => {
104
+ const kit = makeKit(input);
105
+ const functions = new Map<string, CodeNode[]>();
106
+ for (const n of input.baseNodes) {
107
+ if (n.kind !== 'function' || !n.source_file.endsWith('.php')) continue;
108
+ functions.set(n.label, [...(functions.get(n.label) ?? []), n]);
109
+ }
110
+ for (const f of kit.filesWithExt('.php')) {
111
+ const at = lineIndex(f.source);
112
+ for (const m of f.source.matchAll(REGISTRATION)) {
113
+ const line = at(m.index as number);
114
+ const verb = m.groups?.['fn'] === 'add_shortcode' ? 'SHORTCODE' : 'HOOK';
115
+ const id = kit.route(f.path, verb, m.groups?.['name'] as string, line);
116
+ const target = callback(kit, f.path, f.source, (m.index as number) + m[0].length, line, functions, input.resolution);
117
+ if (target) kit.emit(id, target, 'routes-to', 'route-table');
118
+ }
119
+ for (const m of f.source.matchAll(/\bget_template_part\(\s*(['"])(?<slug>[\w\-/]+)\1\s*(?:,\s*(['"])(?<name>[\w-]+)\3)?/g)) {
120
+ const target = templatePart(kit, f.path, m.groups?.['slug'] as string, m.groups?.['name']);
121
+ if (!target) continue;
122
+ kit.fileNode(target);
123
+ kit.emit(kit.enclosing(f.path, at(m.index as number)), target, 'renders', 'directive-scan');
124
+ }
125
+ }
126
+ return kit.result();
127
+ },
128
+ };
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Which `agent-config` a background refresh may start — shared by the
3
+ * `code-graph-refresh` session concern and `code-graph auto-refresh on`, so the
4
+ * opt-in can say at once when nothing usable is installed.
5
+ *
6
+ * The executable is never taken from the repository (its root, its
7
+ * `node_modules/.bin`, a relative PATH entry): the same trust rule the host hook
8
+ * shim applies before it runs the dispatcher. And it must be able to do the job:
9
+ * a global install older than the single-flight refresh ignores the flags it
10
+ * does not know and would build unlocked with an old extractor.
11
+ */
12
+ import * as fs from 'node:fs';
13
+ import * as path from 'node:path';
14
+
15
+ /** A file only a package that implements `refresh --single-flight` ships (the published tarball carries `src/scripts/`). */
16
+ const CAPABILITY_FILE = 'src/scripts/code_graph/refresh_lock.ts';
17
+
18
+ /**
19
+ * The first `agent-config` on PATH whose real location lies outside `root`.
20
+ * Relative PATH entries are skipped: they resolve against the repository.
21
+ */
22
+ export function resolveCli(root: string, envPath: string = process.env['PATH'] ?? ''): string | null {
23
+ let realRoot: string;
24
+ try {
25
+ realRoot = fs.realpathSync(root);
26
+ } catch {
27
+ return null;
28
+ }
29
+ const inside = (p: string): boolean => {
30
+ const rel = path.relative(realRoot, p);
31
+ return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));
32
+ };
33
+ for (const dir of envPath.split(path.delimiter)) {
34
+ if (!path.isAbsolute(dir)) continue;
35
+ const candidate = path.join(dir, 'agent-config');
36
+ try {
37
+ fs.accessSync(candidate, fs.constants.X_OK);
38
+ const real = fs.realpathSync(candidate);
39
+ if (fs.statSync(real).isFile() && !inside(real) && !inside(path.resolve(dir))) return candidate;
40
+ } catch {
41
+ /* not here */
42
+ }
43
+ }
44
+ return null;
45
+ }
46
+
47
+ /** The installed package a CLI belongs to: the nearest `package.json` above its real path naming this package. */
48
+ export function cliPackage(cli: string): { root: string; version: string } | null {
49
+ try {
50
+ for (let dir = path.dirname(fs.realpathSync(cli)); dir !== path.dirname(dir); dir = path.dirname(dir)) {
51
+ const file = path.join(dir, 'package.json');
52
+ if (!fs.existsSync(file)) continue;
53
+ const pkg = JSON.parse(fs.readFileSync(file, 'utf-8')) as { name?: string; version?: string };
54
+ if (pkg.name === '@event4u/agent-config') return { root: dir, version: pkg.version ?? '?' };
55
+ }
56
+ } catch {
57
+ /* unreadable — no package */
58
+ }
59
+ return null;
60
+ }
61
+
62
+ /**
63
+ * Whether the package a CLI belongs to implements the single-flight refresh.
64
+ * An installed CLI is `<package>/dist/cli/agent-config.js`; the check is the
65
+ * presence of a file only such a package ships, so nothing is executed.
66
+ */
67
+ export function cliIsCapable(cli: string): boolean {
68
+ // The same package root the version report uses, so a symlinked or hoisted
69
+ // layout cannot make one call see a package the other does not.
70
+ const pkg = cliPackage(cli);
71
+ return pkg !== null && fs.existsSync(path.join(pkg.root, CAPABILITY_FILE));
72
+ }
73
+
74
+ export type CliVerdict = { cli: string } | { reason: 'no-cli' } | { reason: 'cli-too-old'; cli: string };
75
+
76
+ /** A trusted `agent-config` that implements the single-flight refresh, or why there is none. */
77
+ export function usableCli(root: string, envPath?: string): CliVerdict {
78
+ const cli = resolveCli(root, envPath);
79
+ if (cli === null) return { reason: 'no-cli' };
80
+ return cliIsCapable(cli) ? { cli } : { reason: 'cli-too-old', cli };
81
+ }