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