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,559 @@
1
+ """P8 fact-level provenance and recipient visibility.
2
+
3
+ This module is deliberately a small, pure boundary primitive. It does not
4
+ authenticate a caller, retrieve data, rank memories, or grant authority. A
5
+ host supplies an already-attested :class:`ViewerContextV1`; facts are filtered
6
+ before any relevance, prose generation, or recipient simulation can consume
7
+ their content.
8
+
9
+ ``FactVisibilityV1`` is immutable and binds one fact content digest to one
10
+ source, subject, exact viewer scope, freshness window, confidence, and bounded
11
+ evidence set. A changed fact or widened audience requires a new record and a
12
+ new audit digest.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ from collections import Counter, defaultdict
18
+ from dataclasses import dataclass
19
+ from datetime import datetime, timezone
20
+ import hashlib
21
+ import json
22
+ import math
23
+ import re
24
+ from typing import Any, Iterable, Mapping, Optional, Sequence
25
+
26
+
27
+ SCHEMA_VERSION = 1
28
+ SHAREABILITIES = frozenset({
29
+ "owner_private", "subject_private", "shared", "public",
30
+ })
31
+ MAX_EVIDENCE_REFS = 32
32
+ MAX_VIEWER_AUDIENCES = 16
33
+ MAX_FACT_CONTENT_CHARS = 4_000
34
+ MAX_CANDIDATES = 512
35
+ MAX_PROJECTED_FACTS = 64
36
+ MAX_PROJECTED_CHARS = 24_000
37
+
38
+ _REF_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9_.:@/+\-]{0,191}$")
39
+ _DIGEST_RE = re.compile(r"^[a-f0-9]{64}$")
40
+
41
+
42
+ def _canonical(value: Any) -> str:
43
+ return json.dumps(
44
+ value,
45
+ ensure_ascii=True,
46
+ separators=(",", ":"),
47
+ sort_keys=True,
48
+ default=str,
49
+ )
50
+
51
+
52
+ def _digest(value: Any) -> str:
53
+ return hashlib.sha256(_canonical(value).encode("utf-8")).hexdigest()
54
+
55
+
56
+ def content_digest(content: str) -> str:
57
+ """Bind exact UTF-8 fact content without retaining it in scope records."""
58
+
59
+ if not isinstance(content, str):
60
+ raise ValueError("fact content must be text")
61
+ return hashlib.sha256(content.encode("utf-8")).hexdigest()
62
+
63
+
64
+ def _safe_ref(value: Any, *, field: str, allow_empty: bool = False) -> str:
65
+ normalized = str(value or "").strip()
66
+ if allow_empty and not normalized:
67
+ return ""
68
+ if not _REF_RE.fullmatch(normalized):
69
+ raise ValueError(f"{field} is not a bounded opaque reference")
70
+ return normalized
71
+
72
+
73
+ def _refs(values: Iterable[Any], *, field: str, maximum: int) -> tuple[str, ...]:
74
+ normalized = tuple(dict.fromkeys(
75
+ _safe_ref(value, field=field) for value in values
76
+ ))
77
+ if len(normalized) > maximum:
78
+ raise ValueError(f"{field} exceeds bounded reference limit")
79
+ return tuple(sorted(normalized))
80
+
81
+
82
+ def _as_utc(value: datetime | str, *, field: str) -> datetime:
83
+ if isinstance(value, datetime):
84
+ parsed = value
85
+ else:
86
+ try:
87
+ parsed = datetime.fromisoformat(str(value).replace("Z", "+00:00"))
88
+ except (TypeError, ValueError) as exc:
89
+ raise ValueError(f"{field} must be an ISO-8601 timestamp") from exc
90
+ if parsed.tzinfo is None:
91
+ raise ValueError(f"{field} must include a timezone")
92
+ return parsed.astimezone(timezone.utc)
93
+
94
+
95
+ def _iso(value: datetime | str, *, field: str) -> str:
96
+ return _as_utc(value, field=field).isoformat()
97
+
98
+
99
+ def validate_visibility_scope(
100
+ *,
101
+ viewer_scope: str,
102
+ shareability: str,
103
+ subject_person_id: str,
104
+ ) -> tuple[str, str, str]:
105
+ """Normalize and enforce one non-widening shareability/scope pairing."""
106
+
107
+ subject = _safe_ref(subject_person_id, field="subject_person_id")
108
+ scope = _safe_ref(viewer_scope, field="viewer_scope")
109
+ share = str(shareability or "").strip().lower()
110
+ if share not in SHAREABILITIES:
111
+ raise ValueError("shareability is not a supported P8 value")
112
+ if share == "owner_private" and scope != "owner":
113
+ raise ValueError("owner_private visibility requires viewer_scope=owner")
114
+ if share == "subject_private" and scope != f"person:{subject}":
115
+ raise ValueError(
116
+ "subject_private visibility requires the exact subject person")
117
+ if share == "shared" and not (
118
+ scope.startswith("person:")
119
+ or scope == "audience:shared"
120
+ or scope.startswith("conversation:")
121
+ ):
122
+ raise ValueError(
123
+ "shared visibility requires an exact person, conversation, or "
124
+ "shared audience scope")
125
+ if share == "public" and scope not in {"public", "audience:global"}:
126
+ raise ValueError("public visibility requires public/global scope")
127
+ return scope, share, subject
128
+
129
+
130
+ @dataclass(frozen=True, slots=True)
131
+ class ViewerContextV1:
132
+ """Server-attested viewer input; constructing it grants no authority."""
133
+
134
+ principal_id: str
135
+ viewer_person_id: str
136
+ owner_person_id: str
137
+ audiences: tuple[str, ...] = ()
138
+ conversation_scope: str = ""
139
+ scope_revision: str = ""
140
+ attested: bool = False
141
+ schema_version: int = SCHEMA_VERSION
142
+
143
+ def __post_init__(self) -> None:
144
+ if self.schema_version != SCHEMA_VERSION:
145
+ raise ValueError("unsupported viewer context version")
146
+ principal = _safe_ref(
147
+ self.principal_id, field="principal_id", allow_empty=not self.attested)
148
+ viewer = _safe_ref(
149
+ self.viewer_person_id,
150
+ field="viewer_person_id",
151
+ allow_empty=not self.attested,
152
+ )
153
+ owner = _safe_ref(
154
+ self.owner_person_id,
155
+ field="owner_person_id",
156
+ allow_empty=not self.attested,
157
+ )
158
+ revision = _safe_ref(
159
+ self.scope_revision,
160
+ field="scope_revision",
161
+ allow_empty=not self.attested,
162
+ )
163
+ conversation = _safe_ref(
164
+ self.conversation_scope,
165
+ field="conversation_scope",
166
+ allow_empty=True,
167
+ )
168
+ audiences = _refs(
169
+ self.audiences,
170
+ field="audience",
171
+ maximum=MAX_VIEWER_AUDIENCES,
172
+ )
173
+ if self.attested and not (principal and viewer and owner and revision):
174
+ raise ValueError(
175
+ "attested viewer requires principal, viewer, owner, and revision")
176
+ object.__setattr__(self, "principal_id", principal)
177
+ object.__setattr__(self, "viewer_person_id", viewer)
178
+ object.__setattr__(self, "owner_person_id", owner)
179
+ object.__setattr__(self, "scope_revision", revision)
180
+ object.__setattr__(self, "conversation_scope", conversation)
181
+ object.__setattr__(self, "audiences", audiences)
182
+
183
+ @property
184
+ def audit_digest(self) -> str:
185
+ return _digest({
186
+ "schema_version": self.schema_version,
187
+ "principal_id": self.principal_id,
188
+ "viewer_person_id": self.viewer_person_id,
189
+ "owner_person_id": self.owner_person_id,
190
+ "audiences": self.audiences,
191
+ "conversation_scope": self.conversation_scope,
192
+ "scope_revision": self.scope_revision,
193
+ "attested": self.attested,
194
+ })
195
+
196
+
197
+ @dataclass(frozen=True, slots=True)
198
+ class VisibilityDecisionV1:
199
+ allowed: bool
200
+ reason: str
201
+ visibility_digest: str
202
+
203
+
204
+ def visibility_scope_decision(
205
+ *,
206
+ viewer_scope: str,
207
+ shareability: str,
208
+ subject_person_id: str,
209
+ viewer: ViewerContextV1,
210
+ ) -> tuple[bool, str]:
211
+ """Pure exact-scope decision shared by facts and conversational arcs."""
212
+
213
+ if not viewer.attested or not viewer.viewer_person_id:
214
+ return False, "viewer_unattested"
215
+ if viewer.viewer_person_id == viewer.owner_person_id:
216
+ return True, "owner_scope"
217
+ if shareability == "owner_private":
218
+ return False, "owner_private"
219
+ if shareability == "subject_private":
220
+ return (
221
+ (True, "subject_scope")
222
+ if viewer.viewer_person_id == subject_person_id
223
+ else (False, "subject_scope_mismatch")
224
+ )
225
+ if shareability == "shared":
226
+ if viewer_scope.startswith("person:"):
227
+ target = viewer_scope.split(":", 1)[1]
228
+ return (
229
+ (True, "exact_person_scope")
230
+ if viewer.viewer_person_id == target
231
+ else (False, "person_scope_mismatch")
232
+ )
233
+ if viewer_scope == "audience:shared":
234
+ return (
235
+ (True, "shared_audience")
236
+ if "shared" in viewer.audiences
237
+ else (False, "shared_audience_not_granted")
238
+ )
239
+ if viewer_scope.startswith("conversation:"):
240
+ target = viewer_scope.split(":", 1)[1]
241
+ return (
242
+ (True, "conversation_scope")
243
+ if viewer.conversation_scope == target
244
+ else (False, "conversation_scope_mismatch")
245
+ )
246
+ return False, "unknown_shared_scope"
247
+ if shareability == "public":
248
+ if viewer_scope == "public":
249
+ return True, "public_scope"
250
+ return (
251
+ (True, "global_audience")
252
+ if "global" in viewer.audiences
253
+ else (False, "global_audience_not_granted")
254
+ )
255
+ return False, "unknown_shareability"
256
+
257
+
258
+ @dataclass(frozen=True, slots=True)
259
+ class FactVisibilityV1:
260
+ fact_ref: str
261
+ content_digest: str
262
+ source_ref: str
263
+ subject_person_id: str
264
+ viewer_scope: str
265
+ shareability: str
266
+ confidence: float
267
+ observed_at: str
268
+ fresh_until: str
269
+ evidence_refs: tuple[str, ...]
270
+ schema_version: int = SCHEMA_VERSION
271
+
272
+ def __post_init__(self) -> None:
273
+ if self.schema_version != SCHEMA_VERSION:
274
+ raise ValueError("unsupported fact visibility version")
275
+ fact_ref = _safe_ref(self.fact_ref, field="fact_ref")
276
+ source_ref = _safe_ref(self.source_ref, field="source_ref")
277
+ digest = str(self.content_digest or "").strip().lower()
278
+ if not _DIGEST_RE.fullmatch(digest):
279
+ raise ValueError("content digest must be a SHA-256 hex digest")
280
+ scope, share, subject = validate_visibility_scope(
281
+ viewer_scope=self.viewer_scope,
282
+ shareability=self.shareability,
283
+ subject_person_id=self.subject_person_id,
284
+ )
285
+ try:
286
+ confidence = float(self.confidence)
287
+ except (TypeError, ValueError) as exc:
288
+ raise ValueError("confidence must be finite and between zero and one") from exc
289
+ if not math.isfinite(confidence) or not 0.0 <= confidence <= 1.0:
290
+ raise ValueError("confidence must be finite and between zero and one")
291
+ observed = _as_utc(self.observed_at, field="observed_at")
292
+ fresh_until = _as_utc(self.fresh_until, field="fresh_until")
293
+ if fresh_until <= observed:
294
+ raise ValueError("fresh_until must be later than observed_at")
295
+ evidence = _refs(
296
+ self.evidence_refs,
297
+ field="evidence_ref",
298
+ maximum=MAX_EVIDENCE_REFS,
299
+ )
300
+ if not evidence:
301
+ raise ValueError("at least one evidence reference is required")
302
+ object.__setattr__(self, "fact_ref", fact_ref)
303
+ object.__setattr__(self, "source_ref", source_ref)
304
+ object.__setattr__(self, "content_digest", digest)
305
+ object.__setattr__(self, "subject_person_id", subject)
306
+ object.__setattr__(self, "viewer_scope", scope)
307
+ object.__setattr__(self, "shareability", share)
308
+ object.__setattr__(self, "confidence", confidence)
309
+ object.__setattr__(self, "observed_at", observed.isoformat())
310
+ object.__setattr__(self, "fresh_until", fresh_until.isoformat())
311
+ object.__setattr__(self, "evidence_refs", evidence)
312
+
313
+ @property
314
+ def audit_digest(self) -> str:
315
+ return _digest(self.public())
316
+
317
+ def public(self) -> dict[str, Any]:
318
+ """Reference-only projection; fact text never lives in this record."""
319
+
320
+ return {
321
+ "schema_version": self.schema_version,
322
+ "fact_ref": self.fact_ref,
323
+ "content_digest": self.content_digest,
324
+ "source_ref": self.source_ref,
325
+ "subject_person_id": self.subject_person_id,
326
+ "viewer_scope": self.viewer_scope,
327
+ "shareability": self.shareability,
328
+ "confidence": self.confidence,
329
+ "observed_at": self.observed_at,
330
+ "fresh_until": self.fresh_until,
331
+ "evidence_refs": list(self.evidence_refs),
332
+ }
333
+
334
+ def decision(
335
+ self,
336
+ viewer: ViewerContextV1,
337
+ *,
338
+ now: datetime | str,
339
+ min_confidence: float = 0.0,
340
+ ) -> VisibilityDecisionV1:
341
+ if not viewer.attested or not viewer.viewer_person_id:
342
+ return VisibilityDecisionV1(
343
+ False, "viewer_unattested", self.audit_digest)
344
+ decision_time = _as_utc(now, field="now")
345
+ fact_observed = _as_utc(self.observed_at, field="observed_at")
346
+ if decision_time < fact_observed:
347
+ return VisibilityDecisionV1(
348
+ False, "fact_not_yet_observed", self.audit_digest)
349
+ if decision_time >= _as_utc(self.fresh_until, field="fresh_until"):
350
+ return VisibilityDecisionV1(False, "fact_stale", self.audit_digest)
351
+ if self.confidence < float(min_confidence):
352
+ return VisibilityDecisionV1(
353
+ False, "confidence_below_floor", self.audit_digest)
354
+ allowed, reason = visibility_scope_decision(
355
+ viewer_scope=self.viewer_scope,
356
+ shareability=self.shareability,
357
+ subject_person_id=self.subject_person_id,
358
+ viewer=viewer,
359
+ )
360
+ return VisibilityDecisionV1(allowed, reason, self.audit_digest)
361
+
362
+
363
+ @dataclass(frozen=True, slots=True)
364
+ class FactCandidateV1:
365
+ content: str
366
+ visibility: FactVisibilityV1
367
+
368
+ def __post_init__(self) -> None:
369
+ if not isinstance(self.content, str) or not self.content.strip():
370
+ raise ValueError("fact content is required")
371
+ if len(self.content) > MAX_FACT_CONTENT_CHARS:
372
+ raise ValueError("fact content exceeds bounded length")
373
+ if content_digest(self.content) != self.visibility.content_digest:
374
+ raise ValueError("fact content digest does not match visibility")
375
+
376
+
377
+ @dataclass(frozen=True, slots=True)
378
+ class ProjectedFactV1:
379
+ fact_ref: str
380
+ content: str
381
+ source_ref: str
382
+ subject_person_id: str
383
+ shareability: str
384
+ confidence: float
385
+ fresh_until: str
386
+ evidence_refs: tuple[str, ...]
387
+ visibility_digest: str
388
+
389
+ def public(self) -> dict[str, Any]:
390
+ return {
391
+ "fact_ref": self.fact_ref,
392
+ "content": self.content,
393
+ "source_ref": self.source_ref,
394
+ "subject_person_id": self.subject_person_id,
395
+ "shareability": self.shareability,
396
+ "confidence": self.confidence,
397
+ "fresh_until": self.fresh_until,
398
+ "evidence_refs": list(self.evidence_refs),
399
+ "visibility_digest": self.visibility_digest,
400
+ }
401
+
402
+
403
+ @dataclass(frozen=True, slots=True)
404
+ class FactDenialV1:
405
+ fact_ref: str
406
+ reason: str
407
+
408
+
409
+ @dataclass(frozen=True, slots=True)
410
+ class FactProjectionBatchV1:
411
+ facts: tuple[ProjectedFactV1, ...]
412
+ denied: tuple[FactDenialV1, ...]
413
+ truncated: bool
414
+ viewer_digest: str
415
+ audit_digest: str
416
+
417
+ def public(self) -> dict[str, Any]:
418
+ """Bounded projection; denied fact text and topology are omitted."""
419
+
420
+ return {
421
+ "facts": [fact.public() for fact in self.facts],
422
+ "denied_counts": dict(sorted(Counter(
423
+ denial.reason for denial in self.denied).items())),
424
+ "truncated": self.truncated,
425
+ "viewer_digest": self.viewer_digest,
426
+ "audit_digest": self.audit_digest,
427
+ }
428
+
429
+
430
+ def project_facts(
431
+ candidates: Sequence[FactCandidateV1],
432
+ viewer: ViewerContextV1,
433
+ *,
434
+ now: datetime | str,
435
+ min_confidence: float = 0.0,
436
+ max_facts: int = 24,
437
+ max_total_chars: int = 12_000,
438
+ ) -> FactProjectionBatchV1:
439
+ """Filter, deduplicate, and bound facts before any downstream consumer.
440
+
441
+ Input ordering cannot affect the result. A duplicate opaque fact ref with
442
+ conflicting content is dropped entirely. An empty scoped result remains
443
+ empty; there is no global retry or relevance-based widening.
444
+ """
445
+
446
+ if len(candidates) > MAX_CANDIDATES:
447
+ raise ValueError("fact candidate limit exceeded")
448
+ if not 1 <= int(max_facts) <= MAX_PROJECTED_FACTS:
449
+ raise ValueError("max_facts is outside the bounded projection range")
450
+ if not 1 <= int(max_total_chars) <= MAX_PROJECTED_CHARS:
451
+ raise ValueError(
452
+ "max_total_chars is outside the bounded projection range")
453
+ floor = float(min_confidence)
454
+ if not math.isfinite(floor) or not 0.0 <= floor <= 1.0:
455
+ raise ValueError("min_confidence must be between zero and one")
456
+
457
+ grouped: dict[str, list[FactCandidateV1]] = defaultdict(list)
458
+ for candidate in candidates:
459
+ if not isinstance(candidate, FactCandidateV1):
460
+ raise ValueError("fact candidates must be FactCandidateV1 records")
461
+ grouped[candidate.visibility.fact_ref].append(candidate)
462
+
463
+ def group_key(item: tuple[str, list[FactCandidateV1]]) -> tuple[Any, ...]:
464
+ fact_ref, rows = item
465
+ return (-max(row.visibility.confidence for row in rows), fact_ref)
466
+
467
+ facts: list[ProjectedFactV1] = []
468
+ denied: list[FactDenialV1] = []
469
+ total_chars = 0
470
+ truncated = False
471
+ for fact_ref, rows in sorted(grouped.items(), key=group_key):
472
+ digests = {row.visibility.content_digest for row in rows}
473
+ if len(digests) != 1:
474
+ denied.append(FactDenialV1(
475
+ fact_ref=fact_ref, reason="fact_ref_content_conflict"))
476
+ continue
477
+ allowed: list[tuple[FactCandidateV1, VisibilityDecisionV1]] = []
478
+ decisions: list[VisibilityDecisionV1] = []
479
+ for row in sorted(
480
+ rows,
481
+ key=lambda value: (
482
+ -value.visibility.confidence,
483
+ value.visibility.audit_digest,
484
+ ),
485
+ ):
486
+ decision = row.visibility.decision(
487
+ viewer, now=now, min_confidence=floor)
488
+ decisions.append(decision)
489
+ if decision.allowed:
490
+ allowed.append((row, decision))
491
+ if not allowed:
492
+ reason = decisions[0].reason if decisions else "visibility_unknown"
493
+ denied.append(FactDenialV1(fact_ref=fact_ref, reason=reason))
494
+ continue
495
+ row, decision = allowed[0]
496
+ if len(facts) >= int(max_facts) \
497
+ or total_chars + len(row.content) > int(max_total_chars):
498
+ denied.append(FactDenialV1(
499
+ fact_ref=fact_ref, reason="projection_budget_exhausted"))
500
+ truncated = True
501
+ continue
502
+ visibility = row.visibility
503
+ facts.append(ProjectedFactV1(
504
+ fact_ref=fact_ref,
505
+ content=row.content,
506
+ source_ref=visibility.source_ref,
507
+ subject_person_id=visibility.subject_person_id,
508
+ shareability=visibility.shareability,
509
+ confidence=visibility.confidence,
510
+ fresh_until=visibility.fresh_until,
511
+ evidence_refs=visibility.evidence_refs,
512
+ visibility_digest=decision.visibility_digest,
513
+ ))
514
+ total_chars += len(row.content)
515
+
516
+ payload = {
517
+ "schema_version": SCHEMA_VERSION,
518
+ "viewer_digest": viewer.audit_digest,
519
+ "facts": [
520
+ {
521
+ "fact_ref": fact.fact_ref,
522
+ "content_digest": content_digest(fact.content),
523
+ "visibility_digest": fact.visibility_digest,
524
+ }
525
+ for fact in facts
526
+ ],
527
+ "denied": [
528
+ {"fact_ref": item.fact_ref, "reason": item.reason}
529
+ for item in denied
530
+ ],
531
+ "truncated": truncated,
532
+ "min_confidence": floor,
533
+ "max_facts": int(max_facts),
534
+ "max_total_chars": int(max_total_chars),
535
+ "now": _iso(now, field="now"),
536
+ }
537
+ return FactProjectionBatchV1(
538
+ facts=tuple(facts),
539
+ denied=tuple(denied),
540
+ truncated=truncated,
541
+ viewer_digest=viewer.audit_digest,
542
+ audit_digest=_digest(payload),
543
+ )
544
+
545
+
546
+ __all__ = [
547
+ "FactCandidateV1",
548
+ "FactDenialV1",
549
+ "FactProjectionBatchV1",
550
+ "FactVisibilityV1",
551
+ "ProjectedFactV1",
552
+ "SHAREABILITIES",
553
+ "ViewerContextV1",
554
+ "VisibilityDecisionV1",
555
+ "content_digest",
556
+ "project_facts",
557
+ "validate_visibility_scope",
558
+ "visibility_scope_decision",
559
+ ]