apsimo 1.3.0__py3-none-any.whl

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 (614) hide show
  1. apsimo/__init__.py +38 -0
  2. apsimo/__main__.py +6 -0
  3. apsimo/agent/__init__.py +6 -0
  4. apsimo/agent/client.py +276 -0
  5. apsimo/agent/models.py +46 -0
  6. apsimo/agents/__init__.py +20 -0
  7. apsimo/agents/models.py +264 -0
  8. apsimo/agents/store.py +861 -0
  9. apsimo/agents/websocket.py +522 -0
  10. apsimo/api/__init__.py +1 -0
  11. apsimo/api/auth_telemetry.py +287 -0
  12. apsimo/api/authority.py +1203 -0
  13. apsimo/api/contact_grants.py +347 -0
  14. apsimo/api/middleware.py +483 -0
  15. apsimo/api/routers/__init__.py +1 -0
  16. apsimo/api/routers/commitment_work.py +265 -0
  17. apsimo/api/routers/context_gate.py +123 -0
  18. apsimo/api/routers/executions.py +140 -0
  19. apsimo/api/routers/followup_plans.py +147 -0
  20. apsimo/api/routers/governed_actions.py +162 -0
  21. apsimo/api/routers/host.py +14473 -0
  22. apsimo/api/routers/initiative_work.py +115 -0
  23. apsimo/api/routers/mining.py +104 -0
  24. apsimo/api/routers/observations.py +110 -0
  25. apsimo/api/routers/social_state.py +225 -0
  26. apsimo/api/routers/task_queue.py +2715 -0
  27. apsimo/api/routers/temporal_followups.py +251 -0
  28. apsimo/api/routers/transport.py +110 -0
  29. apsimo/api/routers/transport_ingress_api.py +240 -0
  30. apsimo/api/schemas/__init__.py +1 -0
  31. apsimo/api/schemas/host.py +1949 -0
  32. apsimo/autonomy/cli.py +110 -0
  33. apsimo/autonomy/condition_worker.py +437 -0
  34. apsimo/autonomy/config.py +424 -0
  35. apsimo/autonomy/loop.py +4316 -0
  36. apsimo/autonomy/registry.py +339 -0
  37. apsimo/autonomy/scheduler.py +1822 -0
  38. apsimo/autonomy/synthesis.py +449 -0
  39. apsimo/backup.py +962 -0
  40. apsimo/beliefs/__init__.py +23 -0
  41. apsimo/beliefs/contradictions.py +109 -0
  42. apsimo/beliefs/decay.py +61 -0
  43. apsimo/beliefs/engine.py +479 -0
  44. apsimo/beliefs/models.py +67 -0
  45. apsimo/beliefs/promotion.py +41 -0
  46. apsimo/beliefs/resolve.py +58 -0
  47. apsimo/beliefs/source_claims.py +690 -0
  48. apsimo/beliefs/source_projection.py +883 -0
  49. apsimo/beliefs/source_time.py +208 -0
  50. apsimo/beliefs/store.py +133 -0
  51. apsimo/briefings/aggregators.py +824 -0
  52. apsimo/briefings/composer.py +420 -0
  53. apsimo/briefings/config.py +55 -0
  54. apsimo/briefings/delivery.py +439 -0
  55. apsimo/briefings/engagement.py +97 -0
  56. apsimo/briefings/engine.py +274 -0
  57. apsimo/briefings/enhancer.py +99 -0
  58. apsimo/briefings/models.py +183 -0
  59. apsimo/briefings/scheduler.py +382 -0
  60. apsimo/briefings/store.py +435 -0
  61. apsimo/chain/__init__.py +48 -0
  62. apsimo/chain/block.py +100 -0
  63. apsimo/chain/cli.py +704 -0
  64. apsimo/chain/genesis.py +443 -0
  65. apsimo/chain/identity.py +416 -0
  66. apsimo/chain/keys.py +1025 -0
  67. apsimo/chain/local_keys.py +187 -0
  68. apsimo/chain/manager.py +290 -0
  69. apsimo/chain/node.py +163 -0
  70. apsimo/chain/plugin_transactions.py +371 -0
  71. apsimo/chain/protocol.py +220 -0
  72. apsimo/chain/state_machine.py +676 -0
  73. apsimo/chain/storage.py +503 -0
  74. apsimo/chain/transactions.py +250 -0
  75. apsimo/chain/validation.py +397 -0
  76. apsimo/channels/__init__.py +1 -0
  77. apsimo/channels/manifest.py +31 -0
  78. apsimo/channels/migrations/001_channels_schema.sql +12 -0
  79. apsimo/channels/phone_gateways.py +42 -0
  80. apsimo/channels/presence.py +188 -0
  81. apsimo/channels/router.py +235 -0
  82. apsimo/channels/store.py +231 -0
  83. apsimo/cli.py +2688 -0
  84. apsimo/cognition/__init__.py +11 -0
  85. apsimo/cognition/charter.py +398 -0
  86. apsimo/cognition/drive_governance.py +3530 -0
  87. apsimo/cognition/evidence_pipeline.py +1627 -0
  88. apsimo/cognition/external_events.py +932 -0
  89. apsimo/cognition/goal_spine.py +3488 -0
  90. apsimo/cognition/introspection.py +214 -0
  91. apsimo/cognition/prompt.py +150 -0
  92. apsimo/cognition/runtime.py +108 -0
  93. apsimo/cognition/trigger.py +154 -0
  94. apsimo/commitments/__init__.py +18 -0
  95. apsimo/commitments/local_work.py +355 -0
  96. apsimo/commitments/store.py +1052 -0
  97. apsimo/commitments/work.py +91 -0
  98. apsimo/compat.py +53 -0
  99. apsimo/compression/__init__.py +467 -0
  100. apsimo/connectors/__init__.py +21 -0
  101. apsimo/connectors/base.py +152 -0
  102. apsimo/connectors/caldav_calendar.py +125 -0
  103. apsimo/connectors/fs_documents.py +85 -0
  104. apsimo/connectors/imap_email.py +138 -0
  105. apsimo/connectors/manager.py +218 -0
  106. apsimo/connectors/webhook_pull.py +88 -0
  107. apsimo/contacts/__init__.py +33 -0
  108. apsimo/contacts/comms.py +357 -0
  109. apsimo/contacts/config.py +79 -0
  110. apsimo/contacts/exporters/__init__.py +1 -0
  111. apsimo/contacts/exporters/vcard.py +71 -0
  112. apsimo/contacts/identity_links.py +251 -0
  113. apsimo/contacts/importer.py +280 -0
  114. apsimo/contacts/importers/__init__.py +1 -0
  115. apsimo/contacts/importers/batch.py +43 -0
  116. apsimo/contacts/importers/macos_contacts.py +101 -0
  117. apsimo/contacts/migrations/001_contacts_schema.sql +141 -0
  118. apsimo/contacts/migrations/002_trust_scopes.sql +36 -0
  119. apsimo/contacts/migrations/003_open_gateway_enum.sql +32 -0
  120. apsimo/contacts/migrations/004_contact_provision_operations.sql +18 -0
  121. apsimo/contacts/migrations/005_identity_links.sql +27 -0
  122. apsimo/contacts/models.py +308 -0
  123. apsimo/contacts/scoring.py +16 -0
  124. apsimo/contacts/store.py +1623 -0
  125. apsimo/contacts/transport_ingress.py +252 -0
  126. apsimo/contacts/world_bridge.py +314 -0
  127. apsimo/contextgate/__init__.py +69 -0
  128. apsimo/contextgate/chunker.py +169 -0
  129. apsimo/contextgate/estimate.py +54 -0
  130. apsimo/contextgate/gate.py +313 -0
  131. apsimo/contextgate/retrieve.py +115 -0
  132. apsimo/delivery/__init__.py +16 -0
  133. apsimo/delivery/bridge.py +1260 -0
  134. apsimo/delivery/channels.py +526 -0
  135. apsimo/delivery/classification.py +50 -0
  136. apsimo/delivery/rate_limiter.py +268 -0
  137. apsimo/delivery/reachout_policy.py +206 -0
  138. apsimo/directed/__init__.py +22 -0
  139. apsimo/directed/audit.py +167 -0
  140. apsimo/directed/intake.py +95 -0
  141. apsimo/directed/models.py +191 -0
  142. apsimo/directed/service.py +509 -0
  143. apsimo/directives/__init__.py +25 -0
  144. apsimo/directives/evidence.py +87 -0
  145. apsimo/directives/extractor.py +188 -0
  146. apsimo/directives/guard.py +364 -0
  147. apsimo/directives/models.py +206 -0
  148. apsimo/directives/service.py +372 -0
  149. apsimo/directives/store.py +167 -0
  150. apsimo/doctor.py +2173 -0
  151. apsimo/environment.py +43 -0
  152. apsimo/events/__init__.py +33 -0
  153. apsimo/events/broadcaster.py +98 -0
  154. apsimo/events/bus.py +217 -0
  155. apsimo/events/journal.py +863 -0
  156. apsimo/events/stream.py +131 -0
  157. apsimo/events/types.py +150 -0
  158. apsimo/execution_results.py +357 -0
  159. apsimo/feedback/__init__.py +5 -0
  160. apsimo/feedback/store.py +76 -0
  161. apsimo/feeds/__init__.py +19 -0
  162. apsimo/feeds/cli.py +84 -0
  163. apsimo/feeds/engine.py +437 -0
  164. apsimo/feeds/example-feed.yaml +77 -0
  165. apsimo/feeds/hermes_cron.py +126 -0
  166. apsimo/feeds/manager.py +235 -0
  167. apsimo/feeds/spec.py +250 -0
  168. apsimo/feeds/template.py +202 -0
  169. apsimo/gate/__init__.py +18 -0
  170. apsimo/gate/audit.py +61 -0
  171. apsimo/gate/communication_policy.py +166 -0
  172. apsimo/gate/config.py +72 -0
  173. apsimo/gate/context_provenance.py +170 -0
  174. apsimo/gate/env_risk.py +226 -0
  175. apsimo/gate/guard_audit.py +353 -0
  176. apsimo/gate/layers/__init__.py +1 -0
  177. apsimo/gate/layers/base.py +15 -0
  178. apsimo/gate/layers/l1_recipient.py +66 -0
  179. apsimo/gate/layers/l2_pii.py +134 -0
  180. apsimo/gate/layers/l3_cross_context.py +50 -0
  181. apsimo/gate/layers/l4_trust_tier.py +78 -0
  182. apsimo/gate/layers/l5_injection.py +199 -0
  183. apsimo/gate/layers/l6_review.py +86 -0
  184. apsimo/gate/layers/l7_delay.py +100 -0
  185. apsimo/gate/layers/tom2_epistemic.py +185 -0
  186. apsimo/gate/models.py +64 -0
  187. apsimo/gate/pending_dispatch.py +5 -0
  188. apsimo/gate/pipeline.py +206 -0
  189. apsimo/gate/rejection.py +259 -0
  190. apsimo/gate/response_guard.py +700 -0
  191. apsimo/gate/rulesets/injection_v1.yaml +51 -0
  192. apsimo/gate/surface_policy.py +189 -0
  193. apsimo/gate/taint.py +226 -0
  194. apsimo/genesis.json +9 -0
  195. apsimo/goals/__init__.py +100 -0
  196. apsimo/goals/config.py +38 -0
  197. apsimo/goals/decomposer.py +421 -0
  198. apsimo/goals/engine.py +617 -0
  199. apsimo/goals/inference.py +354 -0
  200. apsimo/goals/models.py +302 -0
  201. apsimo/goals/priority.py +270 -0
  202. apsimo/goals/queue_bridge.py +149 -0
  203. apsimo/goals/replan.py +450 -0
  204. apsimo/goals/schema.sql +89 -0
  205. apsimo/goals/store.py +692 -0
  206. apsimo/governed_actions.py +1708 -0
  207. apsimo/harness_integration/__init__.py +45 -0
  208. apsimo/harness_integration/context.py +41 -0
  209. apsimo/harness_integration/skills.py +231 -0
  210. apsimo/identity/__init__.py +26 -0
  211. apsimo/identity/participants.py +181 -0
  212. apsimo/identity/resolver.py +329 -0
  213. apsimo/identity_bootstrap/__init__.py +5 -0
  214. apsimo/identity_bootstrap/builder.py +208 -0
  215. apsimo/identity_bootstrap/corpus.py +443 -0
  216. apsimo/identity_bootstrap/models.py +54 -0
  217. apsimo/identity_bootstrap/runner.py +353 -0
  218. apsimo/identity_bootstrap/seeders/__init__.py +25 -0
  219. apsimo/identity_bootstrap/seeders/briefings.py +109 -0
  220. apsimo/identity_bootstrap/seeders/chain.py +57 -0
  221. apsimo/identity_bootstrap/seeders/goals.py +128 -0
  222. apsimo/identity_bootstrap/seeders/memory.py +191 -0
  223. apsimo/identity_bootstrap/seeders/neo4j_cognition.py +79 -0
  224. apsimo/identity_bootstrap/seeders/relationship.py +152 -0
  225. apsimo/identity_bootstrap/seeders/sessions.py +67 -0
  226. apsimo/identity_bootstrap/seeders/skills.py +92 -0
  227. apsimo/identity_bootstrap/seeders/task_queue.py +72 -0
  228. apsimo/identity_bootstrap/seeders/world_model.py +143 -0
  229. apsimo/identity_bootstrap/self_query.py +92 -0
  230. apsimo/identity_bootstrap/self_reflection.py +155 -0
  231. apsimo/identity_bootstrap/skill.py +37 -0
  232. apsimo/identity_bootstrap/verifier.py +436 -0
  233. apsimo/initiatives/__init__.py +20 -0
  234. apsimo/initiatives/action_registry.py +454 -0
  235. apsimo/initiatives/approval_authority.py +2105 -0
  236. apsimo/initiatives/approval_policy.py +123 -0
  237. apsimo/initiatives/assignment.py +263 -0
  238. apsimo/initiatives/backup_evidence.py +100 -0
  239. apsimo/initiatives/context_freshness.py +103 -0
  240. apsimo/initiatives/models.py +318 -0
  241. apsimo/initiatives/native_work.py +270 -0
  242. apsimo/initiatives/standing_approvals.py +232 -0
  243. apsimo/initiatives/store.py +1081 -0
  244. apsimo/initiatives/temporal_followup.py +410 -0
  245. apsimo/intelligence/__init__.py +1 -0
  246. apsimo/intelligence/cognition/__init__.py +24 -0
  247. apsimo/intelligence/cognition/gap_detector.py +148 -0
  248. apsimo/intelligence/cognition/metalearner.py +547 -0
  249. apsimo/intelligence/cognition/metrics_collector.py +217 -0
  250. apsimo/intelligence/cognition/performance_index.py +299 -0
  251. apsimo/intelligence/cognition/registry.py +192 -0
  252. apsimo/intelligence/cognition/strategy_adjuster.py +222 -0
  253. apsimo/intelligence/cognition/types.py +16 -0
  254. apsimo/intelligence/components/__init__.py +66 -0
  255. apsimo/intelligence/components/anomaly_detector.py +413 -0
  256. apsimo/intelligence/components/initiative_engine.py +2643 -0
  257. apsimo/intelligence/components/preference_learner.py +521 -0
  258. apsimo/intelligence/components/research_orchestrator.py +358 -0
  259. apsimo/intelligence/components/self_directed_thinker.py +221 -0
  260. apsimo/intelligence/components/self_reflector.py +252 -0
  261. apsimo/intelligence/components/session_continuity.py +154 -0
  262. apsimo/intelligence/components/task_planner.py +320 -0
  263. apsimo/intelligence/components/tool_learner.py +217 -0
  264. apsimo/intelligence/graph/__init__.py +79 -0
  265. apsimo/intelligence/graph/client.py +2483 -0
  266. apsimo/intelligence/graph/consolidator.py +405 -0
  267. apsimo/intelligence/graph/distiller.py +312 -0
  268. apsimo/intelligence/graph/migrations.py +129 -0
  269. apsimo/intelligence/graph/queries.py +248 -0
  270. apsimo/intelligence/graph/recall.py +281 -0
  271. apsimo/intelligence/graph/reconciler.py +144 -0
  272. apsimo/intelligence/graph/schema.py +337 -0
  273. apsimo/intelligence/graph/selection.py +252 -0
  274. apsimo/intelligence/learning/__init__.py +17 -0
  275. apsimo/intelligence/learning/continuous_learner.py +245 -0
  276. apsimo/intelligence/learning/feedback_store.py +321 -0
  277. apsimo/intelligence/mind_model/__init__.py +1 -0
  278. apsimo/intelligence/mind_model/graph_baseline.py +136 -0
  279. apsimo/intelligence/mind_model/signal_collector.py +361 -0
  280. apsimo/intelligence/relationships/__init__.py +11 -0
  281. apsimo/intelligence/relationships/profiler.py +389 -0
  282. apsimo/intelligence/relationships/scorer.py +560 -0
  283. apsimo/intelligence/relationships/signal_floor.py +66 -0
  284. apsimo/intelligence/relationships/trust_tiers.py +300 -0
  285. apsimo/intelligence/synthesis/__init__.py +40 -0
  286. apsimo/intelligence/synthesis/connection_discoverer.py +379 -0
  287. apsimo/intelligence/synthesis/cross_domain_analyzer.py +287 -0
  288. apsimo/intelligence/synthesis/insight_deliverer.py +171 -0
  289. apsimo/intelligence/synthesis/insight_store.py +79 -0
  290. apsimo/intelligence/synthesis/insight_validator.py +183 -0
  291. apsimo/intelligence/synthesis/novelty_scorer.py +267 -0
  292. apsimo/intelligence/turn_middleware/__init__.py +15 -0
  293. apsimo/intelligence/turn_middleware/memory_sync.py +119 -0
  294. apsimo/mcp/__init__.py +41 -0
  295. apsimo/mcp/__main__.py +6 -0
  296. apsimo/mcp/config.py +287 -0
  297. apsimo/mcp/server.py +501 -0
  298. apsimo/migrations.py +187 -0
  299. apsimo/mining/__init__.py +27 -0
  300. apsimo/mining/corpus.py +239 -0
  301. apsimo/mining/escalations.py +289 -0
  302. apsimo/mining/models.py +169 -0
  303. apsimo/mining/store.py +210 -0
  304. apsimo/models/__init__.py +30 -0
  305. apsimo/models/memory.py +80 -0
  306. apsimo/models/mesh.py +72 -0
  307. apsimo/models/person.py +104 -0
  308. apsimo/models/signal.py +108 -0
  309. apsimo/observations/__init__.py +15 -0
  310. apsimo/observations/store.py +277 -0
  311. apsimo/patterns/__init__.py +6 -0
  312. apsimo/patterns/extract.py +187 -0
  313. apsimo/patterns/store.py +227 -0
  314. apsimo/persona/__init__.py +1 -0
  315. apsimo/persona/engine.py +611 -0
  316. apsimo/persona/manifest.py +140 -0
  317. apsimo/projects/__init__.py +28 -0
  318. apsimo/projects/engine.py +1681 -0
  319. apsimo/projects/event_outbox.py +188 -0
  320. apsimo/projects/models.py +216 -0
  321. apsimo/projects/planner.py +181 -0
  322. apsimo/projects/store.py +1446 -0
  323. apsimo/proposals/__init__.py +12 -0
  324. apsimo/proposals/engine.py +114 -0
  325. apsimo/proposals/models.py +207 -0
  326. apsimo/qualification/__init__.py +1 -0
  327. apsimo/qualification/cases.py +75 -0
  328. apsimo/qualification/cli.py +51 -0
  329. apsimo/qualification/memory_cases.py +209 -0
  330. apsimo/qualification/records.py +92 -0
  331. apsimo/qualification/report.py +87 -0
  332. apsimo/qualification/runner.py +311 -0
  333. apsimo/qualification/structured_cases.py +131 -0
  334. apsimo/reasoning/__init__.py +13 -0
  335. apsimo/reasoning/executor.py +506 -0
  336. apsimo/reasoning/loop.py +373 -0
  337. apsimo/reasoning/native_tools/__init__.py +16 -0
  338. apsimo/reasoning/native_tools/calculate.py +141 -0
  339. apsimo/reasoning/native_tools/file_ops.py +150 -0
  340. apsimo/reasoning/native_tools/web_search.py +49 -0
  341. apsimo/reasoning/tool_policy.py +182 -0
  342. apsimo/redact/__init__.py +176 -0
  343. apsimo/repos/__init__.py +5 -0
  344. apsimo/repos/mirrors.py +204 -0
  345. apsimo/research/__init__.py +41 -0
  346. apsimo/research/artifact.py +482 -0
  347. apsimo/research/gatherer.py +387 -0
  348. apsimo/research/pipeline.py +513 -0
  349. apsimo/research/search/__init__.py +7 -0
  350. apsimo/research/search/base.py +41 -0
  351. apsimo/research/search/brave.py +59 -0
  352. apsimo/research/search/cache.py +51 -0
  353. apsimo/research/search/duckduckgo.py +103 -0
  354. apsimo/research/search/orchestrator.py +119 -0
  355. apsimo/research/search/serpapi.py +59 -0
  356. apsimo/research/search/tavily.py +59 -0
  357. apsimo/research/synthesizer.py +309 -0
  358. apsimo/router/__init__.py +30 -0
  359. apsimo/router/complexity_scorer.py +148 -0
  360. apsimo/router/endpoints.py +153 -0
  361. apsimo/router/fallback.py +58 -0
  362. apsimo/router/functions.py +243 -0
  363. apsimo/router/native_policy.py +52 -0
  364. apsimo/router/router.py +762 -0
  365. apsimo/router/self_learning.py +174 -0
  366. apsimo/router/tiers.py +677 -0
  367. apsimo/sandbox/__init__.py +21 -0
  368. apsimo/sandbox/backend.py +195 -0
  369. apsimo/sandbox/manager.py +173 -0
  370. apsimo/scope_bounds.py +7 -0
  371. apsimo/secrets/__init__.py +6 -0
  372. apsimo/secrets/backends/__init__.py +8 -0
  373. apsimo/secrets/backends/base.py +42 -0
  374. apsimo/secrets/backends/env.py +110 -0
  375. apsimo/secrets/backends/keyring.py +72 -0
  376. apsimo/secrets/backends/onepassword.py +232 -0
  377. apsimo/secrets/cli.py +191 -0
  378. apsimo/secrets/manager.py +160 -0
  379. apsimo/secrets/migration.py +101 -0
  380. apsimo/secrets/types.py +98 -0
  381. apsimo/seed.py +41 -0
  382. apsimo/self_model/__init__.py +37 -0
  383. apsimo/self_model/appraisals.py +673 -0
  384. apsimo/self_model/benchmark.py +1314 -0
  385. apsimo/self_model/brief.py +40 -0
  386. apsimo/self_model/event_concerns.py +1128 -0
  387. apsimo/self_model/execution_forecasts.py +353 -0
  388. apsimo/self_model/expectations.py +1595 -0
  389. apsimo/self_model/experiments.py +1150 -0
  390. apsimo/self_model/journal.py +148 -0
  391. apsimo/self_model/judgments.py +705 -0
  392. apsimo/self_model/native_outcomes.py +55 -0
  393. apsimo/self_model/params.py +220 -0
  394. apsimo/self_model/perspective.py +246 -0
  395. apsimo/self_model/reconcile.py +183 -0
  396. apsimo/self_model/reply_forecasts.py +381 -0
  397. apsimo/self_model/runtime_forecasts.py +296 -0
  398. apsimo/self_model/runtime_models.py +67 -0
  399. apsimo/self_model/settlement.py +207 -0
  400. apsimo/self_model/situation.py +1731 -0
  401. apsimo/self_model/store.py +883 -0
  402. apsimo/self_model/supervised.py +137 -0
  403. apsimo/self_model/thinker.py +99 -0
  404. apsimo/self_model/trust.py +388 -0
  405. apsimo/self_model/workspace.py +2388 -0
  406. apsimo/server.py +4197 -0
  407. apsimo/services/__init__.py +1 -0
  408. apsimo/services/agent_bridge.py +474 -0
  409. apsimo/services/initiative_executor.py +914 -0
  410. apsimo/services/instance.py +297 -0
  411. apsimo/sessions/__init__.py +22 -0
  412. apsimo/sessions/config.py +13 -0
  413. apsimo/sessions/context_loader.py +88 -0
  414. apsimo/sessions/federation_session.py +75 -0
  415. apsimo/sessions/isolated_session.py +98 -0
  416. apsimo/sessions/reports.py +84 -0
  417. apsimo/sessions/store.py +148 -0
  418. apsimo/setup.py +2818 -0
  419. apsimo/setup_hermes.py +879 -0
  420. apsimo/setup_local_work.py +218 -0
  421. apsimo/setup_native_goals.py +134 -0
  422. apsimo/setup_native_reviews.py +115 -0
  423. apsimo/skills/__init__.py +10 -0
  424. apsimo/skills/base.py +108 -0
  425. apsimo/skills/budget.py +28 -0
  426. apsimo/skills/executor.py +493 -0
  427. apsimo/skills/executors/__init__.py +1 -0
  428. apsimo/skills/executors/behavioral_correction.py +75 -0
  429. apsimo/skills/executors/capability_gap.py +38 -0
  430. apsimo/skills/executors/data_quality.py +163 -0
  431. apsimo/skills/executors/knowledge_acquisition.py +41 -0
  432. apsimo/skills/executors/operational_hygiene.py +185 -0
  433. apsimo/skills/executors/subsystem_health.py +169 -0
  434. apsimo/skills/hermes_export.py +431 -0
  435. apsimo/skills/index.py +123 -0
  436. apsimo/skills/learning/__init__.py +21 -0
  437. apsimo/skills/learning/novelty_detector.py +206 -0
  438. apsimo/skills/learning/pattern_extractor.py +199 -0
  439. apsimo/skills/learning/triggers.py +159 -0
  440. apsimo/skills/loader.py +246 -0
  441. apsimo/skills/migrations/002_progressive_loading.sql +6 -0
  442. apsimo/skills/migrations/backfill_triggers.py +20 -0
  443. apsimo/skills/models.py +202 -0
  444. apsimo/skills/packager.py +128 -0
  445. apsimo/skills/protocols.py +70 -0
  446. apsimo/skills/registry.py +191 -0
  447. apsimo/skills/runtime.py +58 -0
  448. apsimo/skills/sandbox_runner.py +229 -0
  449. apsimo/skills/scheduler.py +129 -0
  450. apsimo/skills/schema.py +79 -0
  451. apsimo/skills/security/__init__.py +12 -0
  452. apsimo/skills/security/guards.py +53 -0
  453. apsimo/skills/security/scanner.py +223 -0
  454. apsimo/skills_memory/__init__.py +26 -0
  455. apsimo/skills_memory/distill.py +159 -0
  456. apsimo/skills_memory/models.py +85 -0
  457. apsimo/skills_memory/retrieve.py +62 -0
  458. apsimo/skills_memory/store.py +172 -0
  459. apsimo/surprise/__init__.py +6 -0
  460. apsimo/surprise/accumulation.py +57 -0
  461. apsimo/surprise/scorer.py +102 -0
  462. apsimo/surprise/store.py +203 -0
  463. apsimo/task_queue/__init__.py +69 -0
  464. apsimo/task_queue/action_receipts.py +148 -0
  465. apsimo/task_queue/approval_relay_canary.py +108 -0
  466. apsimo/task_queue/config.py +85 -0
  467. apsimo/task_queue/contract.py +361 -0
  468. apsimo/task_queue/events.py +130 -0
  469. apsimo/task_queue/governor.py +1031 -0
  470. apsimo/task_queue/handlers/__init__.py +16 -0
  471. apsimo/task_queue/handlers/base.py +37 -0
  472. apsimo/task_queue/handlers/inference.py +640 -0
  473. apsimo/task_queue/handlers/monitoring.py +116 -0
  474. apsimo/task_queue/handlers/registry.py +75 -0
  475. apsimo/task_queue/handlers/subtask_handler.py +173 -0
  476. apsimo/task_queue/handlers/system_maintenance.py +147 -0
  477. apsimo/task_queue/mesh_integration.py +111 -0
  478. apsimo/task_queue/models.py +317 -0
  479. apsimo/task_queue/queue_manager.py +8286 -0
  480. apsimo/task_queue/routing.py +287 -0
  481. apsimo/task_queue/scheduler.py +252 -0
  482. apsimo/task_queue/schema.sql +197 -0
  483. apsimo/task_queue/work_control.py +342 -0
  484. apsimo/task_queue/worker.py +993 -0
  485. apsimo/telemetry.py +145 -0
  486. apsimo/tom/__init__.py +6 -0
  487. apsimo/tom/affect.py +387 -0
  488. apsimo/tom/approvals.py +171 -0
  489. apsimo/tom/arcs.py +896 -0
  490. apsimo/tom/asymmetry.py +131 -0
  491. apsimo/tom/eligibility.py +248 -0
  492. apsimo/tom/engagement.py +214 -0
  493. apsimo/tom/exposure.py +214 -0
  494. apsimo/tom/extractor.py +306 -0
  495. apsimo/tom/fact_adapters.py +144 -0
  496. apsimo/tom/facts.py +326 -0
  497. apsimo/tom/integration.py +592 -0
  498. apsimo/tom/leveled.py +118 -0
  499. apsimo/tom/levels.py +247 -0
  500. apsimo/tom/recipient_audit.py +995 -0
  501. apsimo/tom/recipient_simulator.py +593 -0
  502. apsimo/tom/source_lineage.py +93 -0
  503. apsimo/tom/tom2.py +277 -0
  504. apsimo/tom/visibility.py +559 -0
  505. apsimo/tom/visibility_store.py +414 -0
  506. apsimo/tools/__init__.py +0 -0
  507. apsimo/tools/definitions.py +740 -0
  508. apsimo/tools/handlers.py +943 -0
  509. apsimo/toolsmith/__init__.py +26 -0
  510. apsimo/toolsmith/authority.py +166 -0
  511. apsimo/toolsmith/engine.py +559 -0
  512. apsimo/toolsmith/integrity.py +100 -0
  513. apsimo/toolsmith/miner.py +145 -0
  514. apsimo/toolsmith/policy.py +110 -0
  515. apsimo/toolsmith/registry.py +635 -0
  516. apsimo/turns/__init__.py +17 -0
  517. apsimo/turns/audio.py +134 -0
  518. apsimo/turns/documents.py +235 -0
  519. apsimo/turns/executions.py +486 -0
  520. apsimo/turns/hermes_history.py +245 -0
  521. apsimo/turns/hermes_kanban.py +268 -0
  522. apsimo/turns/hermes_work.py +96 -0
  523. apsimo/turns/idempotency.py +752 -0
  524. apsimo/turns/local_work.py +115 -0
  525. apsimo/turns/media.py +581 -0
  526. apsimo/turns/reported_workers.py +196 -0
  527. apsimo/turns/source_annotations.py +283 -0
  528. apsimo/turns/source_attribution.py +154 -0
  529. apsimo/turns/source_read.py +351 -0
  530. apsimo/turns/source_vectors.py +263 -0
  531. apsimo/turns/video.py +210 -0
  532. apsimo/util/autonomy_preset.py +220 -0
  533. apsimo/util/instance.py +92 -0
  534. apsimo/util/model_output.py +25 -0
  535. apsimo/util/quiet_hours.py +27 -0
  536. apsimo/util/session_safety.py +37 -0
  537. apsimo/util/temporal.py +343 -0
  538. apsimo/vector/__init__.py +75 -0
  539. apsimo/vector/backfill.py +171 -0
  540. apsimo/vector/caption.py +114 -0
  541. apsimo/vector/collections.py +51 -0
  542. apsimo/vector/config.py +102 -0
  543. apsimo/vector/embedder.py +670 -0
  544. apsimo/vector/image_preprocess.py +406 -0
  545. apsimo/vector/image_store.py +296 -0
  546. apsimo/vector/indexes.py +162 -0
  547. apsimo/vector/migrate.py +334 -0
  548. apsimo/vector/multimodal_provider.py +417 -0
  549. apsimo/vector/multimodal_types.py +87 -0
  550. apsimo/vector/openai_provider.py +119 -0
  551. apsimo/vector/query.py +49 -0
  552. apsimo/vector/reranker.py +565 -0
  553. apsimo/vector/safety_image.py +159 -0
  554. apsimo/vector/scanner.py +197 -0
  555. apsimo/vector/setup.py +289 -0
  556. apsimo/vector/store.py +533 -0
  557. apsimo/vector/tiers.py +263 -0
  558. apsimo/work_orders.py +925 -0
  559. apsimo/workers/__init__.py +21 -0
  560. apsimo/workers/agent_bridge.py +640 -0
  561. apsimo/workers/colony_worker.py +382 -0
  562. apsimo/workers/queue_worker.py +441 -0
  563. apsimo/workers/skills_sync.py +152 -0
  564. apsimo/world_model/__init__.py +71 -0
  565. apsimo/world_model/causal_maintenance.py +131 -0
  566. apsimo/world_model/causal_policy.py +43 -0
  567. apsimo/world_model/causal_query.py +125 -0
  568. apsimo/world_model/confidence.py +54 -0
  569. apsimo/world_model/config.py +64 -0
  570. apsimo/world_model/constants.py +97 -0
  571. apsimo/world_model/entities.py +145 -0
  572. apsimo/world_model/expectation_resolvers.py +177 -0
  573. apsimo/world_model/extraction/__init__.py +7 -0
  574. apsimo/world_model/extraction/base.py +62 -0
  575. apsimo/world_model/extraction/conversation_extractor.py +262 -0
  576. apsimo/world_model/extraction/detector.py +74 -0
  577. apsimo/world_model/extraction/document_extractor.py +78 -0
  578. apsimo/world_model/extraction/formats/__init__.py +24 -0
  579. apsimo/world_model/extraction/formats/csv_fmt.py +68 -0
  580. apsimo/world_model/extraction/formats/html_fmt.py +72 -0
  581. apsimo/world_model/extraction/formats/json_fmt.py +68 -0
  582. apsimo/world_model/extraction/formats/pdf.py +43 -0
  583. apsimo/world_model/extraction/formats/text.py +27 -0
  584. apsimo/world_model/extraction/llm_extractor.py +164 -0
  585. apsimo/world_model/extraction/pipeline.py +73 -0
  586. apsimo/world_model/integrations/__init__.py +5 -0
  587. apsimo/world_model/integrations/mind_model_bridge.py +115 -0
  588. apsimo/world_model/integrations/social_intel_bridge.py +120 -0
  589. apsimo/world_model/jobs/__init__.py +4 -0
  590. apsimo/world_model/jobs/extraction_job.py +168 -0
  591. apsimo/world_model/llm_extract.py +572 -0
  592. apsimo/world_model/neo4j/__init__.py +5 -0
  593. apsimo/world_model/neo4j/backend.py +654 -0
  594. apsimo/world_model/observations.py +155 -0
  595. apsimo/world_model/populator.py +307 -0
  596. apsimo/world_model/postgres/__init__.py +1 -0
  597. apsimo/world_model/postgres/backend.py +683 -0
  598. apsimo/world_model/relationships.py +25 -0
  599. apsimo/world_model/resolution/__init__.py +13 -0
  600. apsimo/world_model/resolution/entity_resolver.py +232 -0
  601. apsimo/world_model/resolution/merge_audit.py +16 -0
  602. apsimo/world_model/resolution/merge_workflow.py +117 -0
  603. apsimo/world_model/source_reports.py +121 -0
  604. apsimo/world_model/sqlite/__init__.py +4 -0
  605. apsimo/world_model/sqlite/backend.py +855 -0
  606. apsimo/world_model/sqlite/schema.sql +132 -0
  607. apsimo/world_model/store.py +545 -0
  608. apsimo-1.3.0.dist-info/METADATA +78 -0
  609. apsimo-1.3.0.dist-info/RECORD +614 -0
  610. apsimo-1.3.0.dist-info/WHEEL +5 -0
  611. apsimo-1.3.0.dist-info/entry_points.txt +11 -0
  612. apsimo-1.3.0.dist-info/licenses/LICENSE +21 -0
  613. apsimo-1.3.0.dist-info/top_level.txt +2 -0
  614. colony_sidecar/__init__.py +4 -0
