shellbrain 0.1.12__tar.gz → 0.1.14__tar.gz
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.
- {shellbrain-0.1.12 → shellbrain-0.1.14}/PKG-INFO +14 -1
- {shellbrain-0.1.12 → shellbrain-0.1.14}/README.md +13 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/core/interfaces/repos.py +12 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/core/policies/_shared/executor.py +2 -2
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/core/use_cases/sync_episode.py +8 -12
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/periphery/admin/analytics_diagnostics.py +1 -1
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/periphery/admin/instance_guard.py +20 -1
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/periphery/cli/main.py +87 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/periphery/db/repos/relational/episodes_repo.py +55 -1
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/periphery/db/repos/relational/evidence_repo.py +10 -1
- shellbrain-0.1.14/app/periphery/metrics/__init__.py +1 -0
- shellbrain-0.1.14/app/periphery/metrics/artifacts.py +44 -0
- shellbrain-0.1.14/app/periphery/metrics/browser.py +12 -0
- shellbrain-0.1.14/app/periphery/metrics/queries.py +192 -0
- shellbrain-0.1.14/app/periphery/metrics/render_html.py +512 -0
- shellbrain-0.1.14/app/periphery/metrics/service.py +425 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/pyproject.toml +1 -1
- {shellbrain-0.1.12 → shellbrain-0.1.14}/shellbrain.egg-info/PKG-INFO +14 -1
- {shellbrain-0.1.12 → shellbrain-0.1.14}/shellbrain.egg-info/SOURCES.txt +6 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/__init__.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/__main__.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/boot/__init__.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/boot/admin_db.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/boot/config.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/boot/create_policy.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/boot/db.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/boot/embeddings.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/boot/home.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/boot/migrations.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/boot/read_policy.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/boot/repos.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/boot/retrieval.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/boot/thresholds.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/boot/update_policy.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/boot/use_cases.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/config/__init__.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/config/defaults/create_policy.yaml +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/config/defaults/read_policy.yaml +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/config/defaults/runtime.yaml +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/config/defaults/thresholds.yaml +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/config/defaults/update_policy.yaml +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/config/loader.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/core/__init__.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/core/contracts/__init__.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/core/contracts/errors.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/core/contracts/requests.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/core/contracts/responses.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/core/entities/__init__.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/core/entities/associations.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/core/entities/episodes.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/core/entities/evidence.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/core/entities/facts.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/core/entities/guidance.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/core/entities/identity.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/core/entities/memory.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/core/entities/runtime_context.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/core/entities/session_state.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/core/entities/telemetry.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/core/entities/utility.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/core/interfaces/__init__.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/core/interfaces/clock.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/core/interfaces/config.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/core/interfaces/embeddings.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/core/interfaces/idgen.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/core/interfaces/retrieval.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/core/interfaces/session_state_store.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/core/interfaces/unit_of_work.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/core/policies/__init__.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/core/policies/_shared/__init__.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/core/policies/_shared/side_effects.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/core/policies/create_policy/__init__.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/core/policies/create_policy/pipeline.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/core/policies/read_policy/__init__.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/core/policies/read_policy/bm25.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/core/policies/read_policy/context_pack_builder.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/core/policies/read_policy/expansion.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/core/policies/read_policy/fusion_rrf.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/core/policies/read_policy/lexical_query.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/core/policies/read_policy/pipeline.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/core/policies/read_policy/scenario_lift.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/core/policies/read_policy/scoring.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/core/policies/read_policy/seed_retrieval.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/core/policies/read_policy/utility_prior.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/core/policies/update_policy/__init__.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/core/policies/update_policy/pipeline.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/core/use_cases/__init__.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/core/use_cases/build_guidance.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/core/use_cases/create_memory.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/core/use_cases/manage_session_state.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/core/use_cases/read_memory.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/core/use_cases/record_episode_sync_telemetry.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/core/use_cases/record_operation_telemetry.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/core/use_cases/update_memory.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/migrations/__init__.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/migrations/env.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/migrations/versions/20260226_0001_initial_schema.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/migrations/versions/20260312_0002_add_hard_invariants.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/migrations/versions/20260312_0003_drop_create_confidence.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/migrations/versions/20260313_0004_episode_sync_hardening.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/migrations/versions/20260313_0005_evidence_episode_event_refs.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/migrations/versions/20260318_0006_usage_telemetry_schema.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/migrations/versions/20260319_0007_identity_session_guidance.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/migrations/versions/20260320_0008_instance_metadata_and_backup_safety.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/migrations/versions/__init__.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/onboarding_assets/__init__.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/onboarding_assets/claude/skills/shellbrain-session-start/SKILL.md +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/onboarding_assets/claude/skills/shellbrain-usage-review/SKILL.md +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/onboarding_assets/codex/shellbrain-session-start/SKILL.md +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/onboarding_assets/codex/shellbrain-session-start/agents/openai.yaml +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/onboarding_assets/codex/shellbrain-session-start/assets/shellbrain-large.svg +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/onboarding_assets/codex/shellbrain-session-start/assets/shellbrain-small.svg +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/onboarding_assets/codex/shellbrain-session-start/references/request-shapes.md +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/onboarding_assets/codex/shellbrain-session-start/references/session-workflow.md +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/onboarding_assets/codex/shellbrain-usage-review/SKILL.md +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/onboarding_assets/codex/shellbrain-usage-review/agents/openai.yaml +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/onboarding_assets/cursor/skills/shellbrain-session-start/SKILL.md +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/onboarding_assets/cursor/skills/shellbrain-usage-review/SKILL.md +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/periphery/__init__.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/periphery/admin/__init__.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/periphery/admin/analytics.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/periphery/admin/analytics_queries.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/periphery/admin/backup.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/periphery/admin/destructive_guard.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/periphery/admin/doctor.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/periphery/admin/external_runtime.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/periphery/admin/init.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/periphery/admin/init_errors.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/periphery/admin/machine_state.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/periphery/admin/managed_runtime.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/periphery/admin/privileges.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/periphery/admin/repo_state.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/periphery/admin/restore.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/periphery/admin/storage_setup.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/periphery/admin/upgrade.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/periphery/cli/__init__.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/periphery/cli/handlers.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/periphery/cli/hydration.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/periphery/cli/presenter_json.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/periphery/cli/schema_validation.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/periphery/db/__init__.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/periphery/db/engine.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/periphery/db/models/__init__.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/periphery/db/models/associations.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/periphery/db/models/episodes.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/periphery/db/models/evidence.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/periphery/db/models/experiences.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/periphery/db/models/instance_metadata.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/periphery/db/models/memories.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/periphery/db/models/metadata.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/periphery/db/models/registry.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/periphery/db/models/telemetry.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/periphery/db/models/utility.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/periphery/db/models/views.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/periphery/db/repos/__init__.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/periphery/db/repos/relational/__init__.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/periphery/db/repos/relational/associations_repo.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/periphery/db/repos/relational/experiences_repo.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/periphery/db/repos/relational/memories_repo.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/periphery/db/repos/relational/read_policy_repo.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/periphery/db/repos/relational/telemetry_repo.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/periphery/db/repos/relational/utility_repo.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/periphery/db/repos/semantic/__init__.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/periphery/db/repos/semantic/keyword_retrieval_repo.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/periphery/db/repos/semantic/semantic_retrieval_repo.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/periphery/db/session.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/periphery/db/uow.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/periphery/embeddings/__init__.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/periphery/embeddings/local_provider.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/periphery/embeddings/query_vector_search.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/periphery/episodes/__init__.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/periphery/episodes/claude_code.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/periphery/episodes/codex.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/periphery/episodes/cursor.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/periphery/episodes/launcher.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/periphery/episodes/normalization.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/periphery/episodes/poller.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/periphery/episodes/poller_lock.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/periphery/episodes/source_discovery.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/periphery/episodes/tool_filter.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/periphery/identity/__init__.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/periphery/identity/claude_hook_install.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/periphery/identity/claude_runtime.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/periphery/identity/codex_runtime.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/periphery/identity/compatibility.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/periphery/identity/resolver.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/periphery/onboarding/__init__.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/periphery/onboarding/host_assets.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/periphery/session_state/__init__.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/periphery/session_state/file_store.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/periphery/telemetry/__init__.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/periphery/telemetry/operation_summary.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/periphery/telemetry/session_selection.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/periphery/telemetry/sync_summary.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/periphery/validation/__init__.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/periphery/validation/integrity_validation.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/app/periphery/validation/semantic_validation.py +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/setup.cfg +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/shellbrain.egg-info/dependency_links.txt +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/shellbrain.egg-info/entry_points.txt +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/shellbrain.egg-info/requires.txt +0 -0
- {shellbrain-0.1.12 → shellbrain-0.1.14}/shellbrain.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: shellbrain
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.14
|
|
4
4
|
Summary: Repo-scoped Shellbrain CLI with explicit evidence-backed writes.
|
|
5
5
|
Requires-Python: >=3.11
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -96,6 +96,19 @@ Fish PATH setup is written to `~/.config/fish/conf.d/shellbrain.fish`.
|
|
|
96
96
|
|
|
97
97
|
---
|
|
98
98
|
|
|
99
|
+
## DB-backed tests
|
|
100
|
+
|
|
101
|
+
**Live memories and DB-backed tests now use different Postgres hosts.**
|
|
102
|
+
|
|
103
|
+
- managed local Shellbrain keeps durable memories on the machine-owned managed instance
|
|
104
|
+
- DB-backed tests and scratch validation should use the dedicated repo-owned test host from `docker-compose.test.yml`
|
|
105
|
+
- `scripts/run_tests` provisions a disposable test database on that dedicated host by default
|
|
106
|
+
- `scripts/storage_status` shows the live managed target, the dedicated test host, and any legacy local test host that is still hanging around
|
|
107
|
+
|
|
108
|
+
If you are running managed local Shellbrain, do not leave a stale `SHELLBRAIN_DB_DSN` export in your shell profile that points at the old local compose database. The machine config wins anyway, and the stale env var just makes the storage layout harder to reason about.
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
99
112
|
## Docs
|
|
100
113
|
|
|
101
114
|
- [shellbrain.ai/humans](https://shellbrain.ai/humans/) — install, upgrade, getting started
|
|
@@ -82,6 +82,19 @@ Fish PATH setup is written to `~/.config/fish/conf.d/shellbrain.fish`.
|
|
|
82
82
|
|
|
83
83
|
---
|
|
84
84
|
|
|
85
|
+
## DB-backed tests
|
|
86
|
+
|
|
87
|
+
**Live memories and DB-backed tests now use different Postgres hosts.**
|
|
88
|
+
|
|
89
|
+
- managed local Shellbrain keeps durable memories on the machine-owned managed instance
|
|
90
|
+
- DB-backed tests and scratch validation should use the dedicated repo-owned test host from `docker-compose.test.yml`
|
|
91
|
+
- `scripts/run_tests` provisions a disposable test database on that dedicated host by default
|
|
92
|
+
- `scripts/storage_status` shows the live managed target, the dedicated test host, and any legacy local test host that is still hanging around
|
|
93
|
+
|
|
94
|
+
If you are running managed local Shellbrain, do not leave a stale `SHELLBRAIN_DB_DSN` export in your shell profile that points at the old local compose database. The machine config wins anyway, and the stale env var just makes the storage layout harder to reason about.
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
85
98
|
## Docs
|
|
86
99
|
|
|
87
100
|
- [shellbrain.ai/humans](https://shellbrain.ai/humans/) — install, upgrade, getting started
|
|
@@ -85,6 +85,14 @@ class IEpisodesRepo(ABC):
|
|
|
85
85
|
def create_episode(self, episode: Episode) -> None:
|
|
86
86
|
"""This method persists an episode row."""
|
|
87
87
|
|
|
88
|
+
@abstractmethod
|
|
89
|
+
def acquire_thread_sync_guard(self, *, repo_id: str, thread_id: str) -> None:
|
|
90
|
+
"""This method serializes sync writes for one repo/thread pair."""
|
|
91
|
+
|
|
92
|
+
@abstractmethod
|
|
93
|
+
def get_or_create_episode_for_thread(self, episode: Episode) -> Episode:
|
|
94
|
+
"""This method returns the canonical episode row for one thread, creating it when missing."""
|
|
95
|
+
|
|
88
96
|
@abstractmethod
|
|
89
97
|
def get_episode_by_thread(
|
|
90
98
|
self,
|
|
@@ -106,6 +114,10 @@ class IEpisodesRepo(ABC):
|
|
|
106
114
|
def append_event(self, event: EpisodeEvent) -> None:
|
|
107
115
|
"""This method appends an event into an episode stream."""
|
|
108
116
|
|
|
117
|
+
@abstractmethod
|
|
118
|
+
def append_event_if_new(self, event: EpisodeEvent) -> bool:
|
|
119
|
+
"""This method appends an event only when its host_event_key is not already present."""
|
|
120
|
+
|
|
109
121
|
@abstractmethod
|
|
110
122
|
def close_episode(self, *, episode_id: str, ended_at: datetime) -> None:
|
|
111
123
|
"""This method marks an active episode closed."""
|
|
@@ -52,7 +52,7 @@ def apply_side_effects(
|
|
|
52
52
|
if effect_type == "memory_evidence.attach":
|
|
53
53
|
refs = params["refs"]
|
|
54
54
|
assert isinstance(refs, list)
|
|
55
|
-
for ref in refs:
|
|
55
|
+
for ref in sorted(str(ref) for ref in refs):
|
|
56
56
|
evidence = uow.evidence.upsert_ref(repo_id=str(params["repo_id"]), ref=str(ref))
|
|
57
57
|
uow.evidence.link_memory_evidence(memory_id=str(params["memory_id"]), evidence_id=evidence.id)
|
|
58
58
|
continue
|
|
@@ -124,7 +124,7 @@ def apply_side_effects(
|
|
|
124
124
|
)
|
|
125
125
|
evidence_refs = params.get("evidence_refs", [])
|
|
126
126
|
assert isinstance(evidence_refs, list)
|
|
127
|
-
for ref in evidence_refs:
|
|
127
|
+
for ref in sorted(str(ref) for ref in evidence_refs):
|
|
128
128
|
evidence = uow.evidence.upsert_ref(repo_id=str(params["repo_id"]), ref=str(ref))
|
|
129
129
|
uow.evidence.link_association_edge_evidence(edge_id=edge.id, evidence_id=evidence.id)
|
|
130
130
|
continue
|
|
@@ -25,12 +25,11 @@ def sync_episode(
|
|
|
25
25
|
"""Import one already-normalized host transcript into episodes and episode events."""
|
|
26
26
|
|
|
27
27
|
counts = _count_normalized_events(normalized_events)
|
|
28
|
-
|
|
28
|
+
uow.episodes.acquire_thread_sync_guard(repo_id=repo_id, thread_id=thread_id)
|
|
29
29
|
imported_count = 0
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
episode = Episode(
|
|
30
|
+
started_at = _earliest_event_timestamp(normalized_events) or datetime.now(timezone.utc)
|
|
31
|
+
episode = uow.episodes.get_or_create_episode_for_thread(
|
|
32
|
+
Episode(
|
|
34
33
|
id=str(uuid4()),
|
|
35
34
|
repo_id=repo_id,
|
|
36
35
|
host_app=host_app,
|
|
@@ -39,17 +38,13 @@ def sync_episode(
|
|
|
39
38
|
started_at=started_at,
|
|
40
39
|
created_at=datetime.now(timezone.utc),
|
|
41
40
|
)
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
existing_keys = set(uow.episodes.list_event_keys(episode_id=episode.id))
|
|
41
|
+
)
|
|
45
42
|
next_seq = uow.episodes.next_event_seq(episode_id=episode.id)
|
|
46
43
|
for normalized_event in normalized_events:
|
|
47
44
|
host_event_key = str(normalized_event["host_event_key"])
|
|
48
|
-
if host_event_key in existing_keys:
|
|
49
|
-
continue
|
|
50
45
|
created_at = _parse_timestamp(str(normalized_event["occurred_at"]))
|
|
51
46
|
source = EpisodeEventSource(str(normalized_event["source"]))
|
|
52
|
-
uow.episodes.
|
|
47
|
+
inserted = uow.episodes.append_event_if_new(
|
|
53
48
|
EpisodeEvent(
|
|
54
49
|
id=str(uuid4()),
|
|
55
50
|
episode_id=episode.id,
|
|
@@ -60,7 +55,8 @@ def sync_episode(
|
|
|
60
55
|
created_at=created_at,
|
|
61
56
|
)
|
|
62
57
|
)
|
|
63
|
-
|
|
58
|
+
if not inserted:
|
|
59
|
+
continue
|
|
64
60
|
next_seq += 1
|
|
65
61
|
imported_count += 1
|
|
66
62
|
|
|
@@ -13,7 +13,7 @@ def classify_operation_failure(
|
|
|
13
13
|
"""Return one stable diagnosis payload for an operation failure."""
|
|
14
14
|
|
|
15
15
|
message = (error_message or "").lower()
|
|
16
|
-
if "uq_evidence_repo_ref" in message:
|
|
16
|
+
if "uq_evidence_repo_ref" in message or "uq_evidence_repo_episode_event" in message:
|
|
17
17
|
return _diagnosis(
|
|
18
18
|
category="duplicate_evidence_ref",
|
|
19
19
|
summary="Evidence refs are being inserted twice for the same repo/event pair.",
|
|
@@ -45,17 +45,36 @@ def dsn_fingerprint(dsn: str) -> str:
|
|
|
45
45
|
return hashlib.sha256(normalize_dsn(dsn).encode("utf-8")).hexdigest()
|
|
46
46
|
|
|
47
47
|
|
|
48
|
+
def host_port_from_dsn(dsn: str) -> tuple[str, int]:
|
|
49
|
+
"""Extract the normalized host/port pair from one DSN."""
|
|
50
|
+
|
|
51
|
+
parsed = urlparse(dsn.replace("+psycopg", ""))
|
|
52
|
+
return ((parsed.hostname or "").lower(), parsed.port or 5432)
|
|
53
|
+
|
|
54
|
+
|
|
48
55
|
def database_name_from_dsn(dsn: str) -> str:
|
|
49
56
|
"""Extract the target database name from one DSN."""
|
|
50
57
|
|
|
51
58
|
return urlparse(dsn.replace("+psycopg", "")).path.lstrip("/")
|
|
52
59
|
|
|
53
60
|
|
|
54
|
-
def assert_disposable_test_dsn(
|
|
61
|
+
def assert_disposable_test_dsn(
|
|
62
|
+
*,
|
|
63
|
+
test_dsn: str,
|
|
64
|
+
protected_dsn: str | None = None,
|
|
65
|
+
protected_host_ports: set[tuple[str, int]] | None = None,
|
|
66
|
+
) -> None:
|
|
55
67
|
"""Refuse to treat a protected or production-shaped database as disposable."""
|
|
56
68
|
|
|
57
69
|
if protected_dsn and dsn_fingerprint(test_dsn) == dsn_fingerprint(protected_dsn):
|
|
58
70
|
raise RuntimeError("Refusing destructive test setup against the protected live database DSN.")
|
|
71
|
+
protected_pairs = set(protected_host_ports or set())
|
|
72
|
+
if protected_dsn:
|
|
73
|
+
protected_pairs.add(host_port_from_dsn(protected_dsn))
|
|
74
|
+
if host_port_from_dsn(test_dsn) in protected_pairs:
|
|
75
|
+
raise RuntimeError(
|
|
76
|
+
"Refusing destructive test setup against the protected live database host/port."
|
|
77
|
+
)
|
|
59
78
|
db_name = database_name_from_dsn(test_dsn).lower()
|
|
60
79
|
if db_name in PROTECTED_DB_NAMES:
|
|
61
80
|
raise RuntimeError(
|
|
@@ -60,6 +60,7 @@ _TOP_LEVEL_HELP = dedent(
|
|
|
60
60
|
Examples:
|
|
61
61
|
shellbrain init
|
|
62
62
|
shellbrain upgrade
|
|
63
|
+
shellbrain metrics --days 30
|
|
63
64
|
shellbrain read --json '{"query":"Have we seen this migration lock timeout before?","kinds":["problem","solution","failed_tactic"]}'
|
|
64
65
|
shellbrain read --json '{"query":"What repo constraints or user preferences matter for this auth refactor?","kinds":["fact","preference","change"]}'
|
|
65
66
|
shellbrain events --json '{"limit":10}'
|
|
@@ -229,6 +230,17 @@ _ANALYTICS_HELP = dedent(
|
|
|
229
230
|
"""
|
|
230
231
|
)
|
|
231
232
|
|
|
233
|
+
_METRICS_HELP = dedent(
|
|
234
|
+
"""\
|
|
235
|
+
Generate one lightweight repo-scoped metrics snapshot, write local artifacts, and open a static dashboard.
|
|
236
|
+
|
|
237
|
+
Examples:
|
|
238
|
+
shellbrain metrics
|
|
239
|
+
shellbrain metrics --days 30
|
|
240
|
+
shellbrain metrics --days 14 --no-open
|
|
241
|
+
"""
|
|
242
|
+
)
|
|
243
|
+
|
|
232
244
|
_INSTALL_CLAUDE_HOOK_HELP = dedent(
|
|
233
245
|
"""\
|
|
234
246
|
Install or update the repo-local Claude Code SessionStart hook used as an explicit repo-local override.
|
|
@@ -344,6 +356,26 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
344
356
|
formatter_class=_HelpFormatter,
|
|
345
357
|
)
|
|
346
358
|
|
|
359
|
+
metrics_parser = subparsers.add_parser(
|
|
360
|
+
"metrics",
|
|
361
|
+
help="Open a lightweight repo-scoped metrics dashboard.",
|
|
362
|
+
description="Generate one local metrics snapshot and open a static Shellbrain dashboard.",
|
|
363
|
+
epilog=_METRICS_HELP,
|
|
364
|
+
formatter_class=_HelpFormatter,
|
|
365
|
+
)
|
|
366
|
+
_add_repo_context_arguments(metrics_parser, suppress_default=True)
|
|
367
|
+
metrics_parser.add_argument(
|
|
368
|
+
"--days",
|
|
369
|
+
type=int,
|
|
370
|
+
default=30,
|
|
371
|
+
help="Number of trailing days to include in the snapshot. Defaults to 30.",
|
|
372
|
+
)
|
|
373
|
+
metrics_parser.add_argument(
|
|
374
|
+
"--no-open",
|
|
375
|
+
action="store_true",
|
|
376
|
+
help="Generate artifacts without opening the dashboard in the browser.",
|
|
377
|
+
)
|
|
378
|
+
|
|
347
379
|
create_parser = subparsers.add_parser(
|
|
348
380
|
"create",
|
|
349
381
|
help="Create one Shellbrain entry from explicit evidence.",
|
|
@@ -523,6 +555,9 @@ def main(argv: Sequence[str] | None = None) -> int:
|
|
|
523
555
|
|
|
524
556
|
return run_upgrade()
|
|
525
557
|
|
|
558
|
+
if args.command == "metrics":
|
|
559
|
+
return _run_metrics_command(args)
|
|
560
|
+
|
|
526
561
|
if args.command == "admin":
|
|
527
562
|
return _run_admin_command(args)
|
|
528
563
|
|
|
@@ -776,6 +811,58 @@ def _run_admin_command(args: argparse.Namespace) -> int:
|
|
|
776
811
|
raise ValueError(f"Unsupported admin command: {args.admin_command}")
|
|
777
812
|
|
|
778
813
|
|
|
814
|
+
def _run_metrics_command(args: argparse.Namespace) -> int:
|
|
815
|
+
"""Generate one repo-scoped metrics snapshot and local dashboard artifacts."""
|
|
816
|
+
|
|
817
|
+
try:
|
|
818
|
+
from app.boot.admin_db import get_optional_admin_db_dsn
|
|
819
|
+
from app.boot.db import get_optional_db_dsn
|
|
820
|
+
from app.periphery.db.engine import get_engine
|
|
821
|
+
from app.periphery.metrics.artifacts import write_metrics_artifacts
|
|
822
|
+
from app.periphery.metrics.browser import open_metrics_dashboard
|
|
823
|
+
from app.periphery.metrics.render_html import render_metrics_dashboard
|
|
824
|
+
from app.periphery.metrics.service import build_metrics_snapshot
|
|
825
|
+
|
|
826
|
+
repo_context = resolve_repo_context(
|
|
827
|
+
repo_root_arg=getattr(args, "repo_root", None),
|
|
828
|
+
repo_id_arg=getattr(args, "repo_id", None),
|
|
829
|
+
)
|
|
830
|
+
_warn_or_fail_on_unsafe_app_role()
|
|
831
|
+
_ensure_repo_registration_for_operation(
|
|
832
|
+
repo_context=repo_context,
|
|
833
|
+
repo_id_override=getattr(args, "repo_id", None),
|
|
834
|
+
)
|
|
835
|
+
dsn = get_optional_db_dsn() or get_optional_admin_db_dsn()
|
|
836
|
+
if not dsn:
|
|
837
|
+
raise RuntimeError("Shellbrain database is not configured. Run `shellbrain init` first.")
|
|
838
|
+
snapshot = build_metrics_snapshot(
|
|
839
|
+
engine=get_engine(dsn),
|
|
840
|
+
repo_id=repo_context.repo_id,
|
|
841
|
+
days=int(args.days),
|
|
842
|
+
)
|
|
843
|
+
html = render_metrics_dashboard(snapshot)
|
|
844
|
+
paths = write_metrics_artifacts(repo_id=repo_context.repo_id, snapshot=snapshot, html=html)
|
|
845
|
+
opened_dashboard = False
|
|
846
|
+
if not bool(getattr(args, "no_open", False)):
|
|
847
|
+
opened_dashboard = bool(open_metrics_dashboard(paths["html_path"]))
|
|
848
|
+
print(f"Generated Shellbrain metrics for {repo_context.repo_id}")
|
|
849
|
+
print(f"Status: {snapshot['status']} ({snapshot['confidence']} confidence)")
|
|
850
|
+
print(f"JSON: {paths['json_path']}")
|
|
851
|
+
print(f"Markdown: {paths['md_path']}")
|
|
852
|
+
print(f"Dashboard: {paths['html_path']}")
|
|
853
|
+
print("Artifacts: updated in place")
|
|
854
|
+
if bool(getattr(args, "no_open", False)):
|
|
855
|
+
print("Browser: skipped")
|
|
856
|
+
elif opened_dashboard:
|
|
857
|
+
print("Browser: opened dashboard")
|
|
858
|
+
else:
|
|
859
|
+
print("Browser: could not open automatically")
|
|
860
|
+
return 0
|
|
861
|
+
except (RuntimeError, ValueError) as exc:
|
|
862
|
+
print(str(exc), file=sys.stderr)
|
|
863
|
+
return 1
|
|
864
|
+
|
|
865
|
+
|
|
779
866
|
def _print_operation_result(result: dict[str, Any]) -> None:
|
|
780
867
|
"""Render one operation result as JSON for agent consumption."""
|
|
781
868
|
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
from datetime import datetime, timezone
|
|
4
4
|
from typing import Sequence
|
|
5
5
|
|
|
6
|
-
from sqlalchemy import func, select, update
|
|
6
|
+
from sqlalchemy import func, select, text, update
|
|
7
|
+
from sqlalchemy.dialects.postgresql import insert
|
|
7
8
|
|
|
8
9
|
from app.core.entities.episodes import Episode, EpisodeEvent, EpisodeEventSource, EpisodeStatus, SessionTransfer
|
|
9
10
|
from app.core.interfaces.repos import IEpisodesRepo
|
|
@@ -36,6 +37,40 @@ class EpisodesRepo(IEpisodesRepo):
|
|
|
36
37
|
)
|
|
37
38
|
)
|
|
38
39
|
|
|
40
|
+
def acquire_thread_sync_guard(self, *, repo_id: str, thread_id: str) -> None:
|
|
41
|
+
"""This method serializes sync writes for one repo/thread pair."""
|
|
42
|
+
|
|
43
|
+
self._session.execute(
|
|
44
|
+
text("SELECT pg_advisory_xact_lock(hashtext(:repo_id), hashtext(:thread_id))"),
|
|
45
|
+
{"repo_id": repo_id, "thread_id": thread_id},
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
def get_or_create_episode_for_thread(self, episode: Episode) -> Episode:
|
|
49
|
+
"""This method returns the canonical episode row for one thread, creating it when missing."""
|
|
50
|
+
|
|
51
|
+
if episode.thread_id is None:
|
|
52
|
+
raise ValueError("thread_id is required when ensuring an episode for sync")
|
|
53
|
+
self._session.execute(
|
|
54
|
+
insert(episodes)
|
|
55
|
+
.values(
|
|
56
|
+
id=episode.id,
|
|
57
|
+
repo_id=episode.repo_id,
|
|
58
|
+
host_app=episode.host_app,
|
|
59
|
+
thread_id=episode.thread_id,
|
|
60
|
+
title=episode.title,
|
|
61
|
+
objective=episode.objective,
|
|
62
|
+
status=episode.status.value,
|
|
63
|
+
started_at=episode.started_at or datetime.now(timezone.utc),
|
|
64
|
+
ended_at=episode.ended_at,
|
|
65
|
+
created_at=episode.created_at or datetime.now(timezone.utc),
|
|
66
|
+
)
|
|
67
|
+
.on_conflict_do_nothing(index_elements=["repo_id", "thread_id"])
|
|
68
|
+
)
|
|
69
|
+
stored = self.get_episode_by_thread(repo_id=episode.repo_id, thread_id=episode.thread_id)
|
|
70
|
+
if stored is None:
|
|
71
|
+
raise RuntimeError("episode ensure failed to return a canonical thread row")
|
|
72
|
+
return stored
|
|
73
|
+
|
|
39
74
|
def get_episode_by_thread(
|
|
40
75
|
self,
|
|
41
76
|
*,
|
|
@@ -100,6 +135,25 @@ class EpisodesRepo(IEpisodesRepo):
|
|
|
100
135
|
)
|
|
101
136
|
)
|
|
102
137
|
|
|
138
|
+
def append_event_if_new(self, event: EpisodeEvent) -> bool:
|
|
139
|
+
"""This method appends an episode event only when its host_event_key is new."""
|
|
140
|
+
|
|
141
|
+
inserted_id = self._session.execute(
|
|
142
|
+
insert(episode_events)
|
|
143
|
+
.values(
|
|
144
|
+
id=event.id,
|
|
145
|
+
episode_id=event.episode_id,
|
|
146
|
+
seq=event.seq,
|
|
147
|
+
host_event_key=event.host_event_key,
|
|
148
|
+
source=event.source.value,
|
|
149
|
+
content=event.content,
|
|
150
|
+
created_at=event.created_at or datetime.now(timezone.utc),
|
|
151
|
+
)
|
|
152
|
+
.on_conflict_do_nothing(index_elements=["episode_id", "host_event_key"])
|
|
153
|
+
.returning(episode_events.c.id)
|
|
154
|
+
).scalar_one_or_none()
|
|
155
|
+
return inserted_id is not None
|
|
156
|
+
|
|
103
157
|
def close_episode(self, *, episode_id: str, ended_at: datetime) -> None:
|
|
104
158
|
"""This method marks an active episode closed."""
|
|
105
159
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
from datetime import datetime, timezone
|
|
4
4
|
from uuid import uuid4
|
|
5
5
|
|
|
6
|
-
from sqlalchemy import select, update
|
|
6
|
+
from sqlalchemy import select, text, update
|
|
7
7
|
from sqlalchemy.dialects.postgresql import insert
|
|
8
8
|
|
|
9
9
|
from app.core.entities.evidence import EvidenceRef
|
|
@@ -24,6 +24,7 @@ class EvidenceRepo(IEvidenceRepo):
|
|
|
24
24
|
def upsert_ref(self, repo_id: str, ref: str) -> EvidenceRef:
|
|
25
25
|
"""This method inserts or returns a canonical evidence reference row."""
|
|
26
26
|
|
|
27
|
+
self._acquire_ref_guard(repo_id=repo_id, ref=ref)
|
|
27
28
|
existing = (
|
|
28
29
|
self._session.execute(
|
|
29
30
|
select(evidence_refs).where(
|
|
@@ -63,6 +64,14 @@ class EvidenceRepo(IEvidenceRepo):
|
|
|
63
64
|
)
|
|
64
65
|
return EvidenceRef(id=evidence_id, repo_id=repo_id, ref=ref, episode_event_id=ref)
|
|
65
66
|
|
|
67
|
+
def _acquire_ref_guard(self, *, repo_id: str, ref: str) -> None:
|
|
68
|
+
"""Serialize concurrent writes for one repo/ref pair within the active transaction."""
|
|
69
|
+
|
|
70
|
+
self._session.execute(
|
|
71
|
+
text("SELECT pg_advisory_xact_lock(hashtext(:repo_id), hashtext(:ref))"),
|
|
72
|
+
{"repo_id": repo_id, "ref": ref},
|
|
73
|
+
)
|
|
74
|
+
|
|
66
75
|
def link_memory_evidence(self, memory_id: str, evidence_id: str) -> None:
|
|
67
76
|
"""This method creates shellbrain-to-evidence link rows."""
|
|
68
77
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Periphery-only helpers for the Shellbrain metrics dashboard."""
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""Artifact helpers for generated metrics snapshots and dashboards."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
import re
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from app.boot.home import get_shellbrain_home
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
_NON_ALNUM = re.compile(r"[^a-z0-9]+")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def get_metrics_artifact_dir(*, repo_id: str) -> Path:
|
|
18
|
+
"""Return the machine-owned artifact directory for one repo's metrics outputs."""
|
|
19
|
+
|
|
20
|
+
normalized = _NON_ALNUM.sub("-", repo_id.lower()).strip("-") or "repo"
|
|
21
|
+
digest = hashlib.sha1(repo_id.encode("utf-8")).hexdigest()[:8]
|
|
22
|
+
return get_shellbrain_home() / "reports" / "metrics" / f"{normalized}-{digest}"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def write_metrics_artifacts(*, repo_id: str, snapshot: dict[str, Any], html: str) -> dict[str, Path]:
|
|
26
|
+
"""Write the latest metrics snapshot, markdown summary, and dashboard HTML."""
|
|
27
|
+
|
|
28
|
+
artifact_dir = get_metrics_artifact_dir(repo_id=repo_id)
|
|
29
|
+
artifact_dir.mkdir(parents=True, exist_ok=True)
|
|
30
|
+
|
|
31
|
+
json_path = artifact_dir / "latest.json"
|
|
32
|
+
md_path = artifact_dir / "latest.md"
|
|
33
|
+
html_path = artifact_dir / "dashboard.html"
|
|
34
|
+
|
|
35
|
+
json_path.write_text(json.dumps(snapshot, indent=2, sort_keys=True), encoding="utf-8")
|
|
36
|
+
md_path.write_text(str(snapshot["summary_md"]).strip() + "\n", encoding="utf-8")
|
|
37
|
+
html_path.write_text(html, encoding="utf-8")
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
"artifact_dir": artifact_dir,
|
|
41
|
+
"json_path": json_path,
|
|
42
|
+
"md_path": md_path,
|
|
43
|
+
"html_path": html_path,
|
|
44
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""Browser helpers for opening generated metrics dashboards."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
import webbrowser
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def open_metrics_dashboard(path: Path) -> bool:
|
|
10
|
+
"""Open one generated dashboard in the default browser."""
|
|
11
|
+
|
|
12
|
+
return bool(webbrowser.open(path.resolve().as_uri()))
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
"""SQL query helpers for repo-scoped Shellbrain metrics snapshots."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
|
|
7
|
+
from sqlalchemy import text
|
|
8
|
+
from sqlalchemy.engine import Connection
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def fetch_daily_utility_rows(
|
|
12
|
+
*,
|
|
13
|
+
conn: Connection,
|
|
14
|
+
repo_id: str,
|
|
15
|
+
start_at: datetime,
|
|
16
|
+
end_at: datetime,
|
|
17
|
+
) -> list[dict[str, object]]:
|
|
18
|
+
"""Return daily utility vote aggregates for one repo and time range."""
|
|
19
|
+
|
|
20
|
+
rows = conn.execute(
|
|
21
|
+
text(
|
|
22
|
+
"""
|
|
23
|
+
SELECT
|
|
24
|
+
date_trunc('day', u.created_at AT TIME ZONE 'UTC') AS day_utc,
|
|
25
|
+
COUNT(*)::INTEGER AS vote_count,
|
|
26
|
+
COALESCE(SUM(u.vote), 0)::DOUBLE PRECISION AS vote_sum
|
|
27
|
+
FROM utility_observations u
|
|
28
|
+
JOIN memories problem_mem ON problem_mem.id = u.problem_id
|
|
29
|
+
WHERE problem_mem.repo_id = :repo_id
|
|
30
|
+
AND u.created_at >= :start_at
|
|
31
|
+
AND u.created_at < :end_at
|
|
32
|
+
GROUP BY date_trunc('day', u.created_at AT TIME ZONE 'UTC')
|
|
33
|
+
ORDER BY day_utc ASC;
|
|
34
|
+
"""
|
|
35
|
+
),
|
|
36
|
+
{"repo_id": repo_id, "start_at": start_at, "end_at": end_at},
|
|
37
|
+
).mappings()
|
|
38
|
+
return [dict(row) for row in rows]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def fetch_daily_followthrough_rows(
|
|
42
|
+
*,
|
|
43
|
+
conn: Connection,
|
|
44
|
+
repo_id: str,
|
|
45
|
+
start_at: datetime,
|
|
46
|
+
end_at: datetime,
|
|
47
|
+
) -> list[dict[str, object]]:
|
|
48
|
+
"""Return daily utility-guidance follow-through counts for one repo and time range."""
|
|
49
|
+
|
|
50
|
+
rows = conn.execute(
|
|
51
|
+
text(
|
|
52
|
+
"""
|
|
53
|
+
WITH pending_threads AS (
|
|
54
|
+
SELECT
|
|
55
|
+
oi.repo_id,
|
|
56
|
+
oi.selected_thread_id,
|
|
57
|
+
MIN(oi.created_at) AS first_guidance_at
|
|
58
|
+
FROM operation_invocations oi
|
|
59
|
+
WHERE oi.repo_id = :repo_id
|
|
60
|
+
AND oi.created_at >= :start_at
|
|
61
|
+
AND oi.created_at < :end_at
|
|
62
|
+
AND oi.selected_thread_id IS NOT NULL
|
|
63
|
+
AND oi.guidance_codes @> '["pending_utility_votes"]'::jsonb
|
|
64
|
+
GROUP BY oi.repo_id, oi.selected_thread_id
|
|
65
|
+
),
|
|
66
|
+
vote_threads AS (
|
|
67
|
+
SELECT
|
|
68
|
+
oi.repo_id,
|
|
69
|
+
oi.selected_thread_id,
|
|
70
|
+
MIN(oi.created_at) AS first_vote_at
|
|
71
|
+
FROM write_invocation_summaries wis
|
|
72
|
+
JOIN operation_invocations oi ON oi.id = wis.invocation_id
|
|
73
|
+
WHERE oi.repo_id = :repo_id
|
|
74
|
+
AND oi.created_at >= :start_at
|
|
75
|
+
AND oi.created_at < :end_at
|
|
76
|
+
AND oi.selected_thread_id IS NOT NULL
|
|
77
|
+
AND wis.update_type IN ('utility_vote', 'utility_vote_batch')
|
|
78
|
+
GROUP BY oi.repo_id, oi.selected_thread_id
|
|
79
|
+
)
|
|
80
|
+
SELECT
|
|
81
|
+
date_trunc('day', pending.first_guidance_at AT TIME ZONE 'UTC') AS day_utc,
|
|
82
|
+
COUNT(*)::INTEGER AS opportunity_count,
|
|
83
|
+
COUNT(*) FILTER (
|
|
84
|
+
WHERE votes.first_vote_at IS NOT NULL
|
|
85
|
+
AND votes.first_vote_at > pending.first_guidance_at
|
|
86
|
+
)::INTEGER AS followthrough_count
|
|
87
|
+
FROM pending_threads pending
|
|
88
|
+
LEFT JOIN vote_threads votes
|
|
89
|
+
ON votes.repo_id = pending.repo_id
|
|
90
|
+
AND votes.selected_thread_id = pending.selected_thread_id
|
|
91
|
+
GROUP BY date_trunc('day', pending.first_guidance_at AT TIME ZONE 'UTC')
|
|
92
|
+
ORDER BY day_utc ASC;
|
|
93
|
+
"""
|
|
94
|
+
),
|
|
95
|
+
{"repo_id": repo_id, "start_at": start_at, "end_at": end_at},
|
|
96
|
+
).mappings()
|
|
97
|
+
return [dict(row) for row in rows]
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def fetch_daily_zero_result_rows(
|
|
101
|
+
*,
|
|
102
|
+
conn: Connection,
|
|
103
|
+
repo_id: str,
|
|
104
|
+
start_at: datetime,
|
|
105
|
+
end_at: datetime,
|
|
106
|
+
) -> list[dict[str, object]]:
|
|
107
|
+
"""Return daily read and zero-result counts for one repo and time range."""
|
|
108
|
+
|
|
109
|
+
rows = conn.execute(
|
|
110
|
+
text(
|
|
111
|
+
"""
|
|
112
|
+
SELECT
|
|
113
|
+
date_trunc('day', oi.created_at AT TIME ZONE 'UTC') AS day_utc,
|
|
114
|
+
COUNT(*)::INTEGER AS read_count,
|
|
115
|
+
COUNT(*) FILTER (WHERE ris.zero_results)::INTEGER AS zero_result_count
|
|
116
|
+
FROM read_invocation_summaries ris
|
|
117
|
+
JOIN operation_invocations oi ON oi.id = ris.invocation_id
|
|
118
|
+
WHERE oi.repo_id = :repo_id
|
|
119
|
+
AND oi.created_at >= :start_at
|
|
120
|
+
AND oi.created_at < :end_at
|
|
121
|
+
GROUP BY date_trunc('day', oi.created_at AT TIME ZONE 'UTC')
|
|
122
|
+
ORDER BY day_utc ASC;
|
|
123
|
+
"""
|
|
124
|
+
),
|
|
125
|
+
{"repo_id": repo_id, "start_at": start_at, "end_at": end_at},
|
|
126
|
+
).mappings()
|
|
127
|
+
return [dict(row) for row in rows]
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def fetch_daily_events_before_write_rows(
|
|
131
|
+
*,
|
|
132
|
+
conn: Connection,
|
|
133
|
+
repo_id: str,
|
|
134
|
+
start_at: datetime,
|
|
135
|
+
end_at: datetime,
|
|
136
|
+
) -> list[dict[str, object]]:
|
|
137
|
+
"""Return daily write counts and events-before-write compliance for one repo."""
|
|
138
|
+
|
|
139
|
+
rows = conn.execute(
|
|
140
|
+
text(
|
|
141
|
+
"""
|
|
142
|
+
SELECT
|
|
143
|
+
date_trunc('day', oi.created_at AT TIME ZONE 'UTC') AS day_utc,
|
|
144
|
+
COUNT(*)::INTEGER AS write_count,
|
|
145
|
+
COUNT(*) FILTER (
|
|
146
|
+
WHERE EXISTS (
|
|
147
|
+
SELECT 1
|
|
148
|
+
FROM operation_invocations prior_events
|
|
149
|
+
WHERE prior_events.repo_id = oi.repo_id
|
|
150
|
+
AND prior_events.selected_thread_id = oi.selected_thread_id
|
|
151
|
+
AND prior_events.command = 'events'
|
|
152
|
+
AND prior_events.created_at < oi.created_at
|
|
153
|
+
)
|
|
154
|
+
)::INTEGER AS compliant_count
|
|
155
|
+
FROM write_invocation_summaries wis
|
|
156
|
+
JOIN operation_invocations oi ON oi.id = wis.invocation_id
|
|
157
|
+
WHERE oi.repo_id = :repo_id
|
|
158
|
+
AND oi.created_at >= :start_at
|
|
159
|
+
AND oi.created_at < :end_at
|
|
160
|
+
GROUP BY date_trunc('day', oi.created_at AT TIME ZONE 'UTC')
|
|
161
|
+
ORDER BY day_utc ASC;
|
|
162
|
+
"""
|
|
163
|
+
),
|
|
164
|
+
{"repo_id": repo_id, "start_at": start_at, "end_at": end_at},
|
|
165
|
+
).mappings()
|
|
166
|
+
return [dict(row) for row in rows]
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def fetch_sync_health_summary(
|
|
170
|
+
*,
|
|
171
|
+
conn: Connection,
|
|
172
|
+
repo_id: str,
|
|
173
|
+
start_at: datetime,
|
|
174
|
+
end_at: datetime,
|
|
175
|
+
) -> dict[str, object]:
|
|
176
|
+
"""Return current-window sync health counts for one repo."""
|
|
177
|
+
|
|
178
|
+
row = conn.execute(
|
|
179
|
+
text(
|
|
180
|
+
"""
|
|
181
|
+
SELECT
|
|
182
|
+
COUNT(*)::INTEGER AS sync_run_count,
|
|
183
|
+
COUNT(*) FILTER (WHERE outcome = 'error')::INTEGER AS failed_sync_count
|
|
184
|
+
FROM episode_sync_runs
|
|
185
|
+
WHERE repo_id = :repo_id
|
|
186
|
+
AND created_at >= :start_at
|
|
187
|
+
AND created_at < :end_at;
|
|
188
|
+
"""
|
|
189
|
+
),
|
|
190
|
+
{"repo_id": repo_id, "start_at": start_at, "end_at": end_at},
|
|
191
|
+
).mappings().one()
|
|
192
|
+
return dict(row)
|