@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,193 @@
1
+ /**
2
+ * Single-flight lock for `code-graph refresh --single-flight` and `map`.
3
+ *
4
+ * Every producer goes through {@link acquire}: the git-hook refresh block in
5
+ * `install-hooks.sh` and the `code-graph-refresh` session concern both run
6
+ * `refresh --single-flight`, so there is one locking protocol, not two.
7
+ * `mkdir` is the primitive because it is atomic on every POSIX filesystem.
8
+ *
9
+ * Ownership is recorded in a SIBLING file, never inside the directory, so the
10
+ * directory is empty and a single `rmdir`-style removal clears it.
11
+ *
12
+ * A lock is reclaimed only on positive evidence that its owner is gone (see
13
+ * {@link isStale}); an owner-less lock — a torn owner write — counts as stale
14
+ * only past {@link STALE_LOCK_MS}. A release removes the lock only while the
15
+ * owner record still carries the releaser's nonce, so a holder that was
16
+ * reclaimed cannot delete its successor's lock on the way out.
17
+ */
18
+ import { randomBytes } from 'node:crypto';
19
+ import * as fs from 'node:fs';
20
+ import * as os from 'node:os';
21
+ import * as path from 'node:path';
22
+
23
+ export const LOCK_REL = path.join('agents', 'runtime', 'state', 'code-graph-refresh.lock');
24
+ /** Written by `code-graph auto-refresh on`; the session concern acts only while it exists. */
25
+ export const AUTO_REFRESH_REL = path.join('agents', 'runtime', 'state', 'code-graph-auto-refresh.json');
26
+ export const STALE_LOCK_MS = 5 * 60 * 1000;
27
+ /**
28
+ * How long an owner on ANOTHER host (a container sharing the checkout) is
29
+ * believed: this host cannot probe its PID, so age is the only signal. An
30
+ * availability compromise, not proof — it reads the owner's own wall clock, so
31
+ * clock skew between hosts shifts it. A same-host owner whose PID is alive is
32
+ * never reclaimed on age (ratification, 2026-10-24, two of two seats): a paused,
33
+ * debugged or overloaded refresh is still a refresh.
34
+ */
35
+ export const MAX_LOCK_AGE_MS = 30 * 60 * 1000;
36
+
37
+ interface Owner {
38
+ pid: number;
39
+ started_at: number;
40
+ nonce: string;
41
+ /** Absent in records written before the host was stored; such an owner is judged by age alone. */
42
+ host?: string;
43
+ }
44
+
45
+ const ownerPath = (lock: string): string => `${lock}.owner.json`;
46
+
47
+ export interface RefreshRecord {
48
+ at: string;
49
+ /** `session-start` (the concern) or `refresh` (the CLI run it starts, or a manual one). */
50
+ by: 'session-start' | 'refresh';
51
+ outcome: string;
52
+ detail?: string | undefined;
53
+ }
54
+
55
+ /** One file per side, so the session concern writes without reading or merging anything. */
56
+ export const refreshRecordPath = (root: string, by: RefreshRecord['by']): string =>
57
+ path.join(root, path.dirname(LOCK_REL), `code-graph-refresh.${by}.json`);
58
+
59
+ /** The last outcome of one side, so a refresh that never happens is visible. Best-effort. */
60
+ export function recordRefresh(root: string, rec: Omit<RefreshRecord, 'at'>): void {
61
+ try {
62
+ fs.writeFileSync(refreshRecordPath(root, rec.by), JSON.stringify({ at: new Date().toISOString(), ...rec }));
63
+ } catch {
64
+ /* never fails a refresh */
65
+ }
66
+ }
67
+
68
+ export function readRefreshRecords(root: string): Partial<Record<RefreshRecord['by'], RefreshRecord>> {
69
+ const out: Partial<Record<RefreshRecord['by'], RefreshRecord>> = {};
70
+ for (const by of ['session-start', 'refresh'] as const) {
71
+ try {
72
+ out[by] = JSON.parse(fs.readFileSync(refreshRecordPath(root, by), 'utf-8')) as RefreshRecord;
73
+ } catch {
74
+ /* never recorded */
75
+ }
76
+ }
77
+ return out;
78
+ }
79
+
80
+ function pidAlive(pid: number): boolean {
81
+ try {
82
+ process.kill(pid, 0);
83
+ return true;
84
+ } catch (e) {
85
+ // EPERM means the process exists but belongs to someone else.
86
+ return (e as NodeJS.ErrnoException).code === 'EPERM';
87
+ }
88
+ }
89
+
90
+ const errno = (e: unknown): string | undefined => (e as NodeJS.ErrnoException).code;
91
+
92
+ /** The owner record; `'none'` when there is no file or no valid record; `'unreadable'` on any other error. */
93
+ function readOwner(lock: string): Owner | 'none' | 'unreadable' {
94
+ let raw: string;
95
+ try {
96
+ raw = fs.readFileSync(ownerPath(lock), 'utf-8');
97
+ } catch (e) {
98
+ return errno(e) === 'ENOENT' ? 'none' : 'unreadable';
99
+ }
100
+ try {
101
+ const o = JSON.parse(raw) as Partial<Owner>;
102
+ if (typeof o.pid === 'number' && typeof o.started_at === 'number' && typeof o.nonce === 'string') return o as Owner;
103
+ } catch {
104
+ /* a torn write; fall through to the age rule */
105
+ }
106
+ return 'none';
107
+ }
108
+
109
+ /**
110
+ * True only on positive evidence: the owner's process is gone, the lock has
111
+ * vanished, or an owner-less lock is provably older than the threshold. A lock
112
+ * or owner record that cannot be inspected is treated as live.
113
+ */
114
+ export function isStale(lock: string, now: number = Date.now()): boolean {
115
+ const owner = readOwner(lock);
116
+ if (owner === 'unreadable') return false;
117
+ if (owner !== 'none') {
118
+ // Only an owner that NAMES this host is judged by its PID. Another host's
119
+ // PID means nothing here, and a record without a host cannot be placed,
120
+ // so both are judged by age alone (see MAX_LOCK_AGE_MS).
121
+ if (owner.host !== os.hostname()) return now - owner.started_at > MAX_LOCK_AGE_MS;
122
+ return !pidAlive(owner.pid);
123
+ }
124
+ try {
125
+ return now - fs.statSync(lock).mtimeMs > STALE_LOCK_MS;
126
+ } catch (e) {
127
+ return errno(e) === 'ENOENT';
128
+ }
129
+ }
130
+
131
+ function take(lock: string): () => void {
132
+ const nonce = randomBytes(16).toString('hex');
133
+ fs.writeFileSync(ownerPath(lock), JSON.stringify({ pid: process.pid, started_at: Date.now(), nonce, host: os.hostname() }));
134
+ return () => {
135
+ const owner = readOwner(lock);
136
+ if (typeof owner === 'string' || owner.nonce !== nonce) return;
137
+ fs.rmSync(ownerPath(lock), { force: true });
138
+ fs.rmSync(lock, { recursive: true, force: true });
139
+ };
140
+ }
141
+
142
+ export const reclaimMutexPath = (lock: string): string => `${lock}.reclaim`;
143
+
144
+ /** True when this process holds the lock (the CLI took it before calling in). */
145
+ export function heldByThisProcess(root: string): boolean {
146
+ const owner = readOwner(path.join(root, LOCK_REL));
147
+ return typeof owner !== 'string' && owner.pid === process.pid;
148
+ }
149
+
150
+ /**
151
+ * Take the lock, or return null when another refresh holds it.
152
+ *
153
+ * A stale lock is removed only while holding a second mkdir mutex, and only
154
+ * after staleness is checked AGAIN under it. A reclaimer that judged the lock
155
+ * stale and was then overtaken finds the successor's live lock on that second
156
+ * check and declines, so it can never delete a lock created after it looked.
157
+ * A reclaim mutex left by a crash inside that short section is never removed
158
+ * automatically: no check before a removal can prove which mutex instance it
159
+ * removes, and removing a successor's would let two reclaimers in (ratification,
160
+ * 2026-10-24). Stale locks then stay held — lost freshness, never lost exclusion
161
+ * — and `code-graph auto-refresh status` names the leftover for a human to remove.
162
+ * `afterStaleCheck` exists for the interleaving test only.
163
+ */
164
+ export function acquire(root: string, afterStaleCheck?: () => void): (() => void) | null {
165
+ const lock = path.join(root, LOCK_REL);
166
+ fs.mkdirSync(path.dirname(lock), { recursive: true });
167
+ try {
168
+ fs.mkdirSync(lock);
169
+ return take(lock);
170
+ } catch (e) {
171
+ if (errno(e) !== 'EEXIST' || !isStale(lock)) return null;
172
+ }
173
+ afterStaleCheck?.();
174
+ const mutex = reclaimMutexPath(lock);
175
+ try {
176
+ fs.mkdirSync(mutex);
177
+ } catch {
178
+ return null;
179
+ }
180
+ try {
181
+ if (!isStale(lock)) return null;
182
+ fs.rmSync(ownerPath(lock), { force: true });
183
+ fs.rmSync(lock, { recursive: true, force: true });
184
+ try {
185
+ fs.mkdirSync(lock);
186
+ } catch {
187
+ return null;
188
+ }
189
+ return take(lock);
190
+ } finally {
191
+ fs.rmSync(mutex, { recursive: true, force: true });
192
+ }
193
+ }
@@ -108,9 +108,10 @@ function isRecord(v: unknown): v is Record<string, unknown> {
108
108
  return typeof v === 'object' && v !== null && !Array.isArray(v);
109
109
  }
