@event4u/agent-config 16.2.0 → 16.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 (332) 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/CHANGELOG.md +89 -237
  5. package/README.md +14 -12
  6. package/dist/agent-src/commands/analyze/inbox.md +1 -1
  7. package/dist/agent-src/commands/analyze/roadmap-repos.md +1 -1
  8. package/dist/agent-src/commands/bug/fix.md +17 -2
  9. package/dist/agent-src/commands/feature/plan.md +1 -1
  10. package/dist/agent-src/commands/feature/roadmap.md +1 -1
  11. package/dist/agent-src/commands/fix/ci.md +1 -1
  12. package/dist/agent-src/commands/implement-ticket.md +1 -1
  13. package/dist/agent-src/commands/jira-ticket.md +1 -1
  14. package/dist/agent-src/commands/roadmap/create.md +1 -1
  15. package/dist/agent-src/commands/roadmap/materialize.md +1 -1
  16. package/dist/agent-src/commands/roadmap/process-full.md +4 -4
  17. package/dist/agent-src/commands/roadmap/resolve-blockers.md +361 -0
  18. package/dist/agent-src/commands/roadmap/triage-parked.md +325 -0
  19. package/dist/agent-src/commands/roadmap.md +18 -4
  20. package/dist/agent-src/commands/tests/create.md +17 -3
  21. package/dist/agent-src/commands/tests/execute.md +22 -5
  22. package/dist/agent-src/contexts/authority/decision-revisit-gate-enforcement.md +43 -0
  23. package/dist/agent-src/contexts/communication/rules-auto/design-fidelity-enforcement.md +24 -0
  24. package/dist/agent-src/contexts/execution/auto-dispatch-classification.md +6 -6
  25. package/dist/agent-src/contexts/execution/roadmap-process-loop.md +32 -2
  26. package/dist/agent-src/contexts/execution/session-canary-enforcement-history.md +67 -0
  27. package/dist/agent-src/contexts/execution/toolchain-resolver.md +101 -7
  28. package/dist/agent-src/rules/active-remediation.md +1 -1
  29. package/dist/agent-src/rules/autonomous-execution.md +1 -0
  30. package/dist/agent-src/rules/broken-access-control.md +1 -0
  31. package/dist/agent-src/rules/command-suggestion-policy.md +1 -0
  32. package/dist/agent-src/rules/context-hygiene.md +1 -1
  33. package/dist/agent-src/rules/decision-revisit-gate.md +16 -33
  34. package/dist/agent-src/rules/design-fidelity.md +13 -16
  35. package/dist/agent-src/rules/doc-screenshot-hygiene.md +1 -0
  36. package/dist/agent-src/rules/domain-safety-disclaimer.md +1 -0
  37. package/dist/agent-src/rules/domain-safety-pii.md +1 -0
  38. package/dist/agent-src/rules/domain-safety-retention.md +1 -0
  39. package/dist/agent-src/rules/engineering-safety-floor.md +1 -0
  40. package/dist/agent-src/rules/evaluator-independence.md +3 -2
  41. package/dist/agent-src/rules/fast-path-marker-visibility.md +1 -0
  42. package/dist/agent-src/rules/finance-safety-floor.md +1 -0
  43. package/dist/agent-src/rules/git-history-discipline.md +1 -0
  44. package/dist/agent-src/rules/image-likeness-and-rights.md +1 -0
  45. package/dist/agent-src/rules/legal-safety-floor.md +3 -2
  46. package/dist/agent-src/rules/lethal-trifecta-guard.md +1 -0
  47. package/dist/agent-src/rules/low-impact-corpus-privacy-floor.md +1 -0
  48. package/dist/agent-src/rules/media-governance-routing.md +14 -17
  49. package/dist/agent-src/rules/persona-governance.md +2 -2
  50. package/dist/agent-src/rules/provider-lifecycle-discipline.md +1 -1
  51. package/dist/agent-src/rules/question-not-instruction.md +1 -0
  52. package/dist/agent-src/rules/roadmap-progress-sync.md +1 -1
  53. package/dist/agent-src/rules/runtime-safety.md +1 -0
  54. package/dist/agent-src/rules/secret-vcs-guard.md +1 -0
  55. package/dist/agent-src/rules/security-sensitive-stop.md +3 -2
  56. package/dist/agent-src/rules/session-canary.md +8 -62
  57. package/dist/agent-src/rules/settings-ask-protocol.md +1 -0
  58. package/dist/agent-src/rules/source-confidentiality.md +1 -0
  59. package/dist/agent-src/rules/strategy-safety-floor.md +1 -0
  60. package/dist/agent-src/rules/tool-safety.md +1 -0
  61. package/dist/agent-src/rules/untrusted-input-defense.md +1 -0
  62. package/dist/agent-src/rules/user-interrupt-priority.md +1 -0
  63. package/dist/agent-src/scripts/archive_completed_roadmaps.ts +115 -20
  64. package/dist/agent-src/scripts/deferral_carry.ts +339 -0
  65. package/dist/agent-src/scripts/update_roadmap_progress.ts +4 -1
  66. package/dist/agent-src/skills/api-design/data/manifest.json +2 -2
  67. package/dist/agent-src/skills/api-testing/SKILL.md +17 -0
  68. package/dist/agent-src/skills/code-intelligence/SKILL.md +14 -2
  69. package/dist/agent-src/skills/design-system-capture/references/import-procedure.md +1 -1
  70. package/dist/agent-src/skills/existing-ui-audit/SKILL.md +2 -2
  71. package/dist/agent-src/skills/fe-design/SKILL.md +1 -1
  72. package/dist/agent-src/skills/iconography/SKILL.md +1 -1
  73. package/dist/agent-src/skills/js-library-packaging/SKILL.md +2 -2
  74. package/dist/agent-src/skills/monorepo-workspace/SKILL.md +1 -1
  75. package/dist/agent-src/skills/quality-tools/SKILL.md +11 -1
  76. package/dist/agent-src/skills/quality-tools/references/go-tools.md +76 -0
  77. package/dist/agent-src/skills/quality-tools/references/python-tools.md +104 -0
  78. package/dist/agent-src/skills/review-routing/SKILL.md +2 -2
  79. package/dist/agent-src/skills/skill-improvement-pipeline/SKILL.md +1 -1
  80. package/dist/agent-src/skills/storybook-workshop/SKILL.md +3 -3
  81. package/dist/agent-src/skills/tailwind-engineer/SKILL.md +1 -1
  82. package/dist/agent-src/skills/test-case-discovery/SKILL.md +7 -0
  83. package/dist/agent-src/skills/test-driven-development/SKILL.md +17 -0
  84. package/dist/agent-src/skills/test-performance/SKILL.md +19 -1
  85. package/dist/agent-src/skills/ui-component-architect/SKILL.md +2 -2
  86. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  87. package/dist/agent-src/templates/scripts/work_engine/directives/ui/apply.ts +48 -6
  88. package/dist/agent-src/templates/scripts/work_engine/stack/runner.ts +774 -109
  89. package/dist/cli/registry.js +1 -1
  90. package/dist/cli-delegate/archive_completed_roadmaps.js +92 -12
  91. package/dist/cli-delegate/check_update_banner.js +12 -1
  92. package/dist/cli-delegate/chunk-523HCFYO.js +90 -0
  93. package/dist/cli-delegate/{chunk-M2DPGFZ4.js → chunk-6V6TA25H.js} +22 -152
  94. package/dist/cli-delegate/chunk-7IQE3D5S.js +191 -0
  95. package/dist/cli-delegate/{chunk-6KFEC3G7.js → chunk-CQWX336L.js} +1 -1
  96. package/dist/cli-delegate/chunk-DEIV7XP7.js +108 -0
  97. package/dist/cli-delegate/chunk-EEHDXSYD.js +2040 -0
  98. package/dist/cli-delegate/{chunk-P6ZHP5W2.js → chunk-GKE7ZVFA.js} +4 -4
  99. package/dist/cli-delegate/chunk-JQOM7TTM.js +1201 -0
  100. package/dist/cli-delegate/{chunk-IGD6ERX6.js → chunk-JQVYKIZA.js} +10 -0
  101. package/dist/cli-delegate/{chunk-FC75PGJE.js → chunk-JY2I2DXV.js} +12 -1
  102. package/dist/cli-delegate/chunk-KB5FZ655.js +208 -0
  103. package/dist/cli-delegate/{chunk-7RQP4UDH.js → chunk-KR6EM23A.js} +235 -1455
  104. package/dist/cli-delegate/{chunk-LTYVIY76.js → chunk-LIYTLH6O.js} +1 -1
  105. package/dist/cli-delegate/{chunk-YJ72UIAK.js → chunk-NQ7WT43P.js} +1186 -244
  106. package/dist/cli-delegate/chunk-NXEILF2K.js +2160 -0
  107. package/dist/cli-delegate/{chunk-5R7HOLAF.js → chunk-NYX3L2PZ.js} +1 -1
  108. package/dist/cli-delegate/{chunk-4E2W63DA.js → chunk-OKJUIAPS.js} +62 -1
  109. package/dist/cli-delegate/{chunk-QAJS5YWY.js → chunk-PHD7OOLE.js} +2 -1
  110. package/dist/cli-delegate/{chunk-RGNIWTEA.js → chunk-QLDHIPWR.js} +1 -1
  111. package/dist/cli-delegate/{chunk-7XGBX6ZH.js → chunk-RQNNW7IF.js} +1314 -560
  112. package/dist/cli-delegate/{chunk-4XFP2Y3E.js → chunk-S4UMZNES.js} +1 -1
  113. package/dist/cli-delegate/{chunk-6KM6ORRY.js → chunk-S4YWNRKO.js} +24 -8
  114. package/dist/cli-delegate/{chunk-FP36SWJD.js → chunk-TRI6DQZ6.js} +2781 -340
  115. package/dist/cli-delegate/chunk-ULQRUR23.js +208 -0
  116. package/dist/cli-delegate/{chunk-NUHLI2RX.js → chunk-YCU3TIXH.js} +2 -0
  117. package/dist/cli-delegate/chunk-ZBZV2G5R.js +210 -0
  118. package/dist/cli-delegate/{chunk-WGXTVKCQ.js → chunk-ZUZKZYCN.js} +5 -3
  119. package/dist/cli-delegate/cmd_analyze_session.js +1 -1
  120. package/dist/cli-delegate/cmd_brand_status.js +1 -1
  121. package/dist/cli-delegate/cmd_conformance.js +1167 -234
  122. package/dist/cli-delegate/cmd_doctor.js +13 -6
  123. package/dist/cli-delegate/cmd_doctor_neighbours.js +23 -0
  124. package/dist/cli-delegate/cmd_explain.js +5 -5
  125. package/dist/cli-delegate/cmd_export.js +11 -8
  126. package/dist/cli-delegate/cmd_fleet.js +3 -3
  127. package/dist/cli-delegate/cmd_handoff.js +10 -7
  128. package/dist/cli-delegate/cmd_mcp_available.js +1 -1
  129. package/dist/cli-delegate/cmd_memory_get.js +3 -1
  130. package/dist/cli-delegate/cmd_migrate.js +2 -2
  131. package/dist/cli-delegate/cmd_packs_active.js +3 -3
  132. package/dist/cli-delegate/cmd_preflight.js +6 -6
  133. package/dist/cli-delegate/cmd_prune.js +11 -8
  134. package/dist/cli-delegate/cmd_refresh.js +2 -1
  135. package/dist/cli-delegate/cmd_route_audit.js +8 -6
  136. package/dist/cli-delegate/cmd_route_explain.js +4 -4
  137. package/dist/cli-delegate/cmd_session_recycle.js +4 -3
  138. package/dist/cli-delegate/cmd_settings_get.js +1 -1
  139. package/dist/cli-delegate/cmd_settings_migrate.js +11 -8
  140. package/dist/cli-delegate/cmd_settings_set.js +4 -4
  141. package/dist/cli-delegate/cmd_sync.js +11 -8
  142. package/dist/cli-delegate/cmd_uninstall.js +11 -8
  143. package/dist/cli-delegate/cmd_update.js +1 -1
  144. package/dist/cli-delegate/cmd_upgrade.js +3 -2
  145. package/dist/cli-delegate/cmd_validate.js +11 -8
  146. package/dist/cli-delegate/cmd_versions.js +1 -1
  147. package/dist/cli-delegate/deferral_carry.js +16 -0
  148. package/dist/cli-delegate/gate_execute.js +2 -2
  149. package/dist/cli-delegate/roadmap_gates.js +5 -5
  150. package/dist/cli-delegate/update_roadmap_progress.js +1 -1
  151. package/dist/discovery/deprecation-report.md +1 -1
  152. package/dist/discovery/discovery-manifest.json +127 -75
  153. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  154. package/dist/discovery/discovery-manifest.summary.md +3 -3
  155. package/dist/discovery/dormancy-report.md +1 -1
  156. package/dist/discovery/orphan-report.md +1 -1
  157. package/dist/discovery/packs.json +7 -5
  158. package/dist/discovery/trust-report.md +2 -2
  159. package/dist/discovery/workspaces.json +6 -4
  160. package/dist/hooks/dispatch.js +5357 -6656
  161. package/dist/hooks/dispatch.sha256 +1 -1
  162. package/dist/install/claudePathsPlan.js +47 -6
  163. package/dist/install/claudeRuleRewrite.js +7 -0
  164. package/dist/install/emit_host_rules_cli.js +39 -36
  165. package/dist/install/globalRuleScope.js +87 -0
  166. package/dist/install/host_rule_ownership.js +160 -0
  167. package/dist/install/install.mjs +2080 -1024
  168. package/dist/install/installThinLayer.js +261 -0
  169. package/dist/install/installedRuleLinks.js +176 -0
  170. package/dist/install/wizard-plan.js +24 -0
  171. package/dist/mcp/registry-manifest.json +3 -3
  172. package/dist/mcp/server.json +2 -2
  173. package/dist/mcp/server.mjs +31965 -27154
  174. package/dist/scripts/_lib/agent_settings.js +11 -0
  175. package/dist/scripts/_lib/claude_settings_hooks.js +10 -8
  176. package/dist/scripts/_lib/installed_layer.js +378 -0
  177. package/dist/scripts/_lib/lean_projection_mode.js +218 -15
  178. package/dist/scripts/_lib/rule_consequence_class.js +45 -0
  179. package/dist/scripts/_lib/rule_injection.js +67 -10
  180. package/dist/scripts/_lib/rule_law_section.js +122 -0
  181. package/dist/scripts/_lib/thin_rules.js +458 -0
  182. package/dist/scripts/_lib/windsurf_render.js +60 -0
  183. package/dist/scripts/condense.js +14 -26
  184. package/dist/scripts/project_thin_rules.js +14 -294
  185. package/dist/server/schemas/settings.js +6 -0
  186. package/dist/shared/skillRanking.js +74 -13
  187. package/docs/contracts/settings-classes.md +7 -5
  188. package/package.json +1 -1
  189. package/src/agent-src/scripts/archive_completed_roadmaps.ts +115 -20
  190. package/src/agent-src/scripts/deferral_carry.ts +339 -0
  191. package/src/agent-src/scripts/update_roadmap_progress.ts +4 -1
  192. package/src/agent-src/templates/scripts/work_engine/directives/ui/apply.ts +48 -6
  193. package/src/agent-src/templates/scripts/work_engine/stack/runner.ts +774 -109
  194. package/src/config/agent-settings.template.yml +62 -22
  195. package/src/config/continuity-surface.json +7 -0
  196. package/src/config/evaluator-budgets.json +4 -4
  197. package/src/config/gate-coverage.yml +96 -3
  198. package/src/config/gate-violation-baselines.json +17 -9
  199. package/src/config/hook-bundle-budget.json +25 -0
  200. package/src/config/hook-token-budget.json +6 -1
  201. package/src/config/host-instruction-limits.json +33 -0
  202. package/src/config/routing-coverage-seed.json +24 -3
  203. package/src/config/rule-activation-census.json +7 -3
  204. package/src/config/rule-consequence-class.json +161 -0
  205. package/src/config/rule-law-ceilings.json +99 -0
  206. package/src/install/claudePathsPlan.ts +46 -4
  207. package/src/install/claudeRuleRewrite.ts +10 -1
  208. package/src/install/emit_host_rules_cli.ts +43 -34
  209. package/src/install/globalRuleScope.ts +109 -0
  210. package/src/install/host_rule_ownership.ts +170 -0
  211. package/src/install/installThinLayer.ts +312 -0
  212. package/src/install/installedRuleLinks.ts +220 -0
  213. package/src/install/wizard-plan.ts +24 -0
  214. package/src/scripts/_cli/cmd_doctor.ts +23 -71
  215. package/src/scripts/_cli/cmd_doctor_neighbours.ts +293 -0
  216. package/src/scripts/_dispatch.bash +16 -2
  217. package/src/scripts/_lib/agent_settings.ts +11 -0
  218. package/src/scripts/_lib/claude_settings_hooks.ts +10 -8
  219. package/src/scripts/_lib/estate_offsets.ts +241 -0
  220. package/src/scripts/_lib/exemption_shape.ts +143 -0
  221. package/src/scripts/_lib/fnv.ts +23 -0
  222. package/src/scripts/_lib/foreign_scan.ts +106 -0
  223. package/src/scripts/_lib/gitignore_block.ts +314 -0
  224. package/src/scripts/_lib/global_deploy_inventory.ts +32 -0
  225. package/src/scripts/_lib/graph_feeder_record.ts +214 -0
  226. package/src/scripts/_lib/hook_settings.ts +73 -0
  227. package/src/scripts/_lib/host_capability.ts +40 -19
  228. package/src/scripts/_lib/host_hook_merge.ts +165 -0
  229. package/src/scripts/_lib/installed_layer.ts +538 -0
  230. package/src/scripts/_lib/json_merge.ts +82 -0
  231. package/src/scripts/_lib/json_pointers.ts +93 -11
  232. package/src/scripts/_lib/lean_projection_mode.ts +275 -15
  233. package/src/scripts/_lib/machine_wake.ts +16 -3
  234. package/src/scripts/_lib/neighbour_census.ts +760 -0
  235. package/src/scripts/_lib/neighbour_scan.ts +122 -0
  236. package/src/scripts/_lib/neighbour_tool_use.ts +189 -0
  237. package/src/scripts/_lib/prompt_shape.ts +46 -0
  238. package/src/scripts/_lib/release_migration_gate.ts +19 -3
  239. package/src/scripts/_lib/reserved_name_sweep.ts +73 -0
  240. package/src/scripts/_lib/review_coverage.ts +134 -0
  241. package/src/scripts/_lib/rule_consequence_class.ts +71 -0
  242. package/src/scripts/_lib/rule_injection.ts +130 -10
  243. package/src/scripts/_lib/rule_law_section.ts +147 -0
  244. package/src/scripts/_lib/rule_layer_overlap.ts +58 -0
  245. package/src/scripts/_lib/skill_origin.ts +254 -0
  246. package/src/scripts/_lib/structured_ask.ts +64 -21
  247. package/src/scripts/_lib/thin_rules.ts +540 -0
  248. package/src/scripts/_lib/touched_file_quality.ts +434 -0
  249. package/src/scripts/_lib/turn_end_refusals.ts +77 -2
  250. package/src/scripts/_lib/windsurf_render.ts +63 -0
  251. package/src/scripts/ai_council/cli_call_budget.ts +13 -5
  252. package/src/scripts/ai_council/clients.ts +26 -57
  253. package/src/scripts/ai_council/config.ts +1 -1
  254. package/src/scripts/ai_council/vendor_defaults.ts +45 -0
  255. package/src/scripts/audit_skill_overlap.ts +261 -0
  256. package/src/scripts/before_complete_hook.ts +243 -12
  257. package/src/scripts/bench_touched_file_quality.ts +238 -0
  258. package/src/scripts/check_enforcement_matrix.ts +377 -20
  259. package/src/scripts/check_estate_count.ts +130 -153
  260. package/src/scripts/check_finding_dispositions.ts +116 -4
  261. package/src/scripts/check_hook_bundle_composition.ts +370 -0
  262. package/src/scripts/check_kernel_edit_ratified.ts +49 -11
  263. package/src/scripts/check_memory.ts +7 -1
  264. package/src/scripts/check_release_highlights.ts +10 -0
  265. package/src/scripts/check_release_pr_shape.ts +38 -15
  266. package/src/scripts/check_standing_rule_delivery.ts +33 -13
  267. package/src/scripts/check_verify_expectation_delta.ts +521 -0
  268. package/src/scripts/code_graph/detect.ts +127 -7
  269. package/src/scripts/code_graph/foreign.ts +194 -0
  270. package/src/scripts/code_graph/query.ts +217 -4
  271. package/src/scripts/code_graph/types.ts +26 -2
  272. package/src/scripts/code_graph/validate.ts +3 -1
  273. package/src/scripts/code_graph/verbs.ts +103 -0
  274. package/src/scripts/compile_hook_manifest.ts +33 -1
  275. package/src/scripts/condense.ts +14 -27
  276. package/src/scripts/estate_exemption_shape.ts +202 -0
  277. package/src/scripts/generate_host_cost_table.ts +1 -1
  278. package/src/scripts/hook_manifest.json +1 -1
  279. package/src/scripts/hook_manifest.yaml +150 -9
  280. package/src/scripts/hooks/block_config_weakening.ts +67 -2
  281. package/src/scripts/hooks/block_plumbing_writes.ts +29 -1
  282. package/src/scripts/hooks/code_graph_context_hook.ts +252 -23
  283. package/src/scripts/hooks/concern_failure_policy.ts +280 -0
  284. package/src/scripts/hooks/dispatch_hook.ts +52 -52
  285. package/src/scripts/hooks/exit_codes.ts +2 -1
  286. package/src/scripts/hooks/host_lowering.json +1 -1
  287. package/src/scripts/hooks/host_lowering.ts +19 -1
  288. package/src/scripts/hooks/one_question_per_ask_hook.ts +41 -25
  289. package/src/scripts/hooks/payload_stub.ts +10 -1
  290. package/src/scripts/hooks/rule_inject_hook.ts +757 -63
  291. package/src/scripts/hooks/table_fingerprint.ts +21 -0
  292. package/src/scripts/hooks/telemetry_usage_hook.ts +64 -8
  293. package/src/scripts/hooks/turn_end_gate_hook.ts +84 -0
  294. package/src/scripts/hooks_status.ts +112 -0
  295. package/src/scripts/injection_scan_hook.ts +7 -0
  296. package/src/scripts/install.ts +72 -185
  297. package/src/scripts/installed_layer_report.ts +103 -0
  298. package/src/scripts/language_mirror_hook.ts +15 -6
  299. package/src/scripts/learning_sidecar.ts +119 -8
  300. package/src/scripts/lint_hook_manifest.ts +53 -0
  301. package/src/scripts/lint_major_migration_sections.ts +156 -26
  302. package/src/scripts/lint_pack_boundaries.ts +63 -5
  303. package/src/scripts/lint_rule_law_section.ts +517 -0
  304. package/src/scripts/lint_workflow_security.ts +161 -5
  305. package/src/scripts/mcp_server/consumer_tool_catalog.json +53 -0
  306. package/src/scripts/mcp_server/graph_tools.ts +72 -3
  307. package/src/scripts/measure_routing_signal.ts +54 -16
  308. package/src/scripts/measure_skill_ranker_baseline.ts +216 -8
  309. package/src/scripts/memory_learn_hook.ts +91 -6
  310. package/src/scripts/model_rule_injection.ts +52 -5
  311. package/src/scripts/project_thin_rules.ts +34 -327
  312. package/src/scripts/report_evidence_temperature.ts +657 -0
  313. package/src/scripts/report_installed_rule_links.ts +157 -0
  314. package/src/scripts/report_label_agreement.ts +233 -0
  315. package/src/scripts/report_obligation_mechanism.ts +230 -0
  316. package/src/scripts/report_single_token_triggers.ts +277 -0
  317. package/src/scripts/report_skill_ranker_confusion.ts +442 -0
  318. package/src/scripts/report_standing_payload_by_host.ts +188 -15
  319. package/src/scripts/report_verification_record_coverage.ts +306 -0
  320. package/src/scripts/rule_activation_census.ts +5 -2
  321. package/src/scripts/rule_trigger_eval.ts +9 -9
  322. package/src/scripts/schemas/review-findings.schema.json +3 -2
  323. package/src/scripts/schemas/rule.schema.json +5 -0
  324. package/src/scripts/self_review_gate.ts +62 -7
  325. package/src/scripts/skill_tools/score_skill_relevance.ts +290 -25
  326. package/src/scripts/sweep_skill_ranker_signals.ts +129 -0
  327. package/src/scripts/sync_gitignore.ts +29 -235
  328. package/src/scripts/trigger_eval_grandfather.json +0 -5
  329. package/src/server/schemas/settings.ts +10 -0
  330. package/src/shared/skillRanking.ts +117 -11
  331. package/dist/cli-delegate/chunk-JNMVLWLR.js +0 -4457
  332. package/dist/cli-delegate/chunk-YDPILCOL.js +0 -152
