failproofai 1.0.7 → 1.0.8

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 (268) hide show
  1. package/.next/standalone/.next/BUILD_ID +1 -1
  2. package/.next/standalone/.next/build-manifest.json +3 -3
  3. package/.next/standalone/.next/prerender-manifest.json +3 -3
  4. package/.next/standalone/.next/required-server-files.json +1 -1
  5. package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
  6. package/.next/standalone/.next/server/app/_global-error/page.js +4 -4
  7. package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
  8. package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  9. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  10. package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
  11. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +6 -6
  12. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
  13. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  14. package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
  15. package/.next/standalone/.next/server/app/_not-found/page.js +4 -4
  16. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  17. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  18. package/.next/standalone/.next/server/app/_not-found.html +1 -1
  19. package/.next/standalone/.next/server/app/_not-found.rsc +15 -15
  20. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +15 -15
  21. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +14 -14
  22. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  23. package/.next/standalone/.next/server/app/api/audit/invite/route.js.nft.json +1 -1
  24. package/.next/standalone/.next/server/app/api/audit/run/route.js +8 -7
  25. package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
  26. package/.next/standalone/.next/server/app/api/audit/status/route.js.nft.json +1 -1
  27. package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
  28. package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
  29. package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
  30. package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
  31. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
  32. package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +2 -2
  33. package/.next/standalone/.next/server/app/audit/page.js +9 -7
  34. package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
  35. package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
  36. package/.next/standalone/.next/server/app/index.html +1 -1
  37. package/.next/standalone/.next/server/app/index.rsc +15 -15
  38. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +14 -14
  39. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +15 -15
  40. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  41. package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
  42. package/.next/standalone/.next/server/app/page.js +6 -6
  43. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  44. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  45. package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +14 -14
  46. package/.next/standalone/.next/server/app/policies/page.js +11 -10
  47. package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
  48. package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
  49. package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
  50. package/.next/standalone/.next/server/app/project/[name]/page.js +8 -7
  51. package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
  52. package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
  53. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
  54. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
  55. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js +6 -6
  56. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
  57. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
  58. package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
  59. package/.next/standalone/.next/server/app/projects/page.js +7 -6
  60. package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
  61. package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  62. package/.next/standalone/.next/server/app/settings/page/server-reference-manifest.json +52 -8
  63. package/.next/standalone/.next/server/app/settings/page.js +12 -7
  64. package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
  65. package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  66. package/.next/standalone/.next/server/chunks/{[externals]__1msfs-h._.js → [externals]__19_pzeq._.js} +1 -1
  67. package/.next/standalone/.next/server/chunks/{[externals]__1_bftcl._.js → [externals]__20kzpkf._.js} +1 -1
  68. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0o07qi9._.js +1 -1
  69. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1ttrwnd._.js +22 -0
  70. package/.next/standalone/.next/server/chunks/_09dz7xv._.js +21 -21
  71. package/.next/standalone/.next/server/chunks/_0tovk6q._.js +1 -1
  72. package/.next/standalone/.next/server/chunks/_0trp3yc._.js +1 -1
  73. package/.next/standalone/.next/server/chunks/_1ek68ln._.js +16 -16
  74. package/.next/standalone/.next/server/chunks/{_1c3k-8x._.js → _1q5i8mb._.js} +2 -2
  75. package/.next/standalone/.next/server/chunks/lib_16xa545._.js +3 -0
  76. package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
  77. package/.next/standalone/.next/server/chunks/src_hooks_1aveq0u._.js +5 -0
  78. package/.next/standalone/.next/server/chunks/src_hooks_1eem5a7._.js +3 -0
  79. package/.next/standalone/.next/server/chunks/src_hooks_custom-hooks-loader_ts_0lnb3n3._.js +4 -2
  80. package/.next/standalone/.next/server/chunks/ssr/[externals]__0ohnuzs._.js +3 -0
  81. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__01bmjsj._.js +3 -0
  82. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__04usis8._.js +4 -0
  83. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__02npjtd._.js → [root-of-the-server]__056wjo4._.js} +3 -3
  84. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__059yza8._.js +3 -0
  85. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__06pflha._.js +3 -0
  86. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0eip4_k._.js +22 -0
  87. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0p-5p8u._.js → [root-of-the-server]__0n0xg95._.js} +3 -3
  88. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0qcb0mg._.js +3 -0
  89. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__013jr2b._.js → [root-of-the-server]__0rwtwpm._.js} +3 -3
  90. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0yrsbd_._.js +3 -0
  91. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0da85px._.js → [root-of-the-server]__11mayhe._.js} +3 -3
  92. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__12e7nhs._.js +3 -0
  93. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__13d-wb6._.js +3 -0
  94. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__13t1zkw._.js +3 -0
  95. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__15578wp._.js +4 -0
  96. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__19evfi8._.js +3 -0
  97. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1dinjii._.js +3 -0
  98. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1m_svbe._.js +4 -0
  99. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0cxe_2_._.js → [root-of-the-server]__1mf3zp6._.js} +3 -3
  100. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0ftmoxc._.js → [root-of-the-server]__1pprgri._.js} +3 -3
  101. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1p2otjt._.js → [root-of-the-server]__1q4p5b8._.js} +3 -3
  102. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1qiz0e4._.js +3 -0
  103. package/.next/standalone/.next/server/chunks/ssr/{_166t73i._.js → _0-vcssj._.js} +1 -1
  104. package/.next/standalone/.next/server/chunks/ssr/_042cgl1._.js +3 -0
  105. package/.next/standalone/.next/server/chunks/ssr/_08x1r5t._.js +1 -1
  106. package/.next/standalone/.next/server/chunks/ssr/_0bqoto4._.js +3 -0
  107. package/.next/standalone/.next/server/chunks/ssr/_0uyu3jf._.js +3 -0
  108. package/.next/standalone/.next/server/chunks/ssr/_13kfn90._.js +23 -0
  109. package/.next/standalone/.next/server/chunks/ssr/{_1es2j7i._.js → _1gb0ifp._.js} +5 -5
  110. package/.next/standalone/.next/server/chunks/ssr/{_1_qswah._.js → _1q46vxx._.js} +2 -2
  111. package/.next/standalone/.next/server/chunks/ssr/_1zopuov._.js +1 -1
  112. package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_policies_page_actions_1sp2-yo.js +13 -13
  113. package/.next/standalone/.next/server/chunks/ssr/app_actions_get-scheduled-audit_ts_0ei9sni._.js +3 -0
  114. package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +1 -1
  115. package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
  116. package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +2 -2
  117. package/.next/standalone/.next/server/chunks/ssr/app_settings_02tf1h4._.js +3 -0
  118. package/.next/standalone/.next/server/chunks/ssr/{node_modules_next_dist_0drixxt._.js → node_modules_next_dist_0w6mzq5._.js} +4 -4
  119. package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_18_d8l1._.js +151 -0
  120. package/.next/standalone/.next/server/chunks/ssr/src_hooks_0-q0umm._.js +3 -0
  121. package/.next/standalone/.next/server/chunks/ssr/src_hooks_06kzv9d._.js +12 -0
  122. package/.next/standalone/.next/server/chunks/ssr/src_hooks_0g194sy._.js +3 -0
  123. package/.next/standalone/.next/server/chunks/ssr/src_hooks_1cv9_c4._.js +4 -2
  124. package/.next/standalone/.next/server/chunks/ssr/src_hooks_1kx9e0d._.js +3 -0
  125. package/.next/standalone/.next/server/chunks/ssr/src_hooks_effective-reviewers_ts_1h4wtvo._.js +5 -0
  126. package/.next/standalone/.next/server/chunks/ssr/src_hooks_fp-config_ts_04t589g._.js +1 -1
  127. package/.next/standalone/.next/server/chunks/ssr/src_hooks_fp-home_ts_0je3xkv._.js +1 -1
  128. package/.next/standalone/.next/server/chunks/ssr/src_hooks_pack-cli_ts_0t7me65._.js +1 -1
  129. package/.next/standalone/.next/server/chunks/ssr/src_hooks_semantic_pack-policies_ts_0gh_bu_._.js +3 -0
  130. package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
  131. package/.next/standalone/.next/server/pages/404.html +1 -1
  132. package/.next/standalone/.next/server/pages/500.html +1 -1
  133. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  134. package/.next/standalone/.next/server/server-reference-manifest.json +67 -23
  135. package/.next/standalone/.next/static/chunks/06dnzbolj00mc.js +6 -0
  136. package/.next/standalone/.next/static/chunks/0bhidk90e-07f.css +2 -0
  137. package/.next/standalone/.next/static/chunks/{2mdh397ghgnvv.js → 0m-9d6yn9hx4j.js} +1 -1
  138. package/.next/standalone/.next/static/chunks/{0__8a7m868fvf.js → 0wbjy0zo-m7is.js} +1 -1
  139. package/.next/standalone/.next/static/chunks/16f3fa-lx38hk.js +1 -0
  140. package/.next/standalone/.next/static/chunks/{0o6qlkgubtoex.js → 21uv-uusw329x.js} +1 -1
  141. package/.next/standalone/.next/static/chunks/2g2tki08kdhie.js +1 -0
  142. package/.next/standalone/.next/static/chunks/2qdpj67x6ifk_.js +69 -0
  143. package/.next/standalone/.next/static/chunks/2qv4hshejedtx.css +1 -0
  144. package/.next/standalone/.next/static/chunks/3-nbtkhg9y-1j.js +1 -0
  145. package/.next/standalone/.next/static/chunks/355km0ihuqo1p.js +1 -0
  146. package/.next/standalone/.next/static/chunks/{13i7-9is-vhys.js → 3c3qbmosdjl6w.js} +1 -1
  147. package/.next/standalone/PROBE-FOLLOWUP.md +186 -0
  148. package/.next/standalone/app/actions/get-jev-config.ts +604 -0
  149. package/.next/standalone/app/actions/pack-actions.ts +12 -0
  150. package/.next/standalone/app/actions/update-jev-config.ts +569 -0
  151. package/.next/standalone/app/components/jev-notices.tsx +96 -0
  152. package/.next/standalone/app/policies/hooks-client.tsx +14 -3
  153. package/.next/standalone/app/settings/jev-panel.tsx +630 -0
  154. package/.next/standalone/app/settings/page.tsx +20 -1
  155. package/.next/standalone/app/settings/settings-client.tsx +27 -1
  156. package/.next/standalone/app/settings/settings.css +79 -0
  157. package/.next/standalone/fp-cloud-cli/CHANGELOG.md +22 -4
  158. package/.next/standalone/fp-cloud-cli/fp_cli/client.py +14 -1
  159. package/.next/standalone/fp-cloud-cli/fp_cli/commands/keys_cmds.py +17 -3
  160. package/.next/standalone/fp-cloud-cli/fp_cli/commands/policies_cmds.py +3 -0
  161. package/.next/standalone/fp-cloud-cli/fp_cli/permissions.py +22 -0
  162. package/.next/standalone/fp-cloud-cli/fp_cli/policy_check.py +169 -0
  163. package/.next/standalone/fp-cloud-cli/skill/references/commands.md +1 -1
  164. package/.next/standalone/fp-cloud-cli/tests/test_keys_queries.py +65 -0
  165. package/.next/standalone/fp-cloud-cli/tests/test_policy_check.py +61 -0
  166. package/.next/standalone/package.json +10 -10
  167. package/.next/standalone/sdk/python/CHANGELOG.md +7 -0
  168. package/.next/standalone/sdk/python/failproofai_sdk/_version.py +1 -1
  169. package/.next/standalone/sdk/typescript/CHANGELOG.md +18 -1
  170. package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/package-lock.json +0 -274
  171. package/.next/standalone/sdk/typescript/scripts/release.mjs +30 -0
  172. package/.next/standalone/server.js +1 -1
  173. package/README.md +3 -2
  174. package/bin/failproofai.mjs +148 -4
  175. package/dist/cli.mjs +18599 -9478
  176. package/dist/index.js +19 -1
  177. package/dist/worker.mjs +10500 -3773
  178. package/package.json +10 -10
  179. package/pi-extension/index.ts +11 -0
  180. package/scripts/build-policy-pack.mjs +53 -2
  181. package/src/audit/features.ts +3 -2
  182. package/src/hooks/builtin-policies.ts +195 -8
  183. package/src/hooks/cloud-connection.ts +190 -1
  184. package/src/hooks/cloud-enrollment-cli.ts +57 -8
  185. package/src/hooks/cloud-introspect.ts +6 -0
  186. package/src/hooks/cloud-managed-policies.ts +22 -0
  187. package/src/hooks/configure-wizard.ts +26 -6
  188. package/src/hooks/custom-hooks-loader.ts +98 -10
  189. package/src/hooks/custom-hooks-registry.ts +45 -1
  190. package/src/hooks/effective-reviewers.ts +243 -0
  191. package/src/hooks/first-run-gate.ts +5 -0
  192. package/src/hooks/flush-cli.ts +35 -8
  193. package/src/hooks/fp-config.ts +266 -1
  194. package/src/hooks/fp-home.ts +23 -0
  195. package/src/hooks/fp-reset.ts +22 -4
  196. package/src/hooks/handler.ts +303 -7
  197. package/src/hooks/hook-activity-store.ts +118 -6
  198. package/src/hooks/hook-telemetry.ts +41 -0
  199. package/src/hooks/jev-activity.ts +385 -0
  200. package/src/hooks/jev-cli.ts +2172 -0
  201. package/src/hooks/jev-cloud-connection.ts +392 -0
  202. package/src/hooks/loader-utils.ts +6 -0
  203. package/src/hooks/manager.ts +45 -7
  204. package/src/hooks/pack-cli.ts +568 -37
  205. package/src/hooks/pack-failclosed.ts +3 -0
  206. package/src/hooks/pack-manifest.ts +489 -7
  207. package/src/hooks/pack-store.ts +194 -11
  208. package/src/hooks/policy-authority.ts +386 -0
  209. package/src/hooks/policy-catalog.ts +206 -0
  210. package/src/hooks/policy-evaluator.ts +963 -796
  211. package/src/hooks/policy-registry.ts +27 -1
  212. package/src/hooks/policy-reviewability.ts +258 -0
  213. package/src/hooks/policy-types.ts +128 -0
  214. package/src/hooks/semantic/combine.ts +581 -0
  215. package/src/hooks/semantic/compile.ts +176 -0
  216. package/src/hooks/semantic/decide.ts +513 -0
  217. package/src/hooks/semantic/envelope.ts +1255 -0
  218. package/src/hooks/semantic/evaluator.ts +556 -0
  219. package/src/hooks/semantic/facts.ts +387 -0
  220. package/src/hooks/semantic/intent.ts +1190 -0
  221. package/src/hooks/semantic/jev-client.ts +1155 -0
  222. package/src/hooks/semantic/jev-config.ts +1142 -0
  223. package/src/hooks/semantic/jev-review.ts +389 -0
  224. package/src/hooks/semantic/jev-stats.ts +289 -0
  225. package/src/hooks/semantic/jev-throttle.ts +427 -0
  226. package/src/hooks/semantic/pack-policies.ts +294 -0
  227. package/src/hooks/semantic/policies.ts +597 -0
  228. package/src/hooks/semantic/precondition-names.ts +60 -0
  229. package/src/hooks/semantic/preconditions.ts +58 -0
  230. package/src/hooks/semantic/redact.ts +2937 -0
  231. package/src/hooks/semantic/session-root.ts +116 -0
  232. package/src/hooks/semantic/types.ts +162 -0
  233. package/src/hooks/semver-precedence.ts +128 -0
  234. package/src/hooks/tui.ts +4 -0
  235. package/src/hooks/types.ts +1 -1
  236. package/src/hooks/worker-server.ts +121 -27
  237. package/src/index.ts +6 -0
  238. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0cuho4x._.js +0 -3
  239. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1_r2rbg._.js +0 -24
  240. package/.next/standalone/.next/server/chunks/src_hooks_0iu54mz._.js +0 -3
  241. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0-_ki57._.js +0 -4
  242. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__01wy8d-._.js +0 -4
  243. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0bd3mje._.js +0 -3
  244. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0cg-bgc._.js +0 -5
  245. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0cpu_mj._.js +0 -3
  246. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0u3w0ll._.js +0 -22
  247. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__17d_ffl._.js +0 -3
  248. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__19d9tgz._.js +0 -5
  249. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1ctpynv._.js +0 -3
  250. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1jiwfsj._.js +0 -3
  251. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1phc187._.js +0 -3
  252. package/.next/standalone/.next/server/chunks/ssr/_06imw3p._.js +0 -5
  253. package/.next/standalone/.next/server/chunks/ssr/_0h_douw._.js +0 -3
  254. package/.next/standalone/.next/server/chunks/ssr/_1-i_gzc._.js +0 -23
  255. package/.next/standalone/.next/server/chunks/ssr/_1u8-lu2._.js +0 -3
  256. package/.next/standalone/.next/server/chunks/ssr/app_settings_settings-client_tsx_20lq-mq._.js +0 -3
  257. package/.next/standalone/.next/server/chunks/ssr/src_hooks_builtin-policies_ts_09j2ndl._.js +0 -3
  258. package/.next/standalone/.next/static/chunks/094xgi4owxaqf.js +0 -1
  259. package/.next/standalone/.next/static/chunks/1rz20_pz828f3.js +0 -6
  260. package/.next/standalone/.next/static/chunks/2k9f4tyv04809.css +0 -1
  261. package/.next/standalone/.next/static/chunks/2klitrtzpaoe0.js +0 -1
  262. package/.next/standalone/.next/static/chunks/2rshywgeqsyzk.css +0 -2
  263. package/.next/standalone/.next/static/chunks/3pzx4chkhko9k.js +0 -1
  264. package/.next/standalone/.next/static/chunks/3rh5o7e16irrm.js +0 -69
  265. package/.next/standalone/.next/static/chunks/43ufqrz8qo3h-.js +0 -1
  266. /package/.next/standalone/.next/static/{PgeWCHmyVbjRznv2VO7KF → 5yKM8NoTOpUI0MoQl8eo5}/_buildManifest.js +0 -0
  267. /package/.next/standalone/.next/static/{PgeWCHmyVbjRznv2VO7KF → 5yKM8NoTOpUI0MoQl8eo5}/_clientMiddlewareManifest.js +0 -0
  268. /package/.next/standalone/.next/static/{PgeWCHmyVbjRznv2VO7KF → 5yKM8NoTOpUI0MoQl8eo5}/_ssgManifest.js +0 -0
