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.
- apsimo/__init__.py +38 -0
- apsimo/__main__.py +6 -0
- apsimo/agent/__init__.py +6 -0
- apsimo/agent/client.py +276 -0
- apsimo/agent/models.py +46 -0
- apsimo/agents/__init__.py +20 -0
- apsimo/agents/models.py +264 -0
- apsimo/agents/store.py +861 -0
- apsimo/agents/websocket.py +522 -0
- apsimo/api/__init__.py +1 -0
- apsimo/api/auth_telemetry.py +287 -0
- apsimo/api/authority.py +1203 -0
- apsimo/api/contact_grants.py +347 -0
- apsimo/api/middleware.py +483 -0
- apsimo/api/routers/__init__.py +1 -0
- apsimo/api/routers/commitment_work.py +265 -0
- apsimo/api/routers/context_gate.py +123 -0
- apsimo/api/routers/executions.py +140 -0
- apsimo/api/routers/followup_plans.py +147 -0
- apsimo/api/routers/governed_actions.py +162 -0
- apsimo/api/routers/host.py +14473 -0
- apsimo/api/routers/initiative_work.py +115 -0
- apsimo/api/routers/mining.py +104 -0
- apsimo/api/routers/observations.py +110 -0
- apsimo/api/routers/social_state.py +225 -0
- apsimo/api/routers/task_queue.py +2715 -0
- apsimo/api/routers/temporal_followups.py +251 -0
- apsimo/api/routers/transport.py +110 -0
- apsimo/api/routers/transport_ingress_api.py +240 -0
- apsimo/api/schemas/__init__.py +1 -0
- apsimo/api/schemas/host.py +1949 -0
- apsimo/autonomy/cli.py +110 -0
- apsimo/autonomy/condition_worker.py +437 -0
- apsimo/autonomy/config.py +424 -0
- apsimo/autonomy/loop.py +4316 -0
- apsimo/autonomy/registry.py +339 -0
- apsimo/autonomy/scheduler.py +1822 -0
- apsimo/autonomy/synthesis.py +449 -0
- apsimo/backup.py +962 -0
- apsimo/beliefs/__init__.py +23 -0
- apsimo/beliefs/contradictions.py +109 -0
- apsimo/beliefs/decay.py +61 -0
- apsimo/beliefs/engine.py +479 -0
- apsimo/beliefs/models.py +67 -0
- apsimo/beliefs/promotion.py +41 -0
- apsimo/beliefs/resolve.py +58 -0
- apsimo/beliefs/source_claims.py +690 -0
- apsimo/beliefs/source_projection.py +883 -0
- apsimo/beliefs/source_time.py +208 -0
- apsimo/beliefs/store.py +133 -0
- apsimo/briefings/aggregators.py +824 -0
- apsimo/briefings/composer.py +420 -0
- apsimo/briefings/config.py +55 -0
- apsimo/briefings/delivery.py +439 -0
- apsimo/briefings/engagement.py +97 -0
- apsimo/briefings/engine.py +274 -0
- apsimo/briefings/enhancer.py +99 -0
- apsimo/briefings/models.py +183 -0
- apsimo/briefings/scheduler.py +382 -0
- apsimo/briefings/store.py +435 -0
- apsimo/chain/__init__.py +48 -0
- apsimo/chain/block.py +100 -0
- apsimo/chain/cli.py +704 -0
- apsimo/chain/genesis.py +443 -0
- apsimo/chain/identity.py +416 -0
- apsimo/chain/keys.py +1025 -0
- apsimo/chain/local_keys.py +187 -0
- apsimo/chain/manager.py +290 -0
- apsimo/chain/node.py +163 -0
- apsimo/chain/plugin_transactions.py +371 -0
- apsimo/chain/protocol.py +220 -0
- apsimo/chain/state_machine.py +676 -0
- apsimo/chain/storage.py +503 -0
- apsimo/chain/transactions.py +250 -0
- apsimo/chain/validation.py +397 -0
- apsimo/channels/__init__.py +1 -0
- apsimo/channels/manifest.py +31 -0
- apsimo/channels/migrations/001_channels_schema.sql +12 -0
- apsimo/channels/phone_gateways.py +42 -0
- apsimo/channels/presence.py +188 -0
- apsimo/channels/router.py +235 -0
- apsimo/channels/store.py +231 -0
- apsimo/cli.py +2688 -0
- apsimo/cognition/__init__.py +11 -0
- apsimo/cognition/charter.py +398 -0
- apsimo/cognition/drive_governance.py +3530 -0
- apsimo/cognition/evidence_pipeline.py +1627 -0
- apsimo/cognition/external_events.py +932 -0
- apsimo/cognition/goal_spine.py +3488 -0
- apsimo/cognition/introspection.py +214 -0
- apsimo/cognition/prompt.py +150 -0
- apsimo/cognition/runtime.py +108 -0
- apsimo/cognition/trigger.py +154 -0
- apsimo/commitments/__init__.py +18 -0
- apsimo/commitments/local_work.py +355 -0
- apsimo/commitments/store.py +1052 -0
- apsimo/commitments/work.py +91 -0
- apsimo/compat.py +53 -0
- apsimo/compression/__init__.py +467 -0
- apsimo/connectors/__init__.py +21 -0
- apsimo/connectors/base.py +152 -0
- apsimo/connectors/caldav_calendar.py +125 -0
- apsimo/connectors/fs_documents.py +85 -0
- apsimo/connectors/imap_email.py +138 -0
- apsimo/connectors/manager.py +218 -0
- apsimo/connectors/webhook_pull.py +88 -0
- apsimo/contacts/__init__.py +33 -0
- apsimo/contacts/comms.py +357 -0
- apsimo/contacts/config.py +79 -0
- apsimo/contacts/exporters/__init__.py +1 -0
- apsimo/contacts/exporters/vcard.py +71 -0
- apsimo/contacts/identity_links.py +251 -0
- apsimo/contacts/importer.py +280 -0
- apsimo/contacts/importers/__init__.py +1 -0
- apsimo/contacts/importers/batch.py +43 -0
- apsimo/contacts/importers/macos_contacts.py +101 -0
- apsimo/contacts/migrations/001_contacts_schema.sql +141 -0
- apsimo/contacts/migrations/002_trust_scopes.sql +36 -0
- apsimo/contacts/migrations/003_open_gateway_enum.sql +32 -0
- apsimo/contacts/migrations/004_contact_provision_operations.sql +18 -0
- apsimo/contacts/migrations/005_identity_links.sql +27 -0
- apsimo/contacts/models.py +308 -0
- apsimo/contacts/scoring.py +16 -0
- apsimo/contacts/store.py +1623 -0
- apsimo/contacts/transport_ingress.py +252 -0
- apsimo/contacts/world_bridge.py +314 -0
- apsimo/contextgate/__init__.py +69 -0
- apsimo/contextgate/chunker.py +169 -0
- apsimo/contextgate/estimate.py +54 -0
- apsimo/contextgate/gate.py +313 -0
- apsimo/contextgate/retrieve.py +115 -0
- apsimo/delivery/__init__.py +16 -0
- apsimo/delivery/bridge.py +1260 -0
- apsimo/delivery/channels.py +526 -0
- apsimo/delivery/classification.py +50 -0
- apsimo/delivery/rate_limiter.py +268 -0
- apsimo/delivery/reachout_policy.py +206 -0
- apsimo/directed/__init__.py +22 -0
- apsimo/directed/audit.py +167 -0
- apsimo/directed/intake.py +95 -0
- apsimo/directed/models.py +191 -0
- apsimo/directed/service.py +509 -0
- apsimo/directives/__init__.py +25 -0
- apsimo/directives/evidence.py +87 -0
- apsimo/directives/extractor.py +188 -0
- apsimo/directives/guard.py +364 -0
- apsimo/directives/models.py +206 -0
- apsimo/directives/service.py +372 -0
- apsimo/directives/store.py +167 -0
- apsimo/doctor.py +2173 -0
- apsimo/environment.py +43 -0
- apsimo/events/__init__.py +33 -0
- apsimo/events/broadcaster.py +98 -0
- apsimo/events/bus.py +217 -0
- apsimo/events/journal.py +863 -0
- apsimo/events/stream.py +131 -0
- apsimo/events/types.py +150 -0
- apsimo/execution_results.py +357 -0
- apsimo/feedback/__init__.py +5 -0
- apsimo/feedback/store.py +76 -0
- apsimo/feeds/__init__.py +19 -0
- apsimo/feeds/cli.py +84 -0
- apsimo/feeds/engine.py +437 -0
- apsimo/feeds/example-feed.yaml +77 -0
- apsimo/feeds/hermes_cron.py +126 -0
- apsimo/feeds/manager.py +235 -0
- apsimo/feeds/spec.py +250 -0
- apsimo/feeds/template.py +202 -0
- apsimo/gate/__init__.py +18 -0
- apsimo/gate/audit.py +61 -0
- apsimo/gate/communication_policy.py +166 -0
- apsimo/gate/config.py +72 -0
- apsimo/gate/context_provenance.py +170 -0
- apsimo/gate/env_risk.py +226 -0
- apsimo/gate/guard_audit.py +353 -0
- apsimo/gate/layers/__init__.py +1 -0
- apsimo/gate/layers/base.py +15 -0
- apsimo/gate/layers/l1_recipient.py +66 -0
- apsimo/gate/layers/l2_pii.py +134 -0
- apsimo/gate/layers/l3_cross_context.py +50 -0
- apsimo/gate/layers/l4_trust_tier.py +78 -0
- apsimo/gate/layers/l5_injection.py +199 -0
- apsimo/gate/layers/l6_review.py +86 -0
- apsimo/gate/layers/l7_delay.py +100 -0
- apsimo/gate/layers/tom2_epistemic.py +185 -0
- apsimo/gate/models.py +64 -0
- apsimo/gate/pending_dispatch.py +5 -0
- apsimo/gate/pipeline.py +206 -0
- apsimo/gate/rejection.py +259 -0
- apsimo/gate/response_guard.py +700 -0
- apsimo/gate/rulesets/injection_v1.yaml +51 -0
- apsimo/gate/surface_policy.py +189 -0
- apsimo/gate/taint.py +226 -0
- apsimo/genesis.json +9 -0
- apsimo/goals/__init__.py +100 -0
- apsimo/goals/config.py +38 -0
- apsimo/goals/decomposer.py +421 -0
- apsimo/goals/engine.py +617 -0
- apsimo/goals/inference.py +354 -0
- apsimo/goals/models.py +302 -0
- apsimo/goals/priority.py +270 -0
- apsimo/goals/queue_bridge.py +149 -0
- apsimo/goals/replan.py +450 -0
- apsimo/goals/schema.sql +89 -0
- apsimo/goals/store.py +692 -0
- apsimo/governed_actions.py +1708 -0
- apsimo/harness_integration/__init__.py +45 -0
- apsimo/harness_integration/context.py +41 -0
- apsimo/harness_integration/skills.py +231 -0
- apsimo/identity/__init__.py +26 -0
- apsimo/identity/participants.py +181 -0
- apsimo/identity/resolver.py +329 -0
- apsimo/identity_bootstrap/__init__.py +5 -0
- apsimo/identity_bootstrap/builder.py +208 -0
- apsimo/identity_bootstrap/corpus.py +443 -0
- apsimo/identity_bootstrap/models.py +54 -0
- apsimo/identity_bootstrap/runner.py +353 -0
- apsimo/identity_bootstrap/seeders/__init__.py +25 -0
- apsimo/identity_bootstrap/seeders/briefings.py +109 -0
- apsimo/identity_bootstrap/seeders/chain.py +57 -0
- apsimo/identity_bootstrap/seeders/goals.py +128 -0
- apsimo/identity_bootstrap/seeders/memory.py +191 -0
- apsimo/identity_bootstrap/seeders/neo4j_cognition.py +79 -0
- apsimo/identity_bootstrap/seeders/relationship.py +152 -0
- apsimo/identity_bootstrap/seeders/sessions.py +67 -0
- apsimo/identity_bootstrap/seeders/skills.py +92 -0
- apsimo/identity_bootstrap/seeders/task_queue.py +72 -0
- apsimo/identity_bootstrap/seeders/world_model.py +143 -0
- apsimo/identity_bootstrap/self_query.py +92 -0
- apsimo/identity_bootstrap/self_reflection.py +155 -0
- apsimo/identity_bootstrap/skill.py +37 -0
- apsimo/identity_bootstrap/verifier.py +436 -0
- apsimo/initiatives/__init__.py +20 -0
- apsimo/initiatives/action_registry.py +454 -0
- apsimo/initiatives/approval_authority.py +2105 -0
- apsimo/initiatives/approval_policy.py +123 -0
- apsimo/initiatives/assignment.py +263 -0
- apsimo/initiatives/backup_evidence.py +100 -0
- apsimo/initiatives/context_freshness.py +103 -0
- apsimo/initiatives/models.py +318 -0
- apsimo/initiatives/native_work.py +270 -0
- apsimo/initiatives/standing_approvals.py +232 -0
- apsimo/initiatives/store.py +1081 -0
- apsimo/initiatives/temporal_followup.py +410 -0
- apsimo/intelligence/__init__.py +1 -0
- apsimo/intelligence/cognition/__init__.py +24 -0
- apsimo/intelligence/cognition/gap_detector.py +148 -0
- apsimo/intelligence/cognition/metalearner.py +547 -0
- apsimo/intelligence/cognition/metrics_collector.py +217 -0
- apsimo/intelligence/cognition/performance_index.py +299 -0
- apsimo/intelligence/cognition/registry.py +192 -0
- apsimo/intelligence/cognition/strategy_adjuster.py +222 -0
- apsimo/intelligence/cognition/types.py +16 -0
- apsimo/intelligence/components/__init__.py +66 -0
- apsimo/intelligence/components/anomaly_detector.py +413 -0
- apsimo/intelligence/components/initiative_engine.py +2643 -0
- apsimo/intelligence/components/preference_learner.py +521 -0
- apsimo/intelligence/components/research_orchestrator.py +358 -0
- apsimo/intelligence/components/self_directed_thinker.py +221 -0
- apsimo/intelligence/components/self_reflector.py +252 -0
- apsimo/intelligence/components/session_continuity.py +154 -0
- apsimo/intelligence/components/task_planner.py +320 -0
- apsimo/intelligence/components/tool_learner.py +217 -0
- apsimo/intelligence/graph/__init__.py +79 -0
- apsimo/intelligence/graph/client.py +2483 -0
- apsimo/intelligence/graph/consolidator.py +405 -0
- apsimo/intelligence/graph/distiller.py +312 -0
- apsimo/intelligence/graph/migrations.py +129 -0
- apsimo/intelligence/graph/queries.py +248 -0
- apsimo/intelligence/graph/recall.py +281 -0
- apsimo/intelligence/graph/reconciler.py +144 -0
- apsimo/intelligence/graph/schema.py +337 -0
- apsimo/intelligence/graph/selection.py +252 -0
- apsimo/intelligence/learning/__init__.py +17 -0
- apsimo/intelligence/learning/continuous_learner.py +245 -0
- apsimo/intelligence/learning/feedback_store.py +321 -0
- apsimo/intelligence/mind_model/__init__.py +1 -0
- apsimo/intelligence/mind_model/graph_baseline.py +136 -0
- apsimo/intelligence/mind_model/signal_collector.py +361 -0
- apsimo/intelligence/relationships/__init__.py +11 -0
- apsimo/intelligence/relationships/profiler.py +389 -0
- apsimo/intelligence/relationships/scorer.py +560 -0
- apsimo/intelligence/relationships/signal_floor.py +66 -0
- apsimo/intelligence/relationships/trust_tiers.py +300 -0
- apsimo/intelligence/synthesis/__init__.py +40 -0
- apsimo/intelligence/synthesis/connection_discoverer.py +379 -0
- apsimo/intelligence/synthesis/cross_domain_analyzer.py +287 -0
- apsimo/intelligence/synthesis/insight_deliverer.py +171 -0
- apsimo/intelligence/synthesis/insight_store.py +79 -0
- apsimo/intelligence/synthesis/insight_validator.py +183 -0
- apsimo/intelligence/synthesis/novelty_scorer.py +267 -0
- apsimo/intelligence/turn_middleware/__init__.py +15 -0
- apsimo/intelligence/turn_middleware/memory_sync.py +119 -0
- apsimo/mcp/__init__.py +41 -0
- apsimo/mcp/__main__.py +6 -0
- apsimo/mcp/config.py +287 -0
- apsimo/mcp/server.py +501 -0
- apsimo/migrations.py +187 -0
- apsimo/mining/__init__.py +27 -0
- apsimo/mining/corpus.py +239 -0
- apsimo/mining/escalations.py +289 -0
- apsimo/mining/models.py +169 -0
- apsimo/mining/store.py +210 -0
- apsimo/models/__init__.py +30 -0
- apsimo/models/memory.py +80 -0
- apsimo/models/mesh.py +72 -0
- apsimo/models/person.py +104 -0
- apsimo/models/signal.py +108 -0
- apsimo/observations/__init__.py +15 -0
- apsimo/observations/store.py +277 -0
- apsimo/patterns/__init__.py +6 -0
- apsimo/patterns/extract.py +187 -0
- apsimo/patterns/store.py +227 -0
- apsimo/persona/__init__.py +1 -0
- apsimo/persona/engine.py +611 -0
- apsimo/persona/manifest.py +140 -0
- apsimo/projects/__init__.py +28 -0
- apsimo/projects/engine.py +1681 -0
- apsimo/projects/event_outbox.py +188 -0
- apsimo/projects/models.py +216 -0
- apsimo/projects/planner.py +181 -0
- apsimo/projects/store.py +1446 -0
- apsimo/proposals/__init__.py +12 -0
- apsimo/proposals/engine.py +114 -0
- apsimo/proposals/models.py +207 -0
- apsimo/qualification/__init__.py +1 -0
- apsimo/qualification/cases.py +75 -0
- apsimo/qualification/cli.py +51 -0
- apsimo/qualification/memory_cases.py +209 -0
- apsimo/qualification/records.py +92 -0
- apsimo/qualification/report.py +87 -0
- apsimo/qualification/runner.py +311 -0
- apsimo/qualification/structured_cases.py +131 -0
- apsimo/reasoning/__init__.py +13 -0
- apsimo/reasoning/executor.py +506 -0
- apsimo/reasoning/loop.py +373 -0
- apsimo/reasoning/native_tools/__init__.py +16 -0
- apsimo/reasoning/native_tools/calculate.py +141 -0
- apsimo/reasoning/native_tools/file_ops.py +150 -0
- apsimo/reasoning/native_tools/web_search.py +49 -0
- apsimo/reasoning/tool_policy.py +182 -0
- apsimo/redact/__init__.py +176 -0
- apsimo/repos/__init__.py +5 -0
- apsimo/repos/mirrors.py +204 -0
- apsimo/research/__init__.py +41 -0
- apsimo/research/artifact.py +482 -0
- apsimo/research/gatherer.py +387 -0
- apsimo/research/pipeline.py +513 -0
- apsimo/research/search/__init__.py +7 -0
- apsimo/research/search/base.py +41 -0
- apsimo/research/search/brave.py +59 -0
- apsimo/research/search/cache.py +51 -0
- apsimo/research/search/duckduckgo.py +103 -0
- apsimo/research/search/orchestrator.py +119 -0
- apsimo/research/search/serpapi.py +59 -0
- apsimo/research/search/tavily.py +59 -0
- apsimo/research/synthesizer.py +309 -0
- apsimo/router/__init__.py +30 -0
- apsimo/router/complexity_scorer.py +148 -0
- apsimo/router/endpoints.py +153 -0
- apsimo/router/fallback.py +58 -0
- apsimo/router/functions.py +243 -0
- apsimo/router/native_policy.py +52 -0
- apsimo/router/router.py +762 -0
- apsimo/router/self_learning.py +174 -0
- apsimo/router/tiers.py +677 -0
- apsimo/sandbox/__init__.py +21 -0
- apsimo/sandbox/backend.py +195 -0
- apsimo/sandbox/manager.py +173 -0
- apsimo/scope_bounds.py +7 -0
- apsimo/secrets/__init__.py +6 -0
- apsimo/secrets/backends/__init__.py +8 -0
- apsimo/secrets/backends/base.py +42 -0
- apsimo/secrets/backends/env.py +110 -0
- apsimo/secrets/backends/keyring.py +72 -0
- apsimo/secrets/backends/onepassword.py +232 -0
- apsimo/secrets/cli.py +191 -0
- apsimo/secrets/manager.py +160 -0
- apsimo/secrets/migration.py +101 -0
- apsimo/secrets/types.py +98 -0
- apsimo/seed.py +41 -0
- apsimo/self_model/__init__.py +37 -0
- apsimo/self_model/appraisals.py +673 -0
- apsimo/self_model/benchmark.py +1314 -0
- apsimo/self_model/brief.py +40 -0
- apsimo/self_model/event_concerns.py +1128 -0
- apsimo/self_model/execution_forecasts.py +353 -0
- apsimo/self_model/expectations.py +1595 -0
- apsimo/self_model/experiments.py +1150 -0
- apsimo/self_model/journal.py +148 -0
- apsimo/self_model/judgments.py +705 -0
- apsimo/self_model/native_outcomes.py +55 -0
- apsimo/self_model/params.py +220 -0
- apsimo/self_model/perspective.py +246 -0
- apsimo/self_model/reconcile.py +183 -0
- apsimo/self_model/reply_forecasts.py +381 -0
- apsimo/self_model/runtime_forecasts.py +296 -0
- apsimo/self_model/runtime_models.py +67 -0
- apsimo/self_model/settlement.py +207 -0
- apsimo/self_model/situation.py +1731 -0
- apsimo/self_model/store.py +883 -0
- apsimo/self_model/supervised.py +137 -0
- apsimo/self_model/thinker.py +99 -0
- apsimo/self_model/trust.py +388 -0
- apsimo/self_model/workspace.py +2388 -0
- apsimo/server.py +4197 -0
- apsimo/services/__init__.py +1 -0
- apsimo/services/agent_bridge.py +474 -0
- apsimo/services/initiative_executor.py +914 -0
- apsimo/services/instance.py +297 -0
- apsimo/sessions/__init__.py +22 -0
- apsimo/sessions/config.py +13 -0
- apsimo/sessions/context_loader.py +88 -0
- apsimo/sessions/federation_session.py +75 -0
- apsimo/sessions/isolated_session.py +98 -0
- apsimo/sessions/reports.py +84 -0
- apsimo/sessions/store.py +148 -0
- apsimo/setup.py +2818 -0
- apsimo/setup_hermes.py +879 -0
- apsimo/setup_local_work.py +218 -0
- apsimo/setup_native_goals.py +134 -0
- apsimo/setup_native_reviews.py +115 -0
- apsimo/skills/__init__.py +10 -0
- apsimo/skills/base.py +108 -0
- apsimo/skills/budget.py +28 -0
- apsimo/skills/executor.py +493 -0
- apsimo/skills/executors/__init__.py +1 -0
- apsimo/skills/executors/behavioral_correction.py +75 -0
- apsimo/skills/executors/capability_gap.py +38 -0
- apsimo/skills/executors/data_quality.py +163 -0
- apsimo/skills/executors/knowledge_acquisition.py +41 -0
- apsimo/skills/executors/operational_hygiene.py +185 -0
- apsimo/skills/executors/subsystem_health.py +169 -0
- apsimo/skills/hermes_export.py +431 -0
- apsimo/skills/index.py +123 -0
- apsimo/skills/learning/__init__.py +21 -0
- apsimo/skills/learning/novelty_detector.py +206 -0
- apsimo/skills/learning/pattern_extractor.py +199 -0
- apsimo/skills/learning/triggers.py +159 -0
- apsimo/skills/loader.py +246 -0
- apsimo/skills/migrations/002_progressive_loading.sql +6 -0
- apsimo/skills/migrations/backfill_triggers.py +20 -0
- apsimo/skills/models.py +202 -0
- apsimo/skills/packager.py +128 -0
- apsimo/skills/protocols.py +70 -0
- apsimo/skills/registry.py +191 -0
- apsimo/skills/runtime.py +58 -0
- apsimo/skills/sandbox_runner.py +229 -0
- apsimo/skills/scheduler.py +129 -0
- apsimo/skills/schema.py +79 -0
- apsimo/skills/security/__init__.py +12 -0
- apsimo/skills/security/guards.py +53 -0
- apsimo/skills/security/scanner.py +223 -0
- apsimo/skills_memory/__init__.py +26 -0
- apsimo/skills_memory/distill.py +159 -0
- apsimo/skills_memory/models.py +85 -0
- apsimo/skills_memory/retrieve.py +62 -0
- apsimo/skills_memory/store.py +172 -0
- apsimo/surprise/__init__.py +6 -0
- apsimo/surprise/accumulation.py +57 -0
- apsimo/surprise/scorer.py +102 -0
- apsimo/surprise/store.py +203 -0
- apsimo/task_queue/__init__.py +69 -0
- apsimo/task_queue/action_receipts.py +148 -0
- apsimo/task_queue/approval_relay_canary.py +108 -0
- apsimo/task_queue/config.py +85 -0
- apsimo/task_queue/contract.py +361 -0
- apsimo/task_queue/events.py +130 -0
- apsimo/task_queue/governor.py +1031 -0
- apsimo/task_queue/handlers/__init__.py +16 -0
- apsimo/task_queue/handlers/base.py +37 -0
- apsimo/task_queue/handlers/inference.py +640 -0
- apsimo/task_queue/handlers/monitoring.py +116 -0
- apsimo/task_queue/handlers/registry.py +75 -0
- apsimo/task_queue/handlers/subtask_handler.py +173 -0
- apsimo/task_queue/handlers/system_maintenance.py +147 -0
- apsimo/task_queue/mesh_integration.py +111 -0
- apsimo/task_queue/models.py +317 -0
- apsimo/task_queue/queue_manager.py +8286 -0
- apsimo/task_queue/routing.py +287 -0
- apsimo/task_queue/scheduler.py +252 -0
- apsimo/task_queue/schema.sql +197 -0
- apsimo/task_queue/work_control.py +342 -0
- apsimo/task_queue/worker.py +993 -0
- apsimo/telemetry.py +145 -0
- apsimo/tom/__init__.py +6 -0
- apsimo/tom/affect.py +387 -0
- apsimo/tom/approvals.py +171 -0
- apsimo/tom/arcs.py +896 -0
- apsimo/tom/asymmetry.py +131 -0
- apsimo/tom/eligibility.py +248 -0
- apsimo/tom/engagement.py +214 -0
- apsimo/tom/exposure.py +214 -0
- apsimo/tom/extractor.py +306 -0
- apsimo/tom/fact_adapters.py +144 -0
- apsimo/tom/facts.py +326 -0
- apsimo/tom/integration.py +592 -0
- apsimo/tom/leveled.py +118 -0
- apsimo/tom/levels.py +247 -0
- apsimo/tom/recipient_audit.py +995 -0
- apsimo/tom/recipient_simulator.py +593 -0
- apsimo/tom/source_lineage.py +93 -0
- apsimo/tom/tom2.py +277 -0
- apsimo/tom/visibility.py +559 -0
- apsimo/tom/visibility_store.py +414 -0
- apsimo/tools/__init__.py +0 -0
- apsimo/tools/definitions.py +740 -0
- apsimo/tools/handlers.py +943 -0
- apsimo/toolsmith/__init__.py +26 -0
- apsimo/toolsmith/authority.py +166 -0
- apsimo/toolsmith/engine.py +559 -0
- apsimo/toolsmith/integrity.py +100 -0
- apsimo/toolsmith/miner.py +145 -0
- apsimo/toolsmith/policy.py +110 -0
- apsimo/toolsmith/registry.py +635 -0
- apsimo/turns/__init__.py +17 -0
- apsimo/turns/audio.py +134 -0
- apsimo/turns/documents.py +235 -0
- apsimo/turns/executions.py +486 -0
- apsimo/turns/hermes_history.py +245 -0
- apsimo/turns/hermes_kanban.py +268 -0
- apsimo/turns/hermes_work.py +96 -0
- apsimo/turns/idempotency.py +752 -0
- apsimo/turns/local_work.py +115 -0
- apsimo/turns/media.py +581 -0
- apsimo/turns/reported_workers.py +196 -0
- apsimo/turns/source_annotations.py +283 -0
- apsimo/turns/source_attribution.py +154 -0
- apsimo/turns/source_read.py +351 -0
- apsimo/turns/source_vectors.py +263 -0
- apsimo/turns/video.py +210 -0
- apsimo/util/autonomy_preset.py +220 -0
- apsimo/util/instance.py +92 -0
- apsimo/util/model_output.py +25 -0
- apsimo/util/quiet_hours.py +27 -0
- apsimo/util/session_safety.py +37 -0
- apsimo/util/temporal.py +343 -0
- apsimo/vector/__init__.py +75 -0
- apsimo/vector/backfill.py +171 -0
- apsimo/vector/caption.py +114 -0
- apsimo/vector/collections.py +51 -0
- apsimo/vector/config.py +102 -0
- apsimo/vector/embedder.py +670 -0
- apsimo/vector/image_preprocess.py +406 -0
- apsimo/vector/image_store.py +296 -0
- apsimo/vector/indexes.py +162 -0
- apsimo/vector/migrate.py +334 -0
- apsimo/vector/multimodal_provider.py +417 -0
- apsimo/vector/multimodal_types.py +87 -0
- apsimo/vector/openai_provider.py +119 -0
- apsimo/vector/query.py +49 -0
- apsimo/vector/reranker.py +565 -0
- apsimo/vector/safety_image.py +159 -0
- apsimo/vector/scanner.py +197 -0
- apsimo/vector/setup.py +289 -0
- apsimo/vector/store.py +533 -0
- apsimo/vector/tiers.py +263 -0
- apsimo/work_orders.py +925 -0
- apsimo/workers/__init__.py +21 -0
- apsimo/workers/agent_bridge.py +640 -0
- apsimo/workers/colony_worker.py +382 -0
- apsimo/workers/queue_worker.py +441 -0
- apsimo/workers/skills_sync.py +152 -0
- apsimo/world_model/__init__.py +71 -0
- apsimo/world_model/causal_maintenance.py +131 -0
- apsimo/world_model/causal_policy.py +43 -0
- apsimo/world_model/causal_query.py +125 -0
- apsimo/world_model/confidence.py +54 -0
- apsimo/world_model/config.py +64 -0
- apsimo/world_model/constants.py +97 -0
- apsimo/world_model/entities.py +145 -0
- apsimo/world_model/expectation_resolvers.py +177 -0
- apsimo/world_model/extraction/__init__.py +7 -0
- apsimo/world_model/extraction/base.py +62 -0
- apsimo/world_model/extraction/conversation_extractor.py +262 -0
- apsimo/world_model/extraction/detector.py +74 -0
- apsimo/world_model/extraction/document_extractor.py +78 -0
- apsimo/world_model/extraction/formats/__init__.py +24 -0
- apsimo/world_model/extraction/formats/csv_fmt.py +68 -0
- apsimo/world_model/extraction/formats/html_fmt.py +72 -0
- apsimo/world_model/extraction/formats/json_fmt.py +68 -0
- apsimo/world_model/extraction/formats/pdf.py +43 -0
- apsimo/world_model/extraction/formats/text.py +27 -0
- apsimo/world_model/extraction/llm_extractor.py +164 -0
- apsimo/world_model/extraction/pipeline.py +73 -0
- apsimo/world_model/integrations/__init__.py +5 -0
- apsimo/world_model/integrations/mind_model_bridge.py +115 -0
- apsimo/world_model/integrations/social_intel_bridge.py +120 -0
- apsimo/world_model/jobs/__init__.py +4 -0
- apsimo/world_model/jobs/extraction_job.py +168 -0
- apsimo/world_model/llm_extract.py +572 -0
- apsimo/world_model/neo4j/__init__.py +5 -0
- apsimo/world_model/neo4j/backend.py +654 -0
- apsimo/world_model/observations.py +155 -0
- apsimo/world_model/populator.py +307 -0
- apsimo/world_model/postgres/__init__.py +1 -0
- apsimo/world_model/postgres/backend.py +683 -0
- apsimo/world_model/relationships.py +25 -0
- apsimo/world_model/resolution/__init__.py +13 -0
- apsimo/world_model/resolution/entity_resolver.py +232 -0
- apsimo/world_model/resolution/merge_audit.py +16 -0
- apsimo/world_model/resolution/merge_workflow.py +117 -0
- apsimo/world_model/source_reports.py +121 -0
- apsimo/world_model/sqlite/__init__.py +4 -0
- apsimo/world_model/sqlite/backend.py +855 -0
- apsimo/world_model/sqlite/schema.sql +132 -0
- apsimo/world_model/store.py +545 -0
- apsimo-1.3.0.dist-info/METADATA +78 -0
- apsimo-1.3.0.dist-info/RECORD +614 -0
- apsimo-1.3.0.dist-info/WHEEL +5 -0
- apsimo-1.3.0.dist-info/entry_points.txt +11 -0
- apsimo-1.3.0.dist-info/licenses/LICENSE +21 -0
- apsimo-1.3.0.dist-info/top_level.txt +2 -0
- colony_sidecar/__init__.py +4 -0
apsimo/tom/arcs.py
ADDED
|
@@ -0,0 +1,896 @@
|
|
|
1
|
+
"""P8 append-only conversational arcs.
|
|
2
|
+
|
|
3
|
+
An arc is not a mutable summary row. It is a deterministic projection of an
|
|
4
|
+
append-only event ledger linking source turns, people, commitments,
|
|
5
|
+
expectations, and projects. Deadlines can make an arc overdue, but only an
|
|
6
|
+
explicit terminal event with evidence can close it.
|
|
7
|
+
|
|
8
|
+
This core has no model calls and no startup or delivery integration. Future
|
|
9
|
+
producers must supply stable event/idempotency identities and server-derived
|
|
10
|
+
scope; future readers must project through an attested ``ViewerContextV1``.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from collections import Counter
|
|
16
|
+
from dataclasses import dataclass, replace
|
|
17
|
+
from datetime import datetime, timezone
|
|
18
|
+
import hashlib
|
|
19
|
+
import json
|
|
20
|
+
import os
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
import re
|
|
23
|
+
import sqlite3
|
|
24
|
+
import threading
|
|
25
|
+
from typing import Any, Iterable, Optional, Sequence
|
|
26
|
+
|
|
27
|
+
from apsimo.tom.visibility import (
|
|
28
|
+
ViewerContextV1,
|
|
29
|
+
validate_visibility_scope,
|
|
30
|
+
visibility_scope_decision,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
SCHEMA_VERSION = 1
|
|
35
|
+
ARC_TYPES = frozenset({
|
|
36
|
+
"promise",
|
|
37
|
+
"open_question",
|
|
38
|
+
"stress_topic",
|
|
39
|
+
"decision",
|
|
40
|
+
"shared_plan",
|
|
41
|
+
"follow_up",
|
|
42
|
+
"unresolved_social_moment",
|
|
43
|
+
})
|
|
44
|
+
EVENT_KINDS = frozenset({
|
|
45
|
+
"open", "link", "hold", "resume", "close", "cancel", "reopen",
|
|
46
|
+
})
|
|
47
|
+
ACTIVE_STATES = frozenset({"open", "held"})
|
|
48
|
+
TERMINAL_STATES = frozenset({"closed", "cancelled"})
|
|
49
|
+
|
|
50
|
+
MAX_PEOPLE = 16
|
|
51
|
+
MAX_REFS_PER_KIND = 64
|
|
52
|
+
MAX_TOPIC_CHARS = 500
|
|
53
|
+
MAX_CLOSURE_REASON_CHARS = 500
|
|
54
|
+
MAX_EVENTS_PER_ARC = 1_024
|
|
55
|
+
MAX_PROJECTED_ARCS = 64
|
|
56
|
+
MAX_PROJECTED_TOPIC_CHARS = 12_000
|
|
57
|
+
MAX_ARCS_SCANNED = 2_000
|
|
58
|
+
|
|
59
|
+
_REF_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9_.:@/+\-]{0,191}$")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class ArcConflictError(ValueError):
|
|
63
|
+
"""An event replay changed content or reused another idempotency key."""
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _canonical(value: Any) -> str:
|
|
67
|
+
return json.dumps(
|
|
68
|
+
value,
|
|
69
|
+
ensure_ascii=True,
|
|
70
|
+
separators=(",", ":"),
|
|
71
|
+
sort_keys=True,
|
|
72
|
+
default=str,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _digest(value: Any) -> str:
|
|
77
|
+
return hashlib.sha256(_canonical(value).encode("utf-8")).hexdigest()
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _ref(value: Any, *, field: str, allow_empty: bool = False) -> str:
|
|
81
|
+
normalized = str(value or "").strip()
|
|
82
|
+
if allow_empty and not normalized:
|
|
83
|
+
return ""
|
|
84
|
+
if not _REF_RE.fullmatch(normalized):
|
|
85
|
+
raise ValueError(f"{field} is not a bounded opaque reference")
|
|
86
|
+
return normalized
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _refs(values: Iterable[Any], *, field: str,
|
|
90
|
+
maximum: int = MAX_REFS_PER_KIND) -> tuple[str, ...]:
|
|
91
|
+
normalized = tuple(sorted(dict.fromkeys(
|
|
92
|
+
_ref(value, field=field) for value in values
|
|
93
|
+
)))
|
|
94
|
+
if len(normalized) > maximum:
|
|
95
|
+
raise ValueError(f"{field} exceeds bounded reference limit")
|
|
96
|
+
return normalized
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _as_utc(value: datetime | str, *, field: str,
|
|
100
|
+
allow_empty: bool = False) -> Optional[datetime]:
|
|
101
|
+
if allow_empty and not value:
|
|
102
|
+
return None
|
|
103
|
+
if isinstance(value, datetime):
|
|
104
|
+
parsed = value
|
|
105
|
+
else:
|
|
106
|
+
try:
|
|
107
|
+
parsed = datetime.fromisoformat(str(value).replace("Z", "+00:00"))
|
|
108
|
+
except (TypeError, ValueError) as exc:
|
|
109
|
+
raise ValueError(f"{field} must be an ISO-8601 timestamp") from exc
|
|
110
|
+
if parsed.tzinfo is None:
|
|
111
|
+
raise ValueError(f"{field} must include a timezone")
|
|
112
|
+
return parsed.astimezone(timezone.utc)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _iso(value: datetime | str, *, field: str,
|
|
116
|
+
allow_empty: bool = False) -> str:
|
|
117
|
+
parsed = _as_utc(value, field=field, allow_empty=allow_empty)
|
|
118
|
+
return parsed.isoformat() if parsed is not None else ""
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _topic(value: Any) -> str:
|
|
122
|
+
normalized = " ".join(str(value or "").split())
|
|
123
|
+
if not normalized or len(normalized) > MAX_TOPIC_CHARS:
|
|
124
|
+
raise ValueError("arc topic is required and bounded")
|
|
125
|
+
return normalized
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@dataclass(frozen=True, slots=True)
|
|
129
|
+
class ArcEventV1:
|
|
130
|
+
event_id: str
|
|
131
|
+
idempotency_key: str
|
|
132
|
+
arc_id: str
|
|
133
|
+
event_kind: str
|
|
134
|
+
source_ref: str
|
|
135
|
+
occurred_at: str
|
|
136
|
+
subject_person_id: str
|
|
137
|
+
arc_type: str = ""
|
|
138
|
+
topic: str = ""
|
|
139
|
+
people: tuple[str, ...] = ()
|
|
140
|
+
turn_refs: tuple[str, ...] = ()
|
|
141
|
+
commitment_refs: tuple[str, ...] = ()
|
|
142
|
+
expectation_refs: tuple[str, ...] = ()
|
|
143
|
+
project_refs: tuple[str, ...] = ()
|
|
144
|
+
evidence_refs: tuple[str, ...] = ()
|
|
145
|
+
viewer_scope: str = ""
|
|
146
|
+
shareability: str = ""
|
|
147
|
+
due_at: str = ""
|
|
148
|
+
next_check_at: str = ""
|
|
149
|
+
closure_reason: str = ""
|
|
150
|
+
sequence: int = 0
|
|
151
|
+
schema_version: int = SCHEMA_VERSION
|
|
152
|
+
|
|
153
|
+
def __post_init__(self) -> None:
|
|
154
|
+
if self.schema_version != SCHEMA_VERSION:
|
|
155
|
+
raise ValueError("unsupported arc event version")
|
|
156
|
+
event_id = _ref(self.event_id, field="event_id")
|
|
157
|
+
idempotency = _ref(self.idempotency_key, field="idempotency_key")
|
|
158
|
+
arc_id = _ref(self.arc_id, field="arc_id")
|
|
159
|
+
source_ref = _ref(self.source_ref, field="source_ref")
|
|
160
|
+
kind = str(self.event_kind or "").strip().lower()
|
|
161
|
+
if kind not in EVENT_KINDS:
|
|
162
|
+
raise ValueError("unknown arc event kind")
|
|
163
|
+
occurred_at = _iso(self.occurred_at, field="occurred_at")
|
|
164
|
+
people = _refs(self.people, field="person", maximum=MAX_PEOPLE)
|
|
165
|
+
turn_refs = _refs(self.turn_refs, field="turn_ref")
|
|
166
|
+
commitment_refs = _refs(
|
|
167
|
+
self.commitment_refs, field="commitment_ref")
|
|
168
|
+
expectation_refs = _refs(
|
|
169
|
+
self.expectation_refs, field="expectation_ref")
|
|
170
|
+
project_refs = _refs(self.project_refs, field="project_ref")
|
|
171
|
+
evidence_refs = _refs(self.evidence_refs, field="evidence_ref")
|
|
172
|
+
due_at = _iso(self.due_at, field="due_at", allow_empty=True)
|
|
173
|
+
next_check_at = _iso(
|
|
174
|
+
self.next_check_at, field="next_check_at", allow_empty=True)
|
|
175
|
+
sequence = int(self.sequence)
|
|
176
|
+
if sequence < 0:
|
|
177
|
+
raise ValueError("arc event sequence cannot be negative")
|
|
178
|
+
|
|
179
|
+
arc_type = str(self.arc_type or "").strip().lower()
|
|
180
|
+
topic = ""
|
|
181
|
+
viewer_scope, shareability, subject = validate_visibility_scope(
|
|
182
|
+
viewer_scope=str(self.viewer_scope or "").strip(),
|
|
183
|
+
shareability=str(self.shareability or "").strip().lower(),
|
|
184
|
+
subject_person_id=self.subject_person_id,
|
|
185
|
+
)
|
|
186
|
+
closure_reason = " ".join(str(self.closure_reason or "").split())
|
|
187
|
+
|
|
188
|
+
if kind == "open":
|
|
189
|
+
if arc_type not in ARC_TYPES:
|
|
190
|
+
raise ValueError("unknown conversational arc type")
|
|
191
|
+
topic = _topic(self.topic)
|
|
192
|
+
if not people:
|
|
193
|
+
raise ValueError("open arc requires at least one person")
|
|
194
|
+
if not turn_refs:
|
|
195
|
+
raise ValueError("open arc requires a source turn")
|
|
196
|
+
if not evidence_refs:
|
|
197
|
+
raise ValueError("open arc requires source evidence")
|
|
198
|
+
if subject not in people:
|
|
199
|
+
raise ValueError("arc subject must be one of its people")
|
|
200
|
+
if closure_reason:
|
|
201
|
+
raise ValueError("open arc cannot carry a closure reason")
|
|
202
|
+
else:
|
|
203
|
+
if arc_type or self.topic:
|
|
204
|
+
raise ValueError(
|
|
205
|
+
"non-open arc events cannot replace arc identity fields")
|
|
206
|
+
if kind == "link" and not any((
|
|
207
|
+
people, turn_refs, commitment_refs, expectation_refs,
|
|
208
|
+
project_refs, evidence_refs, due_at, next_check_at,
|
|
209
|
+
)):
|
|
210
|
+
raise ValueError("link event must add bounded evidence or links")
|
|
211
|
+
if kind in {"hold", "resume", "reopen"} and not evidence_refs:
|
|
212
|
+
raise ValueError(f"{kind} event requires evidence")
|
|
213
|
+
if kind in {"close", "cancel"}:
|
|
214
|
+
if not evidence_refs:
|
|
215
|
+
raise ValueError("arc closure evidence is required")
|
|
216
|
+
if not closure_reason\
|
|
217
|
+
or len(closure_reason) > MAX_CLOSURE_REASON_CHARS:
|
|
218
|
+
raise ValueError("arc closure reason is required and bounded")
|
|
219
|
+
elif closure_reason:
|
|
220
|
+
raise ValueError("only terminal events carry closure reason")
|
|
221
|
+
|
|
222
|
+
object.__setattr__(self, "event_id", event_id)
|
|
223
|
+
object.__setattr__(self, "idempotency_key", idempotency)
|
|
224
|
+
object.__setattr__(self, "arc_id", arc_id)
|
|
225
|
+
object.__setattr__(self, "event_kind", kind)
|
|
226
|
+
object.__setattr__(self, "source_ref", source_ref)
|
|
227
|
+
object.__setattr__(self, "occurred_at", occurred_at)
|
|
228
|
+
object.__setattr__(self, "subject_person_id", subject)
|
|
229
|
+
object.__setattr__(self, "arc_type", arc_type)
|
|
230
|
+
object.__setattr__(self, "topic", topic)
|
|
231
|
+
object.__setattr__(self, "people", people)
|
|
232
|
+
object.__setattr__(self, "turn_refs", turn_refs)
|
|
233
|
+
object.__setattr__(self, "commitment_refs", commitment_refs)
|
|
234
|
+
object.__setattr__(self, "expectation_refs", expectation_refs)
|
|
235
|
+
object.__setattr__(self, "project_refs", project_refs)
|
|
236
|
+
object.__setattr__(self, "evidence_refs", evidence_refs)
|
|
237
|
+
object.__setattr__(self, "viewer_scope", viewer_scope)
|
|
238
|
+
object.__setattr__(self, "shareability", shareability)
|
|
239
|
+
object.__setattr__(self, "due_at", due_at)
|
|
240
|
+
object.__setattr__(self, "next_check_at", next_check_at)
|
|
241
|
+
object.__setattr__(self, "closure_reason", closure_reason)
|
|
242
|
+
object.__setattr__(self, "sequence", sequence)
|
|
243
|
+
|
|
244
|
+
@classmethod
|
|
245
|
+
def open(
|
|
246
|
+
cls,
|
|
247
|
+
*,
|
|
248
|
+
event_id: str,
|
|
249
|
+
idempotency_key: str,
|
|
250
|
+
arc_id: str,
|
|
251
|
+
arc_type: str,
|
|
252
|
+
topic: str,
|
|
253
|
+
people: Sequence[str],
|
|
254
|
+
source_turn_ref: str,
|
|
255
|
+
source_ref: str,
|
|
256
|
+
viewer_scope: str,
|
|
257
|
+
shareability: str,
|
|
258
|
+
subject_person_id: str,
|
|
259
|
+
evidence_refs: Sequence[str],
|
|
260
|
+
occurred_at: str,
|
|
261
|
+
commitment_refs: Sequence[str] = (),
|
|
262
|
+
expectation_refs: Sequence[str] = (),
|
|
263
|
+
project_refs: Sequence[str] = (),
|
|
264
|
+
due_at: str = "",
|
|
265
|
+
next_check_at: str = "",
|
|
266
|
+
) -> "ArcEventV1":
|
|
267
|
+
return cls(
|
|
268
|
+
event_id=event_id,
|
|
269
|
+
idempotency_key=idempotency_key,
|
|
270
|
+
arc_id=arc_id,
|
|
271
|
+
event_kind="open",
|
|
272
|
+
source_ref=source_ref,
|
|
273
|
+
occurred_at=occurred_at,
|
|
274
|
+
subject_person_id=subject_person_id,
|
|
275
|
+
arc_type=arc_type,
|
|
276
|
+
topic=topic,
|
|
277
|
+
people=tuple(people),
|
|
278
|
+
turn_refs=(source_turn_ref,),
|
|
279
|
+
commitment_refs=tuple(commitment_refs),
|
|
280
|
+
expectation_refs=tuple(expectation_refs),
|
|
281
|
+
project_refs=tuple(project_refs),
|
|
282
|
+
evidence_refs=tuple(evidence_refs),
|
|
283
|
+
viewer_scope=viewer_scope,
|
|
284
|
+
shareability=shareability,
|
|
285
|
+
due_at=due_at,
|
|
286
|
+
next_check_at=next_check_at,
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
@classmethod
|
|
290
|
+
def link(
|
|
291
|
+
cls,
|
|
292
|
+
*,
|
|
293
|
+
event_id: str,
|
|
294
|
+
idempotency_key: str,
|
|
295
|
+
arc_id: str,
|
|
296
|
+
source_ref: str,
|
|
297
|
+
subject_person_id: str,
|
|
298
|
+
viewer_scope: str,
|
|
299
|
+
shareability: str,
|
|
300
|
+
occurred_at: str,
|
|
301
|
+
people: Sequence[str] = (),
|
|
302
|
+
turn_refs: Sequence[str] = (),
|
|
303
|
+
commitment_refs: Sequence[str] = (),
|
|
304
|
+
expectation_refs: Sequence[str] = (),
|
|
305
|
+
project_refs: Sequence[str] = (),
|
|
306
|
+
evidence_refs: Sequence[str] = (),
|
|
307
|
+
due_at: str = "",
|
|
308
|
+
next_check_at: str = "",
|
|
309
|
+
) -> "ArcEventV1":
|
|
310
|
+
return cls(
|
|
311
|
+
event_id=event_id,
|
|
312
|
+
idempotency_key=idempotency_key,
|
|
313
|
+
arc_id=arc_id,
|
|
314
|
+
event_kind="link",
|
|
315
|
+
source_ref=source_ref,
|
|
316
|
+
occurred_at=occurred_at,
|
|
317
|
+
subject_person_id=subject_person_id,
|
|
318
|
+
people=tuple(people),
|
|
319
|
+
turn_refs=tuple(turn_refs),
|
|
320
|
+
commitment_refs=tuple(commitment_refs),
|
|
321
|
+
expectation_refs=tuple(expectation_refs),
|
|
322
|
+
project_refs=tuple(project_refs),
|
|
323
|
+
evidence_refs=tuple(evidence_refs),
|
|
324
|
+
due_at=due_at,
|
|
325
|
+
next_check_at=next_check_at,
|
|
326
|
+
viewer_scope=viewer_scope,
|
|
327
|
+
shareability=shareability,
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
@classmethod
|
|
331
|
+
def _transition(
|
|
332
|
+
cls,
|
|
333
|
+
kind: str,
|
|
334
|
+
*,
|
|
335
|
+
event_id: str,
|
|
336
|
+
idempotency_key: str,
|
|
337
|
+
arc_id: str,
|
|
338
|
+
source_ref: str,
|
|
339
|
+
subject_person_id: str,
|
|
340
|
+
viewer_scope: str,
|
|
341
|
+
shareability: str,
|
|
342
|
+
evidence_refs: Sequence[str],
|
|
343
|
+
occurred_at: str,
|
|
344
|
+
closure_reason: str = "",
|
|
345
|
+
) -> "ArcEventV1":
|
|
346
|
+
return cls(
|
|
347
|
+
event_id=event_id,
|
|
348
|
+
idempotency_key=idempotency_key,
|
|
349
|
+
arc_id=arc_id,
|
|
350
|
+
event_kind=kind,
|
|
351
|
+
source_ref=source_ref,
|
|
352
|
+
occurred_at=occurred_at,
|
|
353
|
+
subject_person_id=subject_person_id,
|
|
354
|
+
evidence_refs=tuple(evidence_refs),
|
|
355
|
+
viewer_scope=viewer_scope,
|
|
356
|
+
shareability=shareability,
|
|
357
|
+
closure_reason=closure_reason,
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
@classmethod
|
|
361
|
+
def close(cls, **kwargs: Any) -> "ArcEventV1":
|
|
362
|
+
return cls._transition("close", **kwargs)
|
|
363
|
+
|
|
364
|
+
@classmethod
|
|
365
|
+
def cancel(cls, **kwargs: Any) -> "ArcEventV1":
|
|
366
|
+
return cls._transition("cancel", **kwargs)
|
|
367
|
+
|
|
368
|
+
@classmethod
|
|
369
|
+
def hold(cls, **kwargs: Any) -> "ArcEventV1":
|
|
370
|
+
return cls._transition("hold", **kwargs)
|
|
371
|
+
|
|
372
|
+
@classmethod
|
|
373
|
+
def resume(cls, **kwargs: Any) -> "ArcEventV1":
|
|
374
|
+
return cls._transition("resume", **kwargs)
|
|
375
|
+
|
|
376
|
+
@classmethod
|
|
377
|
+
def reopen(cls, **kwargs: Any) -> "ArcEventV1":
|
|
378
|
+
return cls._transition("reopen", **kwargs)
|
|
379
|
+
|
|
380
|
+
def payload(self) -> dict[str, Any]:
|
|
381
|
+
return {
|
|
382
|
+
"schema_version": self.schema_version,
|
|
383
|
+
"event_id": self.event_id,
|
|
384
|
+
"idempotency_key": self.idempotency_key,
|
|
385
|
+
"arc_id": self.arc_id,
|
|
386
|
+
"event_kind": self.event_kind,
|
|
387
|
+
"source_ref": self.source_ref,
|
|
388
|
+
"occurred_at": self.occurred_at,
|
|
389
|
+
"subject_person_id": self.subject_person_id,
|
|
390
|
+
"arc_type": self.arc_type,
|
|
391
|
+
"topic": self.topic,
|
|
392
|
+
"people": list(self.people),
|
|
393
|
+
"turn_refs": list(self.turn_refs),
|
|
394
|
+
"commitment_refs": list(self.commitment_refs),
|
|
395
|
+
"expectation_refs": list(self.expectation_refs),
|
|
396
|
+
"project_refs": list(self.project_refs),
|
|
397
|
+
"evidence_refs": list(self.evidence_refs),
|
|
398
|
+
"viewer_scope": self.viewer_scope,
|
|
399
|
+
"shareability": self.shareability,
|
|
400
|
+
"due_at": self.due_at,
|
|
401
|
+
"next_check_at": self.next_check_at,
|
|
402
|
+
"closure_reason": self.closure_reason,
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
@property
|
|
406
|
+
def event_digest(self) -> str:
|
|
407
|
+
return _digest(self.payload())
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
@dataclass(frozen=True, slots=True)
|
|
411
|
+
class Arc:
|
|
412
|
+
arc_id: str
|
|
413
|
+
arc_type: str
|
|
414
|
+
topic: str
|
|
415
|
+
subject_person_id: str
|
|
416
|
+
people: tuple[str, ...]
|
|
417
|
+
viewer_scope: str
|
|
418
|
+
shareability: str
|
|
419
|
+
state: str
|
|
420
|
+
turn_refs: tuple[str, ...]
|
|
421
|
+
commitment_refs: tuple[str, ...]
|
|
422
|
+
expectation_refs: tuple[str, ...]
|
|
423
|
+
project_refs: tuple[str, ...]
|
|
424
|
+
evidence_refs: tuple[str, ...]
|
|
425
|
+
closure_evidence_refs: tuple[str, ...]
|
|
426
|
+
due_at: str
|
|
427
|
+
next_check_at: str
|
|
428
|
+
opened_at: str
|
|
429
|
+
updated_at: str
|
|
430
|
+
closed_at: str
|
|
431
|
+
closure_reason: str
|
|
432
|
+
event_count: int
|
|
433
|
+
revision: int
|
|
434
|
+
history_digest: str
|
|
435
|
+
overdue: bool
|
|
436
|
+
needs_check: bool
|
|
437
|
+
schema_version: int = SCHEMA_VERSION
|
|
438
|
+
|
|
439
|
+
@property
|
|
440
|
+
def active(self) -> bool:
|
|
441
|
+
return self.state in ACTIVE_STATES
|
|
442
|
+
|
|
443
|
+
def visible_to(self, viewer: ViewerContextV1) -> bool:
|
|
444
|
+
allowed, _ = visibility_scope_decision(
|
|
445
|
+
viewer_scope=self.viewer_scope,
|
|
446
|
+
shareability=self.shareability,
|
|
447
|
+
subject_person_id=self.subject_person_id,
|
|
448
|
+
viewer=viewer,
|
|
449
|
+
)
|
|
450
|
+
return allowed
|
|
451
|
+
|
|
452
|
+
def public(self) -> dict[str, Any]:
|
|
453
|
+
return {
|
|
454
|
+
"schema_version": self.schema_version,
|
|
455
|
+
"arc_id": self.arc_id,
|
|
456
|
+
"arc_type": self.arc_type,
|
|
457
|
+
"topic": self.topic,
|
|
458
|
+
"subject_person_id": self.subject_person_id,
|
|
459
|
+
"people": list(self.people),
|
|
460
|
+
"viewer_scope": self.viewer_scope,
|
|
461
|
+
"shareability": self.shareability,
|
|
462
|
+
"state": self.state,
|
|
463
|
+
"turn_refs": list(self.turn_refs),
|
|
464
|
+
"commitment_refs": list(self.commitment_refs),
|
|
465
|
+
"expectation_refs": list(self.expectation_refs),
|
|
466
|
+
"project_refs": list(self.project_refs),
|
|
467
|
+
"evidence_refs": list(self.evidence_refs),
|
|
468
|
+
"closure_evidence_refs": list(self.closure_evidence_refs),
|
|
469
|
+
"due_at": self.due_at,
|
|
470
|
+
"next_check_at": self.next_check_at,
|
|
471
|
+
"opened_at": self.opened_at,
|
|
472
|
+
"updated_at": self.updated_at,
|
|
473
|
+
"closed_at": self.closed_at,
|
|
474
|
+
"closure_reason": self.closure_reason,
|
|
475
|
+
"event_count": self.event_count,
|
|
476
|
+
"revision": self.revision,
|
|
477
|
+
"history_digest": self.history_digest,
|
|
478
|
+
"overdue": self.overdue,
|
|
479
|
+
"needs_check": self.needs_check,
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
class ArcReducer:
|
|
484
|
+
"""Strict deterministic event reducer; invalid history never projects."""
|
|
485
|
+
|
|
486
|
+
@staticmethod
|
|
487
|
+
def reduce(
|
|
488
|
+
events: Sequence[ArcEventV1],
|
|
489
|
+
*,
|
|
490
|
+
now: datetime | str | None = None,
|
|
491
|
+
) -> Arc:
|
|
492
|
+
if not events:
|
|
493
|
+
raise ValueError("arc reducer requires an open event")
|
|
494
|
+
if len(events) > MAX_EVENTS_PER_ARC:
|
|
495
|
+
raise ValueError("arc event history exceeds bounded limit")
|
|
496
|
+
ordered = list(events)
|
|
497
|
+
if any(event.sequence for event in ordered):
|
|
498
|
+
if not all(event.sequence for event in ordered):
|
|
499
|
+
raise ValueError("arc history mixes sequenced and unsequenced events")
|
|
500
|
+
ordered.sort(key=lambda event: event.sequence)
|
|
501
|
+
if ordered[0].event_kind != "open":
|
|
502
|
+
raise ValueError("arc history must begin with an open event")
|
|
503
|
+
if sum(1 for event in ordered if event.event_kind == "open") != 1:
|
|
504
|
+
raise ValueError("arc history must contain exactly one open event")
|
|
505
|
+
arc_ids = {event.arc_id for event in ordered}
|
|
506
|
+
if len(arc_ids) != 1:
|
|
507
|
+
raise ValueError("arc history cannot mix arc identities")
|
|
508
|
+
|
|
509
|
+
opened = ordered[0]
|
|
510
|
+
state = "open"
|
|
511
|
+
people = set(opened.people)
|
|
512
|
+
turn_refs = set(opened.turn_refs)
|
|
513
|
+
commitment_refs = set(opened.commitment_refs)
|
|
514
|
+
expectation_refs = set(opened.expectation_refs)
|
|
515
|
+
project_refs = set(opened.project_refs)
|
|
516
|
+
evidence_refs = set(opened.evidence_refs)
|
|
517
|
+
closure_evidence: set[str] = set()
|
|
518
|
+
due_at = opened.due_at
|
|
519
|
+
next_check_at = opened.next_check_at
|
|
520
|
+
closed_at = ""
|
|
521
|
+
closure_reason = ""
|
|
522
|
+
updated_at = opened.occurred_at
|
|
523
|
+
|
|
524
|
+
for event in ordered[1:]:
|
|
525
|
+
if (
|
|
526
|
+
event.viewer_scope != opened.viewer_scope
|
|
527
|
+
or event.shareability != opened.shareability
|
|
528
|
+
):
|
|
529
|
+
raise ValueError(
|
|
530
|
+
"arc event visibility must exactly match its open event")
|
|
531
|
+
if state in TERMINAL_STATES and event.event_kind != "reopen":
|
|
532
|
+
raise ValueError("terminal arc cannot accept non-reopen events")
|
|
533
|
+
kind = event.event_kind
|
|
534
|
+
if kind == "link":
|
|
535
|
+
people.update(event.people)
|
|
536
|
+
if len(people) > MAX_PEOPLE:
|
|
537
|
+
raise ValueError("arc people exceed bounded limit")
|
|
538
|
+
turn_refs.update(event.turn_refs)
|
|
539
|
+
commitment_refs.update(event.commitment_refs)
|
|
540
|
+
expectation_refs.update(event.expectation_refs)
|
|
541
|
+
project_refs.update(event.project_refs)
|
|
542
|
+
evidence_refs.update(event.evidence_refs)
|
|
543
|
+
due_at = event.due_at or due_at
|
|
544
|
+
next_check_at = event.next_check_at or next_check_at
|
|
545
|
+
elif kind == "hold":
|
|
546
|
+
if state != "open":
|
|
547
|
+
raise ValueError("only an open arc can be held")
|
|
548
|
+
state = "held"
|
|
549
|
+
evidence_refs.update(event.evidence_refs)
|
|
550
|
+
elif kind == "resume":
|
|
551
|
+
if state != "held":
|
|
552
|
+
raise ValueError("only a held arc can resume")
|
|
553
|
+
state = "open"
|
|
554
|
+
evidence_refs.update(event.evidence_refs)
|
|
555
|
+
elif kind in {"close", "cancel"}:
|
|
556
|
+
if state not in ACTIVE_STATES:
|
|
557
|
+
raise ValueError("only an active arc can close")
|
|
558
|
+
state = "closed" if kind == "close" else "cancelled"
|
|
559
|
+
closure_evidence.update(event.evidence_refs)
|
|
560
|
+
evidence_refs.update(event.evidence_refs)
|
|
561
|
+
closure_reason = event.closure_reason
|
|
562
|
+
closed_at = event.occurred_at
|
|
563
|
+
elif kind == "reopen":
|
|
564
|
+
if state not in TERMINAL_STATES:
|
|
565
|
+
raise ValueError("only a terminal arc can reopen")
|
|
566
|
+
state = "open"
|
|
567
|
+
evidence_refs.update(event.evidence_refs)
|
|
568
|
+
closed_at = ""
|
|
569
|
+
closure_reason = ""
|
|
570
|
+
else:
|
|
571
|
+
raise ValueError(f"unhandled arc event kind {kind}")
|
|
572
|
+
if event.subject_person_id not in people:
|
|
573
|
+
raise ValueError(
|
|
574
|
+
"arc event subject must be one of the linked people")
|
|
575
|
+
for field, references in (
|
|
576
|
+
("turn_refs", turn_refs),
|
|
577
|
+
("commitment_refs", commitment_refs),
|
|
578
|
+
("expectation_refs", expectation_refs),
|
|
579
|
+
("project_refs", project_refs),
|
|
580
|
+
("evidence_refs", evidence_refs),
|
|
581
|
+
("closure_evidence_refs", closure_evidence),
|
|
582
|
+
):
|
|
583
|
+
if len(references) > MAX_REFS_PER_KIND:
|
|
584
|
+
raise ValueError(f"arc {field} exceeds bounded limit")
|
|
585
|
+
updated_at = event.occurred_at
|
|
586
|
+
|
|
587
|
+
observed = _as_utc(
|
|
588
|
+
now or datetime.now(timezone.utc), field="now")
|
|
589
|
+
assert observed is not None
|
|
590
|
+
due = _as_utc(due_at, field="due_at", allow_empty=True)
|
|
591
|
+
check = _as_utc(
|
|
592
|
+
next_check_at, field="next_check_at", allow_empty=True)
|
|
593
|
+
active = state in ACTIVE_STATES
|
|
594
|
+
return Arc(
|
|
595
|
+
arc_id=opened.arc_id,
|
|
596
|
+
arc_type=opened.arc_type,
|
|
597
|
+
topic=opened.topic,
|
|
598
|
+
subject_person_id=opened.subject_person_id,
|
|
599
|
+
people=tuple(sorted(people)),
|
|
600
|
+
viewer_scope=opened.viewer_scope,
|
|
601
|
+
shareability=opened.shareability,
|
|
602
|
+
state=state,
|
|
603
|
+
turn_refs=tuple(sorted(turn_refs)),
|
|
604
|
+
commitment_refs=tuple(sorted(commitment_refs)),
|
|
605
|
+
expectation_refs=tuple(sorted(expectation_refs)),
|
|
606
|
+
project_refs=tuple(sorted(project_refs)),
|
|
607
|
+
evidence_refs=tuple(sorted(evidence_refs)),
|
|
608
|
+
closure_evidence_refs=tuple(sorted(closure_evidence)),
|
|
609
|
+
due_at=due_at,
|
|
610
|
+
next_check_at=next_check_at,
|
|
611
|
+
opened_at=opened.occurred_at,
|
|
612
|
+
updated_at=updated_at,
|
|
613
|
+
closed_at=closed_at,
|
|
614
|
+
closure_reason=closure_reason,
|
|
615
|
+
event_count=len(ordered),
|
|
616
|
+
revision=max((event.sequence for event in ordered), default=0)
|
|
617
|
+
or len(ordered),
|
|
618
|
+
history_digest=_digest([
|
|
619
|
+
event.event_digest for event in ordered]),
|
|
620
|
+
overdue=bool(active and due is not None and observed >= due),
|
|
621
|
+
needs_check=bool(active and check is not None and observed >= check),
|
|
622
|
+
)
|
|
623
|
+
|
|
624
|
+
|
|
625
|
+
@dataclass(frozen=True, slots=True)
|
|
626
|
+
class ArcAppendResultV1:
|
|
627
|
+
event: ArcEventV1
|
|
628
|
+
appended: bool
|
|
629
|
+
replayed: bool
|
|
630
|
+
|
|
631
|
+
|
|
632
|
+
@dataclass(frozen=True, slots=True)
|
|
633
|
+
class ArcProjectionBatchV1:
|
|
634
|
+
arcs: tuple[Arc, ...]
|
|
635
|
+
denied_count: int
|
|
636
|
+
corrupt_count: int
|
|
637
|
+
truncated: bool
|
|
638
|
+
viewer_digest: str
|
|
639
|
+
audit_digest: str
|
|
640
|
+
|
|
641
|
+
def public(self) -> dict[str, Any]:
|
|
642
|
+
return {
|
|
643
|
+
"arcs": [arc.public() for arc in self.arcs],
|
|
644
|
+
"denied_count": self.denied_count,
|
|
645
|
+
"corrupt_count": self.corrupt_count,
|
|
646
|
+
"truncated": self.truncated,
|
|
647
|
+
"viewer_digest": self.viewer_digest,
|
|
648
|
+
"audit_digest": self.audit_digest,
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
|
|
652
|
+
class ArcStore:
|
|
653
|
+
"""SQLite append-only event ledger with transactional idempotency."""
|
|
654
|
+
|
|
655
|
+
def __init__(self, db_path: str = ":memory:") -> None:
|
|
656
|
+
self.path = db_path
|
|
657
|
+
if db_path != ":memory:":
|
|
658
|
+
path = Path(db_path)
|
|
659
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
660
|
+
if not path.exists():
|
|
661
|
+
path.touch(mode=0o600)
|
|
662
|
+
else:
|
|
663
|
+
os.chmod(path, 0o600)
|
|
664
|
+
self._lock = threading.RLock()
|
|
665
|
+
self._conn = sqlite3.connect(
|
|
666
|
+
db_path, check_same_thread=False, timeout=30.0)
|
|
667
|
+
self._conn.row_factory = sqlite3.Row
|
|
668
|
+
self._conn.execute("PRAGMA busy_timeout=30000")
|
|
669
|
+
self._conn.execute("PRAGMA journal_mode=WAL")
|
|
670
|
+
self._conn.executescript(
|
|
671
|
+
"""
|
|
672
|
+
CREATE TABLE IF NOT EXISTS arc_events (
|
|
673
|
+
seq INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
674
|
+
event_id TEXT NOT NULL UNIQUE,
|
|
675
|
+
idempotency_key TEXT NOT NULL UNIQUE,
|
|
676
|
+
arc_id TEXT NOT NULL,
|
|
677
|
+
event_kind TEXT NOT NULL,
|
|
678
|
+
source_ref TEXT NOT NULL,
|
|
679
|
+
occurred_at TEXT NOT NULL,
|
|
680
|
+
payload_json TEXT NOT NULL,
|
|
681
|
+
event_digest TEXT NOT NULL,
|
|
682
|
+
stored_at TEXT NOT NULL
|
|
683
|
+
);
|
|
684
|
+
CREATE INDEX IF NOT EXISTS idx_arc_events_arc
|
|
685
|
+
ON arc_events(arc_id,seq);
|
|
686
|
+
CREATE TRIGGER IF NOT EXISTS arc_events_no_update
|
|
687
|
+
BEFORE UPDATE ON arc_events BEGIN
|
|
688
|
+
SELECT RAISE(ABORT, 'arc events are append-only');
|
|
689
|
+
END;
|
|
690
|
+
CREATE TRIGGER IF NOT EXISTS arc_events_no_delete
|
|
691
|
+
BEFORE DELETE ON arc_events BEGIN
|
|
692
|
+
SELECT RAISE(ABORT, 'arc events are append-only');
|
|
693
|
+
END;
|
|
694
|
+
"""
|
|
695
|
+
)
|
|
696
|
+
self._conn.commit()
|
|
697
|
+
if db_path != ":memory:":
|
|
698
|
+
os.chmod(db_path, 0o600)
|
|
699
|
+
|
|
700
|
+
@staticmethod
|
|
701
|
+
def _decode(row: sqlite3.Row) -> ArcEventV1:
|
|
702
|
+
try:
|
|
703
|
+
payload = json.loads(row["payload_json"])
|
|
704
|
+
except (TypeError, ValueError) as exc:
|
|
705
|
+
raise ValueError("arc event payload is unreadable") from exc
|
|
706
|
+
event = ArcEventV1(
|
|
707
|
+
**payload,
|
|
708
|
+
sequence=int(row["seq"]),
|
|
709
|
+
)
|
|
710
|
+
if event.event_digest != row["event_digest"]:
|
|
711
|
+
raise ValueError("arc event digest mismatch")
|
|
712
|
+
return event
|
|
713
|
+
|
|
714
|
+
def _events_locked(self, arc_id: str) -> list[ArcEventV1]:
|
|
715
|
+
rows = self._conn.execute(
|
|
716
|
+
"SELECT * FROM arc_events WHERE arc_id=? ORDER BY seq",
|
|
717
|
+
(arc_id,),
|
|
718
|
+
).fetchall()
|
|
719
|
+
return [self._decode(row) for row in rows]
|
|
720
|
+
|
|
721
|
+
def append(self, event: ArcEventV1) -> ArcAppendResultV1:
|
|
722
|
+
if not isinstance(event, ArcEventV1):
|
|
723
|
+
raise ValueError("ArcStore accepts ArcEventV1 only")
|
|
724
|
+
with self._lock:
|
|
725
|
+
self._conn.execute("BEGIN IMMEDIATE")
|
|
726
|
+
try:
|
|
727
|
+
rows = self._conn.execute(
|
|
728
|
+
"SELECT * FROM arc_events WHERE event_id=? "
|
|
729
|
+
"OR idempotency_key=? ORDER BY seq",
|
|
730
|
+
(event.event_id, event.idempotency_key),
|
|
731
|
+
).fetchall()
|
|
732
|
+
if rows:
|
|
733
|
+
for row in rows:
|
|
734
|
+
saved = self._decode(row)
|
|
735
|
+
if (
|
|
736
|
+
saved.event_id == event.event_id
|
|
737
|
+
and saved.idempotency_key == event.idempotency_key
|
|
738
|
+
and saved.event_digest == event.event_digest
|
|
739
|
+
):
|
|
740
|
+
self._conn.commit()
|
|
741
|
+
return ArcAppendResultV1(
|
|
742
|
+
event=saved, appended=False, replayed=True)
|
|
743
|
+
raise ArcConflictError(
|
|
744
|
+
"arc event idempotency identity changed immutable content")
|
|
745
|
+
|
|
746
|
+
# Reject an event that would make the ledger unreducible before
|
|
747
|
+
# it is persisted. BEGIN IMMEDIATE reserves the next sequence,
|
|
748
|
+
# so validation sees the same fully-sequenced history that a
|
|
749
|
+
# subsequent reader will reduce.
|
|
750
|
+
existing = self._events_locked(event.arc_id)
|
|
751
|
+
row = self._conn.execute(
|
|
752
|
+
"SELECT COALESCE(MAX(seq),0) + 1 AS next_seq "
|
|
753
|
+
"FROM arc_events"
|
|
754
|
+
).fetchone()
|
|
755
|
+
next_sequence = int(row["next_seq"] if row else 1)
|
|
756
|
+
ArcReducer.reduce(
|
|
757
|
+
[*existing, replace(event, sequence=next_sequence)],
|
|
758
|
+
now=event.occurred_at,
|
|
759
|
+
)
|
|
760
|
+
cursor = self._conn.execute(
|
|
761
|
+
"INSERT INTO arc_events "
|
|
762
|
+
"(event_id,idempotency_key,arc_id,event_kind,source_ref,"
|
|
763
|
+
"occurred_at,payload_json,event_digest,stored_at) "
|
|
764
|
+
"VALUES (?,?,?,?,?,?,?,?,?)",
|
|
765
|
+
(
|
|
766
|
+
event.event_id,
|
|
767
|
+
event.idempotency_key,
|
|
768
|
+
event.arc_id,
|
|
769
|
+
event.event_kind,
|
|
770
|
+
event.source_ref,
|
|
771
|
+
event.occurred_at,
|
|
772
|
+
_canonical(event.payload()),
|
|
773
|
+
event.event_digest,
|
|
774
|
+
datetime.now(timezone.utc).isoformat(),
|
|
775
|
+
),
|
|
776
|
+
)
|
|
777
|
+
sequence = int(cursor.lastrowid)
|
|
778
|
+
if sequence != next_sequence:
|
|
779
|
+
raise RuntimeError("arc ledger sequence allocation changed")
|
|
780
|
+
self._conn.commit()
|
|
781
|
+
return ArcAppendResultV1(
|
|
782
|
+
event=replace(event, sequence=sequence),
|
|
783
|
+
appended=True,
|
|
784
|
+
replayed=False,
|
|
785
|
+
)
|
|
786
|
+
except Exception:
|
|
787
|
+
self._conn.rollback()
|
|
788
|
+
raise
|
|
789
|
+
|
|
790
|
+
def events(self, arc_id: str) -> tuple[ArcEventV1, ...]:
|
|
791
|
+
normalized = _ref(arc_id, field="arc_id")
|
|
792
|
+
with self._lock:
|
|
793
|
+
return tuple(self._events_locked(normalized))
|
|
794
|
+
|
|
795
|
+
def event_count(self) -> int:
|
|
796
|
+
with self._lock:
|
|
797
|
+
row = self._conn.execute(
|
|
798
|
+
"SELECT COUNT(*) AS n FROM arc_events").fetchone()
|
|
799
|
+
return int(row["n"] if row else 0)
|
|
800
|
+
|
|
801
|
+
def get_arc(
|
|
802
|
+
self,
|
|
803
|
+
arc_id: str,
|
|
804
|
+
*,
|
|
805
|
+
now: datetime | str | None = None,
|
|
806
|
+
) -> Optional[Arc]:
|
|
807
|
+
events = self.events(arc_id)
|
|
808
|
+
return ArcReducer.reduce(events, now=now) if events else None
|
|
809
|
+
|
|
810
|
+
def project_active(
|
|
811
|
+
self,
|
|
812
|
+
viewer: ViewerContextV1,
|
|
813
|
+
*,
|
|
814
|
+
now: datetime | str,
|
|
815
|
+
max_arcs: int = 24,
|
|
816
|
+
max_topic_chars: int = 8_000,
|
|
817
|
+
) -> ArcProjectionBatchV1:
|
|
818
|
+
if not 1 <= int(max_arcs) <= MAX_PROJECTED_ARCS:
|
|
819
|
+
raise ValueError("max_arcs is outside bounded projection range")
|
|
820
|
+
if not 1 <= int(max_topic_chars) <= MAX_PROJECTED_TOPIC_CHARS:
|
|
821
|
+
raise ValueError("max_topic_chars is outside bounded projection range")
|
|
822
|
+
with self._lock:
|
|
823
|
+
rows = self._conn.execute(
|
|
824
|
+
"SELECT arc_id,MAX(seq) AS last_seq FROM arc_events "
|
|
825
|
+
"GROUP BY arc_id ORDER BY last_seq DESC LIMIT ?",
|
|
826
|
+
(MAX_ARCS_SCANNED + 1,),
|
|
827
|
+
).fetchall()
|
|
828
|
+
arc_ids = [str(row["arc_id"]) for row in rows]
|
|
829
|
+
scan_truncated = len(arc_ids) > MAX_ARCS_SCANNED
|
|
830
|
+
arc_ids = arc_ids[:MAX_ARCS_SCANNED]
|
|
831
|
+
eligible: list[Arc] = []
|
|
832
|
+
denied_count = 0
|
|
833
|
+
corrupt_count = 0
|
|
834
|
+
for arc_id in arc_ids:
|
|
835
|
+
try:
|
|
836
|
+
arc = self.get_arc(arc_id, now=now)
|
|
837
|
+
except Exception:
|
|
838
|
+
corrupt_count += 1
|
|
839
|
+
continue
|
|
840
|
+
if arc is None or not arc.active:
|
|
841
|
+
continue
|
|
842
|
+
if not arc.visible_to(viewer):
|
|
843
|
+
denied_count += 1
|
|
844
|
+
continue
|
|
845
|
+
eligible.append(arc)
|
|
846
|
+
eligible.sort(key=lambda arc: (arc.updated_at, arc.arc_id), reverse=True)
|
|
847
|
+
|
|
848
|
+
projected: list[Arc] = []
|
|
849
|
+
chars = 0
|
|
850
|
+
truncated = scan_truncated
|
|
851
|
+
for arc in eligible:
|
|
852
|
+
if len(projected) >= int(max_arcs)\
|
|
853
|
+
or chars + len(arc.topic) > int(max_topic_chars):
|
|
854
|
+
truncated = True
|
|
855
|
+
continue
|
|
856
|
+
projected.append(arc)
|
|
857
|
+
chars += len(arc.topic)
|
|
858
|
+
payload = {
|
|
859
|
+
"schema_version": SCHEMA_VERSION,
|
|
860
|
+
"viewer_digest": viewer.audit_digest,
|
|
861
|
+
"arc_digests": [arc.history_digest for arc in projected],
|
|
862
|
+
"denied_count": denied_count,
|
|
863
|
+
"corrupt_count": corrupt_count,
|
|
864
|
+
"truncated": truncated,
|
|
865
|
+
"now": _iso(now, field="now"),
|
|
866
|
+
"max_arcs": int(max_arcs),
|
|
867
|
+
"max_topic_chars": int(max_topic_chars),
|
|
868
|
+
}
|
|
869
|
+
return ArcProjectionBatchV1(
|
|
870
|
+
arcs=tuple(projected),
|
|
871
|
+
denied_count=denied_count,
|
|
872
|
+
corrupt_count=corrupt_count,
|
|
873
|
+
truncated=truncated,
|
|
874
|
+
viewer_digest=viewer.audit_digest,
|
|
875
|
+
audit_digest=_digest(payload),
|
|
876
|
+
)
|
|
877
|
+
|
|
878
|
+
def close(self) -> None:
|
|
879
|
+
with self._lock:
|
|
880
|
+
try:
|
|
881
|
+
self._conn.close()
|
|
882
|
+
except Exception:
|
|
883
|
+
pass
|
|
884
|
+
|
|
885
|
+
|
|
886
|
+
__all__ = [
|
|
887
|
+
"ACTIVE_STATES",
|
|
888
|
+
"ARC_TYPES",
|
|
889
|
+
"Arc",
|
|
890
|
+
"ArcAppendResultV1",
|
|
891
|
+
"ArcConflictError",
|
|
892
|
+
"ArcEventV1",
|
|
893
|
+
"ArcProjectionBatchV1",
|
|
894
|
+
"ArcReducer",
|
|
895
|
+
"ArcStore",
|
|
896
|
+
]
|