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,436 @@
1
+ /**
2
+ * Smart Router CLI subcommands — library-facing command handlers.
3
+ *
4
+ * Extension wiring lives in `.pi/extensions/smart-router/commands.ts`;
5
+ * this module holds reusable subcommand logic for dogfooding and tests.
6
+ */
7
+
8
+ import { createHash } from 'node:crypto';
9
+ import { mkdirSync, writeFileSync } from 'node:fs';
10
+ import { join } from 'node:path';
11
+
12
+ import {
13
+ attachOutcomeLabelsToExport,
14
+ deriveSuccessLabel,
15
+ indexOutcomesByRequestId,
16
+ } from '../domain/routing/p-success-classifier.js';
17
+ import { SessionPinner } from '../domain/pinning/session-pinner.js';
18
+ import { DATASET_MAX_ENTRIES } from '../infrastructure/telemetry/dataset-limits.js';
19
+ import type { RoutingDatasetRecord, RoutingOutcomeRecord } from '../domain/types/index.js';
20
+ import type { StorePort } from '../domain/types/store-port.js';
21
+
22
+ export const UNPIN_SUBCOMMAND = 'unpin' as const;
23
+ export const EXPORT_TELEMETRY_CONTRIB_COMMAND = 'export telemetry-contrib' as const;
24
+
25
+ export const TELEMETRY_CONTRIB_VERSION = 1 as const;
26
+ export const TELEMETRY_CONTRIB_SCHEMA_PATH =
27
+ 'specs/001-build-smart-router/contracts/telemetry-contrib.schema.json';
28
+
29
+ export const DEFAULT_TELEMETRY_CONTRIB_EXPORT_DIR = '.pi-smart-router/exports';
30
+ export const DEFAULT_TELEMETRY_CONTRIB_EXPORT_LIMIT = DATASET_MAX_ENTRIES;
31
+
32
+ /** Keys stripped from contrib export (install-local identifiers and prompt-adjacent fields). */
33
+ export const TELEMETRY_CONTRIB_STRIP_KEYS = [
34
+ 'request_id',
35
+ 'session_id',
36
+ 'prompt_text',
37
+ 'messages',
38
+ 'prompt',
39
+ 'prompt_fingerprint',
40
+ 'pepper',
41
+ 'install_pepper',
42
+ 'dataset_pepper',
43
+ 'dataset_key',
44
+ 'pepper_key',
45
+ 'install_key',
46
+ 'candidates_json',
47
+ 'context_fit_rejected_json',
48
+ ] as const;
49
+
50
+ /** Mirrors SP-116 `CALIBRATION_CONTRIB_REJECT_KEYS` for ingest-safe export. */
51
+ export const TELEMETRY_CONTRIB_REJECT_KEYS = [
52
+ 'session_id',
53
+ 'prompt_text',
54
+ 'messages',
55
+ 'prompt',
56
+ 'prompt_fingerprint',
57
+ ] as const;
58
+
59
+ /** Mirrors SP-116 tainted key pattern for ingest-safe export. */
60
+ export const TELEMETRY_CONTRIB_TAINTED_KEY_PATTERN =
61
+ /(?:^|_)(prompt|message|messages|content|tool_calls?|secret|password|token|api_key)(?:_|$)/i;
62
+
63
+ export class TelemetryContribValidationError extends Error {
64
+ override readonly name = 'TelemetryContribValidationError';
65
+
66
+ constructor(message: string, options?: ErrorOptions) {
67
+ super(message, options);
68
+ }
69
+ }
70
+
71
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
72
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
73
+ }
74
+
75
+ function collectForbiddenContribKeys(
76
+ value: unknown,
77
+ path = '',
78
+ found: string[] = [],
79
+ ): string[] {
80
+ if (Array.isArray(value)) {
81
+ for (let i = 0; i < value.length; i++) {
82
+ collectForbiddenContribKeys(value[i], `${path}[${i}]`, found);
83
+ }
84
+ return found;
85
+ }
86
+
87
+ if (!isPlainObject(value)) {
88
+ return found;
89
+ }
90
+
91
+ for (const [key, nested] of Object.entries(value)) {
92
+ const keyPath = path ? `${path}.${key}` : key;
93
+
94
+ if (
95
+ (TELEMETRY_CONTRIB_REJECT_KEYS as readonly string[]).includes(key) ||
96
+ TELEMETRY_CONTRIB_TAINTED_KEY_PATTERN.test(key)
97
+ ) {
98
+ found.push(keyPath);
99
+ }
100
+
101
+ collectForbiddenContribKeys(nested, keyPath, found);
102
+ }
103
+
104
+ return found;
105
+ }
106
+
107
+ /** Fail closed when a contrib row contains forbidden or tainted keys (SP-116 parity). */
108
+ export function assertTelemetryContribRecordSafe(
109
+ record: unknown,
110
+ context?: string,
111
+ ): asserts record is Record<string, unknown> {
112
+ if (!isPlainObject(record)) {
113
+ const suffix = context ? ` (${context})` : '';
114
+ throw new TelemetryContribValidationError(
115
+ `Contrib record must be a JSON object${suffix}`,
116
+ );
117
+ }
118
+
119
+ const forbidden = collectForbiddenContribKeys(record);
120
+ if (forbidden.length > 0) {
121
+ const suffix = context ? ` (${context})` : '';
122
+ throw new TelemetryContribValidationError(
123
+ `Tainted contrib record rejected${suffix}: forbidden keys ${forbidden.join(', ')}`,
124
+ );
125
+ }
126
+ }
127
+
128
+ /** Strip install-local pepper fields from a validated contrib row (SP-116 parity). */
129
+ export function sanitizeTelemetryContribRecord(
130
+ record: Record<string, unknown>,
131
+ ): Record<string, unknown> {
132
+ const sanitized = { ...record };
133
+ for (const key of TELEMETRY_CONTRIB_STRIP_KEYS) {
134
+ delete sanitized[key];
135
+ }
136
+ return sanitized;
137
+ }
138
+
139
+ export type UnpinOutcome = 'cleared' | 'noop' | 'unavailable';
140
+
141
+ export interface UnpinCommandResult {
142
+ readonly outcome: UnpinOutcome;
143
+ readonly message: string;
144
+ readonly level: 'info' | 'error';
145
+ readonly previousModelId?: string;
146
+ }
147
+
148
+ export interface UnpinCommandContext {
149
+ readonly sessionId: string;
150
+ readonly sessionPinner: SessionPinner | undefined;
151
+ }
152
+
153
+ export interface ExportTelemetryContribOptions {
154
+ readonly limit?: number;
155
+ readonly cwd?: string;
156
+ readonly writeFile?: boolean;
157
+ }
158
+
159
+ export interface ExportTelemetryContribResult {
160
+ readonly path: string | null;
161
+ readonly recordCount: number;
162
+ readonly json: string;
163
+ }
164
+
165
+ export interface TelemetryContribExportContext {
166
+ readonly store: StorePort;
167
+ readonly cwd: string;
168
+ readonly limit: number;
169
+ }
170
+
171
+ /** Returns true when args invoke `/smart-router unpin`. */
172
+ export function isUnpinInvocation(args: string): boolean {
173
+ const tokens = args.trim().split(/\s+/).filter(Boolean);
174
+ return tokens.length === 1 && tokens[0] === UNPIN_SUBCOMMAND;
175
+ }
176
+
177
+ /** Returns true when args invoke `export telemetry-contrib`. */
178
+ export function isExportTelemetryContribInvocation(args: string): boolean {
179
+ const tokens = args.trim().split(/\s+/).filter(Boolean);
180
+ return (
181
+ tokens.length >= 2 &&
182
+ tokens[0] === 'export' &&
183
+ tokens[1] === 'telemetry-contrib'
184
+ );
185
+ }
186
+
187
+ export function parseExportTelemetryContribArgs(args: string): {
188
+ readonly limit: number;
189
+ } {
190
+ if (!isExportTelemetryContribInvocation(args)) {
191
+ throw new Error(
192
+ 'Usage: export telemetry-contrib [--limit N]',
193
+ );
194
+ }
195
+
196
+ const tokens = args.trim().split(/\s+/).filter(Boolean);
197
+ let limit = DEFAULT_TELEMETRY_CONTRIB_EXPORT_LIMIT;
198
+
199
+ for (let i = 2; i < tokens.length; i++) {
200
+ const token = tokens[i]!;
201
+ if (token === '--limit') {
202
+ const next = tokens[i + 1];
203
+ if (!next) {
204
+ throw new Error('Usage: export telemetry-contrib [--limit N]');
205
+ }
206
+ limit = parsePositiveLimit(next);
207
+ i++;
208
+ continue;
209
+ }
210
+ if (token.startsWith('--limit=')) {
211
+ limit = parsePositiveLimit(token.slice('--limit='.length));
212
+ continue;
213
+ }
214
+ throw new Error(`Unknown argument: ${token}`);
215
+ }
216
+
217
+ return { limit };
218
+ }
219
+
220
+ function parsePositiveLimit(raw: string): number {
221
+ const parsed = Number.parseInt(raw, 10);
222
+ if (!Number.isFinite(parsed) || parsed <= 0) {
223
+ throw new Error('Usage: export telemetry-contrib [--limit N]');
224
+ }
225
+ return Math.min(parsed, DEFAULT_TELEMETRY_CONTRIB_EXPORT_LIMIT);
226
+ }
227
+
228
+ /**
229
+ * Clear the current session pin via SessionPinner.breakPin().
230
+ * Does not modify SessionPinner break rules — operator-initiated unpin only.
231
+ */
232
+ export function executeUnpinCommand(ctx: UnpinCommandContext): UnpinCommandResult {
233
+ const { sessionId, sessionPinner } = ctx;
234
+
235
+ if (!sessionPinner) {
236
+ return {
237
+ outcome: 'unavailable',
238
+ message: 'Session pinner unavailable.',
239
+ level: 'error',
240
+ };
241
+ }
242
+
243
+ const pin = sessionPinner.getPin(sessionId);
244
+ if (!pin) {
245
+ return {
246
+ outcome: 'noop',
247
+ message: 'No session pin to clear.',
248
+ level: 'info',
249
+ };
250
+ }
251
+
252
+ sessionPinner.breakPin(sessionId);
253
+
254
+ return {
255
+ outcome: 'cleared',
256
+ previousModelId: pin.pinned_model_id,
257
+ message: `Cleared session pin (was ${pin.pinned_model_id}). Next request will run full routing.`,
258
+ level: 'info',
259
+ };
260
+ }
261
+
262
+ export function hashSessionIdForContribExport(sessionId: string): string {
263
+ return createHash('sha256').update(sessionId).digest('hex');
264
+ }
265
+
266
+ function resolveSessionIdHash(
267
+ record: RoutingDatasetRecord,
268
+ outcomes: readonly RoutingOutcomeRecord[],
269
+ ): string {
270
+ const sessionId = outcomes[0]?.session_id;
271
+ if (typeof sessionId === 'string' && sessionId.length > 0) {
272
+ return hashSessionIdForContribExport(sessionId);
273
+ }
274
+
275
+ const raw = record as RoutingDatasetRecord & { session_id?: string };
276
+ if (typeof raw.session_id === 'string' && raw.session_id.length > 0) {
277
+ return hashSessionIdForContribExport(raw.session_id);
278
+ }
279
+
280
+ return hashSessionIdForContribExport('');
281
+ }
282
+
283
+ /** Map a dataset row plus linked outcomes to a privacy-safe telemetry contrib record. */
284
+ export function toTelemetryContribRecord(
285
+ record: RoutingDatasetRecord,
286
+ outcomes: readonly RoutingOutcomeRecord[] = [],
287
+ ): Record<string, unknown> {
288
+ const { success, outcome_signals } = deriveSuccessLabel(outcomes);
289
+
290
+ const contrib: Record<string, unknown> = {
291
+ version: TELEMETRY_CONTRIB_VERSION,
292
+ timestamp: record.timestamp,
293
+ session_id_hash: resolveSessionIdHash(record, outcomes),
294
+ turn_type: record.turn_type,
295
+ stage: record.stage,
296
+ reason_code: record.reason_code,
297
+ selected_model_id: record.selected_model_id,
298
+ tier: record.tier,
299
+ routing_latency_ms: record.routing_latency_ms,
300
+ estimated_cost_usd: record.estimated_cost_usd,
301
+ estimated_input_tokens: record.estimated_input_tokens,
302
+ has_tool_context: record.has_tool_context,
303
+ compaction_flag: record.compaction_flag,
304
+ triage_verdict: record.triage_verdict,
305
+ triage_reason_code: record.triage_reason_code,
306
+ triage_cyclomatic_score: record.triage_cyclomatic_score,
307
+ triage_trivial_hits: record.triage_trivial_hits,
308
+ triage_complex_hits: record.triage_complex_hits,
309
+ triage_sanitized_length_delta: record.triage_sanitized_length_delta,
310
+ requirement_reasoning: record.requirement_reasoning,
311
+ requirement_code_gen: record.requirement_code_gen,
312
+ requirement_tool_use: record.requirement_tool_use,
313
+ cluster_id: record.cluster_id,
314
+ cluster_similarity: record.cluster_similarity,
315
+ cluster_margin: record.cluster_margin,
316
+ low_intensity_score: record.low_intensity_score,
317
+ tier_hint: record.tier_hint,
318
+ p_success_cheap: record.p_success_cheap,
319
+ local_eligible_reason: record.local_eligible_reason,
320
+ tier_selection_reason_code: record.tier_selection_reason_code,
321
+ success_label: success,
322
+ outcome_signals,
323
+ };
324
+
325
+ for (const key of TELEMETRY_CONTRIB_STRIP_KEYS) {
326
+ delete contrib[key];
327
+ }
328
+
329
+ return sanitizeTelemetryContribRecord(contrib);
330
+ }
331
+
332
+ export function formatTelemetryContribJson(
333
+ records: readonly Record<string, unknown>[],
334
+ ): string {
335
+ if (records.length === 0) {
336
+ return '[]';
337
+ }
338
+ return JSON.stringify(records, null, 2);
339
+ }
340
+
341
+ export function formatTelemetryContribJsonl(
342
+ records: readonly Record<string, unknown>[],
343
+ ): string {
344
+ if (records.length === 0) {
345
+ return '';
346
+ }
347
+ return records.map((record) => JSON.stringify(record)).join('\n') + '\n';
348
+ }
349
+
350
+ export function formatTelemetryContribExportTimestamp(date: Date = new Date()): string {
351
+ return date.toISOString().replace(/[:.]/g, '-');
352
+ }
353
+
354
+ export function getTelemetryContribExportPath(cwd: string, timestamp: string): string {
355
+ return join(
356
+ cwd,
357
+ DEFAULT_TELEMETRY_CONTRIB_EXPORT_DIR,
358
+ `telemetry-contrib-${timestamp}.json`,
359
+ );
360
+ }
361
+
362
+ /** Defense-in-depth validation using SP-116 contrib safety checks. */
363
+ export function validateTelemetryContribRecord(
364
+ record: unknown,
365
+ context?: string,
366
+ ): Record<string, unknown> {
367
+ assertTelemetryContribRecordSafe(record, context);
368
+ return sanitizeTelemetryContribRecord(record);
369
+ }
370
+
371
+ export function buildTelemetryContribRecords(
372
+ datasetRecords: readonly RoutingDatasetRecord[],
373
+ outcomeRecords: readonly RoutingOutcomeRecord[] = [],
374
+ ): Record<string, unknown>[] {
375
+ const outcomesByRequest = indexOutcomesByRequestId(outcomeRecords);
376
+
377
+ return datasetRecords.map((record) => {
378
+ const linkedOutcomes = outcomesByRequest.get(record.request_id) ?? [];
379
+ const contrib = toTelemetryContribRecord(record, linkedOutcomes);
380
+ validateTelemetryContribRecord(contrib, record.request_id);
381
+ return contrib;
382
+ });
383
+ }
384
+
385
+ export async function exportTelemetryContrib(
386
+ ctx: TelemetryContribExportContext,
387
+ options?: Pick<ExportTelemetryContribOptions, 'writeFile'>,
388
+ ): Promise<ExportTelemetryContribResult> {
389
+ const datasetRecords = await ctx.store.listDatasetRecords({ limit: ctx.limit });
390
+ const outcomeRecords = await ctx.store.listOutcomeRecords({ limit: ctx.limit });
391
+ const records = buildTelemetryContribRecords(datasetRecords, outcomeRecords);
392
+ const json = formatTelemetryContribJson(records);
393
+
394
+ if (records.length === 0 || options?.writeFile === false) {
395
+ return { path: null, recordCount: records.length, json };
396
+ }
397
+
398
+ const timestamp = formatTelemetryContribExportTimestamp();
399
+ const exportDir = join(ctx.cwd, DEFAULT_TELEMETRY_CONTRIB_EXPORT_DIR);
400
+ mkdirSync(exportDir, { recursive: true });
401
+ const exportPath = getTelemetryContribExportPath(ctx.cwd, timestamp);
402
+ writeFileSync(exportPath, `${json}\n`, 'utf8');
403
+
404
+ return { path: exportPath, recordCount: records.length, json };
405
+ }
406
+
407
+ /** Join dataset export shape with outcome labels, then strip to contrib-safe fields. */
408
+ export function datasetExportRowToTelemetryContrib(
409
+ exportRecord: Record<string, unknown>,
410
+ outcomes: readonly RoutingOutcomeRecord[],
411
+ ): Record<string, unknown> {
412
+ const joined = attachOutcomeLabelsToExport(exportRecord, outcomes);
413
+ const contrib: Record<string, unknown> = {
414
+ version: TELEMETRY_CONTRIB_VERSION,
415
+ ...joined,
416
+ };
417
+
418
+ for (const key of TELEMETRY_CONTRIB_STRIP_KEYS) {
419
+ delete contrib[key];
420
+ }
421
+
422
+ if (typeof exportRecord.session_id_hash === 'string') {
423
+ contrib.session_id_hash = exportRecord.session_id_hash;
424
+ } else if (typeof exportRecord.session_id === 'string') {
425
+ contrib.session_id_hash = hashSessionIdForContribExport(exportRecord.session_id);
426
+ }
427
+
428
+ delete contrib.session_id;
429
+
430
+ return validateTelemetryContribRecord(contrib);
431
+ }
432
+
433
+ export const smartRouterCliSubcommands = {
434
+ unpin: executeUnpinCommand,
435
+ exportTelemetryContrib,
436
+ } as const;
File without changes
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Operator configuration defaults (FR-021).
3
+ * Values sourced from specs/001-build-smart-router/data-model.md § Configuration (Operator).
4
+ */
5
+
6
+ import type { OperatorConfig } from '../domain/types/schemas.js';
7
+ import { DEFAULT_LOW_INTENSITY_WEIGHTS } from '../domain/routing/tier-features.js';
8
+
9
+ export const DEFAULT_OPERATOR_CONFIG: Readonly<OperatorConfig> = {
10
+ frugality: {
11
+ lambda_cost: 0.5,
12
+ lambda_latency: 0.1,
13
+ lambda_verbosity: 0.15,
14
+ },
15
+ loop_escalation: {
16
+ threshold: 3,
17
+ },
18
+ pricing: {
19
+ staleness_days: 14,
20
+ },
21
+ local: {
22
+ min_memory_gb_full: 16,
23
+ min_memory_gb_classification: 8,
24
+ battery_threshold_pct: 20,
25
+ },
26
+ hydra: {
27
+ artifact_cache_path: '.pi-smart-router/models/',
28
+ },
29
+ low_intensity: {
30
+ weights: DEFAULT_LOW_INTENSITY_WEIGHTS,
31
+ high_threshold: 0.65,
32
+ low_threshold: 0.35,
33
+ p_success_alpha: 0.5,
34
+ },
35
+ } as const;
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Fleet catalog loader — reads models.yaml and validates against ModelProfileSchema.
3
+ * Maps to T011 in the routing pipeline spec.
4
+ */
5
+
6
+ import { readFileSync } from 'node:fs';
7
+ import { resolve } from 'node:path';
8
+
9
+ import { parse as parseYaml } from 'yaml';
10
+ import { z } from 'zod';
11
+
12
+ import { ModelProfileSchema } from '../domain/types/schemas.js';
13
+ import type { ModelProfile } from '../domain/types/index.js';
14
+
15
+ const FleetCatalogSchema = z.object({
16
+ models: z.array(ModelProfileSchema).min(1, 'Fleet catalog must contain at least one model'),
17
+ });
18
+
19
+ export interface FleetCatalog {
20
+ readonly models: readonly ModelProfile[];
21
+ }
22
+
23
+ export interface LoadModelsOptions {
24
+ readonly filePath?: string;
25
+ }
26
+
27
+ /**
28
+ * Load and validate the fleet model catalog from a YAML file.
29
+ *
30
+ * @throws {ModelsLoaderError} when the file cannot be read or validation fails.
31
+ */
32
+ export function loadModels(options?: LoadModelsOptions): FleetCatalog {
33
+ const filePath = options?.filePath ?? resolve('config', 'models.yaml');
34
+
35
+ let raw: string;
36
+ try {
37
+ raw = readFileSync(filePath, 'utf8');
38
+ } catch (err: unknown) {
39
+ const message = err instanceof Error ? err.message : String(err);
40
+ throw new ModelsLoaderError(`Failed to read models file: ${message}`, { cause: err });
41
+ }
42
+
43
+ let parsed: unknown;
44
+ try {
45
+ parsed = parseYaml(raw);
46
+ } catch (err: unknown) {
47
+ const message = err instanceof Error ? err.message : String(err);
48
+ throw new ModelsLoaderError(`Failed to parse YAML: ${message}`, { cause: err });
49
+ }
50
+
51
+ const result = FleetCatalogSchema.safeParse(parsed);
52
+ if (!result.success) {
53
+ const issues = result.error.issues
54
+ .map((issue) => ` - ${issue.path.join('.')}: ${issue.message}`)
55
+ .join('\n');
56
+ throw new ModelsLoaderError(`Invalid fleet catalog:\n${issues}`, { cause: result.error });
57
+ }
58
+
59
+ return { models: result.data.models };
60
+ }
61
+
62
+ export class ModelsLoaderError extends Error {
63
+ override readonly name = 'ModelsLoaderError';
64
+
65
+ constructor(message: string, options?: ErrorOptions) {
66
+ super(message, options);
67
+ }
68
+ }