@@ -0,0 +1,995 @@
1
+ """Reference-only persistence and coverage for P8 recipient simulation.
2
+
3
+ Coverage is event based. A selected outbound item is first registered as a
4
+ ``sample``; a later ``evaluation`` event references the same outbound item.
5
+ That makes a missing simulation visible instead of deriving a misleading
6
+ success rate solely from results that happened to be written.
7
+
8
+ Rows retain stable references, action/risk codes, and SHA-256 digests only.
9
+ Draft/fact content, fact and arc references, dependency errors, credentials,
10
+ and host topology are intentionally not represented by this schema.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from dataclasses import dataclass
16
+ from datetime import datetime, timezone
17
+ import hashlib
18
+ import json
19
+ import os
20
+ from pathlib import Path
21
+ import re
22
+ import sqlite3
23
+ import threading
24
+ from typing import Any, Iterable, Optional
25
+
26
+ from apsimo.tom.recipient_simulator import (
27
+ FAIL_BEHAVIOR_BY_RISK,
28
+ REALTIME_VOICE_SURFACES,
29
+ RecipientSimulationRequestV1,
30
+ RecipientSimulationResultV1,
31
+ is_realtime_voice_surface,
32
+ recipient_simulator_mode,
33
+ )
34
+ from apsimo.tom.visibility import ViewerContextV1, content_digest
35
+
36
+
37
+ SCHEMA_VERSION = 1
38
+ EVENT_KINDS = frozenset({"sample", "evaluation"})
39
+ MAX_AUDIT_CODES = 64
40
+ MAX_PROJECTED_EVENTS = 256
41
+ MAX_MISSING_REFS = 64
42
+ COVERAGE_FETCH_SIZE = 512
43
+
44
+ AUDIT_RISK_CODES = frozenset({
45
+ "arc_projection_incomplete",
46
+ "fact_ref_not_recipient_authorized",
47
+ "high_salience_provenance_unknown",
48
+ "recipient_identity_unattested",
49
+ "simulation_dependency_error",
50
+ "stress_topic_pressure",
51
+ })
52
+ AUDIT_REPAIR_CODES = frozenset({
53
+ "add_fact_provenance",
54
+ "review_arc_context",
55
+ "soften_pressure_language",
56
+ })
57
+ AUDIT_ACTIONS = frozenset({
58
+ "hold", "no_effect", "observe", "observe_async", "observe_only",
59
+ "repair", "review", "send",
60
+ })
61
+ AUDIT_EVALUATION_PATHS = frozenset({
62
+ "async_observation", "disabled", "pre_send_advisory",
63
+ "shadow_observation",
64
+ })
65
+ AUDIT_SURFACES = REALTIME_VOICE_SURFACES.union({
66
+ "api", "assistant", "chat", "deck", "discord", "email",
67
+ "facetime", "google_chat", "imessage", "matrix", "mms",
68
+ "operator_deck", "rcs", "signal", "slack", "sms", "teams",
69
+ "telegram", "text", "web", "whatsapp", "whatsapp_call",
70
+ })
71
+
72
+ _REF_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9_.:@/+\-]{0,191}$")
73
+ _DIGEST_RE = re.compile(r"^[a-f0-9]{64}$")
74
+
75
+
76
+ class RecipientAuditConflictError(ValueError):
77
+ """An audit identity/outbound reference changed immutable content."""
78
+
79
+
80
+ def _canonical(value: Any) -> str:
81
+ return json.dumps(
82
+ value,
83
+ ensure_ascii=True,
84
+ separators=(",", ":"),
85
+ sort_keys=True,
86
+ )
87
+
88
+
89
+ def _digest(value: Any) -> str:
90
+ return hashlib.sha256(_canonical(value).encode("utf-8")).hexdigest()
91
+
92
+
93
+ def _ref(value: Any, *, field: str, allow_empty: bool = False) -> str:
94
+ normalized = str(value or "").strip()
95
+ if allow_empty and not normalized:
96
+ return ""
97
+ if not _REF_RE.fullmatch(normalized):
98
+ raise ValueError(f"{field} is not a bounded opaque reference")
99
+ return normalized
100
+
101
+
102
+ def _sha(value: Any, *, field: str, allow_empty: bool = False) -> str:
103
+ normalized = str(value or "").strip().lower()
104
+ if allow_empty and not normalized:
105
+ return ""
106
+ if not _DIGEST_RE.fullmatch(normalized):
107
+ raise ValueError(f"{field} must be a SHA-256 hex digest")
108
+ return normalized
109
+
110
+
111
+ def _refs(values: Iterable[Any], *, field: str) -> tuple[str, ...]:
112
+ normalized = tuple(sorted(dict.fromkeys(
113
+ _ref(value, field=field) for value in values
114
+ )))
115
+ if len(normalized) > MAX_AUDIT_CODES:
116
+ raise ValueError(f"{field} exceeds bounded reference limit")
117
+ return normalized
118
+
119
+
120
+ def _iso(value: datetime | str, *, field: str) -> str:
121
+ if isinstance(value, datetime):
122
+ parsed = value
123
+ else:
124
+ try:
125
+ parsed = datetime.fromisoformat(str(value).replace("Z", "+00:00"))
126
+ except (TypeError, ValueError) as exc:
127
+ raise ValueError(f"{field} must be an ISO-8601 timestamp") from exc
128
+ if parsed.tzinfo is None:
129
+ raise ValueError(f"{field} must include a timezone")
130
+ return parsed.astimezone(timezone.utc).isoformat()
131
+
132
+
133
+ @dataclass(frozen=True, slots=True)
134
+ class RecipientAuditEventV1:
135
+ event_id: str
136
+ idempotency_key: str
137
+ event_kind: str
138
+ outbound_item_ref: str
139
+ recipient_person_id: str
140
+ scope_revision: str
141
+ high_salience: bool
142
+ occurred_at: str
143
+ simulation_ref: str = ""
144
+ request_digest: str = ""
145
+ result_digest: str = ""
146
+ draft_digest: str = ""
147
+ surface: str = ""
148
+ risk_class: str = ""
149
+ evaluated: bool = False
150
+ would_action: str = ""
151
+ effective_action: str = ""
152
+ evaluation_path: str = ""
153
+ risk_codes: tuple[str, ...] = ()
154
+ repair_codes: tuple[str, ...] = ()
155
+ schema_version: int = SCHEMA_VERSION
156
+
157
+ def __post_init__(self) -> None:
158
+ if self.schema_version != SCHEMA_VERSION:
159
+ raise ValueError("unsupported recipient audit version")
160
+ event_id = _ref(self.event_id, field="event_id")
161
+ idempotency_key = _ref(
162
+ self.idempotency_key, field="idempotency_key")
163
+ event_kind = str(self.event_kind or "").strip().lower()
164
+ if event_kind not in EVENT_KINDS:
165
+ raise ValueError("unknown recipient audit event kind")
166
+ outbound = _ref(self.outbound_item_ref, field="outbound_item_ref")
167
+ recipient = _ref(
168
+ self.recipient_person_id, field="recipient_person_id")
169
+ revision = _ref(self.scope_revision, field="scope_revision")
170
+ occurred_at = _iso(self.occurred_at, field="occurred_at")
171
+ simulation = _ref(
172
+ self.simulation_ref, field="simulation_ref", allow_empty=True)
173
+ request_digest = _sha(
174
+ self.request_digest, field="request_digest", allow_empty=True)
175
+ result_digest = _sha(
176
+ self.result_digest, field="result_digest", allow_empty=True)
177
+ draft_digest = _sha(self.draft_digest, field="draft_digest")
178
+ surface = _ref(self.surface, field="surface", allow_empty=True).lower()
179
+ risk_class = _ref(
180
+ self.risk_class, field="risk_class", allow_empty=True).lower()
181
+ would = _ref(
182
+ self.would_action, field="would_action", allow_empty=True).lower()
183
+ effective = _ref(
184
+ self.effective_action,
185
+ field="effective_action",
186
+ allow_empty=True,
187
+ ).lower()
188
+ path = _ref(
189
+ self.evaluation_path,
190
+ field="evaluation_path",
191
+ allow_empty=True,
192
+ ).lower()
193
+ risk_codes = _refs(self.risk_codes, field="risk_code")
194
+ repair_codes = _refs(self.repair_codes, field="repair_code")
195
+
196
+ evaluation_values = (
197
+ simulation, request_digest, result_digest, surface,
198
+ risk_class, would, effective, path,
199
+ )
200
+ if event_kind == "sample":
201
+ if any(evaluation_values) or self.evaluated\
202
+ or risk_codes or repair_codes:
203
+ raise ValueError("sample events cannot contain evaluation data")
204
+ else:
205
+ if not all(evaluation_values):
206
+ raise ValueError(
207
+ "evaluation events require complete digest metadata")
208
+ if risk_class not in {"low", "medium", "high", "critical"}:
209
+ raise ValueError("evaluation risk class is not auditable")
210
+ if surface not in AUDIT_SURFACES:
211
+ raise ValueError("evaluation surface is not auditable")
212
+ if would not in AUDIT_ACTIONS or effective not in AUDIT_ACTIONS:
213
+ raise ValueError("evaluation action is not auditable")
214
+ if path not in AUDIT_EVALUATION_PATHS:
215
+ raise ValueError("evaluation path is not auditable")
216
+ for code in risk_codes:
217
+ severity, separator, name = code.partition(":")
218
+ if separator != ":"\
219
+ or severity not in {"low", "medium", "high", "critical"}\
220
+ or name not in AUDIT_RISK_CODES:
221
+ raise ValueError("evaluation risk code is not auditable")
222
+ for code in repair_codes:
223
+ priority, separator, name = code.partition(":")
224
+ if separator != ":"\
225
+ or priority not in {"low", "medium", "high", "critical"}\
226
+ or name not in AUDIT_REPAIR_CODES:
227
+ raise ValueError("evaluation repair code is not auditable")
228
+
229
+ object.__setattr__(self, "event_id", event_id)
230
+ object.__setattr__(self, "idempotency_key", idempotency_key)
231
+ object.__setattr__(self, "event_kind", event_kind)
232
+ object.__setattr__(self, "outbound_item_ref", outbound)
233
+ object.__setattr__(self, "recipient_person_id", recipient)
234
+ object.__setattr__(self, "scope_revision", revision)
235
+ object.__setattr__(self, "high_salience", bool(self.high_salience))
236
+ object.__setattr__(self, "occurred_at", occurred_at)
237
+ object.__setattr__(self, "simulation_ref", simulation)
238
+ object.__setattr__(self, "request_digest", request_digest)
239
+ object.__setattr__(self, "result_digest", result_digest)
240
+ object.__setattr__(self, "draft_digest", draft_digest)
241
+ object.__setattr__(self, "surface", surface)
242
+ object.__setattr__(self, "risk_class", risk_class)
243
+ object.__setattr__(self, "evaluated", bool(self.evaluated))
244
+ object.__setattr__(self, "would_action", would)
245
+ object.__setattr__(self, "effective_action", effective)
246
+ object.__setattr__(self, "evaluation_path", path)
247
+ object.__setattr__(self, "risk_codes", risk_codes)
248
+ object.__setattr__(self, "repair_codes", repair_codes)
249
+
250
+ def public(self) -> dict[str, Any]:
251
+ """Return stable references/codes/digests and no message content."""
252
+
253
+ value: dict[str, Any] = {
254
+ "schema_version": self.schema_version,
255
+ "event_id": self.event_id,
256
+ "idempotency_key": self.idempotency_key,
257
+ "event_kind": self.event_kind,
258
+ "outbound_item_ref": self.outbound_item_ref,
259
+ "recipient_person_id": self.recipient_person_id,
260
+ "scope_revision": self.scope_revision,
261
+ "high_salience": self.high_salience,
262
+ "occurred_at": self.occurred_at,
263
+ "draft_digest": self.draft_digest,
264
+ "evaluated": self.evaluated,
265
+ }
266
+ if self.event_kind == "evaluation":
267
+ value.update({
268
+ "simulation_ref": self.simulation_ref,
269
+ "request_digest": self.request_digest,
270
+ "result_digest": self.result_digest,
271
+ "draft_digest": self.draft_digest,
272
+ "surface": self.surface,
273
+ "risk_class": self.risk_class,
274
+ "would_action": self.would_action,
275
+ "effective_action": self.effective_action,
276
+ "evaluation_path": self.evaluation_path,
277
+ "risk_codes": list(self.risk_codes),
278
+ "repair_codes": list(self.repair_codes),
279
+ })
280
+ return value
281
+
282
+ @property
283
+ def audit_digest(self) -> str:
284
+ return _digest(self.public())
285
+
286
+
287
+ def _attested_recipient(recipient: ViewerContextV1) -> tuple[str, str]:
288
+ if not isinstance(recipient, ViewerContextV1)\
289
+ or not recipient.attested\
290
+ or not recipient.viewer_person_id\
291
+ or not recipient.scope_revision:
292
+ raise ValueError("recipient audit requires an attested recipient")
293
+ return recipient.viewer_person_id, recipient.scope_revision
294
+
295
+
296
+ def sample_event(
297
+ *,
298
+ event_id: str,
299
+ idempotency_key: str,
300
+ outbound_item_ref: str,
301
+ recipient: ViewerContextV1,
302
+ high_salience: bool,
303
+ draft_text: str,
304
+ sampled_at: datetime | str,
305
+ ) -> RecipientAuditEventV1:
306
+ """Build a reference-only selection marker before simulation runs."""
307
+
308
+ person, revision = _attested_recipient(recipient)
309
+ return RecipientAuditEventV1(
310
+ event_id=event_id,
311
+ idempotency_key=idempotency_key,
312
+ event_kind="sample",
313
+ outbound_item_ref=outbound_item_ref,
314
+ recipient_person_id=person,
315
+ scope_revision=revision,
316
+ high_salience=high_salience,
317
+ occurred_at=_iso(sampled_at, field="sampled_at"),
318
+ draft_digest=content_digest(draft_text),
319
+ )
320
+
321
+
322
+ def evaluation_event_from_result(
323
+ *,
324
+ event_id: str,
325
+ idempotency_key: str,
326
+ outbound_item_ref: str,
327
+ request: RecipientSimulationRequestV1,
328
+ result: RecipientSimulationResultV1,
329
+ evaluated_at: datetime | str,
330
+ ) -> RecipientAuditEventV1:
331
+ """Reduce a simulation into digest/code evidence without private refs."""
332
+
333
+ if not isinstance(request, RecipientSimulationRequestV1):
334
+ raise ValueError("request must be RecipientSimulationRequestV1")
335
+ if not isinstance(result, RecipientSimulationResultV1):
336
+ raise ValueError("result must be RecipientSimulationResultV1")
337
+ result_payload = result.public()
338
+ result_payload.pop("audit_digest", None)
339
+ if result.audit_digest != _digest(result_payload):
340
+ raise ValueError("simulation result audit digest is invalid")
341
+ if result.request_digest != request.audit_digest:
342
+ raise ValueError("simulation result request digest does not match request")
343
+ if result.simulation_id != request.simulation_id:
344
+ raise ValueError("simulation result reference does not match request")
345
+ if result.risk_class != request.risk_class:
346
+ raise ValueError("simulation result risk class does not match request")
347
+ if result.external_effect or result.authority_granted\
348
+ or result.synchronous_gate:
349
+ raise ValueError("simulation audit cannot record granted authority/effect")
350
+ if request.surface not in AUDIT_SURFACES:
351
+ raise ValueError("simulation surface is not auditable")
352
+ if result.mode not in {"off", "shadow", "live"}:
353
+ raise ValueError("simulation result mode is not auditable")
354
+ if result.fail_behavior != FAIL_BEHAVIOR_BY_RISK[request.risk_class]:
355
+ raise ValueError("simulation fail behavior does not match risk class")
356
+ if result.would_recommend not in AUDIT_ACTIONS\
357
+ or result.recommended_action not in AUDIT_ACTIONS:
358
+ raise ValueError("simulation action is not auditable")
359
+ if result.evaluation_path not in AUDIT_EVALUATION_PATHS:
360
+ raise ValueError("simulation evaluation path is not auditable")
361
+ unknown_risks = sorted(
362
+ risk.code for risk in result.risks if risk.code not in AUDIT_RISK_CODES)
363
+ if unknown_risks:
364
+ raise ValueError(
365
+ f"simulation risk code is not auditable: {unknown_risks[0]}")
366
+ unknown_repairs = sorted(
367
+ repair.code for repair in result.repairs
368
+ if repair.code not in AUDIT_REPAIR_CODES)
369
+ if unknown_repairs:
370
+ raise ValueError(
371
+ f"simulation repair code is not auditable: {unknown_repairs[0]}")
372
+ severity_rank = {"low": 1, "medium": 2, "high": 3, "critical": 4}
373
+ if any(risk.code == "simulation_dependency_error" for risk in result.risks):
374
+ expected_would = result.fail_behavior
375
+ else:
376
+ rank = max(
377
+ (severity_rank[risk.severity] for risk in result.risks),
378
+ default=0,
379
+ )
380
+ if rank >= 3:
381
+ expected_would = "hold"
382
+ elif rank == 2:
383
+ expected_would = "repair" if result.repairs else "review"
384
+ elif rank == 1:
385
+ expected_would = "observe"
386
+ else:
387
+ expected_would = "send"
388
+ if not result.evaluated:
389
+ expected_would = "no_effect"
390
+ expected_action = "no_effect"
391
+ expected_path = "disabled"
392
+ if result.mode != "off" or result.risks or result.repairs\
393
+ or result.authorized_fact_refs or result.active_arc_refs\
394
+ or result.fact_projection_digest or result.arc_projection_digest:
395
+ raise ValueError("disabled simulation result has impossible state")
396
+ elif result.mode == "off":
397
+ raise ValueError("evaluated simulation result cannot use off mode")
398
+ elif is_realtime_voice_surface(request.surface):
399
+ expected_action = "observe_async"
400
+ expected_path = "async_observation"
401
+ elif result.mode == "shadow":
402
+ expected_action = "observe_only"
403
+ expected_path = "shadow_observation"
404
+ else:
405
+ expected_action = expected_would
406
+ expected_path = "pre_send_advisory"
407
+ if result.would_recommend != expected_would\
408
+ or result.recommended_action != expected_action\
409
+ or result.evaluation_path != expected_path:
410
+ raise ValueError("simulation result state matrix is inconsistent")
411
+ person, revision = _attested_recipient(request.recipient)
412
+ risks = tuple(
413
+ f"{risk.severity}:{risk.code}" for risk in result.risks)
414
+ repairs = tuple(
415
+ f"{repair.priority}:{repair.code}" for repair in result.repairs)
416
+ return RecipientAuditEventV1(
417
+ event_id=event_id,
418
+ idempotency_key=idempotency_key,
419
+ event_kind="evaluation",
420
+ outbound_item_ref=outbound_item_ref,
421
+ recipient_person_id=person,
422
+ scope_revision=revision,
423
+ high_salience=request.high_salience,
424
+ occurred_at=_iso(evaluated_at, field="evaluated_at"),
425
+ simulation_ref=request.simulation_id,
426
+ request_digest=request.audit_digest,
427
+ result_digest=result.audit_digest,
428
+ draft_digest=content_digest(request.draft_text),
429
+ surface=request.surface,
430
+ risk_class=request.risk_class,
431
+ evaluated=result.evaluated,
432
+ would_action=result.would_recommend,
433
+ effective_action=result.recommended_action,
434
+ evaluation_path=result.evaluation_path,
435
+ risk_codes=risks,
436
+ repair_codes=repairs,
437
+ )
438
+
439
+
440
+ @dataclass(frozen=True, slots=True)
441
+ class RecipientAuditAppendV1:
442
+ event: RecipientAuditEventV1
443
+ sequence: int
444
+ appended: bool
445
+ replayed: bool
446
+
447
+
448
+ @dataclass(frozen=True, slots=True)
449
+ class RecipientAuditProjectionV1:
450
+ events: tuple[RecipientAuditEventV1, ...]
451
+ viewer_attested: bool
452
+ corrupt_count: int
453
+ truncated: bool
454
+ viewer_digest: str
455
+ audit_digest: str
456
+ schema_version: int = SCHEMA_VERSION
457
+
458
+ def public(self) -> dict[str, Any]:
459
+ return {
460
+ "schema_version": self.schema_version,
461
+ "events": [event.public() for event in self.events],
462
+ "viewer_attested": self.viewer_attested,
463
+ "corrupt_count": self.corrupt_count,
464
+ "truncated": self.truncated,
465
+ "viewer_digest": self.viewer_digest,
466
+ "audit_digest": self.audit_digest,
467
+ }
468
+
469
+
470
+ @dataclass(frozen=True, slots=True)
471
+ class RecipientAuditCoverageV1:
472
+ sampled_high_salience: int
473
+ evaluated_high_salience: int
474
+ unevaluated_high_salience: int
475
+ missing_item_refs: tuple[str, ...]
476
+ missing_refs_truncated: bool
477
+ corrupt_count: int
478
+ scan_truncated: bool
479
+ status: str
480
+ coverage_complete: bool
481
+ viewer_digest: str
482
+ audit_digest: str
483
+ schema_version: int = SCHEMA_VERSION
484
+
485
+ def public(self) -> dict[str, Any]:
486
+ return {
487
+ "schema_version": self.schema_version,
488
+ "sampled_high_salience": self.sampled_high_salience,
489
+ "evaluated_high_salience": self.evaluated_high_salience,
490
+ "unevaluated_high_salience": self.unevaluated_high_salience,
491
+ "missing_item_refs": list(self.missing_item_refs),
492
+ "missing_refs_truncated": self.missing_refs_truncated,
493
+ "corrupt_count": self.corrupt_count,
494
+ "scan_truncated": self.scan_truncated,
495
+ "status": self.status,
496
+ "coverage_complete": self.coverage_complete,
497
+ "viewer_digest": self.viewer_digest,
498
+ "audit_digest": self.audit_digest,
499
+ }
500
+
501
+
502
+ class RecipientSimulationAuditStore:
503
+ """Append-only recipient-simulation evidence and coverage ledger."""
504
+
505
+ def __init__(self, db_path: str | os.PathLike[str] = ":memory:") -> None:
506
+ self.path = str(db_path)
507
+ if self.path != ":memory:":
508
+ path = Path(self.path)
509
+ path.parent.mkdir(parents=True, exist_ok=True)
510
+ if not path.exists():
511
+ path.touch(mode=0o600)
512
+ else:
513
+ os.chmod(path, 0o600)
514
+ self._lock = threading.RLock()
515
+ self._conn = sqlite3.connect(
516
+ self.path, check_same_thread=False, timeout=30.0)
517
+ self._conn.row_factory = sqlite3.Row
518
+ self._conn.execute("PRAGMA busy_timeout=30000")
519
+ self._conn.execute("PRAGMA recursive_triggers=ON")
520
+ self._conn.execute("PRAGMA journal_mode=WAL")
521
+ self._conn.execute("PRAGMA synchronous=FULL")
522
+ self._conn.executescript(
523
+ """
524
+ CREATE TABLE IF NOT EXISTS recipient_simulation_audit (
525
+ seq INTEGER PRIMARY KEY AUTOINCREMENT,
526
+ event_id TEXT NOT NULL UNIQUE,
527
+ idempotency_key TEXT NOT NULL UNIQUE,
528
+ event_kind TEXT NOT NULL CHECK (
529
+ event_kind IN ('sample','evaluation')),
530
+ outbound_item_ref TEXT NOT NULL,
531
+ recipient_person_id TEXT NOT NULL,
532
+ scope_revision TEXT NOT NULL,
533
+ high_salience INTEGER NOT NULL CHECK (high_salience IN (0,1)),
534
+ occurred_at TEXT NOT NULL,
535
+ simulation_ref TEXT NOT NULL,
536
+ request_digest TEXT NOT NULL,
537
+ result_digest TEXT NOT NULL,
538
+ draft_digest TEXT NOT NULL,
539
+ surface TEXT NOT NULL,
540
+ risk_class TEXT NOT NULL,
541
+ evaluated INTEGER NOT NULL CHECK (evaluated IN (0,1)),
542
+ would_action TEXT NOT NULL,
543
+ effective_action TEXT NOT NULL,
544
+ evaluation_path TEXT NOT NULL,
545
+ risk_codes_json TEXT NOT NULL,
546
+ repair_codes_json TEXT NOT NULL,
547
+ event_digest TEXT NOT NULL UNIQUE,
548
+ payload_json TEXT NOT NULL,
549
+ stored_at TEXT NOT NULL,
550
+ UNIQUE(event_kind,outbound_item_ref,simulation_ref)
551
+ );
552
+ CREATE INDEX IF NOT EXISTS idx_recipient_audit_viewer_time
553
+ ON recipient_simulation_audit(
554
+ recipient_person_id,scope_revision,occurred_at,seq);
555
+ CREATE INDEX IF NOT EXISTS idx_recipient_audit_coverage
556
+ ON recipient_simulation_audit(
557
+ event_kind,high_salience,recipient_person_id,outbound_item_ref);
558
+ CREATE INDEX IF NOT EXISTS idx_recipient_audit_outbound
559
+ ON recipient_simulation_audit(outbound_item_ref,event_kind);
560
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_recipient_audit_simulation_once
561
+ ON recipient_simulation_audit(simulation_ref)
562
+ WHERE event_kind='evaluation';
563
+ CREATE TRIGGER IF NOT EXISTS recipient_audit_no_update
564
+ BEFORE UPDATE ON recipient_simulation_audit BEGIN
565
+ SELECT RAISE(ABORT, 'recipient audit is append-only');
566
+ END;
567
+ CREATE TRIGGER IF NOT EXISTS recipient_audit_no_delete
568
+ BEFORE DELETE ON recipient_simulation_audit BEGIN
569
+ SELECT RAISE(ABORT, 'recipient audit is append-only');
570
+ END;
571
+ CREATE TRIGGER IF NOT EXISTS recipient_audit_no_replace
572
+ BEFORE INSERT ON recipient_simulation_audit
573
+ WHEN EXISTS (
574
+ SELECT 1 FROM recipient_simulation_audit
575
+ WHERE seq=NEW.seq
576
+ OR event_id=NEW.event_id
577
+ OR idempotency_key=NEW.idempotency_key
578
+ OR event_digest=NEW.event_digest
579
+ OR (
580
+ event_kind=NEW.event_kind
581
+ AND outbound_item_ref=NEW.outbound_item_ref
582
+ AND simulation_ref=NEW.simulation_ref
583
+ )
584
+ OR (
585
+ NEW.event_kind='evaluation'
586
+ AND event_kind='evaluation'
587
+ AND simulation_ref=NEW.simulation_ref
588
+ )
589
+ ) BEGIN
590
+ SELECT RAISE(ABORT, 'recipient audit is append-only');
591
+ END;
592
+ """
593
+ )
594
+ self._conn.commit()
595
+ if self.path != ":memory:":
596
+ os.chmod(self.path, 0o600)
597
+
598
+ @staticmethod
599
+ def _decode(row: sqlite3.Row) -> RecipientAuditEventV1:
600
+ try:
601
+ payload = json.loads(row["payload_json"])
602
+ event = RecipientAuditEventV1(**payload)
603
+ risk_codes = json.loads(row["risk_codes_json"])
604
+ repair_codes = json.loads(row["repair_codes_json"])
605
+ except (TypeError, ValueError) as exc:
606
+ raise ValueError("recipient audit payload is unreadable") from exc
607
+ indexed = {
608
+ "event_id": str(row["event_id"]),
609
+ "idempotency_key": str(row["idempotency_key"]),
610
+ "event_kind": str(row["event_kind"]),
611
+ "outbound_item_ref": str(row["outbound_item_ref"]),
612
+ "recipient_person_id": str(row["recipient_person_id"]),
613
+ "scope_revision": str(row["scope_revision"]),
614
+ "high_salience": bool(row["high_salience"]),
615
+ "occurred_at": str(row["occurred_at"]),
616
+ "simulation_ref": str(row["simulation_ref"]),
617
+ "request_digest": str(row["request_digest"]),
618
+ "result_digest": str(row["result_digest"]),
619
+ "draft_digest": str(row["draft_digest"]),
620
+ "surface": str(row["surface"]),
621
+ "risk_class": str(row["risk_class"]),
622
+ "evaluated": bool(row["evaluated"]),
623
+ "would_action": str(row["would_action"]),
624
+ "effective_action": str(row["effective_action"]),
625
+ "evaluation_path": str(row["evaluation_path"]),
626
+ "risk_codes": tuple(risk_codes),
627
+ "repair_codes": tuple(repair_codes),
628
+ "event_digest": str(row["event_digest"]),
629
+ }
630
+ expected = {
631
+ "event_id": event.event_id,
632
+ "idempotency_key": event.idempotency_key,
633
+ "event_kind": event.event_kind,
634
+ "outbound_item_ref": event.outbound_item_ref,
635
+ "recipient_person_id": event.recipient_person_id,
636
+ "scope_revision": event.scope_revision,
637
+ "high_salience": event.high_salience,
638
+ "occurred_at": event.occurred_at,
639
+ "simulation_ref": event.simulation_ref,
640
+ "request_digest": event.request_digest,
641
+ "result_digest": event.result_digest,
642
+ "draft_digest": event.draft_digest,
643
+ "surface": event.surface,
644
+ "risk_class": event.risk_class,
645
+ "evaluated": event.evaluated,
646
+ "would_action": event.would_action,
647
+ "effective_action": event.effective_action,
648
+ "evaluation_path": event.evaluation_path,
649
+ "risk_codes": event.risk_codes,
650
+ "repair_codes": event.repair_codes,
651
+ "event_digest": event.audit_digest,
652
+ }
653
+ if indexed != expected:
654
+ raise ValueError("recipient audit digest/index mismatch")
655
+ return event
656
+
657
+ @staticmethod
658
+ def _same_outbound_authority(
659
+ first: RecipientAuditEventV1,
660
+ second: RecipientAuditEventV1,
661
+ ) -> bool:
662
+ return (
663
+ first.outbound_item_ref == second.outbound_item_ref
664
+ and first.recipient_person_id == second.recipient_person_id
665
+ and first.scope_revision == second.scope_revision
666
+ and first.high_salience == second.high_salience
667
+ and first.draft_digest == second.draft_digest
668
+ )
669
+
670
+ def append(self, event: RecipientAuditEventV1) -> RecipientAuditAppendV1:
671
+ if not isinstance(event, RecipientAuditEventV1):
672
+ raise ValueError("audit store accepts RecipientAuditEventV1 only")
673
+ with self._lock:
674
+ self._conn.execute("BEGIN IMMEDIATE")
675
+ try:
676
+ rows = self._conn.execute(
677
+ "SELECT * FROM recipient_simulation_audit "
678
+ "WHERE event_id=? OR idempotency_key=? "
679
+ "OR (event_kind=? AND outbound_item_ref=? "
680
+ "AND simulation_ref=?) "
681
+ "OR (event_kind='evaluation' AND ?='evaluation' "
682
+ "AND simulation_ref=?) ORDER BY seq",
683
+ (
684
+ event.event_id,
685
+ event.idempotency_key,
686
+ event.event_kind,
687
+ event.outbound_item_ref,
688
+ event.simulation_ref,
689
+ event.event_kind,
690
+ event.simulation_ref,
691
+ ),
692
+ ).fetchall()
693
+ if rows:
694
+ for row in rows:
695
+ saved = self._decode(row)
696
+ if (
697
+ saved.event_id == event.event_id
698
+ and saved.idempotency_key == event.idempotency_key
699
+ and saved.event_kind == event.event_kind
700
+ and saved.outbound_item_ref == event.outbound_item_ref
701
+ and saved.audit_digest == event.audit_digest
702
+ ):
703
+ self._conn.commit()
704
+ return RecipientAuditAppendV1(
705
+ event=saved,
706
+ sequence=int(row["seq"]),
707
+ appended=False,
708
+ replayed=True,
709
+ )
710
+ raise RecipientAuditConflictError(
711
+ "recipient audit identity changed immutable content")
712
+
713
+ counterpart_kind = (
714
+ "evaluation" if event.event_kind == "sample" else "sample")
715
+ counterpart_rows = self._conn.execute(
716
+ "SELECT * FROM recipient_simulation_audit "
717
+ "WHERE event_kind=? AND outbound_item_ref=? ORDER BY seq",
718
+ (counterpart_kind, event.outbound_item_ref),
719
+ ).fetchall()
720
+ if event.event_kind == "evaluation" and not counterpart_rows:
721
+ raise RecipientAuditConflictError(
722
+ "evaluation requires an immutable sample first")
723
+ for counterpart_row in counterpart_rows:
724
+ counterpart = self._decode(counterpart_row)
725
+ if not self._same_outbound_authority(event, counterpart):
726
+ raise RecipientAuditConflictError(
727
+ "evaluation does not match its immutable sample authority")
728
+ sample = event if event.event_kind == "sample" else counterpart
729
+ evaluation = (
730
+ event if event.event_kind == "evaluation" else counterpart)
731
+ if evaluation.occurred_at < sample.occurred_at:
732
+ raise RecipientAuditConflictError(
733
+ "evaluation predates its immutable sample")
734
+
735
+ cursor = self._conn.execute(
736
+ "INSERT INTO recipient_simulation_audit "
737
+ "(event_id,idempotency_key,event_kind,outbound_item_ref,"
738
+ "recipient_person_id,scope_revision,high_salience,occurred_at,"
739
+ "simulation_ref,request_digest,result_digest,draft_digest,"
740
+ "surface,risk_class,evaluated,would_action,effective_action,"
741
+ "evaluation_path,risk_codes_json,repair_codes_json,"
742
+ "event_digest,payload_json,stored_at) "
743
+ "VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
744
+ (
745
+ event.event_id,
746
+ event.idempotency_key,
747
+ event.event_kind,
748
+ event.outbound_item_ref,
749
+ event.recipient_person_id,
750
+ event.scope_revision,
751
+ int(event.high_salience),
752
+ event.occurred_at,
753
+ event.simulation_ref,
754
+ event.request_digest,
755
+ event.result_digest,
756
+ event.draft_digest,
757
+ event.surface,
758
+ event.risk_class,
759
+ int(event.evaluated),
760
+ event.would_action,
761
+ event.effective_action,
762
+ event.evaluation_path,
763
+ _canonical(event.risk_codes),
764
+ _canonical(event.repair_codes),
765
+ event.audit_digest,
766
+ _canonical(event.public()),
767
+ datetime.now(timezone.utc).isoformat(),
768
+ ),
769
+ )
770
+ self._conn.commit()
771
+ return RecipientAuditAppendV1(
772
+ event=event,
773
+ sequence=int(cursor.lastrowid),
774
+ appended=True,
775
+ replayed=False,
776
+ )
777
+ except Exception:
778
+ self._conn.rollback()
779
+ raise
780
+
781
+ @staticmethod
782
+ def _authorized_where(
783
+ viewer: ViewerContextV1,
784
+ *,
785
+ alias: str = "",
786
+ ) -> tuple[str, tuple[Any, ...]]:
787
+ if viewer.viewer_person_id == viewer.owner_person_id:
788
+ return "", ()
789
+ prefix = f"{alias}." if alias else ""
790
+ return (
791
+ f" AND {prefix}recipient_person_id=? "
792
+ f"AND {prefix}scope_revision=?",
793
+ (viewer.viewer_person_id, viewer.scope_revision),
794
+ )
795
+
796
+ def project(
797
+ self,
798
+ viewer: ViewerContextV1,
799
+ *,
800
+ max_events: int = 64,
801
+ ) -> RecipientAuditProjectionV1:
802
+ if not isinstance(viewer, ViewerContextV1):
803
+ raise ValueError("viewer must be ViewerContextV1")
804
+ if not 1 <= int(max_events) <= MAX_PROJECTED_EVENTS:
805
+ raise ValueError("max_events is outside bounded projection range")
806
+ if not viewer.attested or not viewer.viewer_person_id:
807
+ payload = {
808
+ "schema_version": SCHEMA_VERSION,
809
+ "viewer_digest": viewer.audit_digest,
810
+ "viewer_attested": False,
811
+ "event_digests": (),
812
+ "corrupt_count": 0,
813
+ "truncated": False,
814
+ "max_events": int(max_events),
815
+ }
816
+ return RecipientAuditProjectionV1(
817
+ events=(),
818
+ viewer_attested=False,
819
+ corrupt_count=0,
820
+ truncated=False,
821
+ viewer_digest=viewer.audit_digest,
822
+ audit_digest=_digest(payload),
823
+ )
824
+ where, params = self._authorized_where(viewer)
825
+ with self._lock:
826
+ rows = self._conn.execute(
827
+ "SELECT * FROM recipient_simulation_audit WHERE 1=1"
828
+ + where + " ORDER BY seq DESC LIMIT ?",
829
+ (*params, int(max_events) + 1),
830
+ ).fetchall()
831
+ truncated = len(rows) > int(max_events)
832
+ events: list[RecipientAuditEventV1] = []
833
+ corrupt_count = 0
834
+ for row in rows[:int(max_events)]:
835
+ try:
836
+ events.append(self._decode(row))
837
+ except Exception:
838
+ corrupt_count += 1
839
+ payload = {
840
+ "schema_version": SCHEMA_VERSION,
841
+ "viewer_digest": viewer.audit_digest,
842
+ "viewer_attested": True,
843
+ "event_digests": tuple(event.audit_digest for event in events),
844
+ "corrupt_count": corrupt_count,
845
+ "truncated": truncated,
846
+ "max_events": int(max_events),
847
+ }
848
+ return RecipientAuditProjectionV1(
849
+ events=tuple(events),
850
+ viewer_attested=True,
851
+ corrupt_count=corrupt_count,
852
+ truncated=truncated,
853
+ viewer_digest=viewer.audit_digest,
854
+ audit_digest=_digest(payload),
855
+ )
856
+
857
+ def coverage(
858
+ self,
859
+ viewer: ViewerContextV1,
860
+ *,
861
+ max_missing_refs: int = 32,
862
+ ) -> RecipientAuditCoverageV1:
863
+ """Account for all authorized sampled high-salience outbound items."""
864
+
865
+ if not isinstance(viewer, ViewerContextV1):
866
+ raise ValueError("viewer must be ViewerContextV1")
867
+ if not 1 <= int(max_missing_refs) <= MAX_MISSING_REFS:
868
+ raise ValueError("max_missing_refs is outside bounded coverage range")
869
+ if not viewer.attested or not viewer.viewer_person_id:
870
+ payload = {
871
+ "schema_version": SCHEMA_VERSION,
872
+ "viewer_digest": viewer.audit_digest,
873
+ "status": "viewer_unattested",
874
+ "sampled": 0,
875
+ "evaluated": 0,
876
+ "missing": (),
877
+ "missing_refs_truncated": False,
878
+ }
879
+ return RecipientAuditCoverageV1(
880
+ sampled_high_salience=0,
881
+ evaluated_high_salience=0,
882
+ unevaluated_high_salience=0,
883
+ missing_item_refs=(),
884
+ missing_refs_truncated=False,
885
+ corrupt_count=0,
886
+ scan_truncated=False,
887
+ status="viewer_unattested",
888
+ coverage_complete=False,
889
+ viewer_digest=viewer.audit_digest,
890
+ audit_digest=_digest(payload),
891
+ )
892
+
893
+ where, params = self._authorized_where(viewer)
894
+ samples: dict[str, RecipientAuditEventV1] = {}
895
+ evaluated_items: set[str] = set()
896
+ corrupt_count = 0
897
+ with self._lock:
898
+ cursor = self._conn.execute(
899
+ "SELECT * FROM recipient_simulation_audit "
900
+ "WHERE event_kind IN ('sample','evaluation')" + where
901
+ + " ORDER BY seq",
902
+ params,
903
+ )
904
+ while True:
905
+ rows = cursor.fetchmany(COVERAGE_FETCH_SIZE)
906
+ if not rows:
907
+ break
908
+ for row in rows:
909
+ try:
910
+ event = self._decode(row)
911
+ except Exception:
912
+ corrupt_count += 1
913
+ continue
914
+ if event.event_kind == "sample" and event.high_salience:
915
+ samples[event.outbound_item_ref] = event
916
+ elif event.event_kind == "evaluation" and event.evaluated:
917
+ evaluated_items.add(event.outbound_item_ref)
918
+ scan_truncated = False
919
+ sampled = len(samples)
920
+ evaluated = sum(
921
+ outbound in evaluated_items for outbound in samples)
922
+ unevaluated = sampled - evaluated
923
+ all_missing = tuple(sorted(
924
+ outbound for outbound in samples if outbound not in evaluated_items))
925
+ missing_truncated = len(all_missing) > int(max_missing_refs)
926
+ missing = all_missing[:int(max_missing_refs)]
927
+ if corrupt_count:
928
+ status = "indeterminate"
929
+ complete = False
930
+ elif sampled == 0:
931
+ status = "no_samples"
932
+ complete = False
933
+ elif unevaluated:
934
+ status = "incomplete"
935
+ complete = False
936
+ else:
937
+ status = "complete"
938
+ complete = True
939
+ payload = {
940
+ "schema_version": SCHEMA_VERSION,
941
+ "viewer_digest": viewer.audit_digest,
942
+ "status": status,
943
+ "sampled": sampled,
944
+ "evaluated": evaluated,
945
+ "missing": missing,
946
+ "missing_refs_truncated": missing_truncated,
947
+ "corrupt_count": corrupt_count,
948
+ "scan_truncated": scan_truncated,
949
+ }
950
+ return RecipientAuditCoverageV1(
951
+ sampled_high_salience=sampled,
952
+ evaluated_high_salience=evaluated,
953
+ unevaluated_high_salience=unevaluated,
954
+ missing_item_refs=missing,
955
+ missing_refs_truncated=missing_truncated,
956
+ corrupt_count=corrupt_count,
957
+ scan_truncated=scan_truncated,
958
+ status=status,
959
+ coverage_complete=complete,
960
+ viewer_digest=viewer.audit_digest,
961
+ audit_digest=_digest(payload),
962
+ )
963
+
964
+ def close(self) -> None:
965
+ with self._lock:
966
+ try:
967
+ self._conn.close()
968
+ except Exception:
969
+ pass
970
+
971
+
972
+ def open_recipient_simulation_audit_store(
973
+ db_path: str | os.PathLike[str],
974
+ *,
975
+ mode: Optional[str] = None,
976
+ ) -> Optional[RecipientSimulationAuditStore]:
977
+ """Open only for explicit shadow/live mode; off/unknown creates no state."""
978
+
979
+ selected = recipient_simulator_mode() if mode is None else str(mode).strip().lower()
980
+ if selected not in {"shadow", "live"}:
981
+ return None
982
+ return RecipientSimulationAuditStore(db_path)
983
+
984
+
985
+ __all__ = [
986
+ "RecipientAuditAppendV1",
987
+ "RecipientAuditConflictError",
988
+ "RecipientAuditCoverageV1",
989
+ "RecipientAuditEventV1",
990
+ "RecipientAuditProjectionV1",
991
+ "RecipientSimulationAuditStore",
992
+ "evaluation_event_from_result",
993
+ "open_recipient_simulation_audit_store",
994
+ "sample_event",
995
+ ]