pi-smart-router 0.1.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 (310) hide show
  1. package/.pi/extensions/smart-router/command-formatters.ts +167 -0
  2. package/.pi/extensions/smart-router/commands.ts +242 -0
  3. package/.pi/extensions/smart-router/dataset-export.ts +142 -0
  4. package/.pi/extensions/smart-router/delegate-stream.ts +105 -0
  5. package/.pi/extensions/smart-router/delegation-runtime.ts +355 -0
  6. package/.pi/extensions/smart-router/extension-setup.ts +115 -0
  7. package/.pi/extensions/smart-router/fleet-bootstrap.ts +244 -0
  8. package/.pi/extensions/smart-router/index.ts +115 -0
  9. package/.pi/extensions/smart-router/package.json +3 -0
  10. package/.pi/extensions/smart-router/pi-model-scope.ts +55 -0
  11. package/.pi/extensions/smart-router/pricing-lifecycle.ts +43 -0
  12. package/.pi/extensions/smart-router/route-and-delegate.ts +562 -0
  13. package/.pi/extensions/smart-router/routing-context.ts +171 -0
  14. package/.pi/extensions/smart-router/routing-outcomes.ts +44 -0
  15. package/.pi/extensions/smart-router/session-lifecycle.ts +140 -0
  16. package/.pi/extensions/smart-router/stream-delegation.ts +75 -0
  17. package/.pi/extensions/smart-router/types.ts +90 -0
  18. package/.pi/extensions/smart-router/utils.ts +58 -0
  19. package/README.md +593 -0
  20. package/bin/pi-smart-router.mjs +79 -0
  21. package/config/.gitkeep +0 -0
  22. package/config/hydra-projection-weights.json.example +1169 -0
  23. package/config/models.yaml.example +58 -0
  24. package/config/p-success-weights.json.example +19 -0
  25. package/config/routing-calibration.json.example +1605 -0
  26. package/config/routing-centroids.json.example +1570 -0
  27. package/config/routing-clusters.yaml.example +41 -0
  28. package/data/contrib/example.json +25 -0
  29. package/dist/api/explain/router-explain.d.ts +47 -0
  30. package/dist/api/explain/router-explain.d.ts.map +1 -0
  31. package/dist/api/explain/router-explain.js +101 -0
  32. package/dist/api/explain/router-explain.js.map +1 -0
  33. package/dist/api/middleware/pi-router-middleware.d.ts +86 -0
  34. package/dist/api/middleware/pi-router-middleware.d.ts.map +1 -0
  35. package/dist/api/middleware/pi-router-middleware.js +83 -0
  36. package/dist/api/middleware/pi-router-middleware.js.map +1 -0
  37. package/dist/cli/smart-router-cli.d.ts +85 -0
  38. package/dist/cli/smart-router-cli.d.ts.map +1 -0
  39. package/dist/cli/smart-router-cli.js +293 -0
  40. package/dist/cli/smart-router-cli.js.map +1 -0
  41. package/dist/config/defaults.d.ts +7 -0
  42. package/dist/config/defaults.d.ts.map +1 -0
  43. package/dist/config/defaults.js +33 -0
  44. package/dist/config/defaults.js.map +1 -0
  45. package/dist/config/models-loader.d.ts +22 -0
  46. package/dist/config/models-loader.d.ts.map +1 -0
  47. package/dist/config/models-loader.js +51 -0
  48. package/dist/config/models-loader.js.map +1 -0
  49. package/dist/config/pi-model-mapper.d.ts +40 -0
  50. package/dist/config/pi-model-mapper.d.ts.map +1 -0
  51. package/dist/config/pi-model-mapper.js +216 -0
  52. package/dist/config/pi-model-mapper.js.map +1 -0
  53. package/dist/config/routing-clusters-loader.d.ts +35 -0
  54. package/dist/config/routing-clusters-loader.d.ts.map +1 -0
  55. package/dist/config/routing-clusters-loader.js +100 -0
  56. package/dist/config/routing-clusters-loader.js.map +1 -0
  57. package/dist/domain/delegation/delegation-context.d.ts +23 -0
  58. package/dist/domain/delegation/delegation-context.d.ts.map +1 -0
  59. package/dist/domain/delegation/delegation-context.js +73 -0
  60. package/dist/domain/delegation/delegation-context.js.map +1 -0
  61. package/dist/domain/delegation/execution-ledger.d.ts +19 -0
  62. package/dist/domain/delegation/execution-ledger.d.ts.map +1 -0
  63. package/dist/domain/delegation/execution-ledger.js +23 -0
  64. package/dist/domain/delegation/execution-ledger.js.map +1 -0
  65. package/dist/domain/delegation/output-headroom.d.ts +39 -0
  66. package/dist/domain/delegation/output-headroom.d.ts.map +1 -0
  67. package/dist/domain/delegation/output-headroom.js +78 -0
  68. package/dist/domain/delegation/output-headroom.js.map +1 -0
  69. package/dist/domain/matching/cluster-matcher.d.ts +69 -0
  70. package/dist/domain/matching/cluster-matcher.d.ts.map +1 -0
  71. package/dist/domain/matching/cluster-matcher.js +294 -0
  72. package/dist/domain/matching/cluster-matcher.js.map +1 -0
  73. package/dist/domain/matching/embedding-provider.d.ts +21 -0
  74. package/dist/domain/matching/embedding-provider.d.ts.map +1 -0
  75. package/dist/domain/matching/embedding-provider.js +42 -0
  76. package/dist/domain/matching/embedding-provider.js.map +1 -0
  77. package/dist/domain/matching/hydra-input.d.ts +18 -0
  78. package/dist/domain/matching/hydra-input.d.ts.map +1 -0
  79. package/dist/domain/matching/hydra-input.js +40 -0
  80. package/dist/domain/matching/hydra-input.js.map +1 -0
  81. package/dist/domain/matching/hydra-matcher.d.ts +103 -0
  82. package/dist/domain/matching/hydra-matcher.d.ts.map +1 -0
  83. package/dist/domain/matching/hydra-matcher.js +275 -0
  84. package/dist/domain/matching/hydra-matcher.js.map +1 -0
  85. package/dist/domain/pinning/cache-economics.d.ts +53 -0
  86. package/dist/domain/pinning/cache-economics.d.ts.map +1 -0
  87. package/dist/domain/pinning/cache-economics.js +72 -0
  88. package/dist/domain/pinning/cache-economics.js.map +1 -0
  89. package/dist/domain/pinning/loop-escalation.d.ts +41 -0
  90. package/dist/domain/pinning/loop-escalation.d.ts.map +1 -0
  91. package/dist/domain/pinning/loop-escalation.js +137 -0
  92. package/dist/domain/pinning/loop-escalation.js.map +1 -0
  93. package/dist/domain/pinning/session-pinner.d.ts +81 -0
  94. package/dist/domain/pinning/session-pinner.d.ts.map +1 -0
  95. package/dist/domain/pinning/session-pinner.js +237 -0
  96. package/dist/domain/pinning/session-pinner.js.map +1 -0
  97. package/dist/domain/pinning/sub-route-policy.d.ts +41 -0
  98. package/dist/domain/pinning/sub-route-policy.d.ts.map +1 -0
  99. package/dist/domain/pinning/sub-route-policy.js +72 -0
  100. package/dist/domain/pinning/sub-route-policy.js.map +1 -0
  101. package/dist/domain/pipeline/router-pipeline.d.ts +180 -0
  102. package/dist/domain/pipeline/router-pipeline.d.ts.map +1 -0
  103. package/dist/domain/pipeline/router-pipeline.js +807 -0
  104. package/dist/domain/pipeline/router-pipeline.js.map +1 -0
  105. package/dist/domain/pipeline/safe-default.d.ts +25 -0
  106. package/dist/domain/pipeline/safe-default.d.ts.map +1 -0
  107. package/dist/domain/pipeline/safe-default.js +41 -0
  108. package/dist/domain/pipeline/safe-default.js.map +1 -0
  109. package/dist/domain/refactor-guardrails.d.ts +48 -0
  110. package/dist/domain/refactor-guardrails.d.ts.map +1 -0
  111. package/dist/domain/refactor-guardrails.js +129 -0
  112. package/dist/domain/refactor-guardrails.js.map +1 -0
  113. package/dist/domain/routing/context-fit.d.ts +53 -0
  114. package/dist/domain/routing/context-fit.d.ts.map +1 -0
  115. package/dist/domain/routing/context-fit.js +142 -0
  116. package/dist/domain/routing/context-fit.js.map +1 -0
  117. package/dist/domain/routing/expected-cost.d.ts +63 -0
  118. package/dist/domain/routing/expected-cost.d.ts.map +1 -0
  119. package/dist/domain/routing/expected-cost.js +177 -0
  120. package/dist/domain/routing/expected-cost.js.map +1 -0
  121. package/dist/domain/routing/p-success-classifier.d.ts +130 -0
  122. package/dist/domain/routing/p-success-classifier.d.ts.map +1 -0
  123. package/dist/domain/routing/p-success-classifier.js +350 -0
  124. package/dist/domain/routing/p-success-classifier.js.map +1 -0
  125. package/dist/domain/routing/tier-features.d.ts +70 -0
  126. package/dist/domain/routing/tier-features.d.ts.map +1 -0
  127. package/dist/domain/routing/tier-features.js +174 -0
  128. package/dist/domain/routing/tier-features.js.map +1 -0
  129. package/dist/domain/routing/tool-history-guard.d.ts +32 -0
  130. package/dist/domain/routing/tool-history-guard.d.ts.map +1 -0
  131. package/dist/domain/routing/tool-history-guard.js +110 -0
  132. package/dist/domain/routing/tool-history-guard.js.map +1 -0
  133. package/dist/domain/scoring/multi-objective.d.ts +48 -0
  134. package/dist/domain/scoring/multi-objective.d.ts.map +1 -0
  135. package/dist/domain/scoring/multi-objective.js +116 -0
  136. package/dist/domain/scoring/multi-objective.js.map +1 -0
  137. package/dist/domain/triage/triage-engine.d.ts +43 -0
  138. package/dist/domain/triage/triage-engine.d.ts.map +1 -0
  139. package/dist/domain/triage/triage-engine.js +317 -0
  140. package/dist/domain/triage/triage-engine.js.map +1 -0
  141. package/dist/domain/triage/turn-envelope.d.ts +17 -0
  142. package/dist/domain/triage/turn-envelope.d.ts.map +1 -0
  143. package/dist/domain/triage/turn-envelope.js +81 -0
  144. package/dist/domain/triage/turn-envelope.js.map +1 -0
  145. package/dist/domain/types/entities.d.ts +303 -0
  146. package/dist/domain/types/entities.d.ts.map +1 -0
  147. package/dist/domain/types/entities.js +6 -0
  148. package/dist/domain/types/entities.js.map +1 -0
  149. package/dist/domain/types/index.d.ts +4 -0
  150. package/dist/domain/types/index.d.ts.map +1 -0
  151. package/dist/domain/types/index.js +2 -0
  152. package/dist/domain/types/index.js.map +1 -0
  153. package/dist/domain/types/schemas.d.ts +346 -0
  154. package/dist/domain/types/schemas.d.ts.map +1 -0
  155. package/dist/domain/types/schemas.js +241 -0
  156. package/dist/domain/types/schemas.js.map +1 -0
  157. package/dist/domain/types/store-port.d.ts +44 -0
  158. package/dist/domain/types/store-port.d.ts.map +1 -0
  159. package/dist/domain/types/store-port.js +6 -0
  160. package/dist/domain/types/store-port.js.map +1 -0
  161. package/dist/index.d.ts +39 -0
  162. package/dist/index.d.ts.map +1 -0
  163. package/dist/index.js +36 -0
  164. package/dist/index.js.map +1 -0
  165. package/dist/infra/gemini-provider.d.ts +28 -0
  166. package/dist/infra/gemini-provider.d.ts.map +1 -0
  167. package/dist/infra/gemini-provider.js +112 -0
  168. package/dist/infra/gemini-provider.js.map +1 -0
  169. package/dist/infra/telemetry.d.ts +51 -0
  170. package/dist/infra/telemetry.d.ts.map +1 -0
  171. package/dist/infra/telemetry.js +100 -0
  172. package/dist/infra/telemetry.js.map +1 -0
  173. package/dist/infrastructure/delegation/provider-error.d.ts +44 -0
  174. package/dist/infrastructure/delegation/provider-error.d.ts.map +1 -0
  175. package/dist/infrastructure/delegation/provider-error.js +179 -0
  176. package/dist/infrastructure/delegation/provider-error.js.map +1 -0
  177. package/dist/infrastructure/gateway/circuit-breaker.d.ts +68 -0
  178. package/dist/infrastructure/gateway/circuit-breaker.d.ts.map +1 -0
  179. package/dist/infrastructure/gateway/circuit-breaker.js +150 -0
  180. package/dist/infrastructure/gateway/circuit-breaker.js.map +1 -0
  181. package/dist/infrastructure/gateway/gateway-dispatch.d.ts +138 -0
  182. package/dist/infrastructure/gateway/gateway-dispatch.d.ts.map +1 -0
  183. package/dist/infrastructure/gateway/gateway-dispatch.js +308 -0
  184. package/dist/infrastructure/gateway/gateway-dispatch.js.map +1 -0
  185. package/dist/infrastructure/hardware/hardware-probe.d.ts +32 -0
  186. package/dist/infrastructure/hardware/hardware-probe.d.ts.map +1 -0
  187. package/dist/infrastructure/hardware/hardware-probe.js +199 -0
  188. package/dist/infrastructure/hardware/hardware-probe.js.map +1 -0
  189. package/dist/infrastructure/local/local-zero-tier.d.ts +47 -0
  190. package/dist/infrastructure/local/local-zero-tier.d.ts.map +1 -0
  191. package/dist/infrastructure/local/local-zero-tier.js +92 -0
  192. package/dist/infrastructure/local/local-zero-tier.js.map +1 -0
  193. package/dist/infrastructure/persistence/memory-store.d.ts +31 -0
  194. package/dist/infrastructure/persistence/memory-store.d.ts.map +1 -0
  195. package/dist/infrastructure/persistence/memory-store.js +87 -0
  196. package/dist/infrastructure/persistence/memory-store.js.map +1 -0
  197. package/dist/infrastructure/persistence/sqlite-store.d.ts +81 -0
  198. package/dist/infrastructure/persistence/sqlite-store.d.ts.map +1 -0
  199. package/dist/infrastructure/persistence/sqlite-store.js +658 -0
  200. package/dist/infrastructure/persistence/sqlite-store.js.map +1 -0
  201. package/dist/infrastructure/pricing/litellm-fetch.d.ts +46 -0
  202. package/dist/infrastructure/pricing/litellm-fetch.d.ts.map +1 -0
  203. package/dist/infrastructure/pricing/litellm-fetch.js +145 -0
  204. package/dist/infrastructure/pricing/litellm-fetch.js.map +1 -0
  205. package/dist/infrastructure/pricing/price-broker.d.ts +56 -0
  206. package/dist/infrastructure/pricing/price-broker.d.ts.map +1 -0
  207. package/dist/infrastructure/pricing/price-broker.js +133 -0
  208. package/dist/infrastructure/pricing/price-broker.js.map +1 -0
  209. package/dist/infrastructure/pricing/pricing-monitor.d.ts +27 -0
  210. package/dist/infrastructure/pricing/pricing-monitor.d.ts.map +1 -0
  211. package/dist/infrastructure/pricing/pricing-monitor.js +46 -0
  212. package/dist/infrastructure/pricing/pricing-monitor.js.map +1 -0
  213. package/dist/infrastructure/telemetry/dataset-limits.d.ts +13 -0
  214. package/dist/infrastructure/telemetry/dataset-limits.d.ts.map +1 -0
  215. package/dist/infrastructure/telemetry/dataset-limits.js +28 -0
  216. package/dist/infrastructure/telemetry/dataset-limits.js.map +1 -0
  217. package/dist/infrastructure/telemetry/dataset-recorder.d.ts +48 -0
  218. package/dist/infrastructure/telemetry/dataset-recorder.d.ts.map +1 -0
  219. package/dist/infrastructure/telemetry/dataset-recorder.js +154 -0
  220. package/dist/infrastructure/telemetry/dataset-recorder.js.map +1 -0
  221. package/dist/infrastructure/telemetry/outcome-limits.d.ts +12 -0
  222. package/dist/infrastructure/telemetry/outcome-limits.d.ts.map +1 -0
  223. package/dist/infrastructure/telemetry/outcome-limits.js +28 -0
  224. package/dist/infrastructure/telemetry/outcome-limits.js.map +1 -0
  225. package/dist/infrastructure/telemetry/outcome-recorder.d.ts +29 -0
  226. package/dist/infrastructure/telemetry/outcome-recorder.d.ts.map +1 -0
  227. package/dist/infrastructure/telemetry/outcome-recorder.js +49 -0
  228. package/dist/infrastructure/telemetry/outcome-recorder.js.map +1 -0
  229. package/dist/infrastructure/telemetry/routing-telemetry.d.ts +115 -0
  230. package/dist/infrastructure/telemetry/routing-telemetry.d.ts.map +1 -0
  231. package/dist/infrastructure/telemetry/routing-telemetry.js +529 -0
  232. package/dist/infrastructure/telemetry/routing-telemetry.js.map +1 -0
  233. package/dist/infrastructure/telemetry/telemetry-limits.d.ts +16 -0
  234. package/dist/infrastructure/telemetry/telemetry-limits.d.ts.map +1 -0
  235. package/dist/infrastructure/telemetry/telemetry-limits.js +35 -0
  236. package/dist/infrastructure/telemetry/telemetry-limits.js.map +1 -0
  237. package/package.json +110 -0
  238. package/skills/router-backlog-orchestrator/SKILL.md +183 -0
  239. package/skills/router-backlog-orchestrator/references/github-router-issue-template.md +78 -0
  240. package/skills/router-backlog-orchestrator/references/packet-from-issue.md +99 -0
  241. package/skills/router-backlog-orchestrator/references/prioritization-rubric.md +65 -0
  242. package/skills/router-backlog-orchestrator/scripts/collect-backlog.sh +44 -0
  243. package/specs/001-build-smart-router/contracts/telemetry-contrib.schema.json +151 -0
  244. package/src/api/explain/.gitkeep +0 -0
  245. package/src/api/explain/router-explain.ts +146 -0
  246. package/src/api/middleware/.gitkeep +0 -0
  247. package/src/api/middleware/pi-router-middleware.ts +169 -0
  248. package/src/cli/smart-router-cli.ts +436 -0
  249. package/src/config/.gitkeep +0 -0
  250. package/src/config/defaults.ts +35 -0
  251. package/src/config/models-loader.ts +68 -0
  252. package/src/config/pi-model-mapper.ts +285 -0
  253. package/src/config/routing-clusters-loader.ts +148 -0
  254. package/src/domain/delegation/delegation-context.ts +111 -0
  255. package/src/domain/delegation/execution-ledger.ts +34 -0
  256. package/src/domain/delegation/output-headroom.ts +131 -0
  257. package/src/domain/matching/.gitkeep +0 -0
  258. package/src/domain/matching/cluster-matcher.ts +474 -0
  259. package/src/domain/matching/embedding-provider.ts +79 -0
  260. package/src/domain/matching/hydra-input.ts +53 -0
  261. package/src/domain/matching/hydra-matcher.ts +454 -0
  262. package/src/domain/pinning/.gitkeep +0 -0
  263. package/src/domain/pinning/cache-economics.ts +128 -0
  264. package/src/domain/pinning/loop-escalation.ts +181 -0
  265. package/src/domain/pinning/session-pinner.ts +388 -0
  266. package/src/domain/pinning/sub-route-policy.ts +119 -0
  267. package/src/domain/pipeline/.gitkeep +0 -0
  268. package/src/domain/pipeline/router-pipeline.ts +1139 -0
  269. package/src/domain/pipeline/safe-default.ts +70 -0
  270. package/src/domain/refactor-guardrails.ts +202 -0
  271. package/src/domain/routing/context-fit.ts +221 -0
  272. package/src/domain/routing/expected-cost.ts +320 -0
  273. package/src/domain/routing/p-success-classifier.ts +511 -0
  274. package/src/domain/routing/tier-features.ts +290 -0
  275. package/src/domain/routing/tool-history-guard.ts +159 -0
  276. package/src/domain/scoring/.gitkeep +0 -0
  277. package/src/domain/scoring/multi-objective.ts +175 -0
  278. package/src/domain/triage/.gitkeep +0 -0
  279. package/src/domain/triage/triage-engine.ts +373 -0
  280. package/src/domain/triage/turn-envelope.ts +104 -0
  281. package/src/domain/types/.gitkeep +0 -0
  282. package/src/domain/types/entities.ts +384 -0
  283. package/src/domain/types/index.ts +41 -0
  284. package/src/domain/types/schemas.ts +289 -0
  285. package/src/domain/types/store-port.ts +59 -0
  286. package/src/index.ts +103 -0
  287. package/src/infra/gemini-provider.ts +142 -0
  288. package/src/infra/telemetry.ts +169 -0
  289. package/src/infrastructure/delegation/provider-error.ts +251 -0
  290. package/src/infrastructure/gateway/.gitkeep +0 -0
  291. package/src/infrastructure/gateway/circuit-breaker.ts +196 -0
  292. package/src/infrastructure/gateway/gateway-dispatch.ts +444 -0
  293. package/src/infrastructure/hardware/.gitkeep +0 -0
  294. package/src/infrastructure/hardware/hardware-probe.ts +276 -0
  295. package/src/infrastructure/local/.gitkeep +0 -0
  296. package/src/infrastructure/local/local-zero-tier.ts +151 -0
  297. package/src/infrastructure/persistence/.gitkeep +0 -0
  298. package/src/infrastructure/persistence/memory-store.ts +122 -0
  299. package/src/infrastructure/persistence/sqlite-store.ts +900 -0
  300. package/src/infrastructure/pricing/.gitkeep +0 -0
  301. package/src/infrastructure/pricing/litellm-fetch.ts +215 -0
  302. package/src/infrastructure/pricing/price-broker.ts +183 -0
  303. package/src/infrastructure/pricing/pricing-monitor.ts +65 -0
  304. package/src/infrastructure/telemetry/.gitkeep +0 -0
  305. package/src/infrastructure/telemetry/dataset-limits.ts +39 -0
  306. package/src/infrastructure/telemetry/dataset-recorder.ts +215 -0
  307. package/src/infrastructure/telemetry/outcome-limits.ts +42 -0
  308. package/src/infrastructure/telemetry/outcome-recorder.ts +118 -0
  309. package/src/infrastructure/telemetry/routing-telemetry.ts +824 -0
  310. package/src/infrastructure/telemetry/telemetry-limits.ts +50 -0
