@event4u/agent-config 17.2.1 → 17.4.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 (225) 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 +167 -193
  6. package/CREDITS.md +2 -2
  7. package/README.md +1 -1
  8. package/dist/agent-src/skills/code-intelligence/SKILL.md +16 -12
  9. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  10. package/dist/agent-src/templates/scripts/check_memory.ts +1 -1
  11. package/dist/cli/commands/recordTriggerEval.js +1 -1
  12. package/dist/cli/registry.js +2 -2
  13. package/dist/cli-delegate/{chunk-KR5ZIFPO.js → chunk-5AYJGUA3.js} +1 -3
  14. package/dist/cli-delegate/chunk-DIFZBP4G.js +1336 -0
  15. package/dist/cli-delegate/chunk-E7ACTJTJ.js +6792 -0
  16. package/dist/cli-delegate/{chunk-VYZGJLCA.js → chunk-RKNT4LIK.js} +22 -1286
  17. package/dist/cli-delegate/{chunk-BZJ4Y3CI.js → chunk-WY5ZZPGL.js} +2 -2
  18. package/dist/cli-delegate/{chunk-CMZEKSNN.js → chunk-X6LZ7W5A.js} +73 -10
  19. package/dist/cli-delegate/cmd_conformance.js +190 -192
  20. package/dist/cli-delegate/cmd_doctor.js +5 -5
  21. package/dist/cli-delegate/cmd_doctor_neighbours.js +3 -3
  22. package/dist/cli-delegate/cmd_explain.js +1 -1
  23. package/dist/cli-delegate/cmd_export.js +2 -2
  24. package/dist/cli-delegate/cmd_handoff.js +2 -2
  25. package/dist/cli-delegate/cmd_memory_get.js +1 -1
  26. package/dist/cli-delegate/cmd_prune.js +2 -2
  27. package/dist/cli-delegate/cmd_route_audit.js +1 -1
  28. package/dist/cli-delegate/cmd_session_recycle.js +2 -2
  29. package/dist/cli-delegate/cmd_settings_migrate.js +2 -2
  30. package/dist/cli-delegate/cmd_settings_set.js +8 -4
  31. package/dist/cli-delegate/cmd_sync.js +2 -2
  32. package/dist/cli-delegate/cmd_uninstall.js +2 -2
  33. package/dist/cli-delegate/cmd_update.js +24 -13
  34. package/dist/cli-delegate/cmd_upgrade.js +2 -2
  35. package/dist/cli-delegate/cmd_validate.js +2 -2
  36. package/dist/cli-delegate/dist-25AZAHOP.js +3240 -0
  37. package/dist/cli-delegate/dist-P7MFPMF3.js +411 -0
  38. package/dist/cli-delegate/dist-XAS5ZLQB.js +6 -0
  39. package/dist/cli-delegate/{upgrade_unattended-VVL6JLSX.js → upgrade_unattended-QCWOKTUN.js} +178 -163
  40. package/dist/discovery/deprecation-report.md +1 -1
  41. package/dist/discovery/discovery-manifest.json +3 -3
  42. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  43. package/dist/discovery/discovery-manifest.summary.md +1 -1
  44. package/dist/discovery/dormancy-report.md +1 -1
  45. package/dist/discovery/orphan-report.md +1 -1
  46. package/dist/discovery/packs.json +2 -2
  47. package/dist/discovery/trust-report.md +1 -1
  48. package/dist/discovery/workspaces.json +2 -2
  49. package/dist/hooks/code_graph_refresh.chunk.cjs +443 -0
  50. package/dist/hooks/code_graph_refresh.chunk.cjs.sha256 +1 -0
  51. package/dist/hooks/dispatch.js +1001 -938
  52. package/dist/hooks/dispatch.sha256 +1 -1
  53. package/dist/hooks/egress_engine.chunk.cjs +57 -7
  54. package/dist/hooks/egress_engine.chunk.cjs.sha256 +1 -1
  55. package/dist/hooks/update_notice.chunk.cjs +914 -0
  56. package/dist/hooks/update_notice.chunk.cjs.sha256 +1 -0
  57. package/dist/install/install.mjs +6974 -6037
  58. package/dist/mcp/mcp-cloudflare-catalogue.json +1 -0
  59. package/dist/mcp/registry-manifest.json +3 -2
  60. package/dist/mcp/server.json +13 -4
  61. package/dist/mcp/server.mjs +71202 -63282
  62. package/dist/scripts/_lib/egress_rules.js +8 -2
  63. package/dist/scripts/condense.js +18 -15
  64. package/dist/server/routes/discovery.js +1 -1
  65. package/dist/server/routes/install.js +1 -1
  66. package/dist/server/routes/ping.js +1 -1
  67. package/dist/server/routes/schema.js +4 -4
  68. package/dist/server/routes/settings.js +2 -6
  69. package/dist/server/routes/wizard.js +6 -6
  70. package/dist/server/schemas/settings.js +42 -42
  71. package/dist/shared/settingsSurface.js +1 -1
  72. package/dist/shared/userMd/schema.js +1 -1
  73. package/dist/shared/zodJsonSchema.js +47 -0
  74. package/dist/ui/assets/index-B0bcIgi0.js +77 -0
  75. package/dist/ui/index.html +1 -1
  76. package/package.json +6 -3
  77. package/src/agent-src/templates/scripts/check_memory.ts +1 -1
  78. package/src/config/ci-local-parity.yml +11 -0
  79. package/src/config/continuity-surface.json +9 -2
  80. package/src/config/evaluator-budgets.json +9 -8
  81. package/src/config/gate-violation-baselines.json +3 -3
  82. package/src/config/gitignore-block.txt +6 -5
  83. package/src/config/hook-bundle-budget.json +46 -6
  84. package/src/config/pack-size-budget.json +12 -1
  85. package/src/config/packed-binary-manifest.json +18 -0
  86. package/src/config/sigstore/npm_keys.json +50 -0
  87. package/src/config/sigstore/trust-material.json +9 -0
  88. package/src/config/sigstore/trusted_root.json +126 -0
  89. package/src/config/skill-body-budget.json +7 -1
  90. package/src/scripts/_cli/cmd_settings_set.ts +3 -2
  91. package/src/scripts/_cli/cmd_update.ts +48 -20
  92. package/src/scripts/_cli/cmd_upgrade.ts +7 -1
  93. package/src/scripts/_cli/upgrade_unattended.ts +90 -76
  94. package/src/scripts/_dispatch.bash +4 -3
  95. package/src/scripts/_lib/confusables.ts +0 -19
  96. package/src/scripts/_lib/egress_normalize.ts +53 -23
  97. package/src/scripts/_lib/egress_rules.ts +8 -2
  98. package/src/scripts/_lib/egress_text.ts +5 -3
  99. package/src/scripts/_lib/egress_token.ts +7 -5
  100. package/src/scripts/_lib/gate_collector_experimental.ts +633 -0
  101. package/src/scripts/_lib/git_common_dir.ts +10 -0
  102. package/src/scripts/_lib/graph_feeder_record.ts +3 -2
  103. package/src/scripts/_lib/install_reach_checks.ts +32 -2
  104. package/src/scripts/_lib/lineage_backfill.ts +136 -0
  105. package/src/scripts/_lib/lineage_ledger.ts +211 -0
  106. package/src/scripts/_lib/lineage_map.ts +55 -0
  107. package/src/scripts/_lib/lineage_query.ts +140 -0
  108. package/src/scripts/_lib/lineage_seed.ts +116 -0
  109. package/src/scripts/_lib/lineage_watch.ts +249 -0
  110. package/src/scripts/_lib/link_crypto.ts +8 -4
  111. package/src/scripts/_lib/listing_manifests.ts +85 -0
  112. package/src/scripts/_lib/neighbour_census.ts +1 -0
  113. package/src/scripts/_lib/production_reach.ts +3 -3
  114. package/src/scripts/_lib/project_registry.ts +227 -0
  115. package/src/scripts/_lib/provenance_verify.ts +14 -242
  116. package/src/scripts/_lib/release_settings_surface.ts +2 -6
  117. package/src/scripts/_lib/sigstore_provenance.ts +177 -0
  118. package/src/scripts/_lib/source_id.ts +48 -0
  119. package/src/scripts/_lib/source_shape.ts +4 -0
  120. package/src/scripts/_lib/unattended_gates.ts +6 -1
  121. package/src/scripts/adoption_status.ts +231 -27
  122. package/src/scripts/advisories.ts +26 -10
  123. package/src/scripts/ai_council/council_egress.ts +5 -6
  124. package/src/scripts/bench_graph_feeder_latency.ts +10 -3
  125. package/src/scripts/build_mcp_registry_manifest.ts +23 -12
  126. package/src/scripts/check_dependency_floors.ts +14 -0
  127. package/src/scripts/check_egress_mutations.ts +1 -0
  128. package/src/scripts/check_release_manifest_content.ts +17 -2
  129. package/src/scripts/check_release_pr_shape.ts +5 -0
  130. package/src/scripts/code_graph/build.ts +145 -45
  131. package/src/scripts/code_graph/cli.ts +382 -35
  132. package/src/scripts/code_graph/conventions_default.ts +247 -0
  133. package/src/scripts/code_graph/detect.ts +5 -5
  134. package/src/scripts/code_graph/estate_cli.ts +135 -0
  135. package/src/scripts/code_graph/estate_index.ts +394 -0
  136. package/src/scripts/code_graph/estate_manifests.ts +223 -0
  137. package/src/scripts/code_graph/estate_map.ts +141 -0
  138. package/src/scripts/code_graph/extract.ts +8 -2
  139. package/src/scripts/code_graph/extract_more.ts +241 -2
  140. package/src/scripts/code_graph/graph_budget.ts +158 -0
  141. package/src/scripts/code_graph/graph_home.ts +80 -0
  142. package/src/scripts/code_graph/lineage_cli.ts +315 -0
  143. package/src/scripts/code_graph/loader.ts +10 -2
  144. package/src/scripts/code_graph/map.ts +195 -53
  145. package/src/scripts/code_graph/map_model.ts +113 -20
  146. package/src/scripts/code_graph/map_verify.ts +80 -5
  147. package/src/scripts/code_graph/map_viewer/viewer.css +18 -0
  148. package/src/scripts/code_graph/map_viewer/viewer.js +146 -15
  149. package/src/scripts/code_graph/passes.ts +173 -0
  150. package/src/scripts/code_graph/project_ref.ts +25 -0
  151. package/src/scripts/code_graph/projects_cli.ts +254 -0
  152. package/src/scripts/code_graph/providers/angular.ts +55 -0
  153. package/src/scripts/code_graph/providers/aspnetcore.ts +182 -0
  154. package/src/scripts/code_graph/providers/catalog.ts +299 -0
  155. package/src/scripts/code_graph/providers/django.ts +206 -0
  156. package/src/scripts/code_graph/providers/express.ts +25 -0
  157. package/src/scripts/code_graph/providers/fastapi.ts +49 -0
  158. package/src/scripts/code_graph/providers/flask.ts +99 -0
  159. package/src/scripts/code_graph/providers/go_http.ts +121 -0
  160. package/src/scripts/code_graph/providers/index.ts +103 -0
  161. package/src/scripts/code_graph/{laravel_overlay.ts → providers/laravel.ts} +113 -7
  162. package/src/scripts/code_graph/providers/nestjs.ts +34 -0
  163. package/src/scripts/code_graph/providers/nextjs.ts +91 -0
  164. package/src/scripts/code_graph/providers/nuxt.ts +55 -0
  165. package/src/scripts/code_graph/providers/rails.ts +204 -0
  166. package/src/scripts/code_graph/providers/react-router.ts +58 -0
  167. package/src/scripts/code_graph/providers/shared/detect.ts +78 -0
  168. package/src/scripts/code_graph/providers/shared/extractors.ts +249 -0
  169. package/src/scripts/code_graph/providers/shared/js.ts +252 -0
  170. package/src/scripts/code_graph/providers/shared/kit.ts +210 -0
  171. package/src/scripts/code_graph/providers/shared/php.ts +64 -0
  172. package/src/scripts/code_graph/providers/shared/python.ts +223 -0
  173. package/src/scripts/code_graph/providers/spring.ts +175 -0
  174. package/src/scripts/code_graph/providers/sqlalchemy.ts +62 -0
  175. package/src/scripts/code_graph/providers/sveltekit.ts +59 -0
  176. package/src/scripts/code_graph/providers/symfony.ts +142 -0
  177. package/src/scripts/code_graph/providers/typeorm.ts +58 -0
  178. package/src/scripts/code_graph/providers/types.ts +56 -0
  179. package/src/scripts/code_graph/providers/vue-router.ts +21 -0
  180. package/src/scripts/code_graph/providers/wordpress.ts +128 -0
  181. package/src/scripts/code_graph/query.ts +9 -9
  182. package/src/scripts/code_graph/refresh_cli.ts +81 -0
  183. package/src/scripts/code_graph/refresh_lock.ts +197 -0
  184. package/src/scripts/code_graph/repo_identity.ts +169 -0
  185. package/src/scripts/code_graph/resolution_tiers.ts +19 -7
  186. package/src/scripts/code_graph/scip_reader.ts +2 -2
  187. package/src/scripts/code_graph/sqlite_store.ts +31 -28
  188. package/src/scripts/code_graph/types.ts +79 -41
  189. package/src/scripts/code_graph/validate.ts +6 -8
  190. package/src/scripts/condense.ts +15 -15
  191. package/src/scripts/generate_settings_reference.ts +2 -6
  192. package/src/scripts/hook_chunks.mjs +11 -0
  193. package/src/scripts/hook_manifest.json +1 -1
  194. package/src/scripts/hook_manifest.yaml +43 -23
  195. package/src/scripts/hooks/code_graph_refresh_hook.ts +163 -0
  196. package/src/scripts/hooks/dispatch_hook.ts +6 -7
  197. package/src/scripts/hooks/host_semantics.ts +72 -0
  198. package/src/scripts/hooks/lazy_concern.ts +8 -0
  199. package/src/scripts/hooks/user_notice.ts +105 -0
  200. package/src/scripts/install-hooks.sh +54 -33
  201. package/src/scripts/install.ts +2 -6
  202. package/src/scripts/lint_harvest_provenance.ts +137 -11
  203. package/src/scripts/lint_hook_manifest.ts +2 -0
  204. package/src/scripts/lint_marketplace.ts +33 -0
  205. package/src/scripts/lint_mcp_registry_manifest.ts +36 -0
  206. package/src/scripts/mcp_server/consumer_tool_catalog.json +70 -15
  207. package/src/scripts/mcp_server/graph_tools.ts +165 -41
  208. package/src/scripts/network_sources.ts +24 -6
  209. package/src/scripts/release.ts +2 -5
  210. package/src/scripts/release_drill.ts +4 -1
  211. package/src/scripts/release_env.ts +17 -0
  212. package/src/scripts/release_mix_taxonomy.json +3 -0
  213. package/src/scripts/schemas/provenance-mechanism.schema.json +46 -0
  214. package/src/scripts/update_notice_hook.ts +75 -7
  215. package/src/server/schemas/settings.ts +42 -42
  216. package/src/shared/settingsSurface.ts +1 -1
  217. package/src/shared/userMd/schema.ts +1 -1
  218. package/src/shared/zodJsonSchema.ts +41 -0
  219. package/src/vendor/grammars/README.md +8 -6
  220. package/src/vendor/grammars/tree-sitter-c_sharp.wasm +0 -0
  221. package/src/vendor/grammars/tree-sitter-ruby.wasm +0 -0
  222. package/dist/cli-delegate/chunk-XIAPY44N.js +0 -4587
  223. package/dist/ui/assets/index-w70Ado09.js +0 -23
  224. /package/dist/cli-delegate/{chunk-FKYZOWVD.js → chunk-QBAFP67O.js} +0 -0
  225. /package/dist/cli-delegate/{chunk-BMGUGSY6.js → chunk-TQG6UMPC.js} +0 -0