@@ -0,0 +1,389 @@
1
+ /**
2
+ * Starts Jev's review of one tool call, in parallel with the regex policies,
3
+ * for the two-tier evaluator.
4
+ *
5
+ * Loaded by `handler.ts` with a dynamic import, and only once a Jev config
6
+ * (BYOK) exists and the call is a gate event — so a machine without
7
+ * `jev.json` never loads the semantic evaluator at all, and its hook path is
8
+ * exactly what it was before two tiers existed.
9
+ *
10
+ * Everything the request needs comes through the §7 contracts:
11
+ * `transportForConfig` (T1) for the provider, `throttleTransport` (T5) for the
12
+ * cache and rate limit (scoped to the config's provider, endpoint and model;
13
+ * see `throttleScope`), `readIntent` (T4) for what the human asked. Jev's own
14
+ * verdict is intent v1 (`decideV1` with `DEFAULT_THRESHOLDS_V1`).
15
+ *
16
+ * The returned promise never rejects: every failure is a `fallback` review,
17
+ * which the combine turns into today's regex result. A `fallback` means Jev
18
+ * produced NO verdict — a genuine transport or degraded failure (timeout, 429,
19
+ * 402, 5xx, malformed body, model mismatch, no transport, a config that cannot
20
+ * be built) — and it deliberately carries no decision, so a verdict Jev DID
21
+ * produce can never be dropped on the way in.
22
+ *
23
+ * A verdict given on an envelope that could not carry everything is not that.
24
+ * It comes through as `answered` with `requestCut: true` when what did not fit
25
+ * was part of the call: it clears nothing, and it still counts toward the
26
+ * most-severe rule, because padding a call must not be a way to stop Jev's own
27
+ * deny applying. Size reaches neither the degrade path nor a refusal —
28
+ * `buildEnvelope` spends a hard budget and never throws, and being over it is
29
+ * recorded rather than acted on, so the strictest thing a big call can be is
30
+ * whatever Jev says about it (see `envelope.ts` and `combine.ts`).
31
+ *
32
+ * `truncated` — an over-long human turn, the agent's last message, or a prompt
33
+ * the intent store had already capped — rides along for the verdict log and
34
+ * changes nothing. It is not a fallback, it withdraws no clear, and the review
35
+ * it rides on is recorded `jev`.
36
+ *
37
+ * Both of those last two paragraphs are a DEPARTURE from plan §4, which files
38
+ * "the envelope was truncated" in one row with the degraded cases and asks for
39
+ * the regex result plus a `jev-fallback` reason. The shipped rule splits that
40
+ * row: a cut CALL is a fallback that clears nothing but keeps Jev's own
41
+ * severity, and a cut MESSAGE is not a fallback at all. `combine.ts`'s header
42
+ * ("Where this departs from plan §4") is the single statement of that rule and
43
+ * of why it is the safe reading; this file only has to produce the two shapes
44
+ * it distinguishes, which is what `toReview` below does.
45
+ */
46
+ import { BUILTIN_POLICIES } from "../builtin-policies";
47
+ import { normalizePolicyName } from "../policy-registry";
48
+ import { effectiveAuthority, type PolicyAuthority, type RegisteredPolicy } from "../policy-types";
49
+ import type { JevMode, JevReview, TwoTierReview } from "./combine";
50
+ import { DEFAULT_THRESHOLDS_V1 } from "./decide";
51
+ import { DEFAULT_JEV_TIMEOUT_MS, appendVerdictLog, evaluateSemantic, verdictLogRow, type SemanticOutcome } from "./evaluator";
52
+ import * as intentStore from "./intent";
53
+ import { JevError, transportForConfig, type JevTransport } from "./jev-client";
54
+ import { DEFAULT_JEV_MODE, type JevConfig } from "./jev-config";
55
+ import * as jevThrottle from "./jev-throttle";
56
+ import { sessionProjectRoot } from "./session-root";
57
+ import type { SemanticInput } from "./types";
58
+
59
+ /**
60
+ * T5's throttle as this file calls it. §7 declares only
61
+ * `throttleTransport(t)`; T5 added the `scope` option and the
62
+ * `isCachedJevResponse` probe, and both are used through this shape so the
63
+ * call compiles against the contract stub and the real module alike. The
64
+ * probe is optional: without it every answer counts as a fresh one.
65
+ */
66
+ interface JevThrottle {
67
+ throttleTransport(t: JevTransport, opts?: { scope?: string }): JevTransport;
68
+ isCachedJevResponse?(response: unknown): boolean;
69
+ }
70
+ const throttle: JevThrottle = jevThrottle;
71
+
72
+ /**
73
+ * T4's intent store as this file calls it: §7's `readIntent(sessionId)`,
74
+ * returning what the human typed and the agent's last message. That is all it
75
+ * returns.
76
+ *
77
+ * There was a second, optional field here — a store that had CUT either
78
+ * message reporting so out of band, which the evaluator believes
79
+ * (`SemanticOptions.contextTruncated`) — and nothing has ever written it.
80
+ * `readIntent` in `intent.ts` returns the two fields above, so the branch
81
+ * reading the third was dead while its comment claimed the store reports its
82
+ * cuts. Removed rather than left standing: what actually decides is the
83
+ * evaluator's narrower guess (`intentStoreCut` in `evaluator.ts` — its own
84
+ * omission mark in a message that also fills the cap), so a store cut can go
85
+ * unnoticed, and that belongs in the open.
86
+ *
87
+ * Wiring it is one line if T4's store is ever taught to report the cut
88
+ * (`capWithin` in `intent.ts` is what makes it): pass `contextTruncated` to
89
+ * `evaluateSemantic` below. It has to stay out of band like that, never read
90
+ * out of the message text, because the messages are agent-authored — a cut
91
+ * inferred from their content would let repo text switch the semantic tier off
92
+ * for a call.
93
+ */
94
+ interface IntentStore {
95
+ readIntent(sessionId?: string): { userSaid: string[]; agentLastMessage: string | null };
96
+ }
97
+ const intentReader: IntentStore = intentStore;
98
+
99
+ /** Whether T5's cache, not the provider, produced this response. Never throws. */
100
+ function servedFromCache(response: unknown): boolean {
101
+ try {
102
+ const probe = throttle.isCachedJevResponse;
103
+ return typeof probe === "function" && probe(response) === true;
104
+ } catch {
105
+ return false;
106
+ }
107
+ }
108
+
109
+ /**
110
+ * Where a config's answers come from — provider, endpoint (base URL,
111
+ * Cloudflare account) and the model id sent — as T5's cache scope.
112
+ *
113
+ * The cache lives as long as the warm worker, which outlives any one
114
+ * `jev.json`, and the request alone does not say who answered it: two
115
+ * providers asked for the same model build byte-identical requests. Unscoped,
116
+ * an answer one config got — from a plain-http loopback proxy T1 allows only in
117
+ * shadow mode, say — would clear denies under the next config in enforce mode
118
+ * without that provider ever being asked. The scope is only hashed into the
119
+ * cache key, never stored, so a URL carrying a credential is safe here.
120
+ */
121
+ export function throttleScope(cfg: JevConfig, route: { via: string; model: string }): string {
122
+ return JSON.stringify([route.via, cfg.baseUrl ?? null, cfg.accountId ?? null, route.model]);
123
+ }
124
+
125
+ /**
126
+ * The self-protection guard (and anything else `alwaysOn`) is hard whatever
127
+ * it declares. Registered policies do not carry `alwaysOn`, so it is looked up
128
+ * from the builtin definitions by canonical name.
129
+ */
130
+ const ALWAYS_ON: ReadonlySet<string> = new Set(
131
+ BUILTIN_POLICIES.filter((p) => p.alwaysOn).map((p) => normalizePolicyName(p.name)),
132
+ );
133
+
134
+ export function authorityOf(p: Pick<RegisteredPolicy, "name" | "authority" | "reviewedBy">): {
135
+ authority: PolicyAuthority;
136
+ reviewedBy: string[];
137
+ } {
138
+ const authority = effectiveAuthority({ authority: p.authority, reviewedBy: p.reviewedBy, alwaysOn: ALWAYS_ON.has(p.name) });
139
+ if (authority !== "reviewable") return { authority, reviewedBy: [] };
140
+ const reviewedBy = (p.reviewedBy ?? []).filter((n): n is string => typeof n === "string" && n.length > 0);
141
+ return { authority, reviewedBy };
142
+ }
143
+
144
+ export interface JevCallContext {
145
+ eventType: string;
146
+ toolName: string;
147
+ toolInput: unknown;
148
+ cwd?: string;
149
+ permissionMode?: string;
150
+ sessionId?: string;
151
+ cli: string;
152
+ }
153
+
154
+ export function resolveMode(cfg: JevConfig): JevMode {
155
+ return cfg.mode === "shadow" || cfg.mode === "enforce" ? cfg.mode : DEFAULT_JEV_MODE;
156
+ }
157
+
158
+ /**
159
+ * Bounds on how long one call may wait for Jev — the same bounds T1's config
160
+ * loader enforces, applied again here because this is the component that
161
+ * knows what they protect: every millisecond is added to the tool call, and
162
+ * the daemon client gives a whole hook 30 s before it fail-closed denies.
163
+ */
164
+ export const MIN_JEV_TIMEOUT_MS = 100;
165
+ export const MAX_JEV_TIMEOUT_MS = 10_000;
166
+ /**
167
+ * How long past its own timeout a review may run before it is abandoned as a
168
+ * timeout anyway: a transport that ignores its abort signal must not be able
169
+ * to hold a hook open.
170
+ */
171
+ export const JEV_DEADLINE_GRACE_MS = 250;
172
+
173
+ /** The config's timeout, clamped to the bounds above; anything unusable → the 3000 ms default. */
174
+ export function resolveTimeout(cfg: JevConfig): number {
175
+ const t = cfg.timeoutMs;
176
+ if (typeof t !== "number" || !Number.isFinite(t) || t <= 0) return DEFAULT_JEV_TIMEOUT_MS;
177
+ return Math.min(MAX_JEV_TIMEOUT_MS, Math.max(MIN_JEV_TIMEOUT_MS, t));
178
+ }
179
+
180
+ /** The semantic policy an attributable Jev verdict is filed under. */
181
+ function jevDecider(outcome: Extract<SemanticOutcome, { status: "ok" }>) {
182
+ const wanted = outcome.verdict.decision === "deny" ? "deny" : "instruct";
183
+ return outcome.verdict.outcomes.find((o) => o.verdict === wanted);
184
+ }
185
+
186
+ function jevPolicyName(outcome: Extract<SemanticOutcome, { status: "ok" }>): string {
187
+ const first = jevDecider(outcome);
188
+ if (first) return `semantic/${first.policy}`;
189
+ return outcome.verdict.beyondTask ? "semantic/beyond-task" : "semantic/jev";
190
+ }
191
+
192
+ /**
193
+ * A fallback reason as a short stable code (`timeout`, `http-429`,
194
+ * `model-mismatch`, …). Degraded reasons can carry an exception message after
195
+ * a colon; that detail stays in the local verdict log and never reaches the
196
+ * activity row, which telemetry may ship.
197
+ */
198
+ export function fallbackCode(reason: string): string {
199
+ const code = reason.split(":")[0].trim();
200
+ return /^[a-z0-9][a-z0-9-]{0,63}$/.test(code) ? code : "error";
201
+ }
202
+
203
+ /**
204
+ * @param cached - the answer came from T5's cache, not the provider: it is
205
+ * applied like any other, but its ~0 ms is not a latency (`latencyMs`
206
+ * null, so latency percentiles only ever measure the provider).
207
+ */
208
+ export function toReview(outcome: SemanticOutcome, cached = false): JevReview {
209
+ if (outcome.status === "degraded") {
210
+ // No verdict at all. The `fallback` variant carries no decision, by design:
211
+ // see "One rule about a partial picture" in `combine.ts`.
212
+ return { kind: "fallback", reason: fallbackCode(outcome.reason), latencyMs: outcome.latencyMs, model: null };
213
+ }
214
+ const sent = outcome.via !== "none";
215
+ const latencyMs = sent && !cached ? outcome.latencyMs : null;
216
+ const model = sent ? outcome.model : null;
217
+ const outcomes = outcome.verdict.outcomes;
218
+ // `null` when the injection probe was not in the request, which now means
219
+ // only that no request was made: v1 asks it on every call it sends, with or
220
+ // without a recorded human message (see `compile.ts`).
221
+ const injection = sent ? outcome.verdict.injectionSuspected : null;
222
+ // Fired, and no consent softened it: an `instruct` the human's own task did
223
+ // not downgrade from a deny (see "A check that fired without consent keeps
224
+ // the floor" in `combine.ts`).
225
+ const warned = (o: (typeof outcomes)[number]) => o.verdict === "instruct" && o.intent !== "downgraded-task-step";
226
+ return {
227
+ kind: "answered",
228
+ decision: outcome.verdict.decision,
229
+ reason: outcome.verdict.reason,
230
+ policyName: jevPolicyName(outcome),
231
+ ...(jevDecider(outcome)?.origin ? { origin: jevDecider(outcome)?.origin } : {}),
232
+ // Every selected policy's questions were in the request, and `readAnswers`
233
+ // refuses a response missing any of them — so selected == asked, and only
234
+ // when a request was actually made.
235
+ asked: sent ? outcomes.map((o) => o.policy) : [],
236
+ // `none` found nothing, `overridden` is what the human asked for, and a
237
+ // deny the human's task softened to a warning is that task's consent — all
238
+ // three clear. A `deny`, or a warning nobody consented to, keeps the block.
239
+ notDenied: sent ? outcomes.filter((o) => o.verdict !== "deny" && !warned(o)).map((o) => o.policy) : [],
240
+ injectionAsked: injection !== null,
241
+ injected: injection !== null && injection >= DEFAULT_THRESHOLDS_V1.injection,
242
+ // A check that CAN deny warned without consent: nothing may be cleared on
243
+ // this call (see "A check that fired without consent keeps the floor" in
244
+ // `combine.ts`).
245
+ unclearableWarned: outcomes.some((o) => o.mode === "deny" && warned(o)),
246
+ // Something did not fit — a human turn, the agent's message, or what T4's
247
+ // store had already capped before the envelope saw it (it caps to fit the
248
+ // envelope, so the envelope cannot see that cut; only the evaluator's
249
+ // `intentStoreCut` guess can, since nothing reports it out of band — see
250
+ // `IntentStore`). `outcome.truncated` covers both (see `prepareSemantic`).
251
+ // Carried for the verdict log; `combine.ts`
252
+ // reads nothing from it, because the length of what a human typed is not
253
+ // evidence about the call.
254
+ //
255
+ // Only when a request was actually sent: with no semantic policy applying
256
+ // nothing reaches Jev, so nothing was judged on a cut envelope at all.
257
+ truncated: outcome.truncated && sent,
258
+ // §4's row, one field over: part of the CALL — or of the deterministic
259
+ // facts about it — was not shown. Every clear is withdrawn and the call is
260
+ // recorded as `jev-fallback` / `request-cut`; Jev's own deny or instruct
261
+ // still counts, because padding must not be a way to stop it applying, and
262
+ // nothing else is added, because padding must not be a way to invent a
263
+ // deny either.
264
+ //
265
+ // Same "only if a request was sent" rule: with no semantic policy applying
266
+ // nothing was judged, so nothing was hidden from the judging.
267
+ requestCut: outcome.requestCut && sent,
268
+ latencyMs,
269
+ model,
270
+ };
271
+ }
272
+
273
+ function toolInputRecord(raw: unknown): Record<string, unknown> {
274
+ return raw && typeof raw === "object" && !Array.isArray(raw) ? (raw as Record<string, unknown>) : {};
275
+ }
276
+
277
+ /**
278
+ * Kick off the request now; the caller runs the regex policies meanwhile and
279
+ * awaits `review` only if no hard deny decided first.
280
+ */
281
+ export function startJevReview(cfg: JevConfig, call: JevCallContext): TwoTierReview {
282
+ const mode = resolveMode(cfg);
283
+ const controller = new AbortController();
284
+ let settled = false;
285
+ const handle = (review: Promise<JevReview>): TwoTierReview => ({
286
+ mode,
287
+ review: review.finally(() => {
288
+ settled = true;
289
+ }),
290
+ abort: () => {
291
+ if (!settled) controller.abort();
292
+ },
293
+ authorityOf,
294
+ });
295
+
296
+ let route: ReturnType<typeof transportForConfig>;
297
+ let throttled: JevTransport;
298
+ try {
299
+ route = transportForConfig(cfg);
300
+ throttled = throttle.throttleTransport(route.transport, { scope: throttleScope(cfg, route) });
301
+ } catch (err) {
302
+ const reason = err instanceof JevError ? err.code : "config";
303
+ return handle(Promise.resolve({ kind: "fallback", reason, latencyMs: null, model: null }));
304
+ }
305
+
306
+ let intent: ReturnType<IntentStore["readIntent"]>;
307
+ try {
308
+ intent = intentReader.readIntent(call.sessionId);
309
+ } catch {
310
+ intent = { userSaid: [], agentLastMessage: null };
311
+ }
312
+
313
+ // Never throws; null only without a cwd, and then there is nothing to pin.
314
+ const projectRoot = sessionProjectRoot(call.sessionId, call.cwd);
315
+
316
+ const input: SemanticInput = {
317
+ eventType: call.eventType,
318
+ toolName: call.toolName,
319
+ toolInput: toolInputRecord(call.toolInput),
320
+ ...(call.cwd ? { cwd: call.cwd } : {}),
321
+ ...(projectRoot ? { projectRoot } : {}),
322
+ ...(call.permissionMode ? { permissionMode: call.permissionMode } : {}),
323
+ userSaid: intent.userSaid,
324
+ agentLastMessage: intent.agentLastMessage,
325
+ };
326
+
327
+ /** Set when the answer came from T5's cache (a hit carries the original `usage` and no real latency). */
328
+ let cached = false;
329
+ const transport: JevTransport = async (request, signal) => {
330
+ const response = await throttled(request, signal);
331
+ cached = servedFromCache(response);
332
+ return response;
333
+ };
334
+
335
+ const timeoutMs = resolveTimeout(cfg);
336
+ /** Set once the backstop below gave up on the answer: a late one is logged, never applied. */
337
+ let abandoned = false;
338
+ const answered = evaluateSemantic(input, {
339
+ transport,
340
+ cli: call.cli,
341
+ via: route.via,
342
+ model: route.model,
343
+ timeoutMs,
344
+ intent: "v1",
345
+ v1: { thresholds: DEFAULT_THRESHOLDS_V1 },
346
+ signal: controller.signal,
347
+ // No `contextTruncated`: nothing reports a store cut out of band, so the
348
+ // evaluator's own (narrower) guess stands. See `IntentStore` above.
349
+ })
350
+ .then((outcome): JevReview => {
351
+ const hit = cached && outcome.status === "ok";
352
+ const review = toReview(outcome, hit);
353
+ // An aborted request has nothing worth replaying; everything else goes
354
+ // to the local verdict log (never shipped), so any verdict can be
355
+ // re-derived offline from its recorded probabilities. A cache hit is
356
+ // marked, and its tokens are not counted again.
357
+ if (!(outcome.status === "degraded" && outcome.reason === "aborted")) {
358
+ const row = verdictLogRow(input, hit ? { ...outcome, inputTokens: null } : outcome, {
359
+ sessionId: call.sessionId,
360
+ cli: call.cli,
361
+ eventType: call.eventType,
362
+ applied: abandoned || review.kind !== "answered" ? "legacy-fallback" : mode === "shadow" ? "shadow" : "two-tier",
363
+ });
364
+ appendVerdictLog(hit ? { ...row, cached: true } : row);
365
+ }
366
+ return review;
367
+ })
368
+ .catch((): JevReview => ({ kind: "fallback", reason: "error", latencyMs: null, model: null }));
369
+
370
+ // The backstop: `evaluateSemantic` times out through the transport's
371
+ // abort signal, which only works if the transport honours it. Whatever the
372
+ // transport does, the review settles shortly after the configured timeout.
373
+ // Deliberately NOT unref'd: in a one-shot hook process a transport stuck on
374
+ // nothing the event loop can see would otherwise let the process exit with
375
+ // the review still pending — an empty stdout, which every CLI reads as allow.
376
+ let deadline: ReturnType<typeof setTimeout> | undefined;
377
+ const expired = new Promise<JevReview>((resolve) => {
378
+ deadline = setTimeout(() => {
379
+ abandoned = true;
380
+ controller.abort();
381
+ resolve({ kind: "fallback", reason: "timeout", latencyMs: timeoutMs + JEV_DEADLINE_GRACE_MS, model: null });
382
+ }, timeoutMs + JEV_DEADLINE_GRACE_MS);
383
+ });
384
+ return handle(
385
+ Promise.race([answered, expired]).finally(() => {
386
+ if (deadline !== undefined) clearTimeout(deadline);
387
+ }),
388
+ );
389
+ }
@@ -0,0 +1,289 @@
1
+ /**
2
+ * Aggregate Jev activity for `failproofai jev status`: how often Jev fell back
3
+ * and why, how fast it answered, and which reviewable policies it cleared.
4
+ *
5
+ * Computed from the hook activity store (the same rows the dashboard's
6
+ * activity tab lists), over a trailing window. Nothing here is persisted: the
7
+ * store is the record and these numbers are re-derived on demand, so they can
8
+ * never drift from it.
9
+ *
10
+ * T1's CLI calls {@link jevStats} and may print it with {@link formatJevStats}.
11
+ */
12
+ import { getHookActivityEntriesSince, type HookActivityEntry } from "../hook-activity-store";
13
+ import { jevOutcome, sanitizeJevActivity } from "../jev-activity";
14
+
15
+ export interface JevStats {
16
+ windowMs: number;
17
+ /**
18
+ * Gate evaluations in the window that Jev was consulted on: it answered, or
19
+ * it was asked and fell back. A call a hard policy denied before Jev's
20
+ * answer was read, and a call no semantic policy applied to (so no request
21
+ * was sent), say nothing about Jev, so they are not counted here (nor in the
22
+ * fallback rate) but in {@link JevStats.notConsulted} and
23
+ * {@link JevStats.noRequest}.
24
+ */
25
+ total: number;
26
+ /** Share of `total` recorded as `jev-fallback`, 0..1. */
27
+ fallbackRate: number;
28
+ fallbackReasons: Record<string, number>;
29
+ /** Over calls Jev answered (`evaluator: "jev"`); a fallback's latency is the time it took to give up. */
30
+ latencyP50Ms: number | null;
31
+ latencyP95Ms: number | null;
32
+ /**
33
+ * Reviewable policies Jev cleared, by name — enforce mode only, where a
34
+ * clear changed the outcome. Shadow mode's would-be clears are in
35
+ * {@link JevStats.shadowClearsByPolicy}, so a renderer that prints only this
36
+ * field says "nothing cleared" on a shadow-mode machine: print both.
37
+ */
38
+ clearsByPolicy: Record<string, number>;
39
+ // The fields below are optional in the TYPE only so that a caller written
40
+ // against the original seven-field contract still type-checks; everything
41
+ // this module returns carries all of them (see JevStatsDetail).
42
+ /** Epoch ms the window opened. */
43
+ since?: number;
44
+ /** Calls Jev answered. `answered + fallbacks === total`. */
45
+ answered?: number;
46
+ fallbacks?: number;
47
+ /**
48
+ * Calls on a configured machine where a hard policy denied first, so Jev was
49
+ * aborted and never consulted. Outside `total`.
50
+ */
51
+ notConsulted?: number;
52
+ /**
53
+ * Calls on a configured machine that no semantic policy applied to
54
+ * (TodoWrite, Task, …): Jev had nothing to ask and no request was sent.
55
+ * Outside `total`.
56
+ */
57
+ noRequest?: number;
58
+ /**
59
+ * Jev's own verdict on the calls it answered, before combining with the
60
+ * regex results. Sums to `answered` except for a row whose verdict another
61
+ * build wrote in a shape this one cannot read.
62
+ */
63
+ decisions?: { allow: number; instruct: number; deny: number };
64
+ /** Consulted calls (`total`) per rollout mode; a row written without a mode is counted in neither. */
65
+ modes?: { shadow: number; enforce: number };
66
+ /** What Jev would have cleared in shadow mode, where the regex result was enforced instead. */
67
+ shadowClearsByPolicy?: Record<string, number>;
68
+ /** Model ids that answered, by count. */
69
+ models?: Record<string, number>;
70
+ }
71
+
72
+ /** {@link JevStats} with every field present — what this module actually returns. */
73
+ export type JevStatsDetail = Required<JevStats>;
74
+
75
+ export const DEFAULT_JEV_STATS_WINDOW_MS = 24 * 60 * 60 * 1000;
76
+ /** The longest window `jev status` accepts. The store is never pruned, so this bounds the read. */
77
+ export const MAX_JEV_STATS_WINDOW_MS = 90 * 24 * 60 * 60 * 1000;
78
+
79
+ const WINDOW_UNIT_MS: Record<string, number> = { m: 60_000, h: 3_600_000, d: 86_400_000 };
80
+
81
+ /**
82
+ * Parse a `--window` value for `failproofai jev status`: a positive whole
83
+ * number of minutes, hours or days (`30m`, `24h`, `7d`), at most
84
+ * {@link MAX_JEV_STATS_WINDOW_MS}. Null for anything else, so the CLI can say
85
+ * what it accepts instead of guessing.
86
+ */
87
+ export function parseJevStatsWindow(input: string): number | null {
88
+ const m = /^\s*(\d{1,5})\s*([mhd])\s*$/i.exec(input);
89
+ if (!m) return null;
90
+ const ms = Number(m[1]) * WINDOW_UNIT_MS[m[2].toLowerCase()];
91
+ return ms > 0 && ms <= MAX_JEV_STATS_WINDOW_MS ? ms : null;
92
+ }
93
+
94
+ /**
95
+ * The window a stats call really uses. {@link parseJevStatsWindow} bounds what
96
+ * the CLI accepts, but {@link jevStats} is a library call too: a window that is
97
+ * not a positive number (NaN, 0, negative, not a number at all) falls back to
98
+ * {@link DEFAULT_JEV_STATS_WINDOW_MS}, and a longer one — Infinity included —
99
+ * is capped at {@link MAX_JEV_STATS_WINDOW_MS}; that cap is what bounds the
100
+ * store read.
101
+ */
102
+ export function clampJevStatsWindow(windowMs: unknown): number {
103
+ if (typeof windowMs !== "number" || Number.isNaN(windowMs) || windowMs <= 0) return DEFAULT_JEV_STATS_WINDOW_MS;
104
+ return Math.min(windowMs, MAX_JEV_STATS_WINDOW_MS);
105
+ }
106
+
107
+ /** `now` when it is a finite epoch-ms value, else the current time. */
108
+ function clampNow(now: unknown): number {
109
+ return typeof now === "number" && Number.isFinite(now) ? now : Date.now();
110
+ }
111
+
112
+ /** Nearest-rank percentile of an ascending array; null when empty. */
113
+ export function percentile(sortedAsc: ReadonlyArray<number>, p: number): number | null {
114
+ if (sortedAsc.length === 0) return null;
115
+ const rank = Math.ceil((p / 100) * sortedAsc.length);
116
+ return sortedAsc[Math.min(sortedAsc.length, Math.max(1, rank)) - 1];
117
+ }
118
+
119
+ /**
120
+ * Counts by name. A Map rather than an object literal: the names come off disk,
121
+ * and `constructor`, `toString` or `__proto__` must count like any other name
122
+ * instead of hitting `Object.prototype`.
123
+ */
124
+ class Counter {
125
+ private readonly counts = new Map<string, number>();
126
+ bump(k: string): void {
127
+ this.counts.set(k, (this.counts.get(k) ?? 0) + 1);
128
+ }
129
+ /** A plain object with an own property per name — `__proto__` included. */
130
+ toRecord(): Record<string, number> {
131
+ return Object.fromEntries(this.counts);
132
+ }
133
+ }
134
+
135
+ /**
136
+ * The stats for `entries` that fall in `[since, now]`. Pure; {@link jevStats}
137
+ * feeds it from the store. Rows without a Jev evaluator (Jev not configured,
138
+ * a non-gate event, a paused session) are not part of any number here; a
139
+ * call Jev was not consulted on (a hard deny decided first) is counted only in
140
+ * `notConsulted`, and one it had nothing to ask about only in `noRequest`.
141
+ */
142
+ export function computeJevStats(
143
+ entries: ReadonlyArray<HookActivityEntry>,
144
+ opts: { windowMs?: number; now?: number } = {},
145
+ ): JevStatsDetail {
146
+ const windowMs = clampJevStatsWindow(opts.windowMs);
147
+ const now = clampNow(opts.now);
148
+ const since = now - windowMs;
149
+ const stats: JevStatsDetail = {
150
+ windowMs,
151
+ total: 0,
152
+ fallbackRate: 0,
153
+ fallbackReasons: {},
154
+ latencyP50Ms: null,
155
+ latencyP95Ms: null,
156
+ clearsByPolicy: {},
157
+ since,
158
+ answered: 0,
159
+ fallbacks: 0,
160
+ notConsulted: 0,
161
+ noRequest: 0,
162
+ decisions: { allow: 0, instruct: 0, deny: 0 },
163
+ modes: { shadow: 0, enforce: 0 },
164
+ shadowClearsByPolicy: {},
165
+ models: {},
166
+ };
167
+ const latencies: number[] = [];
168
+ const fallbackReasons = new Counter();
169
+ const clearsByPolicy = new Counter();
170
+ const shadowClearsByPolicy = new Counter();
171
+ const models = new Counter();
172
+
173
+ for (const raw of entries) {
174
+ if (typeof raw.timestamp !== "number" || raw.timestamp < since || raw.timestamp > now) continue;
175
+ // Rows are sanitized on write, but this also reads rows another build wrote.
176
+ const e = sanitizeJevActivity(raw);
177
+ const outcome = jevOutcome(e);
178
+ if (outcome === null) continue;
179
+ if (outcome === "not-consulted") {
180
+ stats.notConsulted += 1;
181
+ continue;
182
+ }
183
+ if (outcome === "no-request") {
184
+ stats.noRequest += 1;
185
+ continue;
186
+ }
187
+ stats.total += 1;
188
+ if (e.jevMode) stats.modes[e.jevMode] += 1;
189
+
190
+ if (outcome === "fallback") {
191
+ stats.fallbacks += 1;
192
+ fallbackReasons.bump(e.jevFallbackReason ?? "unknown");
193
+ continue;
194
+ }
195
+
196
+ stats.answered += 1;
197
+ if (e.jevDecision) stats.decisions[e.jevDecision] += 1;
198
+ if (e.jevLatencyMs !== undefined) latencies.push(e.jevLatencyMs);
199
+ if (e.jevModel) models.bump(e.jevModel);
200
+ const clears = e.jevMode === "shadow" ? shadowClearsByPolicy : clearsByPolicy;
201
+ for (const name of e.jevCleared ?? []) clears.bump(name);
202
+ }
203
+
204
+ stats.fallbackReasons = fallbackReasons.toRecord();
205
+ stats.clearsByPolicy = clearsByPolicy.toRecord();
206
+ stats.shadowClearsByPolicy = shadowClearsByPolicy.toRecord();
207
+ stats.models = models.toRecord();
208
+ stats.fallbackRate = stats.total > 0 ? stats.fallbacks / stats.total : 0;
209
+ latencies.sort((a, b) => a - b);
210
+ stats.latencyP50Ms = percentile(latencies, 50);
211
+ stats.latencyP95Ms = percentile(latencies, 95);
212
+ return stats;
213
+ }
214
+
215
+ /**
216
+ * Jev stats over the trailing window (default 24 h, at most 90 days — see
217
+ * {@link clampJevStatsWindow}), read from the hook activity store. Never
218
+ * throws: an unreadable store reads as no activity.
219
+ *
220
+ * A renderer must show {@link JevStats.shadowClearsByPolicy} as well as
221
+ * `clearsByPolicy`: in shadow mode every clear Jev would have made is in the
222
+ * former, and the latter is empty. {@link formatJevStats} prints both.
223
+ */
224
+ export async function jevStats(opts: { windowMs?: number; now?: number } = {}): Promise<JevStatsDetail> {
225
+ const windowMs = clampJevStatsWindow(opts.windowMs);
226
+ const now = clampNow(opts.now);
227
+ let entries: HookActivityEntry[] = [];
228
+ try {
229
+ entries = getHookActivityEntriesSince(now - windowMs);
230
+ } catch {
231
+ entries = [];
232
+ }
233
+ return computeJevStats(entries, { windowMs, now });
234
+ }
235
+
236
+ function formatWindow(ms: number): string {
237
+ const h = ms / 3_600_000;
238
+ if (Number.isInteger(h) && h >= 1) return h % 24 === 0 ? `${h / 24}d` : `${h}h`;
239
+ const m = Math.round(ms / 60_000);
240
+ return m >= 1 ? `${m}m` : `${Math.round(ms / 1000)}s`;
241
+ }
242
+
243
+ function topCounts(m: Record<string, number>, limit = 5): string {
244
+ const rows = Object.entries(m).sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]));
245
+ const shown = rows.slice(0, limit).map(([k, n]) => `${k} ${n}`);
246
+ if (rows.length > limit) shown.push(`+${rows.length - limit} more`);
247
+ return shown.join(", ");
248
+ }
249
+
250
+ /**
251
+ * The stats section of `failproofai jev status`, as plain lines (no colour, no
252
+ * trailing newline). Names policies and reason codes only — the store holds no
253
+ * command or prompt text for it to print.
254
+ */
255
+ export function formatJevStats(s: JevStats): string {
256
+ const win = formatWindow(s.windowMs);
257
+ const notConsulted = s.notConsulted ?? 0;
258
+ const noRequest = s.noRequest ?? 0;
259
+ const notAsked = [
260
+ ...(notConsulted > 0 ? [` Not asked: ${notConsulted} (a hard policy denied first)`] : []),
261
+ ...(noRequest > 0 ? [` No request: ${noRequest} (no semantic policy applied to the call)`] : []),
262
+ ];
263
+ if (s.total === 0) {
264
+ return [`Activity (last ${win}): no Jev evaluations recorded.`, ...notAsked].join("\n");
265
+ }
266
+ const fallbacks = s.fallbacks ?? Object.values(s.fallbackReasons).reduce((a, b) => a + b, 0);
267
+ const answered = s.answered ?? s.total - fallbacks;
268
+ const pct = (n: number) => `${(n * 100).toFixed(1)}%`;
269
+ const lines = [`Activity (last ${win}): ${s.total} evaluation${s.total === 1 ? "" : "s"}`];
270
+ const d = s.decisions;
271
+ lines.push(
272
+ ` Answered: ${answered}` +
273
+ (answered > 0 && d ? ` (allow ${d.allow}, instruct ${d.instruct}, deny ${d.deny})` : ""),
274
+ );
275
+ lines.push(
276
+ ` Fell back: ${fallbacks} (${pct(s.fallbackRate)})` +
277
+ (fallbacks > 0 ? ` — ${topCounts(s.fallbackReasons)}` : ""),
278
+ );
279
+ lines.push(...notAsked);
280
+ if (s.latencyP50Ms !== null) lines.push(` Latency: p50 ${s.latencyP50Ms} ms, p95 ${s.latencyP95Ms} ms`);
281
+ if (Object.keys(s.clearsByPolicy).length > 0) lines.push(` Cleared: ${topCounts(s.clearsByPolicy)}`);
282
+ if (s.shadowClearsByPolicy && Object.keys(s.shadowClearsByPolicy).length > 0) {
283
+ lines.push(` Would clear: ${topCounts(s.shadowClearsByPolicy)} (shadow mode)`);
284
+ }
285
+ if (s.modes && s.modes.shadow > 0 && s.modes.enforce > 0) {
286
+ lines.push(` Modes: enforce ${s.modes.enforce}, shadow ${s.modes.shadow}`);
287
+ }
288
+ return lines.join("\n");
289
+ }