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,1627 @@
1
+ """Receipt-bound cognition evidence reduction.
2
+
3
+ This reducer closes the durable outcome-to-learning seam without trusting a
4
+ worker, model, or external producer to declare its own success. Project and
5
+ WorkOrder events are rejoined to Colony's local immutable project ledger.
6
+ External cognition events remain visible as reported/unverified observations
7
+ unless a future server-owned resolver joins them to an exact local receipt.
8
+
9
+ The host event journal is the only input cursor. Downstream writes use stable
10
+ event identities so a crash after one sink but before cursor commit replays
11
+ without duplicating competence or expectation evidence.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from dataclasses import asdict, dataclass
17
+ from datetime import datetime, timezone
18
+ import hashlib
19
+ import json
20
+ import math
21
+ import os
22
+ import re
23
+ import sqlite3
24
+ import threading
25
+ import time
26
+ from typing import Any, Callable, Dict, Iterable, Mapping, Optional, Sequence
27
+
28
+ from apsimo.events.journal import current_sequence, replay_events
29
+ from apsimo.projects.store import ProjectStore
30
+
31
+
32
+ _CONSUMER_ID = "cognition-evidence-v1"
33
+ _SAFE_REF = re.compile(r"^[A-Za-z0-9][A-Za-z0-9_.:@/+\-]{0,255}$")
34
+ _EVIDENCE_REF = re.compile(r"^[a-z][a-z0-9+.-]{1,31}:[^\s]{1,480}$")
35
+ _AUTHORITY_STATES = frozenset({
36
+ "verified", "unverified", "neutral", "reported_unverified",
37
+ })
38
+ _OUTCOMES = frozenset({"success", "failure", "timeout", "neutral"})
39
+
40
+
41
+ def cognition_evidence_mode() -> str:
42
+ value = os.environ.get("COLONY_COGNITION_EVIDENCE", "off").strip().lower()
43
+ return value if value in {"off", "shadow", "live"} else "off"
44
+
45
+
46
+ def cognition_evidence_enabled() -> bool:
47
+ return cognition_evidence_mode() in {"shadow", "live"}
48
+
49
+
50
+ def _bootstrap_mode() -> str:
51
+ value = os.environ.get(
52
+ "COLONY_COGNITION_EVIDENCE_BOOTSTRAP", "beginning",
53
+ ).strip().lower()
54
+ return value if value in {"beginning", "tail"} else "beginning"
55
+
56
+
57
+ def _canonical(value: Any) -> str:
58
+ return json.dumps(
59
+ value,
60
+ ensure_ascii=False,
61
+ separators=(",", ":"),
62
+ sort_keys=True,
63
+ )
64
+
65
+
66
+ def _digest(value: Any) -> str:
67
+ return hashlib.sha256(_canonical(value).encode("utf-8")).hexdigest()
68
+
69
+
70
+ def _ledger_digest(
71
+ *,
72
+ event_seq: int,
73
+ event_id: str,
74
+ event_type: str,
75
+ raw_digest: str,
76
+ projection_digest: Optional[str],
77
+ disposition: str,
78
+ applied_sinks: Sequence[str],
79
+ recorded_at: float,
80
+ ) -> str:
81
+ """Bind every authoritative evidence-ledger envelope field."""
82
+
83
+ return _digest({
84
+ "schema": "CognitionEvidenceLedgerEntryV1",
85
+ "version": 1,
86
+ "event_seq": int(event_seq),
87
+ "event_id": str(event_id),
88
+ "event_type": str(event_type),
89
+ "raw_digest": str(raw_digest),
90
+ "projection_digest": str(projection_digest or ""),
91
+ "disposition": str(disposition),
92
+ "applied_sinks": list(applied_sinks),
93
+ "recorded_at": float(recorded_at),
94
+ })
95
+
96
+
97
+ def _hex_digest(value: Any, field: str) -> str:
98
+ result = str(value or "").strip()
99
+ if not re.fullmatch(r"[a-f0-9]{64}", result):
100
+ raise ValueError(f"{field} is not a lowercase SHA-256 digest")
101
+ return result
102
+
103
+
104
+ def _ref(value: Any, field: str, *, allow_empty: bool = False) -> str:
105
+ result = str(value or "").strip()
106
+ if allow_empty and not result:
107
+ return ""
108
+ if not _SAFE_REF.fullmatch(result):
109
+ raise ValueError(f"{field} is not a bounded reference")
110
+ return result
111
+
112
+
113
+ def _bounded_text(
114
+ value: Any, field: str, *, maximum: int, allow_empty: bool = False,
115
+ ) -> str:
116
+ result = str(value or "").strip()
117
+ if allow_empty and not result:
118
+ return ""
119
+ if not result or len(result) > maximum:
120
+ raise ValueError(f"{field} is not bounded text")
121
+ return result
122
+
123
+
124
+ def _refs(values: Iterable[Any]) -> tuple[str, ...]:
125
+ result = tuple(sorted(dict.fromkeys(
126
+ _bounded_text(value, "evidence_ref", maximum=512)
127
+ for value in (values or ())
128
+ )))
129
+ if any(not _EVIDENCE_REF.fullmatch(value) for value in result):
130
+ raise ValueError("evidence refs require a bounded URI scheme")
131
+ if len(result) > 512:
132
+ raise ValueError("evidence refs exceed 512 entries")
133
+ return result
134
+
135
+
136
+ def _event_time(value: Any, field: str) -> float:
137
+ if isinstance(value, bool):
138
+ raise ValueError(f"{field} is not a timestamp")
139
+ if isinstance(value, (int, float)):
140
+ result = float(value)
141
+ else:
142
+ parsed = datetime.fromisoformat(str(value or "").replace("Z", "+00:00"))
143
+ if parsed.tzinfo is None:
144
+ raise ValueError(f"{field} requires a timezone")
145
+ result = parsed.astimezone(timezone.utc).timestamp()
146
+ if not math.isfinite(result) or result <= 0.0:
147
+ raise ValueError(f"{field} is outside the supported range")
148
+ return result
149
+
150
+
151
+ def _scope(data: Mapping[str, Any]) -> tuple[str, str, str]:
152
+ subject = _ref(data.get("subject_person_id") or "owner", "subject_person_id")
153
+ viewer = _ref(data.get("viewer_scope") or "owner", "viewer_scope")
154
+ sharing = str(data.get("shareability") or "owner_private").strip().lower()
155
+ if sharing not in {"owner_private", "subject_private", "shared", "public"}:
156
+ raise ValueError("shareability is invalid")
157
+ if sharing == "owner_private" and viewer != "owner":
158
+ raise ValueError("owner-private evidence requires owner viewer scope")
159
+ if sharing == "subject_private" and viewer != f"person:{subject}":
160
+ raise ValueError("subject-private evidence requires subject viewer scope")
161
+ if sharing == "public" and viewer != "public":
162
+ raise ValueError("public evidence requires public viewer scope")
163
+ if sharing == "shared" and not (
164
+ viewer == "shared" or viewer.startswith("shared:")
165
+ ):
166
+ raise ValueError("shared evidence requires shared viewer scope")
167
+ return subject, viewer, sharing
168
+
169
+
170
+ @dataclass(frozen=True)
171
+ class EvidenceProjectionV1:
172
+ event_seq: int
173
+ event_id: str
174
+ event_type: str
175
+ occurred_at: float
176
+ authority_state: str
177
+ outcome: str
178
+ domain: str
179
+ project_id: str
180
+ step_id: str
181
+ work_order_id: str
182
+ result_ref: str
183
+ subject_person_id: str
184
+ viewer_scope: str
185
+ shareability: str
186
+ evidence_refs: tuple[str, ...]
187
+ latency_seconds: Optional[float]
188
+ stated_confidence: Optional[float]
189
+ disposition: str
190
+ local_evidence: Mapping[str, Any]
191
+ schema: str = "EvidenceProjectionV1"
192
+ version: int = 1
193
+
194
+ def __post_init__(self) -> None:
195
+ if self.event_seq < 1:
196
+ raise ValueError("evidence projection requires a positive sequence")
197
+ _ref(self.event_id, "event_id")
198
+ _bounded_text(
199
+ self.project_id, "project_id", maximum=128, allow_empty=True,
200
+ )
201
+ _bounded_text(
202
+ self.step_id, "step_id", maximum=128, allow_empty=True,
203
+ )
204
+ _ref(self.work_order_id, "work_order_id", allow_empty=True)
205
+ _ref(self.result_ref, "result_ref", allow_empty=True)
206
+ if self.authority_state not in _AUTHORITY_STATES:
207
+ raise ValueError("evidence authority state is invalid")
208
+ if self.outcome not in _OUTCOMES:
209
+ raise ValueError("evidence outcome is invalid")
210
+ if _scope({
211
+ "subject_person_id": self.subject_person_id,
212
+ "viewer_scope": self.viewer_scope,
213
+ "shareability": self.shareability,
214
+ }) != (
215
+ self.subject_person_id, self.viewer_scope, self.shareability,
216
+ ):
217
+ raise ValueError("evidence scope is not canonical")
218
+ if _refs(self.evidence_refs) != self.evidence_refs:
219
+ raise ValueError("evidence refs are not canonical")
220
+ if self.latency_seconds is not None and (
221
+ not math.isfinite(self.latency_seconds) or self.latency_seconds < 0.0
222
+ ):
223
+ raise ValueError("evidence latency is invalid")
224
+ if self.stated_confidence is not None and not (
225
+ 0.0 <= self.stated_confidence <= 1.0
226
+ ):
227
+ raise ValueError("stated confidence is invalid")
228
+
229
+ @property
230
+ def projection_digest(self) -> str:
231
+ return _digest(self.payload())
232
+
233
+ def payload(self) -> Dict[str, Any]:
234
+ result = asdict(self)
235
+ result["evidence_refs"] = list(self.evidence_refs)
236
+ result["local_evidence"] = dict(self.local_evidence)
237
+ return result
238
+
239
+
240
+ def _event_identity(
241
+ raw: Mapping[str, Any],
242
+ ) -> tuple[int, str, str, float, Mapping[str, Any], str]:
243
+ sequence = int(raw.get("seq") or 0)
244
+ event_id = _ref(
245
+ raw.get("ulid") or f"journal-seq-{sequence}", "event_id",
246
+ )
247
+ event_type = str(raw.get("type") or "").strip().lower()
248
+ if sequence < 1 or not re.fullmatch(
249
+ r"[a-z0-9][a-z0-9_.:\-]{0,127}", event_type,
250
+ ):
251
+ raise ValueError("journal event identity is invalid")
252
+ occurred = _event_time(
253
+ raw.get("occurredAt") or raw.get("recordedAt"), "occurred_at",
254
+ )
255
+ data = raw.get("data")
256
+ if not isinstance(data, Mapping):
257
+ raise ValueError("journal event data is not an object")
258
+ raw_digest = _digest({
259
+ "event_type": event_type,
260
+ "occurred_at": occurred,
261
+ "data": data,
262
+ })
263
+ return sequence, event_id, event_type, occurred, data, raw_digest
264
+
265
+
266
+ def _validate_self_digest(
267
+ data: Mapping[str, Any], field: str,
268
+ ) -> Dict[str, Any]:
269
+ value = dict(data)
270
+ expected = str(value.pop(field, ""))
271
+ if not re.fullmatch(r"[a-f0-9]{64}", expected) or _digest(value) != expected:
272
+ raise ValueError(f"{field} does not bind the event payload")
273
+ return value
274
+
275
+
276
+ def _mode_at_stage(data: Mapping[str, Any]) -> str:
277
+ mode = str(data.get("cognition_mode_at_stage") or "").strip().lower()
278
+ if mode not in {"off", "shadow", "live"}:
279
+ raise ValueError("project evidence stage mode is invalid")
280
+ return mode
281
+
282
+
283
+ def _require_project_outbox_binding(
284
+ project_store: ProjectStore,
285
+ *,
286
+ event_key: str,
287
+ event_seq: int,
288
+ raw_event_seq: Any,
289
+ event_id: str,
290
+ raw_event_id: Any,
291
+ event_type: str,
292
+ raw_event_type: str,
293
+ occurred_at: float,
294
+ occurred_at_text: str,
295
+ recorded_at_text: str,
296
+ payload: Mapping[str, Any],
297
+ ) -> None:
298
+ """Rejoin a journal event to its exact server-owned outbox projection."""
299
+
300
+ row = project_store.project_event(event_key)
301
+ if row is None:
302
+ raise ValueError("project evidence has no local outbox projection")
303
+ projected_payload = row.get("payload")
304
+ if row.get("payload_error") or not isinstance(projected_payload, dict):
305
+ raise ValueError("project evidence outbox payload is invalid")
306
+ if (
307
+ row.get("state") != "projected"
308
+ or type(raw_event_seq) is not int
309
+ or raw_event_seq != event_seq
310
+ or type(raw_event_id) is not str
311
+ or raw_event_id != event_id
312
+ or int(row.get("journal_seq") or 0) != event_seq
313
+ or str(row.get("journal_event_id") or "") != event_id
314
+ or str(row.get("event_type") or "") != event_type
315
+ or str(row.get("event_type") or "") != raw_event_type
316
+ or str(row.get("occurred_at") or "") != occurred_at_text
317
+ or str(row.get("journal_recorded_at") or "") != recorded_at_text
318
+ or _event_time(row.get("occurred_at"), "outbox_occurred_at")
319
+ != occurred_at
320
+ or _canonical(projected_payload) != _canonical(dict(payload))
321
+ ):
322
+ raise ValueError("project evidence differs from its outbox projection")
323
+ expected_digest = ProjectStore.project_event_envelope_digest(
324
+ event_key=event_key,
325
+ event_type=event_type,
326
+ occurred_at=str(row.get("occurred_at") or ""),
327
+ payload=projected_payload,
328
+ )
329
+ if str(row.get("event_digest") or "") != expected_digest:
330
+ raise ValueError("project evidence outbox envelope digest mismatch")
331
+
332
+
333
+ def _execution_projection(
334
+ raw: Mapping[str, Any], project_store: ProjectStore,
335
+ ) -> EvidenceProjectionV1:
336
+ sequence, event_id, event_type, occurred, data, _ = _event_identity(raw)
337
+ if data.get("schema") != "ProjectExecutionEvidenceV2"\
338
+ or data.get("version") != 2:
339
+ raise ValueError("execution evidence schema is invalid")
340
+ _validate_self_digest(data, "evidence_digest")
341
+ project_id = _bounded_text(
342
+ data.get("project_id"), "project_id", maximum=128,
343
+ )
344
+ step_id = _bounded_text(data.get("step_id"), "step_id", maximum=128)
345
+ work_order_id = _ref(data.get("work_order_id"), "work_order_id")
346
+ result_ref = _ref(data.get("result_ref"), "result_ref")
347
+ run_id = _bounded_text(data.get("run_id"), "run_id", maximum=256)
348
+ attempt_number = int(data.get("attempt_number") or 0)
349
+ if attempt_number < 1:
350
+ raise ValueError("execution evidence attempt number is invalid")
351
+ mode_at_stage = _mode_at_stage(data)
352
+ identity = _digest({
353
+ "work_order_id": work_order_id,
354
+ "run_id": run_id,
355
+ })
356
+ _require_project_outbox_binding(
357
+ project_store,
358
+ event_key=f"project-execution:{identity}",
359
+ event_seq=sequence,
360
+ raw_event_seq=raw.get("seq"),
361
+ event_id=event_id,
362
+ raw_event_id=raw.get("ulid"),
363
+ event_type=event_type,
364
+ raw_event_type=str(raw.get("type") or ""),
365
+ occurred_at=occurred,
366
+ occurred_at_text=str(raw.get("occurredAt") or ""),
367
+ recorded_at_text=str(raw.get("recordedAt") or ""),
368
+ payload=data,
369
+ )
370
+
371
+ work_order = project_store.get_work_order(work_order_id)
372
+ if work_order is None:
373
+ raise ValueError("execution evidence has no local WorkOrder")
374
+ order_payload = work_order["payload"]
375
+ max_attempts = int(order_payload.get("max_attempts") or 0)
376
+ if (
377
+ max_attempts < 1
378
+ or attempt_number > max_attempts
379
+ or
380
+ work_order["project_id"] != project_id
381
+ or work_order["step_id"] != step_id
382
+ or work_order["work_order_digest"] != data.get("work_order_digest")
383
+ or order_payload.get("work_order_id") != work_order_id
384
+ or order_payload.get("work_order_digest") != data.get("work_order_digest")
385
+ ):
386
+ raise ValueError("execution evidence does not match local WorkOrder authority")
387
+
388
+ attempts = [
389
+ item for item in project_store.execution_attempts_for(work_order_id)
390
+ if item["run_id"] == run_id
391
+ and int(item["attempt_number"]) == attempt_number
392
+ ]
393
+ if len(attempts) != 1:
394
+ raise ValueError("execution evidence has no exact local attempt")
395
+ attempt = attempts[0]
396
+ attempt_ref = _ref(attempt.get("attempt_ref"), "attempt_ref")
397
+ result = attempt["result"]
398
+ if (
399
+ result.get("result_ref") != result_ref
400
+ or result.get("work_order_id") != work_order_id
401
+ or result.get("work_order_digest") != data.get("work_order_digest")
402
+ or result.get("run_id") != run_id
403
+ or int(result.get("attempt_number") or 0) != attempt_number
404
+ or _digest(result) != data.get("result_digest")
405
+ or attempt.get("transport_status") != data.get("transport_status")
406
+ ):
407
+ raise ValueError("execution evidence does not match local attempt content")
408
+ for field in (
409
+ "terminal_outcome", "verification_result", "effect_class",
410
+ ):
411
+ if result.get(field) != data.get(field):
412
+ raise ValueError(f"execution evidence {field} mismatch")
413
+ if abs(_event_time(result.get("ended_at"), "ended_at") - occurred) > 0.001:
414
+ raise ValueError("execution event time does not match the local result")
415
+
416
+ project = project_store.get_project(project_id)
417
+ if project is None:
418
+ raise ValueError("execution evidence project is unavailable")
419
+ matching_steps = [
420
+ item for item in project_store.steps_for(project_id) if item.id == step_id
421
+ ]
422
+ if len(matching_steps) != 1:
423
+ raise ValueError("execution evidence step is unavailable")
424
+ step = matching_steps[0]
425
+ if (
426
+ step.work_order_ref != work_order["work_order_ref"]
427
+ or step.work_order_digest != data.get("work_order_digest")
428
+ or step.result_ref != result_ref
429
+ ):
430
+ raise ValueError("execution evidence is not bound to the local step")
431
+ subject, viewer, sharing = _scope(data)
432
+ if (
433
+ subject != (project.subject_person_id or "owner")
434
+ or viewer != (project.viewer_scope or "owner")
435
+ or sharing != (project.shareability or "owner_private")
436
+ ):
437
+ raise ValueError("execution evidence scope differs from local project scope")
438
+
439
+ receipts = _refs(result.get("receipt_refs") or ())
440
+ if receipts != _refs(data.get("receipt_refs") or ()):
441
+ raise ValueError("execution evidence receipt refs mismatch")
442
+ terminal = str(result.get("terminal_outcome") or "")
443
+ verification = str(result.get("verification_result") or "")
444
+ if terminal == "succeeded" and verification == "verified" and receipts:
445
+ authority_state, outcome, disposition, status = (
446
+ "verified", "success", "verified_execution_success", "verified",
447
+ )
448
+ elif terminal == "failed":
449
+ error = str(result.get("error") or "").lower()
450
+ outcome = "timeout" if "timeout" in error else "failure"
451
+ authority_state, disposition, status = (
452
+ "verified", "verified_execution_failure", "failed",
453
+ )
454
+ elif terminal in {"cancelled", "skipped"}:
455
+ authority_state, outcome, disposition, status = (
456
+ "neutral", "neutral", f"neutral_execution_{terminal}", terminal,
457
+ )
458
+ elif terminal == "succeeded":
459
+ authority_state, outcome, disposition, status = (
460
+ "unverified", "neutral", "unverified_execution_success", "unverified",
461
+ )
462
+ else:
463
+ raise ValueError("execution evidence terminal outcome is invalid")
464
+ if data.get("status") != status:
465
+ raise ValueError("execution evidence status projection mismatch")
466
+
467
+ started = _event_time(result.get("started_at"), "started_at")
468
+ ended = _event_time(result.get("ended_at"), "ended_at")
469
+ if ended < started:
470
+ raise ValueError("execution result ends before it starts")
471
+ refs = _refs((attempt_ref, result_ref, *receipts))
472
+ return EvidenceProjectionV1(
473
+ event_seq=sequence,
474
+ event_id=event_id,
475
+ event_type=event_type,
476
+ occurred_at=occurred,
477
+ authority_state=authority_state,
478
+ outcome=outcome,
479
+ domain="project",
480
+ project_id=project_id,
481
+ step_id=step_id,
482
+ work_order_id=work_order_id,
483
+ result_ref=result_ref,
484
+ subject_person_id=subject,
485
+ viewer_scope=viewer,
486
+ shareability=sharing,
487
+ evidence_refs=refs,
488
+ latency_seconds=ended - started,
489
+ stated_confidence=float(step.confidence),
490
+ disposition=disposition,
491
+ local_evidence={
492
+ "work_order_digest": str(data["work_order_digest"]),
493
+ "result_digest": str(data["result_digest"]),
494
+ "attempt_ref": attempt_ref,
495
+ "run_id": run_id,
496
+ "attempt_number": attempt_number,
497
+ "max_attempts": max_attempts,
498
+ "effect_class": str(data["effect_class"]),
499
+ "transport_status": str(data["transport_status"]),
500
+ "verifier_identity": str(data.get("verifier_identity") or ""),
501
+ "cognition_mode_at_stage": mode_at_stage,
502
+ },
503
+ )
504
+
505
+
506
+ def _terminal_projection(
507
+ raw: Mapping[str, Any], project_store: ProjectStore,
508
+ ) -> EvidenceProjectionV1:
509
+ sequence, event_id, event_type, occurred, data, _ = _event_identity(raw)
510
+ if data.get("schema") != "ProjectTerminalEvidenceV2"\
511
+ or data.get("version") != 2:
512
+ raise ValueError("project terminal evidence schema is invalid")
513
+ _validate_self_digest(data, "project_digest")
514
+ project_id = _bounded_text(
515
+ data.get("project_id"), "project_id", maximum=128,
516
+ )
517
+ mode_at_stage = _mode_at_stage(data)
518
+ identity = _digest({
519
+ "project_id": project_id,
520
+ "lifecycle_status": data.get("lifecycle_status"),
521
+ "outcome": data.get("outcome"),
522
+ })
523
+ _require_project_outbox_binding(
524
+ project_store,
525
+ event_key=f"project-terminal:{identity}",
526
+ event_seq=sequence,
527
+ raw_event_seq=raw.get("seq"),
528
+ event_id=event_id,
529
+ raw_event_id=raw.get("ulid"),
530
+ event_type=event_type,
531
+ raw_event_type=str(raw.get("type") or ""),
532
+ occurred_at=occurred,
533
+ occurred_at_text=str(raw.get("occurredAt") or ""),
534
+ recorded_at_text=str(raw.get("recordedAt") or ""),
535
+ payload=data,
536
+ )
537
+ project = project_store.get_project(project_id)
538
+ if project is None:
539
+ raise ValueError("project terminal evidence has no local project")
540
+ if (
541
+ project.status != data.get("lifecycle_status")
542
+ or project.outcome != data.get("outcome")
543
+ or str(project.reason or "")[:128] != data.get("reason_code")
544
+ or str(project.source or "")[:64] != data.get("source")
545
+ ):
546
+ raise ValueError("project terminal evidence differs from local project")
547
+ subject, viewer, sharing = _scope(data)
548
+ if (
549
+ subject != (project.subject_person_id or "owner")
550
+ or viewer != (project.viewer_scope or "owner")
551
+ or sharing != (project.shareability or "owner_private")
552
+ ):
553
+ raise ValueError("project terminal scope differs from local project scope")
554
+
555
+ expected_results: list[Dict[str, str]] = []
556
+ evidence_refs: list[str] = []
557
+ verified = bool(project_store.steps_for(project_id))\
558
+ and project.outcome == "succeeded"
559
+ for step in project_store.steps_for(project_id):
560
+ if step.status != "done" or not step.result_ref:
561
+ verified = False
562
+ continue
563
+ result = project_store.get_execution_result(step.result_ref)
564
+ if result is None:
565
+ verified = False
566
+ continue
567
+ payload = result["payload"]
568
+ work_order = project_store.get_work_order(result["work_order_id"])
569
+ receipts = _refs(payload.get("receipt_refs") or ())
570
+ if (
571
+ work_order is None
572
+ or work_order.get("project_id") != project_id
573
+ or work_order.get("step_id") != step.id
574
+ or work_order.get("work_order_digest")
575
+ != result.get("work_order_digest")
576
+ or payload.get("work_order_id") != result.get("work_order_id")
577
+ or payload.get("work_order_digest")
578
+ != result.get("work_order_digest")
579
+ or
580
+ result.get("terminal_outcome") != "succeeded"
581
+ or result.get("verification_result") != "verified"
582
+ or not receipts
583
+ ):
584
+ verified = False
585
+ expected_results.append({
586
+ "step_id": step.id,
587
+ "result_ref": step.result_ref,
588
+ "result_digest": _digest(payload),
589
+ })
590
+ evidence_refs.extend((step.result_ref, *receipts))
591
+ if list(data.get("result_refs") or ()) != expected_results:
592
+ raise ValueError("project terminal result set differs from local ledger")
593
+ expected_state = "verified" if verified else "unverified"
594
+ if data.get("evidence_status") != expected_state:
595
+ raise ValueError("project terminal evidence status is not locally derived")
596
+ expected_status = (
597
+ "verified" if verified else
598
+ "failed" if project.status == "abandoned" else
599
+ "unverified"
600
+ )
601
+ if data.get("status") != expected_status:
602
+ raise ValueError("project terminal status is not locally derived")
603
+ terminal_ref = f"project-terminal:{str(data['project_digest'])}"
604
+ return EvidenceProjectionV1(
605
+ event_seq=sequence,
606
+ event_id=event_id,
607
+ event_type=event_type,
608
+ occurred_at=occurred,
609
+ authority_state=expected_state,
610
+ outcome="success" if verified else "neutral",
611
+ domain="project",
612
+ project_id=project_id,
613
+ step_id="",
614
+ work_order_id="",
615
+ result_ref="",
616
+ subject_person_id=subject,
617
+ viewer_scope=viewer,
618
+ shareability=sharing,
619
+ evidence_refs=_refs((terminal_ref, *evidence_refs)),
620
+ latency_seconds=None,
621
+ stated_confidence=None,
622
+ disposition=f"{expected_state}_project_terminal",
623
+ local_evidence={
624
+ "project_digest": str(data["project_digest"]),
625
+ "terminal_ref": terminal_ref,
626
+ "lifecycle_status": str(data["lifecycle_status"]),
627
+ "project_outcome": str(data["outcome"]),
628
+ "concern_id": str(data.get("concern_id") or ""),
629
+ "goal_proposal_id": str(data.get("goal_proposal_id") or ""),
630
+ "cognition_mode_at_stage": mode_at_stage,
631
+ },
632
+ )
633
+
634
+
635
+ def _external_projection(raw: Mapping[str, Any]) -> EvidenceProjectionV1:
636
+ if type(raw.get("seq")) is not int or raw.get("seq") < 1:
637
+ raise ValueError("external cognition journal sequence is not canonical")
638
+ from apsimo.cognition.external_events import (
639
+ validate_external_journal_event_id,
640
+ validate_external_journal_projection,
641
+ )
642
+
643
+ validate_external_journal_event_id(raw.get("ulid"))
644
+ sequence, event_id, event_type, occurred, data, _ = _event_identity(raw)
645
+ if type(raw.get("ulid")) is not str or raw.get("ulid") != event_id:
646
+ raise ValueError("external cognition journal ID is not canonical")
647
+ if type(raw.get("type")) is not str or raw.get("type") != event_type:
648
+ raise ValueError("external cognition journal type is not canonical")
649
+
650
+ projected = validate_external_journal_projection(event_type, data)
651
+ projected_occurred = projected["external_occurred_at"]
652
+ if (
653
+ type(raw.get("occurredAt")) is not str
654
+ or raw.get("occurredAt") != projected_occurred
655
+ or _event_time(projected_occurred, "external_occurred_at") != occurred
656
+ ):
657
+ raise ValueError(
658
+ "external cognition host time differs from its server projection"
659
+ )
660
+ external_id = projected["external_event_id"]
661
+ external_digest = projected["external_event_digest"]
662
+ producer = projected["producer_principal_id"]
663
+ revision = projected["producer_revision"]
664
+ subject = projected["subject_person_id"]
665
+ viewer = projected["viewer_scope"]
666
+ sharing = projected["shareability"]
667
+ return EvidenceProjectionV1(
668
+ event_seq=sequence,
669
+ event_id=event_id,
670
+ event_type=event_type,
671
+ occurred_at=occurred,
672
+ authority_state="reported_unverified",
673
+ outcome="neutral",
674
+ domain="external_cognition",
675
+ project_id="",
676
+ step_id="",
677
+ work_order_id="",
678
+ result_ref="",
679
+ subject_person_id=subject,
680
+ viewer_scope=viewer,
681
+ shareability=sharing,
682
+ evidence_refs=(),
683
+ latency_seconds=None,
684
+ stated_confidence=None,
685
+ disposition="reported_external_observation",
686
+ local_evidence={
687
+ "external_event_id": external_id,
688
+ "external_event_digest": external_digest,
689
+ "producer_principal_id": producer,
690
+ "producer_revision": revision,
691
+ },
692
+ )
693
+
694
+
695
+ def project_evidence_event(
696
+ raw: Mapping[str, Any], project_store: ProjectStore,
697
+ ) -> tuple[Optional[EvidenceProjectionV1], str, str]:
698
+ _, _, event_type, _, _, raw_digest = _event_identity(raw)
699
+ if event_type == "work_order.result":
700
+ return _execution_projection(raw, project_store), "", raw_digest
701
+ if event_type in {"project.completed", "project.abandoned"}:
702
+ return _terminal_projection(raw, project_store), "", raw_digest
703
+ if event_type.startswith("cognition.external."):
704
+ return _external_projection(raw), "", raw_digest
705
+ return None, "unmapped_event_type", raw_digest
706
+
707
+
708
+ class CognitionEvidenceStore:
709
+ """Append-only evidence ledger plus one durable host-journal cursor."""
710
+
711
+ def __init__(self, db_path: str) -> None:
712
+ self._lock = threading.RLock()
713
+ self._conn = sqlite3.connect(str(db_path), check_same_thread=False)
714
+ self._conn.row_factory = sqlite3.Row
715
+ with self._lock:
716
+ self._conn.execute("PRAGMA journal_mode=WAL")
717
+ self._conn.executescript(
718
+ """
719
+ CREATE TABLE IF NOT EXISTS cognition_evidence_cursors (
720
+ consumer_id TEXT PRIMARY KEY,
721
+ last_seq INTEGER NOT NULL,
722
+ bootstrap_mode TEXT NOT NULL,
723
+ last_error TEXT,
724
+ updated_at REAL NOT NULL
725
+ );
726
+ CREATE TABLE IF NOT EXISTS cognition_evidence_events (
727
+ event_seq INTEGER PRIMARY KEY,
728
+ event_id TEXT NOT NULL UNIQUE,
729
+ event_type TEXT NOT NULL,
730
+ raw_digest TEXT NOT NULL,
731
+ projection_digest TEXT,
732
+ disposition TEXT NOT NULL,
733
+ authority_state TEXT,
734
+ outcome TEXT,
735
+ domain TEXT,
736
+ project_id TEXT,
737
+ step_id TEXT,
738
+ work_order_id TEXT,
739
+ result_ref TEXT,
740
+ subject_person_id TEXT,
741
+ viewer_scope TEXT,
742
+ shareability TEXT,
743
+ occurred_at REAL,
744
+ evidence_refs_json TEXT,
745
+ projection_json TEXT,
746
+ applied_sinks_json TEXT NOT NULL,
747
+ ledger_digest TEXT,
748
+ recorded_at REAL NOT NULL
749
+ );
750
+ CREATE INDEX IF NOT EXISTS cognition_evidence_project_idx
751
+ ON cognition_evidence_events(project_id,event_seq);
752
+ CREATE INDEX IF NOT EXISTS cognition_evidence_work_idx
753
+ ON cognition_evidence_events(work_order_id,event_seq);
754
+ CREATE TABLE IF NOT EXISTS cognition_evidence_gaps (
755
+ gap_id INTEGER PRIMARY KEY AUTOINCREMENT,
756
+ consumer_id TEXT NOT NULL,
757
+ prior_cursor INTEGER NOT NULL,
758
+ resume_after INTEGER NOT NULL,
759
+ reason TEXT NOT NULL,
760
+ acknowledged_at REAL NOT NULL
761
+ );
762
+ CREATE TABLE IF NOT EXISTS cognition_evidence_passthrough_ranges (
763
+ range_id INTEGER PRIMARY KEY AUTOINCREMENT,
764
+ consumer_id TEXT NOT NULL,
765
+ prior_cursor INTEGER NOT NULL,
766
+ resume_after INTEGER NOT NULL,
767
+ mode TEXT NOT NULL,
768
+ reason TEXT NOT NULL,
769
+ recorded_at REAL NOT NULL
770
+ );
771
+ """
772
+ )
773
+ event_columns = {
774
+ row[1] for row in self._conn.execute(
775
+ "PRAGMA table_info(cognition_evidence_events)"
776
+ ).fetchall()
777
+ }
778
+ if "ledger_digest" not in event_columns:
779
+ # Old pre-release rows deliberately remain unverifiable. Do
780
+ # not bless possibly modified content during an upgrade by
781
+ # manufacturing a digest from the current mutable row.
782
+ self._conn.execute(
783
+ "ALTER TABLE cognition_evidence_events "
784
+ "ADD COLUMN ledger_digest TEXT"
785
+ )
786
+ self._conn.executescript(
787
+ """
788
+ CREATE TRIGGER IF NOT EXISTS cognition_evidence_events_no_update
789
+ BEFORE UPDATE ON cognition_evidence_events
790
+ BEGIN
791
+ SELECT RAISE(ABORT, 'cognition evidence ledger is append-only');
792
+ END;
793
+ CREATE TRIGGER IF NOT EXISTS cognition_evidence_events_no_delete
794
+ BEFORE DELETE ON cognition_evidence_events
795
+ BEGIN
796
+ SELECT RAISE(ABORT, 'cognition evidence ledger is append-only');
797
+ END;
798
+ CREATE TRIGGER IF NOT EXISTS cognition_evidence_gaps_no_update
799
+ BEFORE UPDATE ON cognition_evidence_gaps
800
+ BEGIN
801
+ SELECT RAISE(ABORT, 'cognition evidence gaps are append-only');
802
+ END;
803
+ CREATE TRIGGER IF NOT EXISTS cognition_evidence_gaps_no_delete
804
+ BEFORE DELETE ON cognition_evidence_gaps
805
+ BEGIN
806
+ SELECT RAISE(ABORT, 'cognition evidence gaps are append-only');
807
+ END;
808
+ CREATE TRIGGER IF NOT EXISTS cognition_evidence_passthrough_no_update
809
+ BEFORE UPDATE ON cognition_evidence_passthrough_ranges
810
+ BEGIN
811
+ SELECT RAISE(ABORT, 'cognition evidence passthrough is append-only');
812
+ END;
813
+ CREATE TRIGGER IF NOT EXISTS cognition_evidence_passthrough_no_delete
814
+ BEFORE DELETE ON cognition_evidence_passthrough_ranges
815
+ BEGIN
816
+ SELECT RAISE(ABORT, 'cognition evidence passthrough is append-only');
817
+ END;
818
+ """
819
+ )
820
+ self._conn.commit()
821
+
822
+ def close(self) -> None:
823
+ with self._lock:
824
+ self._conn.close()
825
+
826
+ def cursor(self, consumer_id: str) -> Optional[int]:
827
+ with self._lock:
828
+ row = self._conn.execute(
829
+ "SELECT last_seq FROM cognition_evidence_cursors "
830
+ "WHERE consumer_id=?", (consumer_id,),
831
+ ).fetchone()
832
+ return int(row["last_seq"]) if row is not None else None
833
+
834
+ def initialize_cursor(
835
+ self, consumer_id: str, sequence: int, *, bootstrap_mode: str,
836
+ ) -> int:
837
+ with self._lock:
838
+ self._conn.execute(
839
+ """INSERT OR IGNORE INTO cognition_evidence_cursors
840
+ (consumer_id,last_seq,bootstrap_mode,last_error,updated_at)
841
+ VALUES (?,?,?,NULL,?)""",
842
+ (consumer_id, int(sequence), bootstrap_mode, time.time()),
843
+ )
844
+ self._conn.commit()
845
+ value = self.cursor(consumer_id)
846
+ return int(value if value is not None else sequence)
847
+
848
+ def set_error(self, consumer_id: str, error: str) -> None:
849
+ with self._lock:
850
+ self._conn.execute(
851
+ """UPDATE cognition_evidence_cursors SET last_error=?,updated_at=?
852
+ WHERE consumer_id=?""",
853
+ (str(error or "evidence_reducer_failed")[:500], time.time(), consumer_id),
854
+ )
855
+ self._conn.commit()
856
+
857
+ def acknowledge_gap(
858
+ self, consumer_id: str, *, prior_cursor: int, resume_after: int,
859
+ reason: str,
860
+ ) -> int:
861
+ with self._lock:
862
+ self._conn.execute("BEGIN IMMEDIATE")
863
+ try:
864
+ current = self._conn.execute(
865
+ "SELECT last_seq FROM cognition_evidence_cursors "
866
+ "WHERE consumer_id=?", (consumer_id,),
867
+ ).fetchone()
868
+ if current is None or int(current["last_seq"]) != int(prior_cursor):
869
+ raise ValueError("evidence gap cursor changed")
870
+ self._conn.execute(
871
+ """INSERT INTO cognition_evidence_gaps
872
+ (consumer_id,prior_cursor,resume_after,reason,acknowledged_at)
873
+ VALUES (?,?,?,?,?)""",
874
+ (
875
+ consumer_id, int(prior_cursor), int(resume_after),
876
+ str(reason)[:500], time.time(),
877
+ ),
878
+ )
879
+ self._conn.execute(
880
+ """UPDATE cognition_evidence_cursors SET last_seq=?,
881
+ last_error=NULL,updated_at=? WHERE consumer_id=?""",
882
+ (int(resume_after), time.time(), consumer_id),
883
+ )
884
+ self._conn.commit()
885
+ except Exception:
886
+ self._conn.rollback()
887
+ raise
888
+ return int(resume_after)
889
+
890
+ def checkpoint_passthrough(
891
+ self,
892
+ consumer_id: str,
893
+ *,
894
+ prior_cursor: int,
895
+ resume_after: int,
896
+ mode: str = "off",
897
+ reason: str = "evidence_mode_off_passthrough",
898
+ ) -> int:
899
+ prior = int(prior_cursor)
900
+ resume = int(resume_after)
901
+ if resume <= prior:
902
+ raise ValueError("evidence passthrough cursor must advance")
903
+ normalized_mode = str(mode or "").strip().lower()
904
+ if normalized_mode != "off":
905
+ raise ValueError("only off mode may checkpoint passthrough evidence")
906
+ with self._lock:
907
+ self._conn.execute("BEGIN IMMEDIATE")
908
+ try:
909
+ current = self._conn.execute(
910
+ "SELECT last_seq FROM cognition_evidence_cursors "
911
+ "WHERE consumer_id=?", (consumer_id,),
912
+ ).fetchone()
913
+ if current is None or int(current["last_seq"]) != prior:
914
+ raise ValueError("evidence passthrough cursor changed")
915
+ now = time.time()
916
+ self._conn.execute(
917
+ """INSERT INTO cognition_evidence_passthrough_ranges
918
+ (consumer_id,prior_cursor,resume_after,mode,reason,recorded_at)
919
+ VALUES (?,?,?,?,?,?)""",
920
+ (
921
+ consumer_id, prior, resume, normalized_mode,
922
+ str(reason)[:500], now,
923
+ ),
924
+ )
925
+ self._conn.execute(
926
+ """UPDATE cognition_evidence_cursors SET last_seq=?,
927
+ last_error=NULL,updated_at=? WHERE consumer_id=?""",
928
+ (resume, now, consumer_id),
929
+ )
930
+ self._conn.commit()
931
+ except Exception:
932
+ self._conn.rollback()
933
+ raise
934
+ return resume
935
+
936
+ def apply(
937
+ self,
938
+ *,
939
+ consumer_id: str,
940
+ event_seq: int,
941
+ event_id: str,
942
+ event_type: str,
943
+ raw_digest: str,
944
+ projection: Optional[EvidenceProjectionV1],
945
+ disposition: str,
946
+ applied_sinks: Sequence[str] = (),
947
+ ) -> str:
948
+ sinks = tuple(sorted(dict.fromkeys(str(item) for item in applied_sinks)))
949
+ projection_payload = projection.payload() if projection is not None else None
950
+ projection_digest = (
951
+ projection.projection_digest if projection is not None else None
952
+ )
953
+ recorded_at = time.time()
954
+ entry_digest = _ledger_digest(
955
+ event_seq=event_seq,
956
+ event_id=event_id,
957
+ event_type=event_type,
958
+ raw_digest=raw_digest,
959
+ projection_digest=projection_digest,
960
+ disposition=disposition,
961
+ applied_sinks=sinks,
962
+ recorded_at=recorded_at,
963
+ )
964
+ with self._lock:
965
+ self._conn.execute("BEGIN IMMEDIATE")
966
+ try:
967
+ cursor = self._conn.execute(
968
+ "SELECT last_seq FROM cognition_evidence_cursors "
969
+ "WHERE consumer_id=?", (consumer_id,),
970
+ ).fetchone()
971
+ if cursor is None:
972
+ raise ValueError("evidence cursor is not initialized")
973
+ current = int(cursor["last_seq"])
974
+ existing = self._conn.execute(
975
+ "SELECT * FROM cognition_evidence_events WHERE event_seq=?",
976
+ (int(event_seq),),
977
+ ).fetchone()
978
+ if existing is not None:
979
+ existing_sinks = json.loads(existing["applied_sinks_json"])
980
+ existing_digest = _ledger_digest(
981
+ event_seq=int(existing["event_seq"]),
982
+ event_id=str(existing["event_id"]),
983
+ event_type=str(existing["event_type"]),
984
+ raw_digest=str(existing["raw_digest"]),
985
+ projection_digest=existing["projection_digest"],
986
+ disposition=str(existing["disposition"]),
987
+ applied_sinks=existing_sinks,
988
+ recorded_at=float(existing["recorded_at"]),
989
+ )
990
+ if (
991
+ existing["event_id"] != event_id
992
+ or existing["event_type"] != event_type
993
+ or existing["raw_digest"] != raw_digest
994
+ or existing["projection_digest"] != projection_digest
995
+ or existing["disposition"] != disposition
996
+ or existing["applied_sinks_json"] != _canonical(list(sinks))
997
+ or existing["ledger_digest"] != existing_digest
998
+ ):
999
+ raise ValueError("immutable cognition evidence replay mismatch")
1000
+ result = "duplicate"
1001
+ else:
1002
+ if int(event_seq) != current + 1:
1003
+ raise ValueError(
1004
+ "cognition evidence event is not cursor-contiguous"
1005
+ )
1006
+ self._conn.execute(
1007
+ """INSERT INTO cognition_evidence_events
1008
+ (event_seq,event_id,event_type,raw_digest,
1009
+ projection_digest,disposition,authority_state,outcome,
1010
+ domain,project_id,step_id,work_order_id,result_ref,
1011
+ subject_person_id,viewer_scope,shareability,occurred_at,
1012
+ evidence_refs_json,projection_json,applied_sinks_json,
1013
+ ledger_digest,recorded_at)
1014
+ VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
1015
+ (
1016
+ int(event_seq), event_id, event_type, raw_digest,
1017
+ projection_digest, disposition,
1018
+ projection.authority_state if projection else None,
1019
+ projection.outcome if projection else None,
1020
+ projection.domain if projection else None,
1021
+ projection.project_id if projection else None,
1022
+ projection.step_id if projection else None,
1023
+ projection.work_order_id if projection else None,
1024
+ projection.result_ref if projection else None,
1025
+ projection.subject_person_id if projection else None,
1026
+ projection.viewer_scope if projection else None,
1027
+ projection.shareability if projection else None,
1028
+ projection.occurred_at if projection else None,
1029
+ _canonical(list(projection.evidence_refs))
1030
+ if projection else None,
1031
+ _canonical(projection_payload)
1032
+ if projection_payload is not None else None,
1033
+ _canonical(list(sinks)), entry_digest, recorded_at,
1034
+ ),
1035
+ )
1036
+ result = "recorded"
1037
+ if int(event_seq) > current:
1038
+ self._conn.execute(
1039
+ """UPDATE cognition_evidence_cursors SET last_seq=?,
1040
+ last_error=NULL,updated_at=? WHERE consumer_id=?""",
1041
+ (int(event_seq), time.time(), consumer_id),
1042
+ )
1043
+ self._conn.commit()
1044
+ return result
1045
+ except Exception:
1046
+ self._conn.rollback()
1047
+ raise
1048
+
1049
+ def status(self, consumer_id: str) -> Dict[str, Any]:
1050
+ with self._lock:
1051
+ cursor = self._conn.execute(
1052
+ "SELECT * FROM cognition_evidence_cursors WHERE consumer_id=?",
1053
+ (consumer_id,),
1054
+ ).fetchone()
1055
+ counts = self._conn.execute(
1056
+ "SELECT disposition,COUNT(*) AS n FROM cognition_evidence_events "
1057
+ "GROUP BY disposition"
1058
+ ).fetchall()
1059
+ gap_count = int(self._conn.execute(
1060
+ "SELECT COUNT(*) AS n FROM cognition_evidence_gaps "
1061
+ "WHERE consumer_id=?", (consumer_id,),
1062
+ ).fetchone()["n"])
1063
+ latest_gap = self._conn.execute(
1064
+ "SELECT prior_cursor,resume_after,reason,acknowledged_at "
1065
+ "FROM cognition_evidence_gaps WHERE consumer_id=? "
1066
+ "ORDER BY gap_id DESC LIMIT 1", (consumer_id,),
1067
+ ).fetchone()
1068
+ passthrough_count = int(self._conn.execute(
1069
+ "SELECT COUNT(*) AS n FROM cognition_evidence_passthrough_ranges "
1070
+ "WHERE consumer_id=?", (consumer_id,),
1071
+ ).fetchone()["n"])
1072
+ latest_passthrough = self._conn.execute(
1073
+ "SELECT prior_cursor,resume_after,mode,reason,recorded_at "
1074
+ "FROM cognition_evidence_passthrough_ranges WHERE consumer_id=? "
1075
+ "ORDER BY range_id DESC LIMIT 1", (consumer_id,),
1076
+ ).fetchone()
1077
+ return {
1078
+ "initialized": cursor is not None,
1079
+ "cursor": int(cursor["last_seq"]) if cursor is not None else None,
1080
+ "bootstrap_mode": cursor["bootstrap_mode"] if cursor else None,
1081
+ "last_error": str(cursor["last_error"] or "") if cursor else "",
1082
+ "dispositions": {
1083
+ row["disposition"]: int(row["n"]) for row in counts
1084
+ },
1085
+ "gaps": {
1086
+ "count": gap_count,
1087
+ "latest": dict(latest_gap) if latest_gap is not None else None,
1088
+ },
1089
+ "passthrough": {
1090
+ "count": passthrough_count,
1091
+ "latest": (
1092
+ dict(latest_passthrough)
1093
+ if latest_passthrough is not None else None
1094
+ ),
1095
+ },
1096
+ }
1097
+
1098
+ @staticmethod
1099
+ def _verified_trace_row(raw_row: Mapping[str, Any]) -> Dict[str, Any]:
1100
+ """Decode one ledger row only after every stored binding verifies."""
1101
+
1102
+ row = dict(raw_row)
1103
+ try:
1104
+ sinks_raw = json.loads(str(row["applied_sinks_json"]))
1105
+ if not isinstance(sinks_raw, list) or any(
1106
+ not isinstance(item, str) for item in sinks_raw
1107
+ ):
1108
+ raise ValueError("applied sinks are not a string list")
1109
+ sinks = tuple(sorted(dict.fromkeys(sinks_raw)))
1110
+ if sinks_raw != list(sinks):
1111
+ raise ValueError("applied sinks are not canonical")
1112
+ expected_ledger = _ledger_digest(
1113
+ event_seq=int(row["event_seq"]),
1114
+ event_id=str(row["event_id"]),
1115
+ event_type=str(row["event_type"]),
1116
+ raw_digest=str(row["raw_digest"]),
1117
+ projection_digest=row.get("projection_digest"),
1118
+ disposition=str(row["disposition"]),
1119
+ applied_sinks=sinks,
1120
+ recorded_at=float(row["recorded_at"]),
1121
+ )
1122
+ if row.get("ledger_digest") != expected_ledger:
1123
+ raise ValueError("ledger envelope digest mismatch")
1124
+
1125
+ projection_raw = json.loads(str(row["projection_json"]))
1126
+ if not isinstance(projection_raw, dict):
1127
+ raise ValueError("projection is not an object")
1128
+ projection = EvidenceProjectionV1(**{
1129
+ **projection_raw,
1130
+ "evidence_refs": tuple(projection_raw.get("evidence_refs") or ()),
1131
+ })
1132
+ if projection.projection_digest != row.get("projection_digest"):
1133
+ raise ValueError("projection digest mismatch")
1134
+ expected_columns = {
1135
+ "event_seq": projection.event_seq,
1136
+ "event_id": projection.event_id,
1137
+ "event_type": projection.event_type,
1138
+ "disposition": projection.disposition,
1139
+ "authority_state": projection.authority_state,
1140
+ "outcome": projection.outcome,
1141
+ "domain": projection.domain,
1142
+ "project_id": projection.project_id,
1143
+ "step_id": projection.step_id,
1144
+ "work_order_id": projection.work_order_id,
1145
+ "result_ref": projection.result_ref,
1146
+ "subject_person_id": projection.subject_person_id,
1147
+ "viewer_scope": projection.viewer_scope,
1148
+ "shareability": projection.shareability,
1149
+ "occurred_at": projection.occurred_at,
1150
+ }
1151
+ if any(row.get(key) != value for key, value in expected_columns.items()):
1152
+ raise ValueError("projection columns differ from signed content")
1153
+ evidence_refs = json.loads(str(row["evidence_refs_json"]))
1154
+ if evidence_refs != list(projection.evidence_refs):
1155
+ raise ValueError("projection evidence refs differ from signed content")
1156
+ except (KeyError, TypeError, ValueError, json.JSONDecodeError) as exc:
1157
+ sequence = row.get("event_seq", "unknown")
1158
+ raise ValueError(
1159
+ f"cognition evidence ledger integrity failure at {sequence}: {exc}"
1160
+ ) from exc
1161
+
1162
+ row["projection"] = projection.payload()
1163
+ row["evidence_refs"] = evidence_refs
1164
+ row["applied_sinks"] = list(sinks)
1165
+ row.pop("projection_json", None)
1166
+ row.pop("evidence_refs_json", None)
1167
+ row.pop("applied_sinks_json", None)
1168
+ return row
1169
+
1170
+ def trace(
1171
+ self,
1172
+ *,
1173
+ project_id: str = "",
1174
+ subject_person_id: str = "",
1175
+ viewer_scope: str = "owner",
1176
+ limit: int = 100,
1177
+ ) -> list[Dict[str, Any]]:
1178
+ bound = max(1, min(500, int(limit)))
1179
+ query = (
1180
+ "SELECT * FROM cognition_evidence_events "
1181
+ "WHERE viewer_scope=? AND projection_json IS NOT NULL"
1182
+ )
1183
+ params: list[Any] = [str(viewer_scope)]
1184
+ if project_id:
1185
+ query += " AND project_id=?"
1186
+ params.append(str(project_id))
1187
+ if subject_person_id:
1188
+ query += " AND subject_person_id=?"
1189
+ params.append(str(subject_person_id))
1190
+ query += " ORDER BY event_seq DESC LIMIT ?"
1191
+ params.append(bound)
1192
+ with self._lock:
1193
+ rows = self._conn.execute(query, params).fetchall()
1194
+ return [self._verified_trace_row(row) for row in rows]
1195
+
1196
+
1197
+ class CognitionEvidenceReducer:
1198
+ """Reduce durable journal evidence into trusted, idempotent learning sinks."""
1199
+
1200
+ def __init__(
1201
+ self,
1202
+ store: CognitionEvidenceStore,
1203
+ *,
1204
+ project_store: ProjectStore,
1205
+ self_model: Any = None,
1206
+ expectations: Any = None,
1207
+ project_event_projector: Any = None,
1208
+ consumer_id: str = _CONSUMER_ID,
1209
+ replay_fn: Callable[..., Mapping[str, Any]] = replay_events,
1210
+ current_sequence_fn: Callable[[], int] = current_sequence,
1211
+ ) -> None:
1212
+ self.store = store
1213
+ self.project_store = project_store
1214
+ self.self_model = self_model
1215
+ self.expectations = expectations
1216
+ self.project_event_projector = project_event_projector
1217
+ self.consumer_id = consumer_id
1218
+ self._replay = replay_fn
1219
+ self._current_sequence = current_sequence_fn
1220
+
1221
+ @property
1222
+ def mode(self) -> str:
1223
+ return cognition_evidence_mode()
1224
+
1225
+ @staticmethod
1226
+ def _replay_integrity_error(snapshot: Mapping[str, Any]) -> str:
1227
+ if snapshot.get("replayError"):
1228
+ return "event_journal_unavailable"
1229
+ try:
1230
+ corrupt = int(snapshot.get("corruptCount") or 0)
1231
+ first = int(snapshot.get("firstAvailableSeq") or 0)
1232
+ high = int(snapshot.get("journalLastSeq") or 0)
1233
+ except (TypeError, ValueError):
1234
+ return "event_journal_metadata_invalid"
1235
+ if corrupt < 0 or first < 0 or high < 0 or (first and first > high):
1236
+ return "event_journal_metadata_invalid"
1237
+ if corrupt:
1238
+ return f"journal_corruption_detected:{corrupt}"
1239
+ events = snapshot.get("events") or ()
1240
+ if not isinstance(events, (list, tuple)):
1241
+ return "event_journal_metadata_invalid"
1242
+ return ""
1243
+
1244
+ @staticmethod
1245
+ def _gap_policy() -> str:
1246
+ return os.environ.get(
1247
+ "COLONY_COGNITION_EVIDENCE_GAP_POLICY", "stop",
1248
+ ).strip().lower()
1249
+
1250
+ def _stop(self, error: str, *, outbox: Any, processed: int = 0,
1251
+ last_seq: Optional[int] = None) -> Dict[str, Any]:
1252
+ self.store.set_error(self.consumer_id, error)
1253
+ result: Dict[str, Any] = {
1254
+ "enabled": True,
1255
+ "mode": self.mode,
1256
+ "processed": int(processed),
1257
+ "error": error,
1258
+ "project_outbox": outbox,
1259
+ }
1260
+ if last_seq is not None:
1261
+ result["last_seq"] = int(last_seq)
1262
+ return result
1263
+
1264
+ def _initialize(self) -> Dict[str, Any]:
1265
+ snapshot = self._replay(after_seq=0, limit=1)
1266
+ bootstrap = _bootstrap_mode()
1267
+ error = self._replay_integrity_error(snapshot)
1268
+ if error:
1269
+ initial = 0
1270
+ else:
1271
+ first = int(snapshot.get("firstAvailableSeq") or 0)
1272
+ high = int(snapshot.get("journalLastSeq") or 0)
1273
+ initial = high if bootstrap == "tail" else max(0, first - 1)
1274
+ cursor = self.store.initialize_cursor(
1275
+ self.consumer_id, initial, bootstrap_mode=bootstrap,
1276
+ )
1277
+ result = {
1278
+ "bootstrapped": True,
1279
+ "bootstrap_mode": bootstrap,
1280
+ "cursor": cursor,
1281
+ }
1282
+ if error:
1283
+ self.store.set_error(self.consumer_id, error)
1284
+ result["error"] = error
1285
+ return result
1286
+
1287
+ def _apply_competence(self, projection: EvidenceProjectionV1) -> list[str]:
1288
+ if self.mode != "live"\
1289
+ or projection.local_evidence.get(
1290
+ "cognition_mode_at_stage"
1291
+ ) != "live"\
1292
+ or projection.event_type != "work_order.result"\
1293
+ or projection.authority_state != "verified"\
1294
+ or projection.outcome == "neutral":
1295
+ return []
1296
+ if self.self_model is None:
1297
+ raise RuntimeError("verified cognition evidence has no self-model sink")
1298
+ event_key = f"journal:{projection.event_seq}:{projection.event_id}"
1299
+ source = "cognition_evidence_v1"
1300
+ recorded = self.self_model.record(
1301
+ projection.domain,
1302
+ projection.outcome,
1303
+ latency_secs=projection.latency_seconds,
1304
+ stated_confidence=projection.stated_confidence,
1305
+ source=source,
1306
+ source_ref=str(projection.local_evidence["attempt_ref"]),
1307
+ event_key=event_key,
1308
+ evidence_status="verified",
1309
+ outcome_contract="ExecutionResultV1:1",
1310
+ evidence={
1311
+ "event_ref": event_key,
1312
+ "project_id": projection.project_id,
1313
+ "step_id": projection.step_id,
1314
+ "work_order_id": projection.work_order_id,
1315
+ "result_ref": projection.result_ref,
1316
+ "evidence_refs": list(projection.evidence_refs),
1317
+ **dict(projection.local_evidence),
1318
+ },
1319
+ )
1320
+ competence_store = getattr(self.self_model, "store", None)
1321
+ durable = bool(
1322
+ competence_store is not None
1323
+ and callable(getattr(competence_store, "has_event_key", None))
1324
+ and competence_store.has_event_key(source, event_key)
1325
+ )
1326
+ if not recorded and not durable:
1327
+ raise RuntimeError("verified competence evidence was not made durable")
1328
+ return ["competence:project"]
1329
+
1330
+ def _apply_expectations(self, projection: EvidenceProjectionV1) -> list[str]:
1331
+ if self.mode != "live"\
1332
+ or projection.local_evidence.get(
1333
+ "cognition_mode_at_stage"
1334
+ ) != "live"\
1335
+ or projection.authority_state != "verified"\
1336
+ or projection.outcome == "neutral"\
1337
+ or self.expectations is None:
1338
+ return []
1339
+ if projection.event_type == "work_order.result":
1340
+ # A transient failed attempt is real competence evidence, but it
1341
+ # does not settle the logical task while its bounded retry budget
1342
+ # remains open.
1343
+ if projection.outcome in {"failure", "timeout"} and int(
1344
+ projection.local_evidence.get("attempt_number") or 0
1345
+ ) < int(projection.local_evidence.get("max_attempts") or 0):
1346
+ return []
1347
+ subjects = {
1348
+ f"task:{projection.step_id}",
1349
+ f"task:{projection.work_order_id}",
1350
+ }
1351
+ evidence_ref = str(projection.local_evidence["attempt_ref"])
1352
+ elif projection.event_type in {"project.completed", "project.abandoned"}:
1353
+ subjects = {f"project:{projection.project_id}"}
1354
+ evidence_ref = str(projection.local_evidence["terminal_ref"])
1355
+ else:
1356
+ return []
1357
+ predictions = self.expectations.store.for_subjects(subjects, limit=500)
1358
+ sinks: list[str] = []
1359
+ from apsimo.self_model.expectations import OutcomeObservationV1
1360
+
1361
+ for prediction in predictions:
1362
+ if prediction.schema_version != 2:
1363
+ continue
1364
+ if (
1365
+ prediction.subject_person_id != projection.subject_person_id
1366
+ or prediction.viewer_scope != projection.viewer_scope
1367
+ or prediction.shareability != projection.shareability
1368
+ ):
1369
+ continue
1370
+ identity = _digest({
1371
+ "event_id": projection.event_id,
1372
+ "prediction_id": prediction.prediction_id,
1373
+ })
1374
+ observation_id = f"eo-{identity[:24]}"
1375
+ # Replaying a journal event must not resolve a prediction that did
1376
+ # not exist when the outcome happened. Already-resolved rows are
1377
+ # retained only when this exact observation owns the resolution;
1378
+ # another attempt or resolver must not turn first-writer sealing
1379
+ # into a permanent evidence-pipeline failure.
1380
+ if prediction.created_at > projection.occurred_at:
1381
+ continue
1382
+ if prediction.outcome != "pending" and (
1383
+ prediction.outcome_observation_id != observation_id
1384
+ ):
1385
+ continue
1386
+ observation = OutcomeObservationV1.create(
1387
+ observation_id=observation_id,
1388
+ prediction_id=prediction.prediction_id,
1389
+ value=projection.outcome == "success",
1390
+ observed_at=projection.occurred_at,
1391
+ # The immutable attempt, not the mutable logical-result head,
1392
+ # is the canonical join. It retains the independently checked
1393
+ # result digest/run identity even after a later retry becomes
1394
+ # the WorkOrder's current logical result.
1395
+ evidence_refs=(evidence_ref,),
1396
+ source_kind="work_receipt",
1397
+ subject_person_id=projection.subject_person_id,
1398
+ viewer_scope=projection.viewer_scope,
1399
+ shareability=projection.shareability,
1400
+ )
1401
+ result = self.expectations.ingest_outcome(observation)
1402
+ if result.get("disposition") not in {"resolved", "duplicate"}:
1403
+ raise RuntimeError("expectation evidence did not resolve durably")
1404
+ sinks.append(f"expectation:{prediction.prediction_id}")
1405
+ return sinks
1406
+
1407
+ def status(self) -> Dict[str, Any]:
1408
+ state = self.store.status(self.consumer_id)
1409
+ outbox = (
1410
+ self.project_event_projector.status()
1411
+ if self.project_event_projector is not None else None
1412
+ )
1413
+ high_water = int(self._current_sequence())
1414
+ cursor = state.get("cursor")
1415
+ state.update({
1416
+ "enabled": self.mode != "off",
1417
+ "mode": self.mode,
1418
+ "healthy": not bool(state.get("last_error")) and not bool(
1419
+ (outbox or {}).get("last_error")
1420
+ ),
1421
+ "journal_high_water": high_water,
1422
+ "journal_lag": (
1423
+ max(0, high_water - int(cursor)) if cursor is not None else None
1424
+ ),
1425
+ "project_outbox": outbox,
1426
+ })
1427
+ return state
1428
+
1429
+ def run_once(self, *, limit: int = 100) -> Dict[str, Any]:
1430
+ outbox = (
1431
+ self.project_event_projector.run_once(limit=limit)
1432
+ if self.project_event_projector is not None else None
1433
+ )
1434
+ if self.mode == "off":
1435
+ # Off restores the legacy direct writer, so journal outcomes from
1436
+ # this interval must never be replayed as new receipt-derived
1437
+ # learning after live is re-enabled. Keep only a durable cursor
1438
+ # range acknowledgement; do not validate, trace, or train events.
1439
+ high = int(self._current_sequence())
1440
+ cursor = self.store.cursor(self.consumer_id)
1441
+ skipped = 0
1442
+ if cursor is None:
1443
+ cursor = self.store.initialize_cursor(
1444
+ self.consumer_id, 0, bootstrap_mode="off_passthrough",
1445
+ )
1446
+ if high < cursor:
1447
+ error = f"event_journal_rewind:{cursor}:{high}"
1448
+ self.store.set_error(self.consumer_id, error)
1449
+ return {
1450
+ "enabled": False, "mode": "off", "processed": 0,
1451
+ "cursor": cursor, "error": error,
1452
+ "project_outbox": outbox,
1453
+ }
1454
+ elif high > cursor:
1455
+ prior = cursor
1456
+ cursor = self.store.checkpoint_passthrough(
1457
+ self.consumer_id,
1458
+ prior_cursor=prior,
1459
+ resume_after=high,
1460
+ reason=f"evidence_mode_off_passthrough:{prior}:{high}",
1461
+ )
1462
+ skipped = high - prior
1463
+ return {
1464
+ "enabled": False, "mode": "off", "processed": 0,
1465
+ "cursor": cursor, "passthrough_events": skipped,
1466
+ "project_outbox": outbox,
1467
+ }
1468
+ cursor = self.store.cursor(self.consumer_id)
1469
+ initialized: Dict[str, Any] = {}
1470
+ if cursor is None:
1471
+ initialized = self._initialize()
1472
+ cursor = int(initialized["cursor"])
1473
+ if initialized.get("error"):
1474
+ return self._stop(
1475
+ str(initialized["error"]), outbox=outbox,
1476
+ last_seq=cursor,
1477
+ )
1478
+ if initialized["bootstrap_mode"] == "tail":
1479
+ return {
1480
+ **initialized, "enabled": True, "processed": 0,
1481
+ "project_outbox": outbox,
1482
+ }
1483
+ batch = self._replay(
1484
+ after_seq=cursor, limit=max(1, min(500, int(limit))),
1485
+ )
1486
+ integrity_error = self._replay_integrity_error(batch)
1487
+ if integrity_error:
1488
+ return self._stop(
1489
+ integrity_error, outbox=outbox, last_seq=cursor,
1490
+ )
1491
+ journal_high = int(batch.get("journalLastSeq") or 0)
1492
+ if cursor > journal_high:
1493
+ return self._stop(
1494
+ f"event_journal_rewind:{cursor}:{journal_high}",
1495
+ outbox=outbox,
1496
+ last_seq=cursor,
1497
+ )
1498
+ first = int(batch.get("firstAvailableSeq") or 0)
1499
+ if first and cursor < first - 1:
1500
+ message = f"journal_retention_gap:{cursor}:{first}"
1501
+ if self._gap_policy() != "acknowledge":
1502
+ return self._stop(message, outbox=outbox, last_seq=cursor)
1503
+ cursor = self.store.acknowledge_gap(
1504
+ self.consumer_id,
1505
+ prior_cursor=cursor,
1506
+ resume_after=first - 1,
1507
+ reason=message,
1508
+ )
1509
+ batch = self._replay(
1510
+ after_seq=cursor, limit=max(1, min(500, int(limit))),
1511
+ )
1512
+ integrity_error = self._replay_integrity_error(batch)
1513
+ if integrity_error:
1514
+ return self._stop(
1515
+ integrity_error, outbox=outbox, last_seq=cursor,
1516
+ )
1517
+ journal_high = int(batch.get("journalLastSeq") or 0)
1518
+ if cursor > journal_high:
1519
+ return self._stop(
1520
+ f"event_journal_rewind:{cursor}:{journal_high}",
1521
+ outbox=outbox,
1522
+ last_seq=cursor,
1523
+ )
1524
+
1525
+ counts: Dict[str, int] = {}
1526
+ last_seq = cursor
1527
+ for raw in batch.get("events") or []:
1528
+ try:
1529
+ if not isinstance(raw, Mapping):
1530
+ raise ValueError("journal event is not an object")
1531
+ sequence = int(raw.get("seq") or 0)
1532
+ if sequence <= last_seq:
1533
+ raise ValueError("journal event sequence is not increasing")
1534
+ if sequence != last_seq + 1:
1535
+ message = f"journal_sequence_gap:{last_seq}:{sequence}"
1536
+ if self._gap_policy() != "acknowledge":
1537
+ return self._stop(
1538
+ message,
1539
+ outbox=outbox,
1540
+ processed=sum(counts.values()),
1541
+ last_seq=last_seq,
1542
+ )
1543
+ last_seq = self.store.acknowledge_gap(
1544
+ self.consumer_id,
1545
+ prior_cursor=last_seq,
1546
+ resume_after=sequence - 1,
1547
+ reason=message,
1548
+ )
1549
+ sequence, event_id, event_type, _, _, raw_digest = _event_identity(raw)
1550
+ projection, skip_reason, _ = project_evidence_event(
1551
+ raw, self.project_store,
1552
+ )
1553
+ sinks: list[str] = []
1554
+ disposition = skip_reason
1555
+ if projection is not None:
1556
+ sinks.extend(self._apply_competence(projection))
1557
+ sinks.extend(self._apply_expectations(projection))
1558
+ disposition = projection.disposition
1559
+ applied = self.store.apply(
1560
+ consumer_id=self.consumer_id,
1561
+ event_seq=sequence,
1562
+ event_id=event_id,
1563
+ event_type=event_type,
1564
+ raw_digest=raw_digest,
1565
+ projection=projection,
1566
+ disposition=disposition,
1567
+ applied_sinks=sinks,
1568
+ )
1569
+ except Exception as exc:
1570
+ sequence = int(raw.get("seq") or 0) if isinstance(raw, Mapping) else 0
1571
+ if sequence < 1:
1572
+ error = "malformed_event_without_sequence"
1573
+ else:
1574
+ error = (
1575
+ f"evidence_validation_failed:{sequence}:"
1576
+ f"{type(exc).__name__}:{str(exc)[:200]}"
1577
+ )
1578
+ self.store.set_error(self.consumer_id, error)
1579
+ return {
1580
+ "enabled": True,
1581
+ "mode": self.mode,
1582
+ "processed": sum(counts.values()),
1583
+ "last_seq": last_seq,
1584
+ "error": error,
1585
+ "project_outbox": outbox,
1586
+ }
1587
+ counts[disposition] = counts.get(disposition, 0) + 1
1588
+ if applied == "duplicate":
1589
+ counts["duplicates"] = counts.get("duplicates", 0) + 1
1590
+ last_seq = sequence
1591
+ journal_high = int(batch.get("journalLastSeq") or 0)
1592
+ if not bool(batch.get("hasMore")) and journal_high > last_seq:
1593
+ message = f"journal_sequence_gap:{last_seq}:{journal_high + 1}"
1594
+ if self._gap_policy() != "acknowledge":
1595
+ return self._stop(
1596
+ message,
1597
+ outbox=outbox,
1598
+ processed=sum(counts.values()),
1599
+ last_seq=last_seq,
1600
+ )
1601
+ last_seq = self.store.acknowledge_gap(
1602
+ self.consumer_id,
1603
+ prior_cursor=last_seq,
1604
+ resume_after=journal_high,
1605
+ reason=message,
1606
+ )
1607
+ return {
1608
+ **initialized,
1609
+ "enabled": True,
1610
+ "mode": self.mode,
1611
+ "processed": sum(
1612
+ value for key, value in counts.items() if key != "duplicates"
1613
+ ),
1614
+ "last_seq": last_seq,
1615
+ "dispositions": counts,
1616
+ "project_outbox": outbox,
1617
+ }
1618
+
1619
+
1620
+ __all__ = [
1621
+ "CognitionEvidenceReducer",
1622
+ "CognitionEvidenceStore",
1623
+ "EvidenceProjectionV1",
1624
+ "cognition_evidence_enabled",
1625
+ "cognition_evidence_mode",
1626
+ "project_evidence_event",
1627
+ ]