@@ -17,7 +17,9 @@ import { execFileSync } from 'node:child_process';
17
17
  import * as fs from 'node:fs';
18
18
  import * as path from 'node:path';
19
19
 
20
+ import { load_agent_settings } from '../_lib/agent_settings.js';
20
21
  import { hardenedSpawnEnv } from '../_lib/spawn_env.js';
22
+ import { EXT_LANG } from './types.js';
21
23
  import { validateGraph } from './validate.js';
22
24
 
23
25
  export type SourceKind = 'consumer' | 'scip' | 'native';
@@ -33,6 +35,44 @@ export interface SourceVerdict {
33
35
 
34
36
  const CONSUMER_CANDIDATES = ['graph.json', 'code-graph.json', '.code-graph/graph.json'];
35
37
 
38
+ /**
39
+ * Extra graph paths the CONSUMER declares, from `code_graph.consumer_index_paths`.
40
+ *
41
+ * The default list above stays generic on purpose — three filenames, none of
42
+ * them any particular tool's. A tool that writes its index somewhere else is
43
+ * reachable by the consumer naming the path, never by this package shipping a
44
+ * table of vendor locations: such a table is wrong the day a tool moves its
45
+ * file and wrong forever about one nobody thought of, and
46
+ * `check_no_external_sources` would fail the build for carrying it.
47
+ *
48
+ * Absolute paths and anything escaping the project root are dropped. A setting
49
+ * that could point the walk at `/etc` would be a path-traversal surface in a
50
+ * read a consumer did not ask for.
51
+ */
52
+ export function consumerCandidates(root: string): string[] {
53
+ let declared: unknown;
54
+ try {
55
+ const settings = load_agent_settings({ cwd: root }) ?? {};
56
+ const section = (settings as Record<string, unknown>)['code_graph'];
57
+ declared =
58
+ section !== null && typeof section === 'object' && !Array.isArray(section)
59
+ ? (section as Record<string, unknown>)['consumer_index_paths']
60
+ : undefined;
61
+ } catch {
62
+ declared = undefined;
63
+ }
64
+ const extra = Array.isArray(declared)
65
+ ? declared.filter(
66
+ (p): p is string =>
67
+ typeof p === 'string' &&
68
+ p !== '' &&
69
+ !path.isAbsolute(p) &&
70
+ !path.normalize(p).startsWith('..'),
71
+ )
72
+ : [];
73
+ return [...CONSUMER_CANDIDATES, ...extra.filter((p) => !CONSUMER_CANDIDATES.includes(p))];
74
+ }
75
+
36
76
  function gitLastCommitEpoch(root: string): number | null {
37
77
  try {
38
78
  const out = execFileSync('git', ['-C', root, 'log', '-1', '--format=%ct'], {
@@ -100,7 +140,7 @@ export function detectSources(root: string, nativeCache: string): SourceVerdict[
100
140
  const abs = path.resolve(root);
101
141
  const out: SourceVerdict[] = [];
102
142
 
103
- for (const rel of CONSUMER_CANDIDATES) {
143
+ for (const rel of consumerCandidates(abs)) {
104
144
  const p = path.join(abs, rel);
105
145
  if (fs.existsSync(p)) {
106
146
  const { ok, sha } = looksLikeGraph(p);
@@ -189,7 +229,12 @@ export function computeVerdict(root: string, nativeCache: string): VerdictJSON {
189
229
  export const NATIVE_CACHE_REL = path.join('agents', 'runtime', 'state', 'code-graph-v1.json');
190
230
 
191
231
  /**
192
- * The three-state staleness token every consumer of the graph reports.
232
+ * The four-state staleness token every consumer of the graph reports.
233
+ *
234
+ * `edited` is the fourth and newest (`road-to-a-graph-that-feeds-the-gate` 2.3).
235
+ * Without it an uncommitted change to an indexed file read as `fresh` — the one
236
+ * state that says "nothing stands between this answer and the tree" — which is
237
+ * the reading most likely to be wrong while someone is editing.
193
238
  *
194
239
  * Lives HERE rather than in the PreToolUse hook that first needed it
195
240
  * (`hooks/code_graph_context_hook.ts`, step 1.2). Phase 3's verbs must each
@@ -198,7 +243,73 @@ export const NATIVE_CACHE_REL = path.join('agents', 'runtime', 'state', 'code-gr
198
243
  * the engine, never the other way round. The hook re-exports this so its own
199
244
  * consumers are unaffected.
200
245
  */
201
- export type GraphState = 'absent' | 'fresh' | `behind:${number}`;
246
+ export type GraphState = 'absent' | 'fresh' | 'edited' | `behind:${number}`;
247
+
248
+ /**
249
+ * The working tree carries an uncommitted change to a file the extractor would
250
+ * index.
251
+ *
252
+ * WHY POINT-IN-TIME PORCELAIN AND NOT THE ARTIFACT'S MTIME. An mtime comparison
253
+ * looks cheaper and is wrong in the one direction that matters: restoring a
254
+ * stashed or checked-out file rewrites its mtime, so a tree put back exactly as
255
+ * the graph saw it would read stale forever and never return to `fresh`. The
256
+ * porcelain result describes a CONTENT difference, so a revert is visible as a
257
+ * revert.
258
+ *
259
+ * "Indexed paths" is read as *paths of the kind the extractor indexes* —
260
+ * `EXT_LANG`'s extensions — not as the node set of a loaded graph. Loading the
261
+ * graph to answer a freshness question would mean opening the index on every
262
+ * PreToolUse call this now runs on, which is the latency the Risk Register
263
+ * names; the extension filter answers the same question with no read. It can
264
+ * over-report (a `.ts` file the build excluded) and cannot under-report, and
265
+ * over-reporting `edited` only ever makes an answer look worth less than it is.
266
+ *
267
+ * Untracked files count. A source file that exists only in the working tree is
268
+ * precisely something the index cannot know about.
269
+ *
270
+ * `--untracked-files=all` IS LOAD-BEARING, and the default was wrong. Plain
271
+ * porcelain COLLAPSES an untracked directory to one entry — `?? src/` — which
272
+ * carries no extension, so the filter below skipped it and an entire new source
273
+ * tree read as `fresh`. A completion review reproduced exactly that, and it is
274
+ * the one direction the paragraph above promises cannot happen. The cost is
275
+ * enumerating untracked files rather than untracked directories; `.gitignore`
276
+ * still applies, so a vendored tree stays out of it.
277
+ */
278
+ function hasUncommittedIndexedEdit(root: string): boolean {
279
+ let out: string;
280
+ try {
281
+ out = execFileSync('git', ['-C', root, 'status', '--porcelain', '--untracked-files=all'], {
282
+ env: hardenedSpawnEnv(),
283
+ encoding: 'utf-8',
284
+ stdio: ['ignore', 'pipe', 'ignore'],
285
+ // Both bounds are the review's: Node's 1 MB default would throw
286
+ // ENOBUFS on a large working tree, the catch below would return
287
+ // false, and the state would read `fresh` — a second silent
288
+ // under-report on a probe that runs at PreToolUse and at every stop.
289
+ // The timeout bounds the other failure: this probe sits in front of
290
+ // a tool call, so a hung git must not hold one up.
291
+ maxBuffer: 32 * 1024 * 1024,
292
+ timeout: 5_000,
293
+ });
294
+ } catch {
295
+ // Not a repository, or the probe failed: nothing is known, and an
296
+ // unknown is reported as the state the caller already had.
297
+ return false;
298
+ }
299
+ for (const raw of out.split('\n')) {
300
+ if (raw.length < 4) continue;
301
+ // `XY <path>`; a rename or copy is `XY <old> -> <new>`, and the NEW path
302
+ // is the one on disk. Quotes appear under core.quotepath for non-ASCII.
303
+ let p = raw.slice(3);
304
+ const arrow = p.indexOf(' -> ');
305
+ if (arrow !== -1) p = p.slice(arrow + 4);
306
+ p = p.trim().replace(/^"|"$/g, '');
307
+ const dot = p.lastIndexOf('.');
308
+ if (dot === -1) continue;
309
+ if (p.slice(dot).toLowerCase() in EXT_LANG) return true;
310
+ }
311
+ return false;
312
+ }
202
313
 
203
314
  /**
204
315
  * Resolve the graph's state for `root`.
@@ -206,7 +317,10 @@ export type GraphState = 'absent' | 'fresh' | `behind:${number}`;
206
317
  * `absent` means no source at all — the silent case. A picked source whose
207
318
  * staleness is UNKNOWN reads as `fresh`, mirroring {@link computeVerdict}'s own
208
319
  * `picked.stale ? STALE : FRESH`: unknown is not treated as stale, because
209
- * inventing a commit count would be worse than reporting none.
320
+ * inventing a commit count would be worse than reporting none. A source level
321
+ * with HEAD then reads `edited` instead of `fresh` while the working tree
322
+ * carries an uncommitted change to an indexable file — see
323
+ * {@link hasUncommittedIndexedEdit} for why that probe is porcelain, not mtime.
210
324
  *
211
325
  * `nativeCache` overrides where the native graph is looked for. It exists
212
326
  * because a verb invoked with an explicit `--graph <path>` would otherwise
@@ -218,7 +332,13 @@ export type GraphState = 'absent' | 'fresh' | `behind:${number}`;
218
332
  export function graphState(root: string, nativeCache?: string): GraphState {
219
333
  const picked = pickSource(detectSources(root, nativeCache ?? path.join(root, NATIVE_CACHE_REL)));
220
334
  if (!picked) return 'absent';
221
- if (picked.stale !== true) return 'fresh';
222
- const behind = picked.commits_behind;
223
- return `behind:${typeof behind === 'number' ? behind : 0}`;
335
+ if (picked.stale === true) {
336
+ const behind = picked.commits_behind;
337
+ return `behind:${typeof behind === 'number' ? behind : 0}`;
338
+ }
339
+ // The commit count comes FIRST and wins: `behind:N` already says the index
340
+ // predates committed work, and reporting `edited` instead would hide the
341
+ // larger gap behind the smaller one. Only a graph level with HEAD has an
342
+ // uncommitted edit as its whole remaining distance.
343
+ return hasUncommittedIndexedEdit(root) ? 'edited' : 'fresh';
224
344
  }
@@ -0,0 +1,194 @@
1
+ /**
2
+ * Read a consumer's own code-graph file, in the shape another tool wrote it.
3
+ *
4
+ * The defect this closes: `detect.ts` already ACCEPTS a `links`-shaped
5
+ * graph.json — `looksLikeGraph` passes anything with `nodes[]` and
6
+ * `edges`-or-`links[]` carrying `source`/`target` — and then `query.ts`
7
+ * `loadGraph` throws on it, because `validateGraph` demands `edges`,
8
+ * `schema_version`, `source_checksum` and a per-edge `resolved_via` and
9
+ * `provider`. So a consumer who ships a graph got told it was detected and
10
+ * then watched every verb crash on it. Detected-but-unloadable is the worst
11
+ * of both answers.
12
+ *
13
+ * What this module does NOT do is make those edges trustworthy. A foreign
14
+ * relation vocabulary is another tool's, and mapping it is an interpretation:
15
+ * their `calls` may be our `calls` or may be their build graph's dependency
16
+ * arrow. So every adapted edge is tagged `resolved_via: 'foreign'`, which
17
+ * joins the GUESS set — the same set `name-lookup` and `dynamic` are in — and
18
+ * the gate verbs refuse a graph made only of guesses rather than answering
19
+ * from one.
20
+ *
21
+ * A relation this module cannot map is COUNTED and DROPPED, never guessed
22
+ * into the nearest native relation. A wrong edge that reads as a real one is
23
+ * worse than a missing edge that reads as a number.
24
+ */
25
+
26
+ import { createHash } from 'node:crypto';
27
+
28
+ import type { CodeGraph, CodeEdge, CodeNode, EdgeConfidence, Relation } from './types.js';
29
+ import { SCHEMA_VERSION } from './types.js';
30
+
31
+ /** What a foreign load produced, including what it could not use. */
32
+ export interface ForeignLoadResult {
33
+ graph: CodeGraph;
34
+ /** Edges dropped because their relation has no native counterpart. */
35
+ unmapped_relations: Record<string, number>;
36
+ /** Edges dropped because an endpoint was missing or not a string. */
37
+ malformed_edges: number;
38
+ }
39
+
40
+ /**
41
+ * Foreign relation vocabulary → ours.
42
+ *
43
+ * Deliberately small and literal. Every entry is a word whose meaning is the
44
+ * same in both vocabularies; a word that is merely SIMILAR is left out, so it
45
+ * lands in `unmapped_relations` and is reported rather than mapped on a hunch.
46
+ * This is a list of OUR relation names and their obvious synonyms, not a table
47
+ * of any particular tool's schema.
48
+ */
49
+ const RELATION_MAP: Readonly<Record<string, Relation>> = {
50
+ calls: 'calls',
51
+ call: 'calls',
52
+ invokes: 'calls',
53
+ imports: 'imports',
54
+ import: 'imports',
55
+ requires: 'imports',
56
+ uses: 'uses',
57
+ use: 'uses',
58
+ references: 'uses',
59
+ inherits: 'inherits',
60
+ extends: 'inherits',
61
+ implements: 'inherits',
62
+ member: 'member',
63
+ contains: 'member',
64
+ tests: 'tests',
65
+ test: 'tests',
66
+ };
67
+
68
+ const KINDS = new Set([
69
+ 'file',
70
+ 'class',
71
+ 'interface',
72
+ 'trait',
73
+ 'function',
74
+ 'method',
75
+ 'constant',
76
+ 'type',
77
+ 'enum',
78
+ 'skipped',
79
+ ]);
80
+
81
+ function isObject(v: unknown): v is Record<string, unknown> {
82
+ return typeof v === 'object' && v !== null && !Array.isArray(v);
83
+ }
84
+
85
+ /**
86
+ * Does this document look like a foreign graph rather than one of ours?
87
+ *
88
+ * `links` in place of `edges` is the clearest tell and is the shape
89
+ * `detect.ts` already accepts. A document carrying `edges` but no
90
+ * `schema_version` is foreign too — ours always writes one.
91
+ */
92
+ export function looksForeign(doc: unknown): boolean {
93
+ if (!isObject(doc)) return false;
94
+ if (!Array.isArray(doc['nodes'])) return false;
95
+ if (Array.isArray(doc['links'])) return true;
96
+ return Array.isArray(doc['edges']) && typeof doc['schema_version'] !== 'number';
97
+ }
98
+
99
+ /**
100
+ * Adapt a foreign graph document into the native shape.
101
+ *
102
+ * `schema_version` and `source_checksum` are SYNTHESISED: the first is ours
103
+ * by definition once the document is in our shape, and the second is a digest
104
+ * of the document itself, so a changed foreign file produces a changed
105
+ * checksum and the cache layer behaves as it does for a native graph. Neither
106
+ * is read off the foreign file — a number another tool wrote under the same
107
+ * key would mean something else.
108
+ */
109
+ export function adaptForeignGraph(doc: unknown): ForeignLoadResult | null {
110
+ if (!isObject(doc)) return null;
111
+ const rawNodes = doc['nodes'];
112
+ const rawEdges = doc['links'] ?? doc['edges'];
113
+ if (!Array.isArray(rawNodes) || !Array.isArray(rawEdges)) return null;
114
+
115
+ const nodes: CodeNode[] = [];
116
+ const ids = new Set<string>();
117
+ for (const n of rawNodes) {
118
+ if (!isObject(n)) continue;
119
+ const id = n['id'];
120
+ if (typeof id !== 'string' || id === '') continue;
121
+ const kind = typeof n['kind'] === 'string' && KINDS.has(n['kind']) ? n['kind'] : 'skipped';
122
+ const sourceFile =
123
+ typeof n['source_file'] === 'string'
124
+ ? n['source_file']
125
+ : typeof n['file'] === 'string'
126
+ ? n['file']
127
+ : '';
128
+ nodes.push({
129
+ id,
130
+ label: typeof n['label'] === 'string' ? n['label'] : id,
131
+ kind,
132
+ source_file: sourceFile,
133
+ // A foreign node rarely carries a span. An empty one is honest —
134
+ // it says "no location recorded", where a fabricated [1,1] would
135
+ // point a reader at the top of a file for no reason.
136
+ source_location: Array.isArray(n['source_location']) ? n['source_location'] : [],
137
+ } as CodeNode);
138
+ ids.add(id);
139
+ }
140
+
141
+ const edges: CodeEdge[] = [];
142
+ const unmapped: Record<string, number> = {};
143
+ let malformed = 0;
144
+ for (const e of rawEdges) {
145
+ if (!isObject(e)) {
146
+ malformed += 1;
147
+ continue;
148
+ }
149
+ const source = e['source'];
150
+ const target = e['target'];
151
+ if (typeof source !== 'string' || typeof target !== 'string' || !ids.has(source)) {
152
+ malformed += 1;
153
+ continue;
154
+ }
155
+ const rawRelation = typeof e['relation'] === 'string' ? e['relation'] : '';
156
+ const relation = RELATION_MAP[rawRelation.toLowerCase()];
157
+ if (relation === undefined) {
158
+ const key = rawRelation === '' ? '(none)' : rawRelation;
159
+ unmapped[key] = (unmapped[key] ?? 0) + 1;
160
+ continue;
161
+ }
162
+ edges.push({
163
+ source,
164
+ target,
165
+ relation,
166
+ // Not EXTRACTED: this package did not extract it and cannot say
167
+ // how the tool that did arrived at it.
168
+ confidence: 'AMBIGUOUS' as EdgeConfidence,
169
+ resolved_via: 'foreign',
170
+ provider: 'foreign',
171
+ } as CodeEdge);
172
+ }
173
+
174
+ const counts: Record<string, number> = { EXTRACTED: 0, INFERRED: 0, AMBIGUOUS: edges.length };
175
+ const graph = {
176
+ schema_version: SCHEMA_VERSION,
177
+ source_checksum: createHash('sha256')
178
+ .update(JSON.stringify(doc), 'utf-8')
179
+ .digest('hex'),
180
+ languages: [],
181
+ grammar_abi: 0,
182
+ edge_confidence_counts: counts,
183
+ suppressed_edge_counts: { dynamic_no_candidate: 0 },
184
+ nodes,
185
+ edges,
186
+ } as unknown as CodeGraph;
187
+
188
+ return { graph, unmapped_relations: unmapped, malformed_edges: malformed };
189
+ }
190
+
191
+ /** True when every edge in `g` came from a foreign file. */
192
+ export function isAllForeign(g: CodeGraph): boolean {
193
+ return g.edges.length > 0 && g.edges.every((e) => e.resolved_via === 'foreign');
194
+ }
@@ -16,8 +16,9 @@ import {
16
16
  loadSerializedFromTwin,
17
17
  openGraphIndex,
18
18
  } from './sqlite_store.js';
19
- import type { CodeEdge, CodeGraph, CodeNode } from './types.js';
19
+ import type { CodeEdge, CodeGraph, CodeNode, Relation } from './types.js';
20
20
  import { validateGraph } from './validate.js';
21
+ import { adaptForeignGraph, looksForeign } from './foreign.js';
21
22
 
22
23
  const APPROX_CHARS_PER_TOKEN = 4;
23
24
 
@@ -145,10 +146,27 @@ export function loadGraph(graphPath: string, source = graphPath): LoadedGraph {
145
146
  const fromTwin = loadSerializedFromTwin(graphPath);
146
147
  const raw = fromTwin ?? fs.readFileSync(graphPath, 'utf-8');
147
148
  const parsed = JSON.parse(raw) as unknown;
148
- const v = validateGraph(parsed);
149
+ // A consumer's own graph file, written by another tool, is DETECTED by
150
+ // `detect.ts` and used to throw here — detected-but-unloadable, the worst
151
+ // of both answers. Adapt it instead: its edges carry `resolved_via:
152
+ // 'foreign'`, which is in the guess set, so the gate verbs refuse it by
153
+ // name rather than answering from it.
154
+ let foreignAdapted = false;
155
+ let candidate = parsed;
156
+ if (looksForeign(parsed)) {
157
+ const adapted = adaptForeignGraph(parsed);
158
+ if (adapted !== null) {
159
+ candidate = adapted.graph;
160
+ foreignAdapted = true;
161
+ }
162
+ }
163
+ const v = validateGraph(candidate);
149
164
  if (!v.ok) throw new Error(`invalid graph at ${graphPath}: ${v.errors.slice(0, 3).join('; ')}`);
150
- const graph = parsed as CodeGraph;
151
- if (fromTwin === null) {
165
+ const graph = candidate as CodeGraph;
166
+ // The twin is a cache of a file's bytes keyed on its own checksum. An
167
+ // adapted graph's bytes are not the file's, so emitting one would cache a
168
+ // derivation under the source's identity.
169
+ if (fromTwin === null && !foreignAdapted) {
152
170
  emitSqliteTwin(graph, raw, graphPath);
153
171
  }
154
172
  const byId = new Map(graph.nodes.map((n) => [n.id, n]));
@@ -203,6 +221,47 @@ function resolveSeedsTiered(g: LoadedGraph, seed: string, limit = 5): SeedResolu
203
221
  return { ids: ranked.slice(0, limit).map((r) => r.id), weak: true };
204
222
  }
205
223
 
224
+ /**
225
+ * The EXACT half of the ladder above — id, then label — with no BM25 tier.
226
+ *
227
+ * `graph_node` is a node tool: every field it returns (location, degree, the
228
+ * edge lists) is a statement about ONE node, so answering about a best-guess
229
+ * node is worse than refusing. D4 of `road-to-a-graph-that-feeds-the-gate` makes
230
+ * that explicit for this tool — free-text retrieval is the row the graph lost,
231
+ * so a node answer never rests on a score.
232
+ *
233
+ * Still the same ladder and not a second resolver: this calls
234
+ * {@link resolveSeedsTiered} and reads its `weak` flag, so a change to the
235
+ * exact tiers reaches both callers.
236
+ *
237
+ * `ambiguous` carries the candidates when a LABEL resolves to several nodes.
238
+ * Picking the first would be the silent guess this function exists to avoid; the
239
+ * caller re-asks with one of the ids.
240
+ */
241
+ export function resolveExactNode(
242
+ g: LoadedGraph,
243
+ seed: string,
244
+ ): { id: string | null; ambiguous: string[]; more: boolean } {
245
+ // The ladder's default limit is 5, and inheriting it made the refusal LIE:
246
+ // a label on twelve nodes refused with "is a label on 5 nodes" and listed
247
+ // five of them with nothing saying the list was cut. A completion review
248
+ // reproduced it. Ask for one more than we will show, so truncation is
249
+ // detectable rather than invisible.
250
+ const r = resolveSeedsTiered(g, seed, AMBIGUITY_SHOWN + 1);
251
+ if (r.weak || r.ids.length === 0) return { id: null, ambiguous: [], more: false };
252
+ if (r.ids.length > 1) {
253
+ return {
254
+ id: null,
255
+ ambiguous: r.ids.slice(0, AMBIGUITY_SHOWN),
256
+ more: r.ids.length > AMBIGUITY_SHOWN,
257
+ };
258
+ }
259
+ return { id: r.ids[0] as string, ambiguous: [], more: false };
260
+ }
261
+
262
+ /** How many candidate ids an ambiguity refusal lists before it says "and more". */
263
+ const AMBIGUITY_SHOWN = 25;
264
+
206
265
  /** Resolve a free-text seed to node ids: exact id → exact label → BM25. */
207
266
  export function resolveSeeds(g: LoadedGraph, seed: string, limit = 5): string[] {
208
267
  return resolveSeedsTiered(g, seed, limit).ids;
@@ -421,3 +480,157 @@ export function mergeRecommendedReads(results: readonly QueryResult[]): Recommen
421
480
  for (const r of results) for (const rr of r.recommended_reads) merged.set(readKey(rr), rr);
422
481
  return [...merged.values()];
423
482
  }
483
+
484
+ // ---------------------------------------------------------------------------
485
+ // `node` — one node, its location, its edges, its degree (3.1)
486
+ // ---------------------------------------------------------------------------
487
+
488
+ /** One edge as the node view renders it: who, how, which way, how far. */
489
+ export interface NodeEdgeLine {
490
+ /** The node at the OTHER end — the caller for an in-edge, the callee for an out-edge. */
491
+ id: string;
492
+ label: string;
493
+ relation: Relation;
494
+ confidence: string;
495
+ hops: number;
496
+ }
497
+
498
+ export interface NodeResult {
499
+ source: string;
500
+ /** The resolved id, or `''` when nothing resolved and `refusal` is set. */
501
+ id: string;
502
+ label: string;
503
+ kind: string;
504
+ /** File + 1-based inclusive line range, or `null` for a placeholder node. */
505
+ location: RecommendedRead | null;
506
+ /** DIRECT edge counts, always 1-hop — a degree is not a property of a walk. */
507
+ in_degree: number;
508
+ out_degree: number;
509
+ in: NodeEdgeLine[];
510
+ out: NodeEdgeLine[];
511
+ truncated: boolean;
512
+ /** Set when the seed did not resolve exactly; every other field is empty. */
513
+ refusal?: string;
514
+ }
515
+
516
+ export interface NodeOptions {
517
+ direction?: 'in' | 'out' | 'both';
518
+ /** 1-3. Anything outside the range is clamped rather than refused. */
519
+ depth?: number;
520
+ /** Restrict to one relation of the closed vocabulary. */
521
+ relation?: Relation;
522
+ }
523
+
524
+ /** Hard cap per direction, so one hub node cannot return an unbounded payload. */
525
+ const NODE_EDGE_CAP = 200;
526
+
527
+ /**
528
+ * Walk one direction from `start`, recording each edge once, breadth-first.
529
+ *
530
+ * `seen` is keyed on the NODE, so a cycle terminates; the edge list can still
531
+ * carry two lines into the same node by different relations, which is a real
532
+ * distinction a caller asked for when it passed no `relation`.
533
+ */
534
+ function walkDirection(
535
+ g: LoadedGraph,
536
+ start: string,
537
+ dir: 'in' | 'out',
538
+ depth: number,
539
+ relation: Relation | undefined,
540
+ ): { lines: NodeEdgeLine[]; truncated: boolean } {
541
+ const lines: NodeEdgeLine[] = [];
542
+ const seen = new Set<string>([start]);
543
+ let frontier = [start];
544
+ let truncated = false;
545
+ for (let hop = 1; hop <= depth && frontier.length > 0; hop += 1) {
546
+ const next: string[] = [];
547
+ for (const cur of frontier) {
548
+ for (const e of (dir === 'out' ? g.out : g.in).get(cur) ?? []) {
549
+ if (relation !== undefined && e.relation !== relation) continue;
550
+ const other = dir === 'out' ? e.target : e.source;
551
+ if (lines.length >= NODE_EDGE_CAP) {
552
+ truncated = true;
553
+ break;
554
+ }
555
+ lines.push({
556
+ id: sanitizeLabel(other),
557
+ label: label(g, other),
558
+ relation: e.relation,
559
+ confidence: e.confidence,
560
+ hops: hop,
561
+ });
562
+ if (!seen.has(other)) {
563
+ seen.add(other);
564
+ next.push(other);
565
+ }
566
+ }
567
+ if (truncated) break;
568
+ }
569
+ if (truncated) break;
570
+ frontier = next;
571
+ }
572
+ return { lines, truncated };
573
+ }
574
+
575
+ /**
576
+ * `node <seed>` — what one node IS, rather than what a walk from it reaches.
577
+ *
578
+ * The five readers before it all answer a question about a neighbourhood
579
+ * (`query`, `explain`, `affected`, `path`) or about a set (`dead`, `untested`).
580
+ * None answers "where does this live, who reaches it, what does it reach, how
581
+ * connected is it" — which is the question an agent asks first and had to
582
+ * assemble from two calls and a file read.
583
+ *
584
+ * Nothing new is walked: the edge maps are the ones `affected` already uses, so
585
+ * this adds a view, not an index.
586
+ */
587
+ export function node(g: LoadedGraph, seed: string, opts: NodeOptions = {}): NodeResult {
588
+ const empty = {
589
+ source: g.source,
590
+ id: '',
591
+ label: '',
592
+ kind: '',
593
+ location: null,
594
+ in_degree: 0,
595
+ out_degree: 0,
596
+ in: [] as NodeEdgeLine[],
597
+ out: [] as NodeEdgeLine[],
598
+ truncated: false,
599
+ };
600
+ const { id, ambiguous, more } = resolveExactNode(g, seed);
601
+ if (id === null) {
602
+ return {
603
+ ...empty,
604
+ refusal:
605
+ ambiguous.length > 0
606
+ ? `'${sanitizeLabel(seed)}' is a label on ${more ? 'more than ' : ''}` +
607
+ `${String(ambiguous.length)} nodes — re-ask with one id: ` +
608
+ `${ambiguous.map((a) => sanitizeLabel(a)).join(', ')}` +
609
+ (more ? ' (list truncated)' : '')
610
+ : `'${sanitizeLabel(seed)}' matches no node id and no node label. This tool ` +
611
+ 'resolves exactly (id, then label) and does not score free text — use ' +
612
+ '`query` if a best-guess match is what you want.',
613
+ };
614
+ }
615
+ const n = g.byId.get(id);
616
+ const direction = opts.direction ?? 'both';
617
+ const depth = Math.min(3, Math.max(1, Math.trunc(opts.depth ?? 1)));
618
+ const inWalk =
619
+ direction === 'out' ? { lines: [], truncated: false } : walkDirection(g, id, 'in', depth, opts.relation);
620
+ const outWalk =
621
+ direction === 'in' ? { lines: [], truncated: false } : walkDirection(g, id, 'out', depth, opts.relation);
622
+ const degreeOf = (edges: readonly CodeEdge[] | undefined): number =>
623
+ opts.relation === undefined ? (edges?.length ?? 0) : (edges ?? []).filter((e) => e.relation === opts.relation).length;
624
+ return {
625
+ source: g.source,
626
+ id: sanitizeLabel(id),
627
+ label: label(g, id),
628
+ kind: n?.kind ?? '',
629
+ location: recommendedReadFor(g, id),
630
+ in_degree: degreeOf(g.in.get(id)),
631
+ out_degree: degreeOf(g.out.get(id)),
632
+ in: inWalk.lines,
633
+ out: outWalk.lines,
634
+ truncated: inWalk.truncated || outWalk.truncated,
635
+ };
636
+ }
@@ -66,7 +66,16 @@ export type ResolvedVia =
66
66
  | 'route-table'
67
67
  | 'test-import'
68
68
  | 'name-lookup'
69
- | 'dynamic';
69
+ | 'dynamic'
70
+ /**
71
+ * Read from a consumer's own graph file, written by another tool.
72
+ *
73
+ * Not a mechanism THIS package used — a statement that it used none. The
74
+ * relation came from someone else's vocabulary and was mapped, which is
75
+ * an interpretation, so the value belongs in the guess set below and the
76
+ * gate verbs refuse a graph made only of these.
77
+ */
78
+ | 'foreign';
70
79
 
71
80
  /**
72
81
  * WHICH engine produced the edge.
@@ -77,7 +86,17 @@ export type ResolvedVia =
77
86
  * apart. Kill register K2 forbids an external provider entering `src/`, so this
78
87
  * stays a single-member union until a decision changes that.
79
88
  */
80
- export type EdgeProvider = 'native';
89
+ export type EdgeProvider =
90
+ | 'native'
91
+ /**
92
+ * The edge was read from a graph file this package did not build.
93
+ *
94
+ * This is a DATA tag on an edge, not an engine in `src/`: no foreign
95
+ * extractor is vendored, imported or executed anywhere, and K2 is about
96
+ * the latter. The field exists precisely so a mixed graph can be told
97
+ * apart, and a consumer-shipped graph is the first case where it is.
98
+ */
99
+ | 'foreign';
81
100
 
82
101
  /**
83
102
  * The two mechanisms that are GUESSES rather than statements.
@@ -99,6 +118,11 @@ export type EdgeProvider = 'native';
99
118
  export const GUESS_RESOLVED_VIA: ReadonlySet<ResolvedVia> = new Set<ResolvedVia>([
100
119
  'name-lookup',
101
120
  'dynamic',
121
+ // The accepted-edge filter is a DENYLIST, so a new value is accepted by
122
+ // default — which is exactly backwards for this one. `foreign` must be
123
+ // listed here or a consumer's graph would silently answer the gate verbs
124
+ // as if this package had resolved its edges.
125
+ 'foreign',
102
126
  ]);
103
127
 
104
128
  /** Does this edge's target rest on a stated fact rather than a guess? */
@@ -19,8 +19,10 @@ const RESOLVED_VIA = new Set([
19
19
  'test-import',
20
20
  'name-lookup',
21
21
  'dynamic',
22
+ // Adapted from a consumer's own graph file; see `foreign.ts`.
23
+ 'foreign',
22
24
  ]);
23
- const PROVIDERS = new Set(['native']);
25
+ const PROVIDERS = new Set(['native', 'foreign']);
24
26
  const KINDS = new Set([
25
27
  'file',
26
28
  'class',