@@ -63,6 +63,7 @@ import {
63
63
  emptyLedger,
64
64
  validateAcceptance,
65
65
  } from './_lib/advisory_verdicts.js';
66
+ import { readRegistry, recordProject } from './_lib/project_registry.js';
66
67
  import { event4u_root } from './_lib/user_global_paths.js';
67
68
  import {
68
69
  advance,
@@ -363,25 +364,40 @@ export function markOwnActionsPrivate<T extends { entries: Array<{ ecosystem: st
363
364
  return inv;
364
365
  }
365
366
 
366
- /** The user-global index of project roots — roots and fingerprints only (5.4). */
367
+ /**
368
+ * The project list is the shared user-global registry (`_lib/project_registry`,
369
+ * ADR-299): a sibling of the advisories machine dir, so an injected dir keeps
370
+ * its registry beside it. Roots and fingerprints only (5.4).
371
+ */
372
+ function registryDirFor(mdir: string): string {
373
+ return path.join(path.dirname(mdir), 'projects');
374
+ }
375
+
367
376
  function recordProjects(mdir: string, rows: Array<{ root: string; fingerprint: string }>, now: Date): void {
377
+ for (const r of rows) recordProject(r.root, { fingerprint: r.fingerprint }, { dir: registryDirFor(mdir), now });
378
+ }
379
+
380
+ /** The pre-registry `projects.json`, imported once and then moved aside. */
381
+ function migrateLegacyProjects(mdir: string): void {
368
382
  const file = path.join(mdir, 'projects.json');
369
- let doc: Record<string, { fingerprint: string; at: string }> = {};
383
+ let doc: Record<string, { fingerprint?: string; at?: string }>;
370
384
  try {
371
385
  doc = JSON.parse(fs.readFileSync(file, 'utf-8'));
372
386
  } catch {
373
- doc = {};
387
+ return;
388
+ }
389
+ for (const [root, e] of Object.entries(doc ?? {})) {
390
+ const at = typeof e?.at === 'string' && !Number.isNaN(Date.parse(e.at)) ? new Date(e.at) : new Date();
391
+ recordProject(root, { fingerprint: typeof e?.fingerprint === 'string' ? e.fingerprint : null }, { dir: registryDirFor(mdir), now: at });
374
392
  }
375
- for (const r of rows) doc[r.root] = { fingerprint: r.fingerprint, at: now.toISOString() };
376
- writeJson(file, doc);
393
+ fs.renameSync(file, `${file}.migrated`);
377
394
  }
378
395
 
379
396
  export function knownProjects(mdir: string = machineDir()): string[] {
380
- try {
381
- return Object.keys(JSON.parse(fs.readFileSync(path.join(mdir, 'projects.json'), 'utf-8'))).filter((r) => fs.existsSync(r));
382
- } catch {
383
- return [];
384
- }
397
+ migrateLegacyProjects(mdir);
398
+ return Object.values(readRegistry({ dir: registryDirFor(mdir) }).projects)
399
+ .map((e) => e.root)
400
+ .filter((r) => fs.existsSync(r));
385
401
  }
386
402
 
387
403
  // ---------------------------------------------------------------------------
@@ -17,8 +17,8 @@
17
17
  * decision D8, which this module does not take.
18
18
  */
19
19
 
20
+ import { detectNormalized, detectWithExtent } from '../_lib/egress_normalize.js';
20
21
  import {
21
- detect,
22
22
  EGRESS_RULES,
23
23
  type EgressRule,
24
24
  type EgressSpan,
@@ -26,8 +26,6 @@ import {
26
26
  prefixesOf,
27
27
  rawKeyPrefixPattern,
28
28
  replaceSpans,
29
- resolveOverlaps,
30
- findSpans,
31
29
  REVIEW_CREDENTIAL_ROWS,
32
30
  } from '../_lib/egress_rules.js';
33
31
  import { parseTokens, TokenScope } from '../_lib/egress_token.js';
@@ -88,9 +86,10 @@ export class CouncilEgress {
88
86
  * proven is replaced through the nearest END marker, as the line masks did.
89
87
  */
90
88
  replace(text: string): string {
91
- const spans = resolveOverlaps(findSpans(text, BUNDLE_RULES))
89
+ const at = detectWithExtent(text, BUNDLE_RULES);
90
+ const spans = at.spans
92
91
  .filter((s) => s.class === 'credential' || this.opts.identifiers)
93
- .map((s) => ({ ...s, end: maskingEnd(text, s) }));
92
+ .map((s) => ({ ...s, end: at.end(s, maskingEnd) ?? s.end }));
94
93
  spans.sort((a, b) => a.start - b.start);
95
94
  const merged: EgressSpan[] = [];
96
95
  for (const s of spans) {
@@ -124,7 +123,7 @@ export class CouncilEgress {
124
123
  for (const value of this.replaced) {
125
124
  if (body.includes(value)) return { reason: 'replaced-value-present', what: 'value replaced in this run' };
126
125
  }
127
- for (const s of detect(body)) {
126
+ for (const s of detectNormalized(body)) {
128
127
  const high = s.class === 'credential' ? s.rule !== 'generic-assignment' : s.validated && this.opts.identifiers;
129
128
  if (high) return { reason: 'high-confidence-finding', what: s.rule };
130
129
  }
@@ -66,7 +66,8 @@ import { gitEnv } from './_lib/git_env.js';
66
66
  import { graphUntestedVerdict } from './_lib/graph_feeder_record.js';
67
67
  import { LexicalIndex } from './_lib/lexical_index.js';
68
68
  import { buildFromRepo } from './code_graph/build.js';
69
- import { graphState, NATIVE_CACHE_REL } from './code_graph/detect.js';
69
+ import { graphState } from './code_graph/detect.js';
70
+ import { graphHome } from './code_graph/graph_home.js';
70
71
  import { loadGraph } from './code_graph/query.js';
71
72
  import { emitSqliteTwin, sqliteTwinPath } from './code_graph/sqlite_store.js';
72
73
  import type { CodeEdge, CodeGraph } from './code_graph/types.js';
@@ -159,7 +160,13 @@ export async function makeFixture(root: string, files: number, withGraph: boolea
159
160
  git(dir, ['init', '-q']);
160
161
  git(dir, ['add', '-A']);
161
162
  git(dir, ['commit', '-q', '-m', 'fixture']);
162
- if (withGraph) await buildFromRepo(dir, path.join(dir, NATIVE_CACHE_REL));
163
+ if (withGraph) {
164
+ // A project home, as in every repository the feeder serves: the hooks the
165
+ // bench spawns run under their own user-global root, where a global
166
+ // graph home would not be found.
167
+ fs.mkdirSync(path.join(dir, 'agents'));
168
+ await buildFromRepo(dir, graphHome(dir).cachePath);
169
+ }
163
170
  fs.writeFileSync(path.join(dir, 'src', 'service.ts'), 'export function handle(x: string): string {\n return x.trim();\n}\n');
164
171
  return dir;
165
172
  }
@@ -404,7 +411,7 @@ export function benchLoadSplit(opts: { graph: string; runs?: number | undefined
404
411
  if (!v.ok) throw new Error(`invalid graph at ${opts.graph}: ${v.errors.slice(0, 3).join('; ')}`);
405
412
  const graph = parsed as CodeGraph;
406
413
  const t3 = performance.now();
407
- const wrote = emitSqliteTwin(graph, raw, copy);
414
+ const wrote = emitSqliteTwin(graph, copy);
408
415
  const t4 = performance.now();
409
416
  const byId = new Map(graph.nodes.map((n) => [n.id, n]));
410
417
  const outM = new Map<string, CodeEdge[]>();
@@ -208,31 +208,40 @@ function _render_cf_json(m: JsonObject): string {
208
208
  return pyJsonDumps(payload, { indent: 2, sortKeys: true }) + '\n';
209
209
  }
210
210
 
211
+ /** The registry schema the payload is validated against; its `description` caps at 100 characters. */
212
+ export const SERVER_SCHEMA_URL = 'https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json';
213
+ export const REGISTRY_DESCRIPTION_MAX = 100;
214
+
211
215
  /**
212
216
  * Official MCP Registry server.json (registry.modelcontextprotocol.io).
213
- * Minimal valid shape per the registry's server.schema.json: reverse-DNS
214
- * name (matches package.json `mcpName` + the README `mcp-name:` marker),
215
- * the npm package pointer, and the stdio transport. The version is asserted
216
- * equal to package.json.version by lint_mcp_registry_manifest (CI).
217
+ * The name matches package.json `mcpName` + the README `mcp-name:` marker; the
218
+ * description is `agentConfig.registryDescription`, because the npm description
219
+ * is longer than the registry accepts. The positional `mcp-server` argument is
220
+ * what makes a registry client start the server instead of the bare CLI.
221
+ * Version- and length-equality are asserted by lint_mcp_registry_manifest (CI).
217
222
  */
218
- function _render_server_json(m: JsonObject): string {
219
- const pkg = m['package'] as JsonObject;
223
+ export function renderServerJson(pkg: JsonObject): string {
220
224
  const version = pkg['version'] as string;
225
+ const agentConfig = (pkg['agentConfig'] as JsonObject | undefined) ?? {};
221
226
  const payload: JsonObject = {
222
- $schema: 'https://static.modelcontextprotocol.io/schemas/2025-07-09/server.schema.json',
223
- name: 'io.github.event4u-app/agent-config',
224
- description: pkg['description'],
227
+ $schema: SERVER_SCHEMA_URL,
228
+ name: pkg['mcpName'] as string,
229
+ title: 'agent-config',
230
+ description: (agentConfig['registryDescription'] as string | undefined) ?? '',
231
+ websiteUrl: pkg['homepage'] as string,
225
232
  repository: {
226
- url: pkg['repository'],
233
+ url: _repo_url(pkg),
227
234
  source: 'github',
228
235
  },
229
236
  version,
230
237
  packages: [
231
238
  {
232
239
  registryType: 'npm',
233
- identifier: '@event4u/agent-config',
240
+ identifier: pkg['name'] as string,
234
241
  version,
242
+ runtimeHint: 'npx',
235
243
  transport: { type: 'stdio' },
244
+ packageArguments: [{ type: 'positional', value: 'mcp-server' }],
236
245
  },
237
246
  ],
238
247
  };
@@ -280,7 +289,9 @@ export function main(argv: readonly string[]): number {
280
289
  const manifest_text = pyJsonDumps(manifest, { indent: 2, sortKeys: true }) + '\n';
281
290
  const row_md = _render_row_md(manifest);
282
291
  const cf_json = _render_cf_json(manifest);
283
- const server_json = _render_server_json(manifest);
292
+ const server_json = renderServerJson(
293
+ JSON.parse(fs.readFileSync(_config.PKG_FILE, 'utf-8')) as JsonObject,
294
+ );
284
295
 
285
296
  const outputs: Array<[string, string]> = [
286
297
  [_config.OUT_MANIFEST, manifest_text],
@@ -60,6 +60,13 @@ export const EXACT_PIN_EXCEPTIONS: Readonly<Record<string, string>> = {
60
60
  'which would make every grammar load throw rather than degrade — so the runtime ' +
61
61
  'and its grammars move together, in one reviewed change, or not at all. ' +
62
62
  'Refresh procedure: src/vendor/grammars/README.md.',
63
+ // The three sigstore modules are the trusted computing base of the
64
+ // unattended upgrade's provenance check (sigstore_provenance.ts): a
65
+ // patch release of any of them changes what "verified" means, so each
66
+ // version is reviewed rather than resolved. They move together.
67
+ '@sigstore/verify': 'Trusted computing base of the release provenance check; moves only in a reviewed change, with its two siblings.',
68
+ '@sigstore/bundle': 'Parses the bundle the provenance check verifies; pinned with @sigstore/verify for the same reason.',
69
+ '@sigstore/protobuf-specs': 'Decodes the pinned trust root and npm keys; pinned with @sigstore/verify for the same reason.',
63
70
  };
64
71
 
65
72
  /**
@@ -75,6 +82,13 @@ export const SECURITY_FLOOR_EXCEPTIONS: Readonly<Record<string, { required: stri
75
82
  'high-severity route-guard bypass via path traversal (CVSS 7.5) affects <=10.1.1; ' +
76
83
  '10.1.2 is the fix. Verified with npm audit on a clean 10.1.0 install, 2026-07-31.',
77
84
  },
85
+ zod: {
86
+ required: '^4.6.6',
87
+ reason:
88
+ 'high-severity memory exhaustion while validating oversized constrained collections ' +
89
+ '(GHSA-mrhx-c7xc-m3w5) affects <4.6.6; 4.6.6 is the fix. Verified with npm audit on ' +
90
+ 'clean 4.6.5 (1 high) and 4.6.6 (0) installs, 2026-10-24.',
91
+ },
78
92
  };
79
93
 
80
94
  /** A settled caret floor: `^X.Y.0`. */
@@ -33,6 +33,7 @@ export const ENGINE_MODULES: readonly string[] = [
33
33
  'src/scripts/_lib/egress_rules.ts',
34
34
  'src/scripts/_lib/egress_token.ts',
35
35
  'src/scripts/_lib/egress_text.ts',
36
+ 'src/scripts/_lib/egress_normalize.ts',
36
37
  ];
37
38
 
38
39
  export interface Recipe {
@@ -4,7 +4,7 @@
4
4
  * `check_release_pr_shape`.
5
5
  *
6
6
  * The shape check lets a release PR carry `package.json`, `package-lock.json`
7
- * and the three plugin manifests, and it reads only their NAMES. Smoke is
7
+ * and the plugin and extension manifests, and it reads only their NAMES. Smoke is
8
8
  * skipped on release heads (`docs/contracts/release-pr-gating.md`), so an added
9
9
  * script, a new dependency or a changed `bin` path could ride a release PR with
10
10
  * no gate reading it. This compares each manifest against the base and fails on
@@ -27,6 +27,7 @@ import * as path from 'node:path';
27
27
  import { fileURLToPath, pathToFileURL } from 'node:url';
28
28
 
29
29
  import { CONSUMER_IMPACT_VALUES } from './_lib/consumer_impact.js';
30
+ import { PINNED_PACKAGE_ARG_RE } from './_lib/listing_manifests.js';
30
31
  import { DeadScopeError, reportScanned } from './_lib/scan_scope.js';
31
32
 
32
33
  const _HERE = fileURLToPath(import.meta.url);
@@ -43,8 +44,17 @@ export const VERSION_FIELDS: Readonly<Record<string, readonly Pattern[]>> = {
43
44
  '.claude-plugin/marketplace.json': [['version'], ['metadata', 'version'], ['plugins', '*', 'version']],
44
45
  '.augment-plugin/plugin.json': [['version']],
45
46
  '.augment-plugin/marketplace.json': [['version'], ['metadata', 'version'], ['plugins', '*', 'version']],
47
+ '.github/plugin/plugin.json': [['version'], ['mcpServers', 'agent-config', 'args', '*']],
48
+ 'gemini-extension.json': [['version'], ['mcpServers', 'agent-config', 'args', '*']],
46
49
  };
47
50
 
51
+ /**
52
+ * Patterns admitted only as a re-pin: an `npx` argument may move from one
53
+ * `@event4u/agent-config@X.Y.Z` to another, never to any other string — the
54
+ * arguments around it are the command a crawler runs.
55
+ */
56
+ const PINNED_ARG_PATTERN: Pattern = ['mcpServers', 'agent-config', 'args', '*'];
57
+
48
58
  function _matches(p: readonly Segment[], pat: Pattern): boolean {
49
59
  if (p.length !== pat.length) return false;
50
60
  return pat.every((seg, i) => seg === p[i] || (seg === '*' && /^\d+$/u.test(p[i] ?? '')));
@@ -137,7 +147,12 @@ export function manifestDifferences(file: string, base: string | null, head: str
137
147
  before = (before as Record<string, unknown> | undefined)?.[s];
138
148
  after = (after as Record<string, unknown> | undefined)?.[s];
139
149
  }
140
- if (atVersion && typeof before === 'string' && typeof after === 'string') continue;
150
+ const pinOnly = _matches(p, PINNED_ARG_PATTERN);
151
+ const inShape =
152
+ typeof before === 'string' &&
153
+ typeof after === 'string' &&
154
+ (!pinOnly || (PINNED_PACKAGE_ARG_RE.test(before) && PINNED_PACKAGE_ARG_RE.test(after)));
155
+ if (atVersion && inShape) continue;
141
156
  out.push(`${file}: ${_render(p)} differs outside the version fields`);
142
157
  }
143
158
  return out;
@@ -36,6 +36,11 @@ const ALLOWLIST_GLOBS = [
36
36
  // the tarball (publish-surface.json roots), so a release PR must carry them.
37
37
  '.augment-plugin/plugin.json',
38
38
  '.augment-plugin/marketplace.json',
39
+ // Listing manifests — version and the `npx` pin rewritten by release.ts
40
+ // set_plugin_manifest_versions; neither ships in the tarball, but a crawler
41
+ // reads them from the default branch.
42
+ '.github/plugin/plugin.json',
43
+ 'gemini-extension.json',
39
44
  'src/packs/*/pack.yaml',
40
45
  'src/packs/*/README.md',
41
46
  'src/domains/*/pack.yaml',
@@ -17,8 +17,9 @@ import { fileURLToPath } from 'node:url';
17
17
 
18
18
  import { write_atomic } from '../_lib/fs_atomic.js';
19
19
  import { extractFile, type FileExtract, type RawEdge } from './extract.js';
20
- import { buildLaravelOverlay } from './laravel_overlay.js';
21
20
  import { grammarWasmPath } from './loader.js';
21
+ import { type PassReport, runPasses } from './passes.js';
22
+ import { EMPTY_PROBE, PROBE_MARKERS, type ProjectProbe } from './providers/index.js';
22
23
  import {
23
24
  ALIAS_VIA,
24
25
  aliasRulesFrom,
@@ -30,8 +31,9 @@ import {
30
31
  resolveAlias,
31
32
  resolvePsr4,
32
33
  } from './resolution_tiers.js';
33
- import { DOC_EXTS, docsLayer } from './docs_layer.js';
34
- import { readScipIndex, type ScipDocument, scipEdges, supersededByScip } from './scip_reader.js';
34
+ import { templateBase } from './conventions_default.js';
35
+ import { DOC_EXTS } from './docs_layer.js';
36
+ import { readScipIndex, type ScipDocument } from './scip_reader.js';
35
37
  import { emitSqliteTwin } from './sqlite_store.js';
36
38
  import {
37
39
  type CodeEdge,
@@ -77,7 +79,21 @@ const CODE_EXTS: ReadonlySet<string> = new Set(Object.keys(EXT_LANG));
77
79
  /** Generated output under the project root: the engine never reads its own artefacts back. */
78
80
  const GENERATED_DIRS: ReadonlySet<string> = new Set(['agents/map', 'agents/runtime']);
79
81
 
82
+ /** Whether the discovery walk would enter every directory of repo-relative `rel` (posix). */
83
+ export function walkedPath(rel: string): boolean {
84
+ const dirs = rel.split('/').slice(0, -1);
85
+ for (let i = 0; i < dirs.length; i++) {
86
+ const d = dirs[i] as string;
87
+ if (IGNORE_DIRS.has(d) || d.startsWith('.') || GENERATED_DIRS.has(dirs.slice(0, i + 1).join('/'))) return false;
88
+ }
89
+ return true;
90
+ }
91
+
80
92
  export function discoverFiles(root: string, exts: ReadonlySet<string> = CODE_EXTS): string[] {
93
+ return walkFiles(root, (name) => exts.has(path.extname(name).toLowerCase()));
94
+ }
95
+
96
+ function walkFiles(root: string, accept: (name: string) => boolean): string[] {
81
97
  const abs = path.resolve(root);
82
98
  // Resolve the root itself first: a repo checked out under a symlinked
83
99
  // path (e.g. macOS /tmp → /private/tmp) must still confine correctly —
@@ -106,8 +122,7 @@ export function discoverFiles(root: string, exts: ReadonlySet<string> = CODE_EXT
106
122
  continue;
107
123
  }
108
124
  if (!e.isFile()) continue;
109
- const ext = path.extname(e.name).toLowerCase();
110
- if (!exts.has(ext)) continue;
125
+ if (!accept(e.name)) continue;
111
126
  let real: string;
112
127
  try {
113
128
  real = fs.realpathSync(full);
@@ -122,6 +137,33 @@ export function discoverFiles(root: string, exts: ReadonlySet<string> = CODE_EXT
122
137
  return out.sort();
123
138
  }
124
139
 
140
+ /** Manifests a provider may read to detect a stack, by basename. */
141
+ export const MANIFEST_NAME = /^(?:composer\.json|package\.json|pyproject\.toml|requirements[\w.-]*\.txt|pom\.xml|build\.gradle(?:\.kts)?|go\.mod|[\w.-]+\.csproj|Gemfile)$/;
142
+
143
+ /** The project's manifests and marker files, for provider detection (ADR-298). */
144
+ export function readProjectProbe(root: string): ProjectProbe {
145
+ const abs = path.resolve(root);
146
+ const manifests = new Map<string, string>();
147
+ const markers = new Set<string>();
148
+ const paths: string[] = [];
149
+ for (const full of walkFiles(abs, () => true)) {
150
+ const rel = toPosixRel(abs, full);
151
+ paths.push(rel);
152
+ const name = path.basename(full);
153
+ if (!MANIFEST_NAME.test(name) && !PROBE_MARKERS.has(name)) continue;
154
+ if (PROBE_MARKERS.has(name)) {
155
+ markers.add(rel);
156
+ continue;
157
+ }
158
+ try {
159
+ manifests.set(rel, fs.readFileSync(full, 'utf-8'));
160
+ } catch {
161
+ continue;
162
+ }
163
+ }
164
+ return { manifests, markers, paths };
165
+ }
166
+
125
167
  function toPosixRel(root: string, full: string): string {
126
168
  return path.relative(root, full).split(path.sep).join('/');
127
169
  }
@@ -234,10 +276,21 @@ function resolveSpecifier(fromFile: string, spec: string, fileIds: ReadonlySet<s
234
276
  * go `example.com/x/mod` → every non-test `.go` file of the most specific
235
277
  * directory whose path ends the import path — a Go import names a
236
278
  * package, and a package is all of its files.
279
+ * ruby `require_relative 'x'` → `x.rb` beside the importer; `require 'a/b'`
280
+ * → the one `a/b.rb` under any directory (a load-path root).
281
+ * csharp `using A.B` → the one file declaring namespace `A.B` (`namespaces`).
282
+ * A namespace declared by several files is left to name lookup: it
283
+ * binds no file, since a `using` names no single one.
237
284
  *
238
285
  * A suffix that matches two files is refused rather than guessed.
239
286
  */
240
- export function resolveLangModule(lang: Lang, fromFile: string, spec: string, fileIds: readonly string[]): string[] {
287
+ export function resolveLangModule(
288
+ lang: Lang,
289
+ fromFile: string,
290
+ spec: string,
291
+ fileIds: readonly string[],
292
+ namespaces: ReadonlyMap<string, readonly string[]> = new Map(),
293
+ ): string[] {
241
294
  const unique = (suffixes: string[]): string[] => {
242
295
  const hits = fileIds.filter((f) => suffixes.some((s) => f === s || f.endsWith(`/${s}`)));
243
296
  return hits.length === 1 ? hits : [];
@@ -264,6 +317,16 @@ export function resolveLangModule(lang: Lang, fromFile: string, spec: string, fi
264
317
  }
265
318
  return best ? goFiles.filter((f) => idDirname(f) === best).sort() : [];
266
319
  }
320
+ if (lang === 'ruby') {
321
+ if (spec.startsWith('.')) {
322
+ const dir = idDirname(fromFile);
323
+ const joined = path.posix.normalize(dir && dir !== '.' ? `${dir}/${spec}` : spec);
324
+ const c = joined.endsWith('.rb') ? joined : `${joined}.rb`;
325
+ return fileIds.includes(c) ? [c] : [];
326
+ }
327
+ return unique([spec.endsWith('.rb') ? spec : `${spec}.rb`]);
328
+ }
329
+ if (lang === 'csharp') return [...(namespaces.get(spec) ?? [])];
267
330
  return [];
268
331
  }
269
332
 
@@ -309,6 +372,8 @@ export function buildGraph(
309
372
  resolution: ResolutionConfig = EMPTY_RESOLUTION_CONFIG,
310
373
  scip: readonly ScipDocument[] = [],
311
374
  docs: readonly SourceFile[] = [],
375
+ probe: ProjectProbe = EMPTY_PROBE,
376
+ report?: { passes?: PassReport },
312
377
  ): CodeGraph {
313
378
  const nodes = extracts.flatMap((e) => e.nodes);
314
379
  const nodeIds = new Set(nodes.map((n) => n.id));
@@ -383,6 +448,21 @@ export function buildGraph(
383
448
  const fileIdList = [...fileIds].sort();
384
449
  /** Go imports name a package of several files; the binding takes the first, these carry the rest. */
385
450
  const extraImports: Array<{ source: string; target: string }> = [];
451
+ const namespaceFiles = new Map<string, string[]>();
452
+ for (const ex of extracts) for (const ns of ex.namespaces ?? []) push(namespaceFiles, ns, ex.file);
453
+ // C# name lookup is scoped by namespace, not by file: a type is visible
454
+ // from its own namespace, every enclosing one, and each `using`. Within
455
+ // that set a unique name IS the binding the compiler makes.
456
+ const csTypes = new Map<string, string[]>();
457
+ const csVisible = new Map<string, string[]>();
458
+ for (const ex of extracts) {
459
+ if (ex.lang !== 'csharp') continue;
460
+ for (const [id, ns] of Object.entries(ex.typeNamespaces ?? {})) push(csTypes, `${ns}\0${id.slice(id.indexOf('#') + 1)}`, id);
461
+ const seen = new Set<string>();
462
+ for (const ns of ex.namespaces ?? []) for (let p = ns; p; p = p.includes('.') ? p.slice(0, p.lastIndexOf('.')) : '') seen.add(p);
463
+ for (const r of ex.rawEdges) if (r.relation === 'imports' && r.importKind === 'namespace' && !r.localName && r.moduleSpecifier) seen.add(r.moduleSpecifier);
464
+ csVisible.set(ex.file, [...seen]);
465
+ }
386
466
  for (const ex of extracts) {
387
467
  const locals = new Map<string, string[]>();
388
468
  for (const n of ex.nodes) if (n.kind !== 'file') push(locals, n.label, n.id);
@@ -419,8 +499,9 @@ export function buildGraph(
419
499
  if (hit) imports.set(local, { target: hit, exact: false, via: 'name-lookup' });
420
500
  continue;
421
501
  }
422
- if (ex.lang === 'python' || ex.lang === 'java' || ex.lang === 'go') {
423
- const hits = resolveLangModule(ex.lang, ex.file, spec, fileIdList);
502
+ if (ex.lang === 'csharp' && (namespaceFiles.get(spec)?.length ?? 0) > 1) continue;
503
+ if (ex.lang === 'python' || ex.lang === 'java' || ex.lang === 'go' || ex.lang === 'csharp' || ex.lang === 'ruby') {
504
+ const hits = resolveLangModule(ex.lang, ex.file, spec, fileIdList, namespaceFiles);
424
505
  const first = hits[0];
425
506
  if (first === undefined) {
426
507
  imports.set(local, {
@@ -502,6 +583,11 @@ export function buildGraph(
502
583
  // reported as ones.
503
584
  via: bound.via,
504
585
  };
586
+ if (lang === 'csharp') {
587
+ const ids = new Set((csVisible.get(file) ?? []).flatMap((ns) => csTypes.get(`${ns}\0${name}`) ?? []));
588
+ const fit = [...ids].filter((id) => satisfies(nodeKind.get(id), want));
589
+ if (fit.length === 1) return { target: fit[0] as string, confidence: 'EXTRACTED', via: 'namespace-scope' };
590
+ }
505
591
  if (scopeOnly) return null;
506
592
  const wide =
507
593
  want === 'constructible'
@@ -607,6 +693,9 @@ export function buildGraph(
607
693
  // against a same-named declaration elsewhere in the repository is
608
694
  // the defect this roadmap removes, applied to a second relation.
609
695
  const hit = resolveInScope(ex.file, r.targetName, 'any', ex.lang, true);
696
+ // A C# type reference to a framework type (`List`, `Task`) names no
697
+ // project file, and a `symbol:` edge per mention would only be noise.
698
+ if (!hit && ex.lang === 'csharp') return;
610
699
  emit({
611
700
  source: r.sourceId,
612
701
  target: hit?.target ?? `symbol:${r.targetName}`,
@@ -670,6 +759,28 @@ export function buildGraph(
670
759
  }
671
760
  return;
672
761
  }
762
+ // scoped `C::m()` whose class the file binds by a stated mechanism
763
+ // (an import, PSR-4, a local declaration, a C# namespace): the class
764
+ // is known, so a method it declares or inherits is the target, and the
765
+ // edge is exactly as trustworthy as the class binding.
766
+ if ((r.callStyle === 'scoped' || r.callStyle === 'dynamic') && r.scopeName) {
767
+ const cls = resolveInScope(ex.file, r.scopeName, 'any', ex.lang, true);
768
+ const kind = cls ? nodeKind.get(cls.target) : undefined;
769
+ if (cls && isStatedResolution(cls.via) && (kind === 'class' || kind === 'interface' || kind === 'enum')) {
770
+ const m = resolveHierMethod(cls.target, r.targetName);
771
+ if (m) {
772
+ emit({
773
+ source: r.sourceId,
774
+ target: m,
775
+ relation: 'calls',
776
+ confidence: m.startsWith(`${cls.target}::`) ? cls.confidence : 'INFERRED',
777
+ resolved_via: cls.via,
778
+ provider: 'native',
779
+ });
780
+ return;
781
+ }
782
+ }
783
+ }
673
784
  // scoped (Class::m unresolved / facade) or dynamic ($obj->m) → AMBIGUOUS
674
785
  const cands = methodCandidates(r.targetName, ex.lang);
675
786
  if (r.callStyle === 'dynamic' && cands.length === 0) {
@@ -754,39 +865,9 @@ export function buildGraph(
754
865
  });
755
866
  }
756
867
 
757
- // Framework links no import states (routes, views, tables); rule-read,
758
- // emitted only when the target exists. Also drops the base edges it
759
- // proves false: a fluent `->middleware()` bound by name to some
760
- // unrelated in-repo `middleware()`.
761
- const overlay = buildLaravelOverlay(files, nodes, resolution);
762
- let routeBuilderCalls = 0;
763
- if (overlay.nodes.length > 0 || overlay.edges.length > 0) {
764
- const kept = edges.filter((e) => !overlay.dropEdge(e));
765
- routeBuilderCalls = edges.length - kept.length;
766
- // Element-wise: a spread of ~200k edges overflows the call stack.
767
- edges.length = 0;
768
- for (const e of kept) edges.push(e);
769
- for (const e of overlay.edges) edges.push(e);
770
- for (const n of overlay.nodes) nodes.push(n);
771
- }
772
-
773
- if (docs.length > 0) {
774
- const allFiles = new Set([...files.map((f) => f.path), ...docs.map((d) => d.path)]);
775
- const layer = docsLayer(docs, allFiles);
776
- for (const n of layer.nodes) nodes.push(n);
777
- for (const e of layer.edges) edges.push(e);
778
- }
779
-
780
- // A project's own SCIP index states references the compiler resolved;
781
- // each one supersedes the native name guess it duplicates.
782
- if (scip.length > 0) {
783
- const stated = scipEdges(scip, nodes);
784
- const dropped = supersededByScip(stated, nodes);
785
- const kept = edges.filter((e) => !dropped(e));
786
- edges.length = 0;
787
- for (const e of kept) edges.push(e);
788
- for (const e of stated) edges.push(e);
789
- }
868
+ // Everything after resolution: convention providers, docs, SCIP (passes.ts).
869
+ const passes = runPasses({ files, docs, scip, resolution, probe }, nodes, edges);
870
+ if (report) report.passes = passes;
790
871
 
791
872
  // deterministic ordering
792
873
  nodes.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
@@ -800,7 +881,9 @@ export function buildGraph(
800
881
  for (const e of edges) counts[e.confidence] += 1;
801
882
  const suppressed: CodeGraph['suppressed_edge_counts'] = {
802
883
  dynamic_no_candidate: suppressedKeys.size,
803
- ...(routeBuilderCalls > 0 ? { route_builder_call: routeBuilderCalls } : {}),
884
+ ...(Object.keys(passes.suppressed).length > 0
885
+ ? { contested_by_provider: Object.fromEntries(Object.entries(passes.suppressed).sort()) }
886
+ : {}),
804
887
  };
805
888
 
806
889
  const languages = [...new Set(extracts.map((e) => e.lang))].sort() as Lang[];
@@ -819,6 +902,18 @@ export function buildGraph(
819
902
  h.update(createHash('sha256').update(d.source, 'utf-8').digest('hex'));
820
903
  h.update('\n');
821
904
  }
905
+ // Manifests and markers decide which providers run: a changed manifest is
906
+ // a changed input even when no source file moved.
907
+ for (const [p, text] of [...probe.manifests].sort((a, b) => (a[0] < b[0] ? -1 : 1))) {
908
+ h.update(`manifest\0${p}\0`);
909
+ h.update(createHash('sha256').update(text, 'utf-8').digest('hex'));
910
+ h.update('\n');
911
+ }
912
+ for (const p of [...probe.markers].sort()) h.update(`marker\0${p}\n`);
913
+ // Template files are no source, yet their presence decides `renders` and
914
+ // default-pass edges: they are inputs too. Other non-code paths (caches,
915
+ // logs, assets) are not read and stay out of the identity.
916
+ for (const p of probe.paths) if (templateBase(p) !== null) h.update(`template\0${p}\n`);
822
917
  // The SCIP index is an input like any source file: a new index must not
823
918
  // read as the same graph.
824
919
  for (const d of [...scip].sort((a, b) => (a.path < b.path ? -1 : 1))) {
@@ -833,6 +928,7 @@ export function buildGraph(
833
928
  grammar_abi: EXPECTED_GRAMMAR_ABI,
834
929
  edge_confidence_counts: counts,
835
930
  suppressed_edge_counts: suppressed,
931
+ ...(passes.providers.length > 0 ? { providers: passes.providers } : {}),
836
932
  nodes,
837
933
  edges,
838
934
  };
@@ -908,6 +1004,7 @@ export function serializeGraph(g: CodeGraph): string {
908
1004
  'grammar_abi',
909
1005
  'edge_confidence_counts',
910
1006
  'suppressed_edge_counts',
1007
+ 'providers',
911
1008
  'nodes',
912
1009
  'edges',
913
1010
  ];
@@ -936,13 +1033,14 @@ export function serializeGraph(g: CodeGraph): string {
936
1033
  // byte what it was before this field existed.
937
1034
  if (e.ambiguity_reason) base['ambiguity_reason'] = e.ambiguity_reason;
938
1035
  if (e.candidates) base['candidates'] = e.candidates;
1036
+ if (e.provider_id) base['provider_id'] = e.provider_id;
939
1037
  return base;
940
1038
  };
941
1039
  const ordered: Record<string, unknown> = {};
942
1040
  for (const k of key_order) {
943
1041
  if (k === 'nodes') ordered[k] = g.nodes.map(node);
944
1042
  else if (k === 'edges') ordered[k] = g.edges.map(edge);
945
- else ordered[k] = g[k];
1043
+ else if (g[k] !== undefined) ordered[k] = g[k];
946
1044
  }
947
1045
  return JSON.stringify(ordered, null, 2) + '\n';
948
1046
  }
@@ -1086,14 +1184,16 @@ export async function buildFromRepo(
1086
1184
  continue;
1087
1185
  }
1088
1186
  }
1089
- const graph = buildGraph(files, extracts, readResolutionConfig(root), scip, docs);
1187
+ const report: { passes?: PassReport } = {};
1188
+ const graph = buildGraph(files, extracts, readResolutionConfig(root), scip, docs, readProjectProbe(abs), report);
1189
+ for (const f of report.passes?.failures ?? []) process.stderr.write(`code-graph: pass ${f.pass} failed — ${f.reason}\n`);
1090
1190
  const json = serializeGraph(graph);
1091
1191
  if (cachePath) {
1092
1192
  write_atomic(cachePath, json);
1093
1193
  write_atomic(sidecarPath(cachePath), JSON.stringify(nextSidecar));
1094
1194
  // Derived SQLite twin (ADR-129): best-effort accelerator beside the
1095
1195
  // canonical JSON — never fails the build, no-op on old Node.
1096
- emitSqliteTwin(graph, json, cachePath);
1196
+ emitSqliteTwin(graph, cachePath);
1097
1197
  }
1098
1198
  return { graph, json, fileCount: files.length, reExtracted, reused };
1099
1199
  }