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,883 @@
|
|
|
1
|
+
"""CompetenceStore -- per-domain outcome counts + latency, from real work.
|
|
2
|
+
|
|
3
|
+
Domains are capability classes: initiative types ("research", "follow_up"),
|
|
4
|
+
"project" (project steps/outcomes), "directed" (directed-action audits),
|
|
5
|
+
"delivery" (real pushes), and worker job types (item 5). Every recording site
|
|
6
|
+
is a completion/failure chokepoint that already exists; the store never polls.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import hashlib
|
|
12
|
+
import json
|
|
13
|
+
import logging
|
|
14
|
+
import math
|
|
15
|
+
import os
|
|
16
|
+
import sqlite3
|
|
17
|
+
import threading
|
|
18
|
+
import time
|
|
19
|
+
from typing import Any, Dict, Iterable, List, Optional, Tuple
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
# Exponential moving average weight for latency (recent work dominates).
|
|
24
|
+
_EWMA_ALPHA = 0.3
|
|
25
|
+
|
|
26
|
+
_OUTCOMES = ("success", "failure", "timeout")
|
|
27
|
+
|
|
28
|
+
_DISPOSITIONS = ("invalidate", "replace")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def self_model_enabled() -> bool:
|
|
32
|
+
return os.environ.get("COLONY_SELF_MODEL_ENABLED", "true").strip().lower() != "false"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _norm_outcome(outcome: str) -> str:
|
|
36
|
+
o = (outcome or "").strip().lower()
|
|
37
|
+
if o in ("success", "completed", "clean", "actioned", "done", "ok"):
|
|
38
|
+
return "success"
|
|
39
|
+
if o in ("timeout", "timed_out"):
|
|
40
|
+
return "timeout"
|
|
41
|
+
return "failure"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _canonical_json(value: Any) -> str:
|
|
45
|
+
return json.dumps(value, sort_keys=True, separators=(",", ":"),
|
|
46
|
+
ensure_ascii=True)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def event_fingerprint(event: Dict[str, Any]) -> str:
|
|
50
|
+
"""Stable identity for one immutable raw competence event.
|
|
51
|
+
|
|
52
|
+
The reconciliation tool requires this fingerprint as well as the row id,
|
|
53
|
+
so a plan made against one database cannot silently target a different
|
|
54
|
+
event after a restore or migration.
|
|
55
|
+
"""
|
|
56
|
+
evidence = event.get("evidence")
|
|
57
|
+
if isinstance(evidence, str):
|
|
58
|
+
try:
|
|
59
|
+
evidence = json.loads(evidence)
|
|
60
|
+
except ValueError:
|
|
61
|
+
pass
|
|
62
|
+
payload = {
|
|
63
|
+
"id": int(event["id"]),
|
|
64
|
+
"domain": str(event["domain"]),
|
|
65
|
+
"outcome": str(event["outcome"]),
|
|
66
|
+
"shadow": int(event.get("shadow") or 0),
|
|
67
|
+
"violation": int(event.get("violation") or 0),
|
|
68
|
+
"stated_confidence": event.get("stated_confidence"),
|
|
69
|
+
"source": event.get("source") or "legacy_unattributed",
|
|
70
|
+
"source_ref": event.get("source_ref"),
|
|
71
|
+
"evidence_status": (
|
|
72
|
+
event.get("evidence_status") or "legacy_unattributed"),
|
|
73
|
+
"outcome_contract": (
|
|
74
|
+
event.get("outcome_contract") or "legacy.unversioned"),
|
|
75
|
+
"evidence": evidence,
|
|
76
|
+
"ts": float(event["ts"]),
|
|
77
|
+
}
|
|
78
|
+
return hashlib.sha256(_canonical_json(payload).encode("utf-8")).hexdigest()
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _ledger_id(payload: Dict[str, Any]) -> str:
|
|
82
|
+
digest = hashlib.sha256(
|
|
83
|
+
_canonical_json(payload).encode("utf-8")).hexdigest()
|
|
84
|
+
return f"cr_{digest[:32]}"
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class CompetenceStore:
|
|
88
|
+
"""SQLite persistence of per-domain success/failure/timeout + ewma latency."""
|
|
89
|
+
|
|
90
|
+
def __init__(self, db_path: Optional[str] = None) -> None:
|
|
91
|
+
self._lock = threading.RLock()
|
|
92
|
+
self._conn = sqlite3.connect(str(db_path) if db_path else ":memory:",
|
|
93
|
+
check_same_thread=False)
|
|
94
|
+
self._conn.row_factory = sqlite3.Row
|
|
95
|
+
with self._lock:
|
|
96
|
+
self._conn.execute(
|
|
97
|
+
"""CREATE TABLE IF NOT EXISTS competence (
|
|
98
|
+
domain TEXT PRIMARY KEY,
|
|
99
|
+
success INTEGER DEFAULT 0,
|
|
100
|
+
failure INTEGER DEFAULT 0,
|
|
101
|
+
timeout INTEGER DEFAULT 0,
|
|
102
|
+
ewma_latency_secs REAL,
|
|
103
|
+
last_outcome TEXT,
|
|
104
|
+
last_outcome_at REAL
|
|
105
|
+
)""")
|
|
106
|
+
# Per-event log for windowed circuit-breaker queries and
|
|
107
|
+
# calibration-vs-real evidence separation (shadow=1 events are
|
|
108
|
+
# calibration runs; they graduate a class out of shadow but never
|
|
109
|
+
# count toward act-first confidence).
|
|
110
|
+
self._conn.execute(
|
|
111
|
+
"""CREATE TABLE IF NOT EXISTS competence_events (
|
|
112
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
113
|
+
domain TEXT NOT NULL, outcome TEXT NOT NULL,
|
|
114
|
+
shadow INTEGER DEFAULT 0, violation INTEGER DEFAULT 0,
|
|
115
|
+
stated_confidence REAL,
|
|
116
|
+
source TEXT,
|
|
117
|
+
source_ref TEXT,
|
|
118
|
+
event_key TEXT,
|
|
119
|
+
evidence_status TEXT,
|
|
120
|
+
outcome_contract TEXT,
|
|
121
|
+
evidence TEXT,
|
|
122
|
+
ts REAL NOT NULL
|
|
123
|
+
)""")
|
|
124
|
+
# Additive migrations: old events stay byte-for-byte intact and
|
|
125
|
+
# are explicitly labelled legacy_unattributed on the read side.
|
|
126
|
+
try:
|
|
127
|
+
cols = {r[1] for r in self._conn.execute(
|
|
128
|
+
"PRAGMA table_info(competence_events)").fetchall()}
|
|
129
|
+
additions = {
|
|
130
|
+
"stated_confidence": "REAL",
|
|
131
|
+
"source": "TEXT",
|
|
132
|
+
"source_ref": "TEXT",
|
|
133
|
+
"event_key": "TEXT",
|
|
134
|
+
"evidence_status": "TEXT",
|
|
135
|
+
"outcome_contract": "TEXT",
|
|
136
|
+
"evidence": "TEXT",
|
|
137
|
+
}
|
|
138
|
+
for name, sql_type in additions.items():
|
|
139
|
+
if name in cols:
|
|
140
|
+
continue
|
|
141
|
+
self._conn.execute(
|
|
142
|
+
"ALTER TABLE competence_events "
|
|
143
|
+
f"ADD COLUMN {name} {sql_type}")
|
|
144
|
+
except Exception:
|
|
145
|
+
pass
|
|
146
|
+
self._conn.execute(
|
|
147
|
+
"CREATE INDEX IF NOT EXISTS idx_cev_domain_ts "
|
|
148
|
+
"ON competence_events(domain, ts)")
|
|
149
|
+
self._conn.execute(
|
|
150
|
+
"CREATE UNIQUE INDEX IF NOT EXISTS idx_cev_source_event_key "
|
|
151
|
+
"ON competence_events(source, event_key) "
|
|
152
|
+
"WHERE event_key IS NOT NULL")
|
|
153
|
+
# This is the sole correction mechanism. Rows are append-only;
|
|
154
|
+
# the raw event and the all-time aggregate are never rewritten.
|
|
155
|
+
self._conn.execute(
|
|
156
|
+
"""CREATE TABLE IF NOT EXISTS competence_reconciliations (
|
|
157
|
+
seq INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
158
|
+
reconciliation_id TEXT NOT NULL UNIQUE,
|
|
159
|
+
kind TEXT NOT NULL,
|
|
160
|
+
event_id INTEGER,
|
|
161
|
+
target_fingerprint TEXT,
|
|
162
|
+
disposition TEXT,
|
|
163
|
+
replacement_outcome TEXT,
|
|
164
|
+
domain TEXT,
|
|
165
|
+
since_ts REAL,
|
|
166
|
+
until_ts REAL,
|
|
167
|
+
supersedes TEXT,
|
|
168
|
+
reason TEXT NOT NULL,
|
|
169
|
+
provenance TEXT NOT NULL,
|
|
170
|
+
created_by TEXT NOT NULL,
|
|
171
|
+
created_at REAL NOT NULL,
|
|
172
|
+
CHECK (kind IN ('event_correction', 'evidence_gap',
|
|
173
|
+
'gap_resolution')),
|
|
174
|
+
CHECK (disposition IS NULL OR
|
|
175
|
+
disposition IN ('invalidate', 'replace'))
|
|
176
|
+
)""")
|
|
177
|
+
self._conn.execute(
|
|
178
|
+
"CREATE UNIQUE INDEX IF NOT EXISTS idx_crec_supersedes "
|
|
179
|
+
"ON competence_reconciliations(supersedes) "
|
|
180
|
+
"WHERE supersedes IS NOT NULL")
|
|
181
|
+
self._conn.execute(
|
|
182
|
+
"CREATE INDEX IF NOT EXISTS idx_crec_event "
|
|
183
|
+
"ON competence_reconciliations(event_id, seq)")
|
|
184
|
+
self._conn.execute(
|
|
185
|
+
"CREATE INDEX IF NOT EXISTS idx_crec_gap "
|
|
186
|
+
"ON competence_reconciliations(domain, since_ts, until_ts)")
|
|
187
|
+
self._conn.execute(
|
|
188
|
+
"""CREATE TRIGGER IF NOT EXISTS trg_crec_no_update
|
|
189
|
+
BEFORE UPDATE ON competence_reconciliations
|
|
190
|
+
BEGIN
|
|
191
|
+
SELECT RAISE(ABORT, 'competence ledger is append-only');
|
|
192
|
+
END""")
|
|
193
|
+
self._conn.execute(
|
|
194
|
+
"""CREATE TRIGGER IF NOT EXISTS trg_crec_no_delete
|
|
195
|
+
BEFORE DELETE ON competence_reconciliations
|
|
196
|
+
BEGIN
|
|
197
|
+
SELECT RAISE(ABORT, 'competence ledger is append-only');
|
|
198
|
+
END""")
|
|
199
|
+
self._conn.commit()
|
|
200
|
+
|
|
201
|
+
def close(self) -> None:
|
|
202
|
+
with self._lock:
|
|
203
|
+
self._conn.close()
|
|
204
|
+
|
|
205
|
+
def record(self, domain: str, outcome: str,
|
|
206
|
+
latency_secs: Optional[float] = None,
|
|
207
|
+
shadow: bool = False, violation: bool = False,
|
|
208
|
+
stated_confidence: Optional[float] = None, *,
|
|
209
|
+
source: Optional[str] = None,
|
|
210
|
+
source_ref: Optional[str] = None,
|
|
211
|
+
event_key: Optional[str] = None,
|
|
212
|
+
evidence_status: str = "observed",
|
|
213
|
+
outcome_contract: str = "legacy.unversioned",
|
|
214
|
+
evidence: Optional[Dict[str, Any]] = None) -> bool:
|
|
215
|
+
"""Record one outcome for a domain. Never raises.
|
|
216
|
+
|
|
217
|
+
stated_confidence: the confidence the model STATED before doing the
|
|
218
|
+
work (charter contract); stored per event so calibration (stated vs
|
|
219
|
+
realized) is measurable and autonomy is earned against it.
|
|
220
|
+
"""
|
|
221
|
+
try:
|
|
222
|
+
domain = str(domain or "unknown").strip().lower()
|
|
223
|
+
outcome = _norm_outcome(str(outcome or ""))
|
|
224
|
+
source = str(source or "legacy_callsite").strip()[:128]
|
|
225
|
+
source_ref = str(source_ref or "").strip()[:512] or None
|
|
226
|
+
event_key = str(event_key or "").strip()[:512] or None
|
|
227
|
+
evidence_status = str(
|
|
228
|
+
evidence_status or "observed").strip().lower()
|
|
229
|
+
if evidence_status not in {
|
|
230
|
+
"verified", "observed", "unverified",
|
|
231
|
+
"legacy_unattributed"}:
|
|
232
|
+
evidence_status = "unverified"
|
|
233
|
+
outcome_contract = str(
|
|
234
|
+
outcome_contract or "legacy.unversioned").strip()[:128]
|
|
235
|
+
evidence_json = _canonical_json(evidence) if evidence else None
|
|
236
|
+
except Exception as exc:
|
|
237
|
+
logger.debug("competence record normalization failed: %s", exc)
|
|
238
|
+
return False
|
|
239
|
+
now = time.time()
|
|
240
|
+
try:
|
|
241
|
+
with self._lock:
|
|
242
|
+
if event_key is not None:
|
|
243
|
+
duplicate = self._conn.execute(
|
|
244
|
+
"""SELECT 1 FROM competence_events
|
|
245
|
+
WHERE source = ? AND event_key = ? LIMIT 1""",
|
|
246
|
+
(source, event_key),
|
|
247
|
+
).fetchone()
|
|
248
|
+
if duplicate is not None:
|
|
249
|
+
return False
|
|
250
|
+
row = self._conn.execute(
|
|
251
|
+
"SELECT ewma_latency_secs FROM competence WHERE domain=?",
|
|
252
|
+
(domain,)).fetchone()
|
|
253
|
+
ewma = row["ewma_latency_secs"] if row else None
|
|
254
|
+
if latency_secs is not None:
|
|
255
|
+
ewma = (float(latency_secs) if ewma is None else
|
|
256
|
+
_EWMA_ALPHA * float(latency_secs) + (1 - _EWMA_ALPHA) * float(ewma))
|
|
257
|
+
self._conn.execute(
|
|
258
|
+
f"""INSERT INTO competence
|
|
259
|
+
(domain, {outcome}, ewma_latency_secs, last_outcome, last_outcome_at)
|
|
260
|
+
VALUES (?, 1, ?, ?, ?)
|
|
261
|
+
ON CONFLICT(domain) DO UPDATE SET
|
|
262
|
+
{outcome}={outcome}+1, ewma_latency_secs=?,
|
|
263
|
+
last_outcome=?, last_outcome_at=?""",
|
|
264
|
+
(domain, ewma, outcome, now, ewma, outcome, now))
|
|
265
|
+
self._conn.execute(
|
|
266
|
+
"INSERT INTO competence_events (domain, outcome, shadow, "
|
|
267
|
+
"violation, stated_confidence, source, source_ref, event_key, "
|
|
268
|
+
"evidence_status, outcome_contract, evidence, ts) "
|
|
269
|
+
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
|
270
|
+
(domain, outcome, 1 if shadow else 0,
|
|
271
|
+
1 if violation else 0, stated_confidence, source,
|
|
272
|
+
source_ref, event_key, evidence_status, outcome_contract,
|
|
273
|
+
evidence_json, now))
|
|
274
|
+
# Do not prune evidence here. The reconciliation ledger must
|
|
275
|
+
# remain auditable against the immutable event it targets.
|
|
276
|
+
self._conn.commit()
|
|
277
|
+
return True
|
|
278
|
+
except Exception as exc:
|
|
279
|
+
# The aggregate update and immutable event insert are one logical
|
|
280
|
+
# evidence write. Never leave an uncommitted aggregate increment
|
|
281
|
+
# that a later, unrelated record could accidentally commit.
|
|
282
|
+
try:
|
|
283
|
+
with self._lock:
|
|
284
|
+
self._conn.rollback()
|
|
285
|
+
except Exception:
|
|
286
|
+
logger.debug(
|
|
287
|
+
"competence rollback failed for %s", domain,
|
|
288
|
+
exc_info=True,
|
|
289
|
+
)
|
|
290
|
+
logger.debug("competence record failed for %s: %s", domain, exc)
|
|
291
|
+
return False
|
|
292
|
+
|
|
293
|
+
def has_event_key(self, source: str, event_key: str) -> bool:
|
|
294
|
+
"""Whether one stable evidence event was already folded."""
|
|
295
|
+
|
|
296
|
+
source_value = str(source or "").strip()[:128]
|
|
297
|
+
key_value = str(event_key or "").strip()[:512]
|
|
298
|
+
if not source_value or not key_value:
|
|
299
|
+
return False
|
|
300
|
+
with self._lock:
|
|
301
|
+
row = self._conn.execute(
|
|
302
|
+
"""SELECT 1 FROM competence_events
|
|
303
|
+
WHERE source = ? AND event_key = ? LIMIT 1""",
|
|
304
|
+
(source_value, key_value),
|
|
305
|
+
).fetchone()
|
|
306
|
+
return row is not None
|
|
307
|
+
|
|
308
|
+
# -- append-only reconciliation -------------------------------------
|
|
309
|
+
@staticmethod
|
|
310
|
+
def _decode_json(value: Any) -> Any:
|
|
311
|
+
if value in (None, ""):
|
|
312
|
+
return None
|
|
313
|
+
try:
|
|
314
|
+
return json.loads(str(value))
|
|
315
|
+
except (TypeError, ValueError):
|
|
316
|
+
return value
|
|
317
|
+
|
|
318
|
+
def _ledger_rows(self) -> List[Dict[str, Any]]:
|
|
319
|
+
rows = self._conn.execute(
|
|
320
|
+
"SELECT * FROM competence_reconciliations ORDER BY seq ASC"
|
|
321
|
+
).fetchall()
|
|
322
|
+
out = []
|
|
323
|
+
for raw in rows:
|
|
324
|
+
row = dict(raw)
|
|
325
|
+
row["provenance"] = self._decode_json(row.get("provenance"))
|
|
326
|
+
out.append(row)
|
|
327
|
+
return out
|
|
328
|
+
|
|
329
|
+
def reconciliation_ledger(self) -> List[Dict[str, Any]]:
|
|
330
|
+
"""The immutable correction/gap audit trail, oldest first."""
|
|
331
|
+
with self._lock:
|
|
332
|
+
return self._ledger_rows()
|
|
333
|
+
|
|
334
|
+
@staticmethod
|
|
335
|
+
def _effective_corrections(
|
|
336
|
+
rows: Iterable[Dict[str, Any]]) -> Dict[int, Dict[str, Any]]:
|
|
337
|
+
effective: Dict[int, Dict[str, Any]] = {}
|
|
338
|
+
for row in rows:
|
|
339
|
+
if row.get("kind") != "event_correction":
|
|
340
|
+
continue
|
|
341
|
+
event_id = row.get("event_id")
|
|
342
|
+
if event_id is not None:
|
|
343
|
+
effective[int(event_id)] = row
|
|
344
|
+
return effective
|
|
345
|
+
|
|
346
|
+
def active_evidence_gaps(
|
|
347
|
+
self, domain: Optional[str] = None,
|
|
348
|
+
since: Optional[float] = None,
|
|
349
|
+
until: Optional[float] = None) -> List[Dict[str, Any]]:
|
|
350
|
+
"""Unresolved half-open windows whose events cannot be trusted.
|
|
351
|
+
|
|
352
|
+
A gap is deliberately broader than an event invalidation: it says the
|
|
353
|
+
available provenance cannot identify the contaminated rows exactly.
|
|
354
|
+
Consumers must report the affected metric unavailable, not guess.
|
|
355
|
+
"""
|
|
356
|
+
wanted = (domain or "").strip().lower() or None
|
|
357
|
+
with self._lock:
|
|
358
|
+
ledger = self._ledger_rows()
|
|
359
|
+
resolved = {
|
|
360
|
+
str(r.get("supersedes")) for r in ledger
|
|
361
|
+
if r.get("kind") == "gap_resolution" and r.get("supersedes")
|
|
362
|
+
}
|
|
363
|
+
out = []
|
|
364
|
+
for row in ledger:
|
|
365
|
+
if row.get("kind") != "evidence_gap":
|
|
366
|
+
continue
|
|
367
|
+
if row["reconciliation_id"] in resolved:
|
|
368
|
+
continue
|
|
369
|
+
if wanted is not None and row.get("domain") != wanted:
|
|
370
|
+
continue
|
|
371
|
+
gap_since = float(row["since_ts"])
|
|
372
|
+
gap_until = float(row["until_ts"])
|
|
373
|
+
if since is not None and gap_until <= float(since):
|
|
374
|
+
continue
|
|
375
|
+
if until is not None and gap_since >= float(until):
|
|
376
|
+
continue
|
|
377
|
+
out.append(row)
|
|
378
|
+
return out
|
|
379
|
+
|
|
380
|
+
def reconciliation_revision(
|
|
381
|
+
self, domains: Optional[Iterable[str]] = None,
|
|
382
|
+
since: Optional[float] = None,
|
|
383
|
+
until: Optional[float] = None) -> int:
|
|
384
|
+
"""Highest ledger sequence affecting the requested evidence slice."""
|
|
385
|
+
wanted = None
|
|
386
|
+
if domains is not None:
|
|
387
|
+
wanted = {(d or "").strip().lower() for d in domains if d}
|
|
388
|
+
with self._lock:
|
|
389
|
+
ledger = self._ledger_rows()
|
|
390
|
+
events = {int(r["id"]): dict(r) for r in self._conn.execute(
|
|
391
|
+
"SELECT id, domain, ts FROM competence_events").fetchall()}
|
|
392
|
+
gaps = {r["reconciliation_id"]: r for r in ledger
|
|
393
|
+
if r.get("kind") == "evidence_gap"}
|
|
394
|
+
|
|
395
|
+
def in_slice(domain: str, start: float, end: float) -> bool:
|
|
396
|
+
if wanted is not None and domain not in wanted:
|
|
397
|
+
return False
|
|
398
|
+
if since is not None and end <= float(since):
|
|
399
|
+
return False
|
|
400
|
+
if until is not None and start >= float(until):
|
|
401
|
+
return False
|
|
402
|
+
return True
|
|
403
|
+
|
|
404
|
+
def point_in_slice(domain: str, ts: float) -> bool:
|
|
405
|
+
if wanted is not None and domain not in wanted:
|
|
406
|
+
return False
|
|
407
|
+
if since is not None and ts < float(since):
|
|
408
|
+
return False
|
|
409
|
+
if until is not None and ts >= float(until):
|
|
410
|
+
return False
|
|
411
|
+
return True
|
|
412
|
+
|
|
413
|
+
revision = 0
|
|
414
|
+
for row in ledger:
|
|
415
|
+
kind = row.get("kind")
|
|
416
|
+
if kind == "event_correction":
|
|
417
|
+
ev = events.get(int(row.get("event_id") or -1))
|
|
418
|
+
if ev and point_in_slice(
|
|
419
|
+
str(ev["domain"]), float(ev["ts"])):
|
|
420
|
+
revision = max(revision, int(row["seq"]))
|
|
421
|
+
elif kind == "evidence_gap":
|
|
422
|
+
if in_slice(str(row["domain"]), float(row["since_ts"]),
|
|
423
|
+
float(row["until_ts"])):
|
|
424
|
+
revision = max(revision, int(row["seq"]))
|
|
425
|
+
elif kind == "gap_resolution":
|
|
426
|
+
gap = gaps.get(str(row.get("supersedes") or ""))
|
|
427
|
+
if gap and in_slice(str(gap["domain"]),
|
|
428
|
+
float(gap["since_ts"]),
|
|
429
|
+
float(gap["until_ts"])):
|
|
430
|
+
revision = max(revision, int(row["seq"]))
|
|
431
|
+
return revision
|
|
432
|
+
|
|
433
|
+
def events(self, domain: str, since: Optional[float] = None,
|
|
434
|
+
include_shadow: bool = True, *,
|
|
435
|
+
until: Optional[float] = None,
|
|
436
|
+
include_invalid: bool = False,
|
|
437
|
+
include_unavailable: bool = False,
|
|
438
|
+
limit: Optional[int] = None) -> List[Dict[str, Any]]:
|
|
439
|
+
"""Effective events for a domain, newest first.
|
|
440
|
+
|
|
441
|
+
Exact invalidations/replacements and unresolved evidence gaps are
|
|
442
|
+
applied at read time. Raw rows are never updated. There is no default
|
|
443
|
+
limit: weekly scorecards must not silently truncate a busy domain at
|
|
444
|
+
the old 1,000-event ceiling.
|
|
445
|
+
"""
|
|
446
|
+
domain = (domain or "").strip().lower()
|
|
447
|
+
q = "SELECT * FROM competence_events WHERE domain=?"
|
|
448
|
+
params: List[Any] = [domain]
|
|
449
|
+
if since is not None:
|
|
450
|
+
q += " AND ts >= ?"
|
|
451
|
+
params.append(float(since))
|
|
452
|
+
if until is not None:
|
|
453
|
+
q += " AND ts < ?"
|
|
454
|
+
params.append(float(until))
|
|
455
|
+
if not include_shadow:
|
|
456
|
+
q += " AND shadow=0"
|
|
457
|
+
q += " ORDER BY ts DESC, id DESC"
|
|
458
|
+
if limit is not None:
|
|
459
|
+
q += " LIMIT ?"
|
|
460
|
+
params.append(max(1, int(limit)))
|
|
461
|
+
with self._lock:
|
|
462
|
+
raw_rows = [dict(r) for r in self._conn.execute(q, params).fetchall()]
|
|
463
|
+
ledger = self._ledger_rows()
|
|
464
|
+
corrections = self._effective_corrections(ledger)
|
|
465
|
+
gaps = self.active_evidence_gaps(domain, since=since, until=until)
|
|
466
|
+
out: List[Dict[str, Any]] = []
|
|
467
|
+
for row in raw_rows:
|
|
468
|
+
row["source"] = row.get("source") or "legacy_unattributed"
|
|
469
|
+
row["evidence_status"] = (
|
|
470
|
+
row.get("evidence_status") or "legacy_unattributed")
|
|
471
|
+
row["outcome_contract"] = (
|
|
472
|
+
row.get("outcome_contract") or "legacy.unversioned")
|
|
473
|
+
row["evidence"] = self._decode_json(row.get("evidence"))
|
|
474
|
+
row["fingerprint"] = event_fingerprint(row)
|
|
475
|
+
row["valid"] = True
|
|
476
|
+
row["evidence_available"] = True
|
|
477
|
+
correction = corrections.get(int(row["id"]))
|
|
478
|
+
if correction is not None:
|
|
479
|
+
row["recorded_outcome"] = row["outcome"]
|
|
480
|
+
row["reconciliation"] = {
|
|
481
|
+
"id": correction["reconciliation_id"],
|
|
482
|
+
"disposition": correction["disposition"],
|
|
483
|
+
"reason": correction["reason"],
|
|
484
|
+
"provenance": correction["provenance"],
|
|
485
|
+
"created_by": correction["created_by"],
|
|
486
|
+
"created_at": correction["created_at"],
|
|
487
|
+
}
|
|
488
|
+
if correction["disposition"] == "invalidate":
|
|
489
|
+
row["valid"] = False
|
|
490
|
+
else:
|
|
491
|
+
row["outcome"] = correction["replacement_outcome"]
|
|
492
|
+
covering = [g for g in gaps
|
|
493
|
+
if float(g["since_ts"]) <= float(row["ts"])
|
|
494
|
+
< float(g["until_ts"])]
|
|
495
|
+
if covering:
|
|
496
|
+
row["evidence_available"] = False
|
|
497
|
+
row["evidence_gap_ids"] = [
|
|
498
|
+
g["reconciliation_id"] for g in covering]
|
|
499
|
+
if not row["valid"] and not include_invalid:
|
|
500
|
+
continue
|
|
501
|
+
if not row["evidence_available"] and not include_unavailable:
|
|
502
|
+
continue
|
|
503
|
+
out.append(row)
|
|
504
|
+
return out
|
|
505
|
+
|
|
506
|
+
def inspect_events(self, domain: str, since: float,
|
|
507
|
+
until: float) -> List[Dict[str, Any]]:
|
|
508
|
+
"""Raw, fingerprinted candidates for a human reconciliation plan."""
|
|
509
|
+
return self.events(
|
|
510
|
+
domain, since=since, until=until, include_shadow=True,
|
|
511
|
+
include_invalid=True, include_unavailable=True)
|
|
512
|
+
|
|
513
|
+
@staticmethod
|
|
514
|
+
def _provenance_text(value: Any) -> str:
|
|
515
|
+
if isinstance(value, str):
|
|
516
|
+
text = value.strip()
|
|
517
|
+
if not text:
|
|
518
|
+
raise ValueError("provenance must not be empty")
|
|
519
|
+
return _canonical_json({"statement": text})
|
|
520
|
+
if not isinstance(value, (dict, list)) or not value:
|
|
521
|
+
raise ValueError("provenance must be a non-empty object, list, or string")
|
|
522
|
+
try:
|
|
523
|
+
return _canonical_json(value)
|
|
524
|
+
except (TypeError, ValueError) as exc:
|
|
525
|
+
raise ValueError(f"provenance is not JSON-serializable: {exc}") from exc
|
|
526
|
+
|
|
527
|
+
def _prepare_reconciliation(
|
|
528
|
+
self, manifest: Dict[str, Any]) -> List[Dict[str, Any]]:
|
|
529
|
+
if not isinstance(manifest, dict):
|
|
530
|
+
raise ValueError("reconciliation manifest must be an object")
|
|
531
|
+
if manifest.get("schema") != "colony.competence-reconciliation/v1":
|
|
532
|
+
raise ValueError("unsupported reconciliation manifest schema")
|
|
533
|
+
created_by = str(manifest.get("created_by") or "").strip()
|
|
534
|
+
if not created_by:
|
|
535
|
+
raise ValueError("created_by is required")
|
|
536
|
+
default_reason = str(manifest.get("reason") or "").strip()
|
|
537
|
+
default_provenance = manifest.get("provenance")
|
|
538
|
+
ledger = self._ledger_rows()
|
|
539
|
+
by_id = {r["reconciliation_id"]: r for r in ledger}
|
|
540
|
+
current = self._effective_corrections(ledger)
|
|
541
|
+
active_gaps = {g["reconciliation_id"]: g
|
|
542
|
+
for g in self.active_evidence_gaps()}
|
|
543
|
+
prepared: List[Dict[str, Any]] = []
|
|
544
|
+
seen_event_ids = set()
|
|
545
|
+
groups: Dict[str, List[Dict[str, Any]]] = {}
|
|
546
|
+
for name in ("event_corrections", "evidence_gaps", "resolve_gaps"):
|
|
547
|
+
entries = manifest.get(name) or []
|
|
548
|
+
if not isinstance(entries, list) or any(
|
|
549
|
+
not isinstance(e, dict) for e in entries):
|
|
550
|
+
raise ValueError(f"{name} must be a list of objects")
|
|
551
|
+
groups[name] = entries
|
|
552
|
+
requested = sum(len(entries) for entries in groups.values())
|
|
553
|
+
|
|
554
|
+
def common(entry: Dict[str, Any]) -> Tuple[str, str]:
|
|
555
|
+
reason = str(entry.get("reason") or default_reason).strip()
|
|
556
|
+
if not reason:
|
|
557
|
+
raise ValueError("every reconciliation needs a reason")
|
|
558
|
+
provenance = self._provenance_text(
|
|
559
|
+
entry.get("provenance", default_provenance))
|
|
560
|
+
return reason[:2000], provenance
|
|
561
|
+
|
|
562
|
+
for entry in groups["event_corrections"]:
|
|
563
|
+
event_id = int(entry.get("event_id") or 0)
|
|
564
|
+
if event_id in seen_event_ids:
|
|
565
|
+
raise ValueError(
|
|
566
|
+
f"event {event_id} appears more than once in one manifest")
|
|
567
|
+
seen_event_ids.add(event_id)
|
|
568
|
+
raw = self._conn.execute(
|
|
569
|
+
"SELECT * FROM competence_events WHERE id=?", (event_id,)
|
|
570
|
+
).fetchone()
|
|
571
|
+
if raw is None:
|
|
572
|
+
raise ValueError(f"event {event_id} does not exist")
|
|
573
|
+
raw_dict = dict(raw)
|
|
574
|
+
fingerprint = str(entry.get("target_fingerprint") or "").lower()
|
|
575
|
+
actual = event_fingerprint(raw_dict)
|
|
576
|
+
if fingerprint != actual:
|
|
577
|
+
raise ValueError(
|
|
578
|
+
f"event {event_id} fingerprint mismatch: expected {actual}")
|
|
579
|
+
disposition = str(entry.get("disposition") or "").lower()
|
|
580
|
+
if disposition not in _DISPOSITIONS:
|
|
581
|
+
raise ValueError(
|
|
582
|
+
f"event {event_id} disposition must be invalidate or replace")
|
|
583
|
+
replacement = entry.get("replacement_outcome")
|
|
584
|
+
if disposition == "replace":
|
|
585
|
+
replacement = str(replacement or "").strip().lower()
|
|
586
|
+
if replacement not in _OUTCOMES:
|
|
587
|
+
raise ValueError(
|
|
588
|
+
f"event {event_id} replacement_outcome is invalid")
|
|
589
|
+
if replacement == raw_dict["outcome"]:
|
|
590
|
+
raise ValueError(
|
|
591
|
+
f"event {event_id} replacement equals recorded outcome")
|
|
592
|
+
elif replacement not in (None, ""):
|
|
593
|
+
raise ValueError(
|
|
594
|
+
f"event {event_id} invalidation cannot have a replacement")
|
|
595
|
+
else:
|
|
596
|
+
replacement = None
|
|
597
|
+
supersedes = str(entry.get("supersedes") or "").strip() or None
|
|
598
|
+
reason, provenance = common(entry)
|
|
599
|
+
identity = {
|
|
600
|
+
"kind": "event_correction", "event_id": event_id,
|
|
601
|
+
"target_fingerprint": fingerprint,
|
|
602
|
+
"disposition": disposition,
|
|
603
|
+
"replacement_outcome": replacement,
|
|
604
|
+
"supersedes": supersedes, "reason": reason,
|
|
605
|
+
"provenance": provenance, "created_by": created_by,
|
|
606
|
+
}
|
|
607
|
+
identity["reconciliation_id"] = _ledger_id(identity)
|
|
608
|
+
if identity["reconciliation_id"] in by_id:
|
|
609
|
+
continue
|
|
610
|
+
prior = current.get(event_id)
|
|
611
|
+
if prior is None and supersedes is not None:
|
|
612
|
+
raise ValueError(
|
|
613
|
+
f"event {event_id} has no correction to supersede")
|
|
614
|
+
if prior is not None and supersedes != prior["reconciliation_id"]:
|
|
615
|
+
raise ValueError(
|
|
616
|
+
f"event {event_id} already corrected; explicitly supersede "
|
|
617
|
+
f"{prior['reconciliation_id']}")
|
|
618
|
+
prepared.append(identity)
|
|
619
|
+
|
|
620
|
+
for entry in groups["evidence_gaps"]:
|
|
621
|
+
domain = str(entry.get("domain") or "").strip().lower()
|
|
622
|
+
if not domain or domain == "*":
|
|
623
|
+
raise ValueError("evidence gap requires one exact domain")
|
|
624
|
+
since_ts = float(entry.get("since_ts"))
|
|
625
|
+
until_ts = float(entry.get("until_ts"))
|
|
626
|
+
if (not math.isfinite(since_ts) or not math.isfinite(until_ts)
|
|
627
|
+
or since_ts >= until_ts):
|
|
628
|
+
raise ValueError("evidence gap must have since_ts < until_ts")
|
|
629
|
+
reason, provenance = common(entry)
|
|
630
|
+
identity = {
|
|
631
|
+
"kind": "evidence_gap", "domain": domain,
|
|
632
|
+
"since_ts": since_ts, "until_ts": until_ts,
|
|
633
|
+
"reason": reason, "provenance": provenance,
|
|
634
|
+
"created_by": created_by,
|
|
635
|
+
}
|
|
636
|
+
identity["reconciliation_id"] = _ledger_id(identity)
|
|
637
|
+
prepared.append(identity)
|
|
638
|
+
|
|
639
|
+
for entry in groups["resolve_gaps"]:
|
|
640
|
+
gap_id = str(entry.get("gap_id") or "").strip()
|
|
641
|
+
reason, provenance = common(entry)
|
|
642
|
+
identity = {
|
|
643
|
+
"kind": "gap_resolution", "supersedes": gap_id,
|
|
644
|
+
"reason": reason, "provenance": provenance,
|
|
645
|
+
"created_by": created_by,
|
|
646
|
+
}
|
|
647
|
+
identity["reconciliation_id"] = _ledger_id(identity)
|
|
648
|
+
if identity["reconciliation_id"] in by_id:
|
|
649
|
+
continue
|
|
650
|
+
if gap_id not in active_gaps:
|
|
651
|
+
raise ValueError(f"gap {gap_id!r} is not active")
|
|
652
|
+
prepared.append(identity)
|
|
653
|
+
|
|
654
|
+
if requested == 0:
|
|
655
|
+
raise ValueError("manifest contains no reconciliation entries")
|
|
656
|
+
ids = [r["reconciliation_id"] for r in prepared]
|
|
657
|
+
if len(ids) != len(set(ids)):
|
|
658
|
+
raise ValueError("manifest contains duplicate entries")
|
|
659
|
+
# Re-applying the exact same gap/correction manifest is idempotent.
|
|
660
|
+
return [r for r in prepared if r["reconciliation_id"] not in by_id]
|
|
661
|
+
|
|
662
|
+
def plan_reconciliation(self, manifest: Dict[str, Any]) -> Dict[str, Any]:
|
|
663
|
+
"""Validate a manifest without mutating the database."""
|
|
664
|
+
with self._lock:
|
|
665
|
+
rows = self._prepare_reconciliation(manifest)
|
|
666
|
+
return {"valid": True, "would_append": len(rows), "entries": rows}
|
|
667
|
+
|
|
668
|
+
def apply_reconciliation(self, manifest: Dict[str, Any]) -> Dict[str, Any]:
|
|
669
|
+
"""Atomically append a validated manifest to the audit ledger."""
|
|
670
|
+
with self._lock:
|
|
671
|
+
try:
|
|
672
|
+
self._conn.execute("BEGIN IMMEDIATE")
|
|
673
|
+
rows = self._prepare_reconciliation(manifest)
|
|
674
|
+
now = time.time()
|
|
675
|
+
for row in rows:
|
|
676
|
+
self._conn.execute(
|
|
677
|
+
"""INSERT INTO competence_reconciliations
|
|
678
|
+
(reconciliation_id, kind, event_id,
|
|
679
|
+
target_fingerprint, disposition,
|
|
680
|
+
replacement_outcome, domain, since_ts, until_ts,
|
|
681
|
+
supersedes, reason, provenance, created_by,
|
|
682
|
+
created_at)
|
|
683
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
|
|
684
|
+
(row["reconciliation_id"], row["kind"],
|
|
685
|
+
row.get("event_id"), row.get("target_fingerprint"),
|
|
686
|
+
row.get("disposition"),
|
|
687
|
+
row.get("replacement_outcome"), row.get("domain"),
|
|
688
|
+
row.get("since_ts"), row.get("until_ts"),
|
|
689
|
+
row.get("supersedes"), row["reason"],
|
|
690
|
+
row["provenance"], row["created_by"], now))
|
|
691
|
+
self._conn.commit()
|
|
692
|
+
except Exception:
|
|
693
|
+
self._conn.rollback()
|
|
694
|
+
raise
|
|
695
|
+
return {"applied": len(rows),
|
|
696
|
+
"reconciliation_ids": [r["reconciliation_id"] for r in rows]}
|
|
697
|
+
|
|
698
|
+
def calibration(self, domain: str) -> Optional[Dict[str, Any]]:
|
|
699
|
+
"""Stated-vs-realized calibration for a domain: mean absolute error
|
|
700
|
+
between the confidence the model stated and the realized outcome
|
|
701
|
+
(success=1, else 0), over non-shadow events that stated one."""
|
|
702
|
+
events = [e for e in self.events(domain, include_shadow=False)
|
|
703
|
+
if e.get("stated_confidence") is not None]
|
|
704
|
+
if not events:
|
|
705
|
+
return None
|
|
706
|
+
errs = [abs(float(e["stated_confidence"])
|
|
707
|
+
- (1.0 if e["outcome"] == "success" else 0.0))
|
|
708
|
+
for e in events]
|
|
709
|
+
realized = sum(1.0 for e in events
|
|
710
|
+
if e["outcome"] == "success") / len(events)
|
|
711
|
+
return {"n": len(events),
|
|
712
|
+
"mean_abs_error": round(sum(errs) / len(errs), 3),
|
|
713
|
+
"mean_stated": round(sum(float(e["stated_confidence"])
|
|
714
|
+
for e in events) / len(events), 3),
|
|
715
|
+
"mean_realized": round(realized, 3)}
|
|
716
|
+
|
|
717
|
+
def get(self, domain: str) -> Optional[Dict[str, Any]]:
|
|
718
|
+
domain = (domain or "").strip().lower()
|
|
719
|
+
with self._lock:
|
|
720
|
+
row = self._conn.execute(
|
|
721
|
+
"SELECT * FROM competence WHERE domain=?", (domain,)).fetchone()
|
|
722
|
+
return self._annotate_domain(dict(row)) if row else None
|
|
723
|
+
|
|
724
|
+
def snapshot(self) -> List[Dict[str, Any]]:
|
|
725
|
+
with self._lock:
|
|
726
|
+
rows = self._conn.execute(
|
|
727
|
+
"SELECT * FROM competence ORDER BY last_outcome_at DESC").fetchall()
|
|
728
|
+
return [self._annotate_domain(dict(r)) for r in rows]
|
|
729
|
+
|
|
730
|
+
def _annotate_domain(self, d: Dict[str, Any]) -> Dict[str, Any]:
|
|
731
|
+
"""Apply ledger deltas without rewriting the legacy aggregate row."""
|
|
732
|
+
domain = str(d.get("domain") or "")
|
|
733
|
+
base_n = sum(int(d.get(o) or 0) for o in _OUTCOMES)
|
|
734
|
+
effective = self.events(
|
|
735
|
+
domain, include_shadow=True, include_invalid=True,
|
|
736
|
+
include_unavailable=True)
|
|
737
|
+
excluded_ids = set()
|
|
738
|
+
corrected = 0
|
|
739
|
+
for event in effective:
|
|
740
|
+
reconciliation = event.get("reconciliation")
|
|
741
|
+
if reconciliation is not None:
|
|
742
|
+
corrected += 1
|
|
743
|
+
recorded = str(event.get("recorded_outcome") or "")
|
|
744
|
+
if recorded in _OUTCOMES:
|
|
745
|
+
d[recorded] = max(0, int(d.get(recorded) or 0) - 1)
|
|
746
|
+
if event.get("valid"):
|
|
747
|
+
replacement = str(event.get("outcome") or "")
|
|
748
|
+
if replacement in _OUTCOMES:
|
|
749
|
+
d[replacement] = int(d.get(replacement) or 0) + 1
|
|
750
|
+
else:
|
|
751
|
+
excluded_ids.add(int(event["id"]))
|
|
752
|
+
if (event.get("valid")
|
|
753
|
+
and not event.get("evidence_available", True)):
|
|
754
|
+
outcome = str(event.get("outcome") or "")
|
|
755
|
+
if outcome in _OUTCOMES:
|
|
756
|
+
d[outcome] = max(0, int(d.get(outcome) or 0) - 1)
|
|
757
|
+
excluded_ids.add(int(event["id"]))
|
|
758
|
+
gaps = self.active_evidence_gaps(domain)
|
|
759
|
+
available = self.events(domain, include_shadow=True)
|
|
760
|
+
if available:
|
|
761
|
+
d["last_outcome"] = available[0]["outcome"]
|
|
762
|
+
d["last_outcome_at"] = available[0]["ts"]
|
|
763
|
+
elif base_n:
|
|
764
|
+
d["last_outcome"] = None
|
|
765
|
+
d["last_outcome_at"] = None
|
|
766
|
+
n = sum(int(d.get(o) or 0) for o in _OUTCOMES)
|
|
767
|
+
d["n"] = n
|
|
768
|
+
# A declared gap means even the denominator is not knowable. Expose
|
|
769
|
+
# the remaining evidence counts, but never turn them into a rate.
|
|
770
|
+
d["success_rate"] = (
|
|
771
|
+
round(int(d.get("success") or 0) / n, 3)
|
|
772
|
+
if n and not gaps else None)
|
|
773
|
+
d["timeout_rate"] = (
|
|
774
|
+
round(int(d.get("timeout") or 0) / n, 3)
|
|
775
|
+
if n and not gaps else None)
|
|
776
|
+
d["evidence_available"] = not gaps
|
|
777
|
+
d["evidence_gaps"] = [g["reconciliation_id"] for g in gaps]
|
|
778
|
+
d["reconciled_events"] = corrected
|
|
779
|
+
d["excluded_events"] = len(excluded_ids)
|
|
780
|
+
d["event_history_complete"] = len(effective) >= base_n
|
|
781
|
+
return d
|
|
782
|
+
|
|
783
|
+
|
|
784
|
+
class SelfModel:
|
|
785
|
+
"""Competence store + live load probe + brief rendering.
|
|
786
|
+
|
|
787
|
+
Load is read live from the wired subsystems (never cached): in-progress
|
|
788
|
+
executor initiatives, active projects, queued jobs. Any missing subsystem
|
|
789
|
+
contributes zero.
|
|
790
|
+
"""
|
|
791
|
+
|
|
792
|
+
def __init__(self, store: CompetenceStore, registry: Any = None,
|
|
793
|
+
trust: Any = None, journal: Any = None) -> None:
|
|
794
|
+
self.store = store
|
|
795
|
+
self._registry = registry
|
|
796
|
+
self.trust = trust # TrustEngine (Amendment 1)
|
|
797
|
+
self.journal = journal # ActionJournal (Amendment 1)
|
|
798
|
+
self.perspective = None # Optional source-backed working judgments; never authority.
|
|
799
|
+
|
|
800
|
+
# -- recording (thin passthrough; trust engine hooks demotion) -------
|
|
801
|
+
def record(self, domain: str, outcome: str,
|
|
802
|
+
latency_secs: Optional[float] = None,
|
|
803
|
+
shadow: bool = False, violation: bool = False,
|
|
804
|
+
stated_confidence: Optional[float] = None, *,
|
|
805
|
+
source: Optional[str] = None,
|
|
806
|
+
source_ref: Optional[str] = None,
|
|
807
|
+
event_key: Optional[str] = None,
|
|
808
|
+
evidence_status: str = "observed",
|
|
809
|
+
outcome_contract: str = "legacy.unversioned",
|
|
810
|
+
evidence: Optional[Dict[str, Any]] = None,
|
|
811
|
+
defer_trust: bool = False) -> bool:
|
|
812
|
+
recorded = self.store.record(domain, outcome, latency_secs=latency_secs,
|
|
813
|
+
shadow=shadow, violation=violation,
|
|
814
|
+
stated_confidence=stated_confidence,
|
|
815
|
+
source=source, source_ref=source_ref,
|
|
816
|
+
event_key=event_key,
|
|
817
|
+
evidence_status=evidence_status,
|
|
818
|
+
outcome_contract=outcome_contract,
|
|
819
|
+
evidence=evidence)
|
|
820
|
+
trust = getattr(self, "trust", None)
|
|
821
|
+
if recorded and trust is not None and not defer_trust:
|
|
822
|
+
try:
|
|
823
|
+
trust.after_outcome(domain)
|
|
824
|
+
except Exception:
|
|
825
|
+
logger.debug("trust after_outcome failed", exc_info=True)
|
|
826
|
+
return recorded
|
|
827
|
+
|
|
828
|
+
def reconcile_trust(self, domain: str) -> None:
|
|
829
|
+
"""Apply deterministic trust transitions and propagate failures."""
|
|
830
|
+
|
|
831
|
+
trust = getattr(self, "trust", None)
|
|
832
|
+
if trust is not None:
|
|
833
|
+
trust.after_outcome(domain)
|
|
834
|
+
|
|
835
|
+
# -- live load -------------------------------------------------------
|
|
836
|
+
def load(self) -> Dict[str, int]:
|
|
837
|
+
active_initiatives = 0
|
|
838
|
+
active_projects = 0
|
|
839
|
+
queued_jobs = 0
|
|
840
|
+
reg = self._registry
|
|
841
|
+
if reg is not None:
|
|
842
|
+
try:
|
|
843
|
+
istore = getattr(reg, "initiative_store", None)
|
|
844
|
+
if istore is not None and hasattr(istore, "count"):
|
|
845
|
+
active_initiatives = int(
|
|
846
|
+
istore.count(status=["assigned", "acknowledged"]) or 0)
|
|
847
|
+
except Exception:
|
|
848
|
+
pass
|
|
849
|
+
try:
|
|
850
|
+
pengine = getattr(reg, "project_engine", None)
|
|
851
|
+
pstore = getattr(pengine, "store", None)
|
|
852
|
+
if pstore is not None and hasattr(pstore, "count"):
|
|
853
|
+
active_projects = int(pstore.count(status="active") or 0)
|
|
854
|
+
except Exception:
|
|
855
|
+
pass
|
|
856
|
+
try:
|
|
857
|
+
queue = getattr(reg, "task_queue", None)
|
|
858
|
+
if queue is not None and hasattr(queue, "count_pending"):
|
|
859
|
+
queued_jobs = int(queue.count_pending() or 0)
|
|
860
|
+
except Exception:
|
|
861
|
+
pass
|
|
862
|
+
return {
|
|
863
|
+
"active_initiatives": active_initiatives,
|
|
864
|
+
"active_projects": active_projects,
|
|
865
|
+
"queued_jobs": queued_jobs,
|
|
866
|
+
"total": active_initiatives + active_projects + queued_jobs,
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
def status(self) -> Dict[str, Any]:
|
|
870
|
+
out = {"domains": self.store.snapshot(), "load": self.load()}
|
|
871
|
+
if self.trust is not None:
|
|
872
|
+
try:
|
|
873
|
+
out["trust"] = self.trust.snapshot()
|
|
874
|
+
except Exception:
|
|
875
|
+
pass
|
|
876
|
+
return out
|
|
877
|
+
|
|
878
|
+
def brief(self) -> str:
|
|
879
|
+
from apsimo.self_model.brief import self_brief
|
|
880
|
+
parts = [self_brief(self.store.snapshot(), self.load())]
|
|
881
|
+
if self.perspective is not None:
|
|
882
|
+
parts.append(self.perspective.brief())
|
|
883
|
+
return '\n'.join(part for part in parts if part)
|