110
110
 
111
- /** Normalise a repo-relative path fragment: no leading `./`, no trailing `/`. */
111
+ /** Normalise a repo-relative path fragment: no leading `./`, no trailing `/`, and `.` is the root (`''`). */
112
112
  function normalizeFragment(p: string): string {
113
- return p.replace(/^\.\//, '').replace(/\/+$/, '');
113
+ const s = p.replace(/^(\.\/)+/, '').replace(/\/+$/, '');
114
+ return s === '.' ? '' : s;
114
115
  }
115
116
 
116
117
  /**
@@ -119,14 +120,19 @@ function normalizeFragment(p: string): string {
119
120
  * `baseUrl` is resolved relative to the tsconfig's own directory, which for a
120
121
  * root tsconfig is the repo root — so `baseUrl: "./src"` plus `"@shared/*":
121
122
  * ["shared/*"]` yields the repo-relative prefix `src/shared/`.
123
+ *
124
+ * A `baseUrl` also resolves every other non-relative specifier against
125
+ * itself, which TypeScript does after the `paths` entries: `baseUrl: "."`
126
+ * makes `components/cart` the file `components/cart.tsx`. That fallback is the
127
+ * last rule, an empty wildcard prefix, so a `paths` entry always wins.
122
128
  */
123
129
  export function aliasRulesFrom(tsconfig: unknown): AliasRule[] {
124
130
  if (!isRecord(tsconfig)) return [];
125
131
  const co = tsconfig['compilerOptions'];
126
132
  if (!isRecord(co)) return [];
127
- const paths = co['paths'];
128
- if (!isRecord(paths)) return [];
129
- const baseUrl = typeof co['baseUrl'] === 'string' ? normalizeFragment(co['baseUrl']) : '';
133
+ const paths = isRecord(co['paths']) ? co['paths'] : {};
134
+ const hasBaseUrl = typeof co['baseUrl'] === 'string';
135
+ const baseUrl = hasBaseUrl ? normalizeFragment(co['baseUrl'] as string) : '';
130
136
  const rules: AliasRule[] = [];
131
137
  for (const [pattern, valueRaw] of Object.entries(paths)) {
132
138
  if (!Array.isArray(valueRaw)) continue;
@@ -136,7 +142,8 @@ export function aliasRulesFrom(tsconfig: unknown): AliasRule[] {
136
142
  for (const v of valueRaw) {
137
143
  if (typeof v !== 'string') continue;
138
144
  const tgt = wildcard ? (v.endsWith('*') ? v.slice(0, -1) : v) : v;
139
- const joined = baseUrl ? `${baseUrl}/${normalizeFragment(tgt)}` : normalizeFragment(tgt);
145
+ const frag = normalizeFragment(tgt);
146
+ const joined = baseUrl && frag ? `${baseUrl}/${frag}` : baseUrl || frag;
140
147
  targets.push(joined.replace(/\/+$/, ''));
141
148
  }
142
149
  if (targets.length) rules.push({ prefix, wildcard, targets });
@@ -144,6 +151,7 @@ export function aliasRulesFrom(tsconfig: unknown): AliasRule[] {
144
151
  // Longest prefix first: `@shared/deep/` must beat `@shared/` when both are
145
152
  // declared, which is TypeScript's own rule and not a preference.
146
153
  rules.sort((a, b) => b.prefix.length - a.prefix.length);
154
+ if (hasBaseUrl) rules.push({ prefix: '', wildcard: true, targets: [baseUrl] });
147
155
  return rules;
148
156
  }
149
157
 
@@ -196,9 +204,13 @@ export function resolveAlias(spec: string, config: ResolutionConfig, fileIds: Re
196
204
  for (const rule of config.aliases) {
197
205
  if (rule.wildcard) {
198
206
  if (!spec.startsWith(rule.prefix)) continue;
207
+ // The baseUrl fallback never claims a relative or absolute specifier.
208
+ if (rule.prefix === '' && /^[./]/.test(spec)) continue;
199
209
  const rest = spec.slice(rule.prefix.length);
200
210
  for (const t of rule.targets) {
201
- const hit = firstExisting(rest ? `${t}/${rest}` : t, fileIds);
211
+ const candidate = rest && t ? `${t}/${rest}` : rest || t;
212
+ if (!candidate) continue;
213
+ const hit = firstExisting(candidate, fileIds);
202
214
  if (hit) return hit;
203
215
  }
204
216
  } else {
@@ -14,7 +14,7 @@
14
14
  * still reads. A malformed buffer throws; the caller treats that as "no SCIP".
15
15
  */
16
16
  import type { CodeEdge, CodeNode } from './types.js';
17
- import { GUESS_RESOLVED_VIA } from './types.js';
17
+ import { isStatedResolution } from './types.js';
18
18
 
19
19
  export interface ScipOccurrence {
20
20
  /** 0-based start line. */
@@ -192,7 +192,7 @@ export function supersededByScip(scip: readonly CodeEdge[], nodes: readonly Code
192
192
  named.set(e.source, set);
193
193
  }
194
194
  return (e: CodeEdge): boolean => {
195
- if (!GUESS_RESOLVED_VIA.has(e.resolved_via)) return false;
195
+ if (isStatedResolution(e.resolved_via)) return false;
196
196
  const set = named.get(e.source);
197
197
  if (!set) return false;
198
198
  const target = label.get(e.target) ?? e.target.replace(/^symbol:/, '');
@@ -52,8 +52,11 @@ import type { CodeEdge, CodeGraph, CodeNode } from './types.js';
52
52
  * back cleanly, answer `tests-for` with the empty set, and the caller could not
53
53
  * tell "nothing tests this" from "this twin predates the relation". An index
54
54
  * that answers a question it cannot answer is worse than one that refuses.
55
+ *
56
+ * 4 → 5 (`road-to-graph-providers-for-every-stack` 2.1): `edges` gained
57
+ * `provider_id`, the convention provider that drew a convention edge.
55
58
  */
56
- export const GRAPH_STORE_VERSION = 4;
59
+ export const GRAPH_STORE_VERSION = 5;
57
60
 
58
61
  /**
59
62
  * Edge count at or above which the indexed read path replaces the blob path.
@@ -101,7 +104,7 @@ export function emitSqliteTwin(graph: CodeGraph, serialized: string, jsonPath: s
101
104
  db.exec(
102
105
  'CREATE TABLE meta (key TEXT PRIMARY KEY, value TEXT NOT NULL);' +
103
106
  'CREATE TABLE nodes (id TEXT PRIMARY KEY, label TEXT NOT NULL, kind TEXT, source_file TEXT, l0 INTEGER, l1 INTEGER, l2 INTEGER, l3 INTEGER);' +
104
- 'CREATE TABLE edges (source TEXT NOT NULL, target TEXT NOT NULL, relation TEXT NOT NULL, confidence TEXT NOT NULL, candidates TEXT, ambiguity_reason TEXT, resolved_via TEXT, provider TEXT);' +
107
+ 'CREATE TABLE edges (source TEXT NOT NULL, target TEXT NOT NULL, relation TEXT NOT NULL, confidence TEXT NOT NULL, candidates TEXT, ambiguity_reason TEXT, resolved_via TEXT, provider TEXT, provider_id TEXT);' +
105
108
  'CREATE INDEX edges_source ON edges(source);' +
106
109
  'CREATE INDEX edges_target ON edges(target);' +
107
110
  // Seed resolution and the changed-file seed set are the two
@@ -140,8 +143,8 @@ export function emitSqliteTwin(graph: CodeGraph, serialized: string, jsonPath: s
140
143
  );
141
144
  }
142
145
  const insEdge = db.prepare(
143
- 'INSERT INTO edges (source, target, relation, confidence, candidates, ambiguity_reason, resolved_via, provider) ' +
144
- 'VALUES (?, ?, ?, ?, ?, ?, ?, ?)',
146
+ 'INSERT INTO edges (source, target, relation, confidence, candidates, ambiguity_reason, resolved_via, provider, provider_id) ' +
147
+ 'VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)',
145
148
  );
146
149
  for (const e of graph.edges) {
147
150
  insEdge.run(
@@ -153,6 +156,7 @@ export function emitSqliteTwin(graph: CodeGraph, serialized: string, jsonPath: s
153
156
  e.ambiguity_reason ?? null,
154
157
  e.resolved_via,
155
158
  e.provider,
159
+ e.provider_id ?? null,
156
160
  );
157
161
  }
158
162
  db.exec('COMMIT');
@@ -271,6 +275,7 @@ interface EdgeRow {
271
275
  ambiguity_reason: string | null;
272
276
  resolved_via: string | null;
273
277
  provider: string | null;
278
+ provider_id: string | null;
274
279
  }
275
280
 
276
281
  function toNode(r: NodeRow): CodeNode {
@@ -303,6 +308,7 @@ function toEdge(r: EdgeRow): CodeEdge {
303
308
  if (r.ambiguity_reason === 'receiver-unknown' || r.ambiguity_reason === 'hierarchy-unresolved') {
304
309
  e.ambiguity_reason = r.ambiguity_reason;
305
310
  }
311
+ if (r.provider_id) e.provider_id = r.provider_id;
306
312
  return e;
307
313
  }
308
314
 
@@ -9,7 +9,7 @@
9
9
  */
10
10
 
11
11
  /** Launch set — the stacks this suite's consumers actually run. */
12
- export type Lang = 'php' | 'typescript' | 'javascript' | 'python' | 'go' | 'java';
12
+ export type Lang = 'php' | 'typescript' | 'javascript' | 'python' | 'go' | 'java' | 'csharp';
13
13
 
14
14
  /** Same scale as `discovery_graph.ts::EdgeConfidence` (reused, not forked). */
15
15
  export type EdgeConfidence = 'EXTRACTED' | 'INFERRED' | 'AMBIGUOUS';
@@ -29,11 +29,12 @@ export type EdgeConfidence = 'EXTRACTED' | 'INFERRED' | 'AMBIGUOUS';
29
29
  * grammar can see whether a file is a test.
30
30
  */
31
31
  /**
32
- * The last seven are the framework links the Laravel overlay reads by rule
33
- * (`laravel_overlay.ts`): a route to its handler, a method to the view it
34
- * renders, a view to the view it includes, code to the translation or config
35
- * file it reads, a migration to its table, a model to its table, a model to
36
- * the model an Eloquent relation names.
32
+ * `routes-to` … `injects` are the convention links a registered provider reads
33
+ * by rule (`providers/`, ADR-298): a route to its handler, code to the template
34
+ * it renders, a template to the template it includes, code to the translation
35
+ * or config file it reads, a migration to its table, a model to its table, a
36
+ * model to the model a relation names, a class to the service a container
37
+ * injects into it. The vocabulary is fixed and names no framework.
37
38
  */
38
39
  export type Relation =
39
40
  | 'calls'
@@ -49,6 +50,7 @@ export type Relation =
49
50
  | 'migrates'
50
51
  | 'persists'
51
52
  | 'relates'
53
+ | 'injects'
52
54
  /** A Markdown document links to, or names, the file (`docs_layer.ts`). */
53
55
  | 'documents';
54
56
 
@@ -86,12 +88,20 @@ export type ResolvedVia =
86
88
  | 'path-alias'
87
89
  | 'psr4'
88
90
  | 'route-table'
89
- /** A Blade directive, a `view()` / `__()` / `config()` call, or a Markdown link naming a file. */
91
+ /** A template directive, a render / translation / config call naming a file, or a Markdown link. */
90
92
  | 'directive-scan'
91
- /** A `Schema::create|table` call, or a model whose table one of them creates. */
93
+ /** A schema or migration call creating a table, or a model mapped to such a table. */
92
94
  | 'schema-call'
93
- /** `$this->hasMany(Other::class)` and its siblings inside a model. */
94
- | 'eloquent-relation'
95
+ /** An ORM relation declared inside a model, naming the related model. */
96
+ | 'orm-relation'
97
+ /** A dependency-injection declaration naming the injected class. */
98
+ | 'di-binding'
99
+ /** A string literal equal to exactly one file's path (the default pass). */
100
+ | 'literal-path'
101
+ /** A dotted name equal to exactly one template's path (the default pass). */
102
+ | 'literal-dotted'
103
+ /** A page module whose path is its route (the default pass). */
104
+ | 'fs-route'
95
105
  /** Stated by a compiler-grade SCIP index the project ships (`scip_reader.ts`). */
96
106
  | 'compiler-index'
97
107
  | 'test-import'
@@ -131,33 +141,40 @@ export type EdgeProvider =
131
141
  | 'scip';
132
142
 
133
143
  /**
134
- * The two mechanisms that are GUESSES rather than statements.
144
+ * Every mechanism, classified as a statement (the source names the target) or
145
+ * a guess. Trust is this ALLOWLIST (ADR-298): it replaced a denylist of guesses,
146
+ * under which a new mechanism was trusted merely because nobody had listed it —
147
+ * wrong once several providers emit edges. A string that is no member at all
148
+ * (an older or foreign graph) is a guess. The type below fails to compile when a
149
+ * `ResolvedVia` member is in neither list.
135
150
  *
136
- * `name-lookup` is the repo-wide same-name table (or a lookup that found
137
- * nothing); `dynamic` is a receiver the extractor cannot type. Everything else
138
- * names where the target came from.
139
- *
140
- * Declared HERE, beside `ResolvedVia`, rather than in the query tier that first
141
- * needed it — because the BUILD pass needs it too, and the reason is a defect an
142
- * independent review demonstrated: a derived edge stamped with a trustworthy
143
- * mechanism while its own evidence was a guess LAUNDERS the guess. A PHP
144
- * `use Two\Mailer` with no `composer.json` binds by base name, so the `imports`
145
- * edge is `name-lookup` and may point at `One\Mailer`; deriving a `tests` edge
146
- * from it and tagging that `test-import` made a wrong target trustworthy to
147
- * every consumer of the filter. One set, two readers, and the build pass can no
148
- * longer disagree with the query tier about what a guess is.
149
- *
150
- * The accepted-edge filter is a DENYLIST, so a new value is accepted by
151
- * default — which is exactly backwards for `foreign`. It must be listed here
152
- * or a consumer's graph would silently answer the gate verbs as if this
153
- * package had resolved its edges. (Kept out of the literal: esbuild carries a
154
- * comment inside an expression into the hook bundle.)
151
+ * The guesses: `name-lookup` is the repo-wide same-name table (or a lookup that
152
+ * found nothing); `dynamic` is a receiver the extractor cannot type; `foreign`
153
+ * was read from another tool's graph and only mapped. The BUILD pass needs the
154
+ * split as much as the query tier: a derived edge stamped with a trustworthy
155
+ * mechanism while its own evidence was a guess LAUNDERS the guess — a PHP
156
+ * `use Two\Mailer` with no `composer.json` binds by base name, and a `tests`
157
+ * edge derived from it made a wrong target trustworthy to every consumer.
155
158
  */
156
- export const GUESS_RESOLVED_VIA: ReadonlySet<ResolvedVia> = new Set<ResolvedVia>(['name-lookup', 'dynamic', 'foreign']);
159
+ // One space-separated string per list: this module ships in the hook bundle,
160
+ // where an array literal costs a line per entry. `Words` splits it in the type.
161
+ const STATED_WORDS =
162
+ 'same-file import-specifier path-alias psr4 route-table directive-scan schema-call orm-relation di-binding compiler-index test-import literal-path literal-dotted fs-route' as const;
163
+ const GUESS_WORDS = 'name-lookup dynamic foreign' as const;
164
+ type Words<S extends string> = S extends `${infer H} ${infer T}` ? H | Words<T> : S;
165
+ /** Mechanisms in neither list; must be `never` (a test asserts it). */
166
+ export type UnclassifiedResolvedVia = Exclude<ResolvedVia, Words<typeof STATED_WORDS> | Words<typeof GUESS_WORDS>>;
167
+ /** Words that are no mechanism at all; must be `never` too. */
168
+ export type UnknownTrustWord = Exclude<Words<typeof STATED_WORDS> | Words<typeof GUESS_WORDS>, ResolvedVia>;
169
+ /** Fails to compile while either list is incomplete or carries a stray word. */
170
+ export type TrustListsComplete = [UnclassifiedResolvedVia | UnknownTrustWord] extends [never] ? true : never;
171
+ export const TRUST_LISTS_COMPLETE: TrustListsComplete = true;
172
+ export const STATED_RESOLVED_VIA: ReadonlySet<string> = new Set(STATED_WORDS.split(' '));
173
+ export const GUESS_RESOLVED_VIA: ReadonlySet<ResolvedVia> = new Set(GUESS_WORDS.split(' ') as ResolvedVia[]);
157
174
 
158
- /** Does this edge's target rest on a stated fact rather than a guess? */
175
+ /** Does this edge's target rest on a stated fact? Anything not listed as stated is a guess. */
159
176
  export function isStatedResolution(resolved_via: ResolvedVia): boolean {
160
- return !GUESS_RESOLVED_VIA.has(resolved_via);
177
+ return STATED_RESOLVED_VIA.has(resolved_via);
161
178
  }
162
179
 
163
180
  export interface CodeNode {
@@ -188,9 +205,9 @@ export interface CodeNode {
188
205
  | 'type'
189
206
  | 'enum'
190
207
  | 'skipped'
191
- /** A declared route, `<VERB> <path>` (Laravel overlay). */
208
+ /** A declared route or dispatched entry, `<VERB> <path>` (a convention provider). */
192
209
  | 'route'
193
- /** A database table a migration creates (Laravel overlay). */
210
+ /** A database table a migration or schema creates (a convention provider). */
194
211
  | 'table';
195
212
  source_file: string;
196
213
  /** 1-based [startLine, startCol, endLine, endCol]; [] for synthetic nodes. */
@@ -240,6 +257,14 @@ export interface CodeEdge {
240
257
  * target.
241
258
  */
242
259
  ambiguity_reason?: 'receiver-unknown' | 'hierarchy-unresolved';
260
+ /** The convention provider that drew this edge (ADR-298); absent on every other edge. */
261
+ provider_id?: string;
262
+ }
263
+
264
+ /** A convention provider active in this build, with the package roots it applied to. */
265
+ export interface ActiveProviderRecord {
266
+ id: string;
267
+ roots: string[];
243
268
  }
244
269
 
245
270
  export interface CodeGraph {
@@ -267,13 +292,15 @@ export interface CodeGraph {
267
292
  suppressed_edge_counts: {
268
293
  dynamic_no_candidate: number;
269
294
  /**
270
- * A fluent call on Laravel's route builder (`->middleware()`,
271
- * `->prefix()`) that name lookup had bound to an unrelated in-repo
272
- * method of the same name. Present only when it is non-zero, so a
273
- * graph without routes keeps its bytes.
295
+ * Base edges a convention provider proved false and removed, per
296
+ * provider id — e.g. a fluent route-builder call that name lookup had
297
+ * bound to an unrelated in-repo method of the same name. Present only
298
+ * when some count is non-zero.
274
299
  */
275
- route_builder_call?: number;
300
+ contested_by_provider?: Record<string, number>;
276
301
  };
302
+ /** Convention providers active in this build; present only when one was. */
303
+ providers?: ActiveProviderRecord[];
277
304
  nodes: CodeNode[];
278
305
  edges: CodeEdge[];
279
306
  }
@@ -307,8 +334,13 @@ export interface CodeGraph {
307
334
  * Bumped 4 → 5 by `road-to-a-map-that-draws-itself` 1.3: same-file
308
335
  * same-name declarations now carry `@2`, `@3` suffixes, so a cache written
309
336
  * at v4 holds ids a v5 build would not produce.
337
+ *
338
+ * Bumped 5 → 6 by `road-to-graph-providers-for-every-stack` 2.1: framework
339
+ * names left the schema (`orm-relation`, `contested_by_provider`), provider
340
+ * edges carry `provider_id`, the graph records its active providers, and the
341
+ * vocabulary gained `injects` / `di-binding`.
310
342
  */
311
- export const SCHEMA_VERSION = 5;
343
+ export const SCHEMA_VERSION = 6;
312
344
 
313
345
  /** Deterministic per-file byte cap — files above this become a SKIPPED node. */
314
346
  export const MAX_FILE_BYTES = 1_000_000;
@@ -329,6 +361,7 @@ export const EXT_LANG: Readonly<Record<string, Lang>> = {
329
361
  '.py': 'python',
330
362
  '.go': 'go',
331
363
  '.java': 'java',
364
+ '.cs': 'csharp',
332
365
  '.vue': 'typescript',
333
366
  };
334
367
 
@@ -340,6 +373,7 @@ export const GRAMMAR_WASM: Readonly<Record<Lang, string>> = {
340
373
  python: 'tree-sitter-python.wasm',
341
374
  go: 'tree-sitter-go.wasm',
342
375
  java: 'tree-sitter-java.wasm',
376
+ csharp: 'tree-sitter-c_sharp.wasm',
343
377
  };
344
378
 
345
379
  /** The single ABI version the pinned grammar set is built against. */
@@ -2,7 +2,7 @@
2
2
  * Schema gate — validate a graph.json (native cache or consumer-shipped)
3
3
  * before any consumer trusts it. Pure, no IO.
4
4
  */
5
- import type { CodeGraph } from './types.js';
5
+ import { type CodeGraph, GUESS_RESOLVED_VIA, STATED_RESOLVED_VIA } from './types.js';
6
6
 
7
7
  /** One space-separated string per vocabulary: this module ships in the hook bundle, paid on every dispatch. */
8
8
  const words = (s: string): ReadonlySet<string> => new Set(s.split(' '));
@@ -10,15 +10,11 @@ const words = (s: string): ReadonlySet<string> => new Set(s.split(' '));
10
10
  /** Mirrors `Relation` in types.ts. `tests` is derived by the build pass (3.2),
11
11
  * not extracted, and is validated on exactly the same terms as the rest. */
12
12
  const RELATIONS = words(
13
- 'calls imports uses inherits member tests routes-to renders includes reads migrates persists relates documents',
13
+ 'calls imports uses inherits member tests routes-to renders includes reads migrates persists relates injects documents',
14
14
  );
15
15
  const CONFIDENCES = new Set(['EXTRACTED', 'INFERRED', 'AMBIGUOUS']);
16
- /** Mirrors `ResolvedVia` in types.ts — the full union, including the four
17
- * values no build emits yet (2.3 and Phase 3 add them). */
18
- // `foreign`: adapted from a consumer's own graph file; see `foreign.ts`.
19
- const RESOLVED_VIA = words(
20
- 'same-file import-specifier path-alias psr4 route-table directive-scan schema-call eloquent-relation compiler-index test-import name-lookup dynamic foreign',
21
- );
16
+ /** Every `ResolvedVia` member: the trust lists in types.ts, so the two cannot drift. */
17
+ const RESOLVED_VIA: ReadonlySet<string> = new Set([...STATED_RESOLVED_VIA, ...GUESS_RESOLVED_VIA]);
22
18
  const PROVIDERS = new Set(['native', 'foreign', 'scip']);
23
19
  const KINDS = words('file class interface trait function method constant type enum skipped route table');
24
20
 
@@ -74,6 +70,8 @@ export function validateGraph(g: unknown): ValidateResult {
74
70
  if (ee['provider'] === undefined) push(`edge[${i}].provider is required`);
75
71
  else if (!PROVIDERS.has(ee['provider'] as string))
76
72
  push(`edge[${i}].provider invalid: ${String(ee['provider'])}`);
73
+ if (ee['provider_id'] !== undefined && typeof ee['provider_id'] !== 'string')
74
+ push(`edge[${i}].provider_id must be a string`);
77
75
  // a source must be a known node; targets may be unresolved `symbol:` refs
78
76
  if (typeof ee['source'] === 'string' && !ids.has(ee['source'] as string))
79
77
  push(`edge[${i}].source is not a known node: ${String(ee['source'])}`);
@@ -2150,19 +2150,19 @@ export const PLUGIN_POINTER_SLUG = 'install-agent-config';
2150
2150
  export const PLUGIN_POINTER_SKILL_BODY = `---
2151
2151
  name: install-agent-config
2152
2152
  description: >-
2153
- Install or upgrade the event4u agent-config suite. This marketplace plugin
2154
- is a bootstrap shim — it ships hooks plus this pointer only; ALL skills,
2155
- rules, and commands are installed via \`npx -y @event4u/agent-config init\`.
2156
- Use when the user asks to install, set up, or upgrade agent-config, or
2153
+ Governance rules, skills and commands for AI coding agents in Claude Code, Cursor, GitHub Copilot and Gemini CLI —
2154
+ install or upgrade the full event4u agent-config suite. No plugin that ships this pointer is the full suite (the Claude
2155
+ Code one adds hooks, the Copilot one a read-only MCP server); ALL content installs via \`npx -y @event4u/agent-config init\`.
2156
+ Use when the user asks to install, set up, or upgrade agent-config, wants coding-agent rules or skills, or
2157
2157
  wonders why this plugin contains no other skills.
2158
2158
  ---
2159
2159
 
2160
2160
  # Install agent-config
2161
2161
 
2162
- This plugin is a **bootstrap shim** kept so the marketplace listing stays
2163
- discoverable. Its hooks run only alongside a global npm install of the package (never a project-local one) —
2164
- on a plugin-only install no hook runs, so the \`fail_closed\` guards (\`block-no-verify\`, \`block-kernel-rule-writes\`,
2165
- \`block-plumbing-writes\`) are **off**; the session start says so once. Content ships as a file projection.
2162
+ The plugin that delivered this skill is a **discovery entry**, not the suite. The Claude Code plugin adds hooks that
2163
+ run only alongside a global npm install of the package (never a project-local one) — on a plugin-only install no hook
2164
+ runs, so the \`fail_closed\` guards (\`block-no-verify\`, \`block-kernel-rule-writes\`, \`block-plumbing-writes\`) are
2165
+ **off**; the session start says so once. The Copilot plugin adds a read-only MCP server and no hooks.
2166
2166
 
2167
2167
  ## Install (canonical, one command)
2168
2168
 
@@ -2172,7 +2172,7 @@ npx -y @event4u/agent-config init
2172
2172
 
2173
2173
  The installer writes the full content projection, registers the dispatcher
2174
2174
  hooks as a managed block in \`~/.claude/settings.json\` (byte-identical to
2175
- this plugin's hooks, so nothing double-fires), and keeps everything
2175
+ the Claude Code plugin's hooks, so nothing double-fires), and keeps everything
2176
2176
  upgradeable via \`agent-config upgrade\`.
2177
2177
 
2178
2178
  ## Already installed both?
@@ -33,7 +33,7 @@ import * as fs from 'node:fs';
33
33
  import * as path from 'node:path';
34
34
  import { fileURLToPath, pathToFileURL } from 'node:url';
35
35
 
36
- import { zodToJsonSchema } from 'zod-to-json-schema';
36
+ import { toJsonSchema7 } from '../shared/zodJsonSchema.js';
37
37
 
38
38
  import { settingsSchema } from '../server/schemas/settings.js';
39
39
  import {
@@ -97,11 +97,7 @@ export function renderProse(value: string | undefined): string {
97
97
  }
98
98
 
99
99
  export function buildRows(contractMarkdown: string): ReferenceRow[] {
100
- const jsonSchema = zodToJsonSchema(settingsSchema, {
101
- name: 'AgentSettings',
102
- $refStrategy: 'none',
103
- target: 'jsonSchema7',
104
- }) as Parameters<typeof flattenSurface>[0];
100
+ const jsonSchema = toJsonSchema7(settingsSchema, 'AgentSettings') as Parameters<typeof flattenSurface>[0];
105
101
  // The version is a required parameter of the shared flattener and is
106
102
  // discarded here on purpose — see the module header.
107
103
  const surface = flattenSurface(jsonSchema, 'unversioned');
@@ -45,6 +45,10 @@ export const HOOK_CHUNKS = Object.freeze([
45
45
  entry: 'src/scripts/hooks/instructions_observer_hook.ts',
46
46
  outfile: 'dist/hooks/instructions_observer.chunk.cjs',
47
47
  }),
48
+ Object.freeze({
49
+ entry: 'src/scripts/hooks/code_graph_refresh_hook.ts',
50
+ outfile: 'dist/hooks/code_graph_refresh.chunk.cjs',
51
+ }),
48
52
  ]);
49
53
 
50
54
  /**