@@ -0,0 +1,373 @@
1
+ /**
2
+ * Triage engine — FR-003, FR-004, US2.
3
+ *
4
+ * Deterministic fast-path classifier: obvious-trivial → economical tier,
5
+ * obvious-complex → frontier tier, otherwise ambiguous for deeper stages.
6
+ *
7
+ * Pipeline: sanitize (T026, FR-004) → Aho-Corasick keyword scan (T025) →
8
+ * cyclomatic scan (T025b) → verdict.
9
+ */
10
+
11
+ // ─── Public types ─────────────────────────────────────────────────────────────
12
+
13
+ export type TriageVerdict = 'trivial' | 'complex' | 'ambiguous';
14
+
15
+ export interface TriageResult {
16
+ readonly verdict: TriageVerdict;
17
+ readonly reason_code: string;
18
+ readonly trivial_hits: number;
19
+ readonly complex_hits: number;
20
+ readonly cyclomatic_score: number;
21
+ readonly sanitized_length_delta: number;
22
+ }
23
+
24
+ // ─── Thresholds ───────────────────────────────────────────────────────────────
25
+
26
+ export const CYCLOMATIC_THRESHOLD = 15;
27
+
28
+ // ─── Adversarial Sanitization (T026, FR-004) ──────────────────────────────────
29
+
30
+ const RE_BASE64_BLOCK = /[A-Za-z0-9+/=]{64,}/g;
31
+ const RE_HEX_BLOCK = /(?:0x)?[0-9a-fA-F]{32,}/g;
32
+ const RE_HTML_TAGS = /<\/?[a-z][^>]*>/gi;
33
+ const RE_HTML_COMMENT = /<!--[\s\S]*?-->/g;
34
+ const RE_URL_ENCODED = /%[0-9A-Fa-f]{2}/g;
35
+ const RE_REPEATED_CHARS = /(.)\1{4,}/g;
36
+ const RE_MULTI_HSPACE = /[^\S\r\n]{2,}/g;
37
+ const RE_MULTI_NEWLINE = /\n{3,}/g;
38
+
39
+ /**
40
+ * Strip adversarial complexity-inflation patterns from prompt text (FR-004).
41
+ *
42
+ * Removes base64 blocks, long hex runs, HTML/XML markup, URL-encoded sequences,
43
+ * and excessive character repetition. Preserves newlines and indentation so code
44
+ * blocks remain extractable by the cyclomatic scanner.
45
+ */
46
+ export function sanitize(raw: string): string {
47
+ let text = raw;
48
+ text = text.replace(/\r\n?/g, '\n');
49
+ text = text.replace(RE_BASE64_BLOCK, ' ');
50
+ text = text.replace(RE_HEX_BLOCK, ' ');
51
+ text = text.replace(RE_HTML_COMMENT, ' ');
52
+ text = text.replace(RE_HTML_TAGS, ' ');
53
+ text = text.replace(RE_URL_ENCODED, '');
54
+ text = text.replace(RE_REPEATED_CHARS, '$1$1');
55
+ text = text.replace(RE_MULTI_HSPACE, ' ');
56
+ text = text.replace(RE_MULTI_NEWLINE, '\n\n');
57
+ return text.trim();
58
+ }
59
+
60
+ // ─── Aho-Corasick Multi-Pattern Matcher (T025) ───────────────────────────────
61
+
62
+ type KeywordSet = 'trivial' | 'complex';
63
+
64
+ interface PatternMeta {
65
+ readonly length: number;
66
+ readonly set: KeywordSet;
67
+ }
68
+
69
+ interface AhoState {
70
+ children: Map<string, number>;
71
+ fail: number;
72
+ outputs: PatternMeta[];
73
+ }
74
+
75
+ function isWordChar(ch: string): boolean {
76
+ const code = ch.charCodeAt(0);
77
+ return (
78
+ (code >= 48 && code <= 57) ||
79
+ (code >= 65 && code <= 90) ||
80
+ (code >= 97 && code <= 122) ||
81
+ code === 95
82
+ );
83
+ }
84
+
85
+ /**
86
+ * Aho-Corasick automaton for simultaneous multi-pattern matching.
87
+ *
88
+ * Builds a trie with failure links for O(n + m + z) scanning where n is text
89
+ * length, m is total pattern length, and z is the number of matches. Word-boundary
90
+ * checks prevent substring false positives (e.g. "format" in "information").
91
+ */
92
+ class AhoCorasick {
93
+ private readonly states: AhoState[];
94
+
95
+ constructor(patterns: ReadonlyArray<{ text: string; set: KeywordSet }>) {
96
+ this.states = [{ children: new Map(), fail: 0, outputs: [] }];
97
+ this.buildGoto(patterns);
98
+ this.buildFailure();
99
+ }
100
+
101
+ private buildGoto(patterns: ReadonlyArray<{ text: string; set: KeywordSet }>): void {
102
+ for (const { text, set } of patterns) {
103
+ let current = 0;
104
+ const lower = text.toLowerCase();
105
+ for (const ch of lower) {
106
+ const state = this.states[current]!;
107
+ let next = state.children.get(ch);
108
+ if (next === undefined) {
109
+ next = this.states.length;
110
+ this.states.push({ children: new Map(), fail: 0, outputs: [] });
111
+ state.children.set(ch, next);
112
+ }
113
+ current = next;
114
+ }
115
+ this.states[current]!.outputs.push({ length: lower.length, set });
116
+ }
117
+ }
118
+
119
+ private buildFailure(): void {
120
+ const queue: number[] = [];
121
+ const root = this.states[0]!;
122
+
123
+ for (const [, child] of root.children) {
124
+ this.states[child]!.fail = 0;
125
+ queue.push(child);
126
+ }
127
+
128
+ let head = 0;
129
+ while (head < queue.length) {
130
+ const r = queue[head++]!;
131
+ const rState = this.states[r]!;
132
+
133
+ for (const [ch, s] of rState.children) {
134
+ queue.push(s);
135
+ let f = rState.fail;
136
+ while (f !== 0 && !this.states[f]!.children.has(ch)) {
137
+ f = this.states[f]!.fail;
138
+ }
139
+ const target = this.states[f]!.children.get(ch);
140
+ const failTarget = target !== undefined && target !== s ? target : 0;
141
+ this.states[s]!.fail = failTarget;
142
+
143
+ const failOutputs = this.states[failTarget]!.outputs;
144
+ if (failOutputs.length > 0) {
145
+ this.states[s]!.outputs = [...this.states[s]!.outputs, ...failOutputs];
146
+ }
147
+ }
148
+ }
149
+ }
150
+
151
+ /**
152
+ * Scan text and return unique keyword hits per set.
153
+ * Word boundaries prevent substring false positives.
154
+ */
155
+ search(text: string): { trivialHits: number; complexHits: number } {
156
+ const lower = text.toLowerCase();
157
+ let current = 0;
158
+ let trivialHits = 0;
159
+ let complexHits = 0;
160
+ const seen = new Set<string>();
161
+
162
+ for (let i = 0; i < lower.length; i++) {
163
+ const ch = lower[i]!;
164
+ while (current !== 0 && !this.states[current]!.children.has(ch)) {
165
+ current = this.states[current]!.fail;
166
+ }
167
+ current = this.states[current]!.children.get(ch) ?? 0;
168
+
169
+ for (const entry of this.states[current]!.outputs) {
170
+ const start = i - entry.length + 1;
171
+
172
+ if (start > 0 && isWordChar(lower[start - 1]!)) continue;
173
+ if (i < lower.length - 1 && isWordChar(lower[i + 1]!)) continue;
174
+
175
+ const key = `${start}:${entry.length}`;
176
+ if (seen.has(key)) continue;
177
+ seen.add(key);
178
+
179
+ if (entry.set === 'trivial') trivialHits++;
180
+ else complexHits++;
181
+ }
182
+ }
183
+
184
+ return { trivialHits, complexHits };
185
+ }
186
+ }
187
+
188
+ // ─── Keyword Dictionaries ─────────────────────────────────────────────────────
189
+
190
+ export const TRIVIAL_KEYWORDS: readonly string[] = [
191
+ 'format',
192
+ 'formatting',
193
+ 'lint',
194
+ 'linting',
195
+ 'rename',
196
+ 'indent',
197
+ 'indentation',
198
+ 'prettier',
199
+ 'eslint',
200
+ 'semicolon',
201
+ 'whitespace',
202
+ 'spacing',
203
+ 'typo',
204
+ 'boilerplate',
205
+ 'template',
206
+ 'uncomment',
207
+ 'sort imports',
208
+ 'fix import',
209
+ 'fix imports',
210
+ 'add export',
211
+ 'remove unused',
212
+ 'unused import',
213
+ 'unused variable',
214
+ 'fix spacing',
215
+ 'fix whitespace',
216
+ 'simple test',
217
+ 'move file',
218
+ ];
219
+
220
+ export const COMPLEX_KEYWORDS: readonly string[] = [
221
+ 'architect',
222
+ 'architecture',
223
+ 'refactor',
224
+ 'refactoring',
225
+ 'debug',
226
+ 'debugging',
227
+ 'distributed',
228
+ 'microservice',
229
+ 'microservices',
230
+ 'concurrency',
231
+ 'concurrent',
232
+ 'deadlock',
233
+ 'race condition',
234
+ 'migration',
235
+ 'migrate',
236
+ 'scalability',
237
+ 'infrastructure',
238
+ 'optimize',
239
+ 'optimization',
240
+ 'performance tuning',
241
+ 'security audit',
242
+ 'vulnerability',
243
+ 'exploit',
244
+ 'algorithm',
245
+ 'algorithmic',
246
+ 'system design',
247
+ 'design pattern',
248
+ 'design patterns',
249
+ 'memory leak',
250
+ 'memory management',
251
+ 'state machine',
252
+ 'error handling strategy',
253
+ 'api design',
254
+ 'schema design',
255
+ ];
256
+
257
+ /** Module-level singleton — built once at import time. */
258
+ const MATCHER = new AhoCorasick([
259
+ ...TRIVIAL_KEYWORDS.map((text) => ({ text, set: 'trivial' as const })),
260
+ ...COMPLEX_KEYWORDS.map((text) => ({ text, set: 'complex' as const })),
261
+ ]);
262
+
263
+ // ─── AST Cyclomatic Scan (T025b) ─────────────────────────────────────────────
264
+
265
+ const RE_CODE_FENCE = /```[\w]*\n([\s\S]*?)```/g;
266
+ const RE_INDENTED_BLOCK = /(?:^|\n)((?:(?: {4}|\t).+(?:\n|$))+)/g;
267
+
268
+ function extractCode(text: string): string {
269
+ const blocks: string[] = [];
270
+
271
+ let m: RegExpExecArray | null;
272
+ const fenceRe = new RegExp(RE_CODE_FENCE.source, RE_CODE_FENCE.flags);
273
+ while ((m = fenceRe.exec(text)) !== null) {
274
+ if (m[1]) blocks.push(m[1]);
275
+ }
276
+
277
+ if (blocks.length === 0) {
278
+ const indentRe = new RegExp(RE_INDENTED_BLOCK.source, RE_INDENTED_BLOCK.flags);
279
+ while ((m = indentRe.exec(text)) !== null) {
280
+ if (m[1]) blocks.push(m[1]);
281
+ }
282
+ }
283
+
284
+ return blocks.join('\n');
285
+ }
286
+
287
+ const DECISION_PATTERNS: readonly RegExp[] = [
288
+ /\bif\b/g,
289
+ /\belif\b/g,
290
+ /\bfor\b/g,
291
+ /\bwhile\b/g,
292
+ /\bcase\b/g,
293
+ /\bcatch\b/g,
294
+ /&&/g,
295
+ /\|\|/g,
296
+ /\?\?/g,
297
+ ];
298
+
299
+ /**
300
+ * Estimate cyclomatic complexity of code embedded in the prompt.
301
+ * Extracts fenced or indented code blocks and counts decision points.
302
+ * Returns 1 (baseline) when no code is detected.
303
+ */
304
+ export function cyclomaticScan(text: string): number {
305
+ const code = extractCode(text);
306
+ if (code.length === 0) return 1;
307
+
308
+ let score = 1;
309
+ for (const pattern of DECISION_PATTERNS) {
310
+ const matches = code.match(new RegExp(pattern.source, 'g'));
311
+ if (matches) score += matches.length;
312
+ }
313
+ return score;
314
+ }
315
+
316
+ // ─── Triage Entry Point ──────────────────────────────────────────────────────
317
+
318
+ /**
319
+ * Classify a prompt as trivial, complex, or ambiguous for fast-path routing.
320
+ *
321
+ * Deterministic and synchronous — runs in the triage pipeline stage (FR-003).
322
+ * Sanitizes adversarial content before scoring (FR-004).
323
+ */
324
+ export function triage(promptText: string): TriageResult {
325
+ if (!promptText || promptText.trim().length === 0) {
326
+ return {
327
+ verdict: 'ambiguous',
328
+ reason_code: 'empty_prompt',
329
+ trivial_hits: 0,
330
+ complex_hits: 0,
331
+ cyclomatic_score: 0,
332
+ sanitized_length_delta: 0,
333
+ };
334
+ }
335
+
336
+ const sanitized = sanitize(promptText);
337
+ const delta = promptText.length - sanitized.length;
338
+
339
+ const { trivialHits, complexHits } = MATCHER.search(sanitized);
340
+ const cyclomatic = cyclomaticScan(sanitized);
341
+
342
+ let verdict: TriageVerdict;
343
+ let reason: string;
344
+
345
+ if (cyclomatic >= CYCLOMATIC_THRESHOLD) {
346
+ verdict = 'complex';
347
+ reason = 'cyclomatic_high';
348
+ } else if (complexHits > 0 && trivialHits === 0) {
349
+ verdict = 'complex';
350
+ reason = 'keyword_frontier';
351
+ } else if (trivialHits > 0 && complexHits === 0) {
352
+ verdict = 'trivial';
353
+ reason = 'keyword_economical';
354
+ } else if (complexHits > trivialHits) {
355
+ verdict = 'complex';
356
+ reason = 'keyword_frontier';
357
+ } else if (trivialHits > complexHits) {
358
+ verdict = 'trivial';
359
+ reason = 'keyword_economical';
360
+ } else {
361
+ verdict = 'ambiguous';
362
+ reason = 'no_fast_path';
363
+ }
364
+
365
+ return {
366
+ verdict,
367
+ reason_code: reason,
368
+ trivial_hits: trivialHits,
369
+ complex_hits: complexHits,
370
+ cyclomatic_score: cyclomatic,
371
+ sanitized_length_delta: delta,
372
+ };
373
+ }
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Turn envelope classifier — T029, <2ms budget.
3
+ *
4
+ * Derives turn_type from the messages envelope using deterministic
5
+ * heuristics (no neural inference). Falls back to 'unknown' when
6
+ * the envelope is empty or unclassifiable.
7
+ *
8
+ * Classification priority (first match wins):
9
+ * 1. tool_result — last message is role=tool
10
+ * 2. planning — planning/architecture signals in recent content
11
+ * 3. subagent — subagent/exploration context markers
12
+ * 4. main_loop — default agent loop turn (messages present)
13
+ * 5. unknown — no messages or empty envelope
14
+ */
15
+
16
+ import type { Message, TurnType } from '../types/index.js';
17
+
18
+ // ─── Constants ────────────────────────────────────────────────────────────────
19
+
20
+ const TOOL_RESULT_SIZE_THRESHOLD = 50_000;
21
+
22
+ const PLANNING_PATTERNS: readonly RegExp[] = [
23
+ /\b(?:plan|planning|architect(?:ure)?|design|refactor|migration)\b/i,
24
+ /\b(?:step\s*\d|phase\s*\d|breakdown|strategy|trade-?off)\b/i,
25
+ /^#+\s*(?:plan|design|architecture)/im,
26
+ ];
27
+
28
+ const SUBAGENT_PATTERNS: readonly RegExp[] = [
29
+ /\b(?:subagent|sub-agent|exploration|explore|search|investigate)\b/i,
30
+ /\b(?:spawned|delegat(?:e|ed|ing)|parallel\s+agent)\b/i,
31
+ /\b(?:Task|Agent)\.(?:create|spawn|launch)\b/,
32
+ ];
33
+
34
+ // ─── Classifier ───────────────────────────────────────────────────────────────
35
+
36
+ export function classifyTurnEnvelope(
37
+ messages: readonly Message[] | undefined,
38
+ ): TurnType {
39
+ if (!messages || messages.length === 0) {
40
+ return 'unknown';
41
+ }
42
+
43
+ const lastMessage = messages[messages.length - 1]!;
44
+
45
+ if (isToolResult(lastMessage)) {
46
+ return 'tool_result';
47
+ }
48
+
49
+ if (hasPlanningSignals(messages)) {
50
+ return 'planning';
51
+ }
52
+
53
+ if (hasSubagentSignals(messages)) {
54
+ return 'subagent';
55
+ }
56
+
57
+ return 'main_loop';
58
+ }
59
+
60
+ // ─── Signal detectors ─────────────────────────────────────────────────────────
61
+
62
+ function isToolResult(message: Message): boolean {
63
+ if (message.role !== 'tool') return false;
64
+ return message.content.length <= TOOL_RESULT_SIZE_THRESHOLD;
65
+ }
66
+
67
+ function hasPlanningSignals(messages: readonly Message[]): boolean {
68
+ const window = recentWindow(messages, 3);
69
+ for (const msg of window) {
70
+ if (matchesAny(msg.content, PLANNING_PATTERNS)) {
71
+ return true;
72
+ }
73
+ }
74
+ return false;
75
+ }
76
+
77
+ function hasSubagentSignals(messages: readonly Message[]): boolean {
78
+ const window = recentWindow(messages, 3);
79
+ for (const msg of window) {
80
+ if (matchesAny(msg.content, SUBAGENT_PATTERNS)) {
81
+ return true;
82
+ }
83
+ }
84
+ return false;
85
+ }
86
+
87
+ // ─── Helpers ──────────────────────────────────────────────────────────────────
88
+
89
+ function recentWindow(
90
+ messages: readonly Message[],
91
+ count: number,
92
+ ): readonly Message[] {
93
+ const start = Math.max(0, messages.length - count);
94
+ return messages.slice(start);
95
+ }
96
+
97
+ function matchesAny(text: string, patterns: readonly RegExp[]): boolean {
98
+ for (const pattern of patterns) {
99
+ if (pattern.test(text)) {
100
+ return true;
101
+ }
102
+ }
103
+ return false;
104
+ }
File without changes