coding-os 0.3.2__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.
- adapters/claude/README.md +6 -0
- adapters/claude/_install_helpers/extract_stacks.py +43 -0
- adapters/claude/_install_helpers/update_mcp_json.py +75 -0
- adapters/claude/adapter.yaml +164 -0
- adapters/claude/hooks/README.md +40 -0
- adapters/claude/hooks/agent_memory_sync.py +131 -0
- adapters/claude/hooks/ensure-agent-memory-link.sh +36 -0
- adapters/claude/hooks/sync-agent-memory.sh +21 -0
- adapters/claude/install.sh +86 -0
- adapters/claude/sdk_dispatcher.py +871 -0
- adapters/claude/settings.local.template.json +31 -0
- adapters/claude/settings.template.json +808 -0
- adapters/claude/update_mcp_json.py +85 -0
- adapters/codex/adapter.yaml +254 -0
- adapters/codex/chat_provider.py +230 -0
- adapters/codex/commands/formula-f1.md +129 -0
- adapters/codex/commands/formula-f10.md +100 -0
- adapters/codex/commands/formula-f11.md +123 -0
- adapters/codex/commands/formula-f2.md +139 -0
- adapters/codex/commands/formula-f3.md +127 -0
- adapters/codex/commands/formula-f4.md +101 -0
- adapters/codex/commands/formula-f5.md +135 -0
- adapters/codex/commands/formula-f6.md +147 -0
- adapters/codex/commands/formula-f7.md +111 -0
- adapters/codex/commands/formula-f8.md +133 -0
- adapters/codex/commands/formula-f9.md +112 -0
- adapters/codex/enable_codex_hooks.py +94 -0
- adapters/codex/ensure_codex_mcp.py +124 -0
- adapters/codex/hooks/codex-merge-hook-output.py +72 -0
- adapters/codex/hooks/codex-normalize-edit.py +96 -0
- adapters/codex/hooks/codex-postedit-dispatch.sh +75 -0
- adapters/codex/hooks/codex-posttool-dispatch.sh +70 -0
- adapters/codex/hooks/codex-preedit-dispatch.sh +83 -0
- adapters/codex/hooks/codex-pretool-dispatch.sh +82 -0
- adapters/codex/hooks/codex-sessionend-dispatch.sh +20 -0
- adapters/codex/hooks/codex-sessionstart-dispatch.sh +68 -0
- adapters/codex/hooks/codex-stop-dispatch.sh +73 -0
- adapters/codex/hooks/codex-userpromptsubmit-dispatch.sh +74 -0
- adapters/codex/hooks.template.json +208 -0
- adapters/codex/install.sh +83 -0
- adapters/codex/sdk_dispatcher.py +449 -0
- board_os/__init__.py +39 -0
- board_os/_agent_runtime.py +256 -0
- board_os/config.py +421 -0
- board_os/git_coherence.py +107 -0
- board_os/hub_adapter_manifest.py +140 -0
- board_os/mcp_tools.py +3228 -0
- board_os/migration.py +166 -0
- board_os/parser.py +317 -0
- board_os/presence.py +156 -0
- board_os/sync.py +320 -0
- board_os/transition_gates.py +224 -0
- board_os/transition_gates_cli.py +272 -0
- board_os/transition_gates_validator.py +551 -0
- board_os/verify_suites.py +126 -0
- board_os/verify_suites_cli.py +328 -0
- board_os/workflow.py +967 -0
- cli/__init__.py +0 -0
- cli/_data_types.py +248 -0
- cli/_init_helpers.py +587 -0
- cli/_resources.py +100 -0
- cli/adapter_registry.py +239 -0
- cli/add_stack.py +315 -0
- cli/aggregator.py +438 -0
- cli/board_commands.py +1218 -0
- cli/brain_commands.py +255 -0
- cli/cognition.py +345 -0
- cli/config_composer.py +349 -0
- cli/core_version.py +41 -0
- cli/cron_commands.py +278 -0
- cli/db_reset.py +298 -0
- cli/doc_commands.py +111 -0
- cli/doctor.py +2953 -0
- cli/doctor_board.py +365 -0
- cli/doctor_extras.py +1121 -0
- cli/doctor_graph.py +608 -0
- cli/doctor_tokens.py +254 -0
- cli/graph_commands.py +1265 -0
- cli/hook_renderer.py +393 -0
- cli/hub_commands.py +580 -0
- cli/list_adapters.py +79 -0
- cli/list_stacks.py +105 -0
- cli/logs_commands.py +89 -0
- cli/main.py +3070 -0
- cli/materialize_file.py +65 -0
- cli/mcp_start.py +153 -0
- cli/module_commands.py +513 -0
- cli/pr_commands.py +2024 -0
- cli/preset_commands.py +126 -0
- cli/preset_registry.py +171 -0
- cli/project_overrides.py +119 -0
- cli/registry.py +365 -0
- cli/remove_stack.py +492 -0
- cli/renderer.py +620 -0
- cli/setup.py +464 -0
- cli/skill_commands.py +689 -0
- cli/skill_registry.py +235 -0
- cli/skills_list.py +332 -0
- cli/stack_lint.py +351 -0
- cli/stack_registry.py +688 -0
- cli/subsystems.py +335 -0
- cli/sync_all.py +310 -0
- cli/tail_command.py +410 -0
- cli/update.py +608 -0
- cli/verify_since_edit.py +439 -0
- coding_os-0.3.2.dist-info/METADATA +508 -0
- coding_os-0.3.2.dist-info/RECORD +1304 -0
- coding_os-0.3.2.dist-info/WHEEL +5 -0
- coding_os-0.3.2.dist-info/entry_points.txt +5 -0
- coding_os-0.3.2.dist-info/licenses/LICENSE +201 -0
- coding_os-0.3.2.dist-info/top_level.txt +10 -0
- core/__init__.py +0 -0
- core/board_os/__init__.py +39 -0
- core/board_os/_agent_runtime.py +256 -0
- core/board_os/config.py +421 -0
- core/board_os/git_coherence.py +107 -0
- core/board_os/hub_adapter_manifest.py +140 -0
- core/board_os/mcp_tools.py +3228 -0
- core/board_os/migration.py +166 -0
- core/board_os/parser.py +317 -0
- core/board_os/presence.py +156 -0
- core/board_os/sync.py +320 -0
- core/board_os/transition-gates.yaml +176 -0
- core/board_os/transition_gates.py +224 -0
- core/board_os/transition_gates_cli.py +272 -0
- core/board_os/transition_gates_validator.py +551 -0
- core/board_os/verify-suites.yaml +113 -0
- core/board_os/verify_suites.py +126 -0
- core/board_os/verify_suites_cli.py +328 -0
- core/board_os/workflow.py +967 -0
- core/commands/board.md +27 -0
- core/commands/classify.md +23 -0
- core/commands/compose.md +23 -0
- core/commands/daily.md +31 -0
- core/commands/diagnose.md +7 -0
- core/commands/memory-search.md +23 -0
- core/commands/new-project.md +33 -0
- core/commands/retro.md +38 -0
- core/commands/review.md +14 -0
- core/commands/task.md +17 -0
- core/commands/verify.md +34 -0
- core/docs/thinking_os-final-edition.md +1449 -0
- core/doctor-config.yaml +74 -0
- core/graph_os/__init__.py +29 -0
- core/graph_os/backend.py +233 -0
- core/graph_os/backends/__init__.py +13 -0
- core/graph_os/backends/sqlite_backend.py +1053 -0
- core/graph_os/bench/__init__.py +17 -0
- core/graph_os/bench/fixtures.py +56 -0
- core/graph_os/bench/harness.py +95 -0
- core/graph_os/bench/persian_precision.py +142 -0
- core/graph_os/bench/scale_500k.py +120 -0
- core/graph_os/bench/token_cost.py +170 -0
- core/graph_os/bench/viewer_fps.py +110 -0
- core/graph_os/communities.py +410 -0
- core/graph_os/enterprise.py +218 -0
- core/graph_os/entry_points.py +226 -0
- core/graph_os/extractors/__init__.py +25 -0
- core/graph_os/extractors/code_generic.py +914 -0
- core/graph_os/extractors/code_go.py +1422 -0
- core/graph_os/extractors/code_json.py +340 -0
- core/graph_os/extractors/code_php.py +979 -0
- core/graph_os/extractors/code_python.py +1454 -0
- core/graph_os/extractors/code_shell.py +538 -0
- core/graph_os/extractors/code_toml.py +302 -0
- core/graph_os/extractors/code_ts.py +1665 -0
- core/graph_os/extractors/code_yaml.py +394 -0
- core/graph_os/extractors/contracts.py +1592 -0
- core/graph_os/extractors/md_links.py +890 -0
- core/graph_os/extractors/task_deps.py +345 -0
- core/graph_os/groups/__init__.py +22 -0
- core/graph_os/groups/cross_repo.py +156 -0
- core/graph_os/groups/manifest.py +141 -0
- core/graph_os/ingest/__init__.py +19 -0
- core/graph_os/ingest/base.py +306 -0
- core/graph_os/ingest/github.py +112 -0
- core/graph_os/ingest/zip.py +95 -0
- core/graph_os/toolchain.py +393 -0
- core/graph_os/tools/__init__.py +9 -0
- core/graph_os/tools/graph.py +5573 -0
- core/graph_os/tools/reindex_dispatch.py +730 -0
- core/graph_os/tree_sitter_overlay.py +235 -0
- core/graph_os/types.py +252 -0
- core/graph_os/vec_index.py +277 -0
- core/graph_os/viewer/__init__.py +12 -0
- core/graph_os/viewer/exporter.py +93 -0
- core/graph_os/viewer/template.py +189 -0
- core/hooks/_helpers/_paths.py +40 -0
- core/hooks/_helpers/advance_role.py +72 -0
- core/hooks/_helpers/auto_compose.py +228 -0
- core/hooks/_helpers/auto_validate_lessons.py +55 -0
- core/hooks/_helpers/branch_guard_check.py +796 -0
- core/hooks/_helpers/check_commit_message.py +108 -0
- core/hooks/_helpers/check_dangerous_rm.py +80 -0
- core/hooks/_helpers/check_git_bypass.py +154 -0
- core/hooks/_helpers/check_git_destructive.py +77 -0
- core/hooks/_helpers/check_settings_write.py +97 -0
- core/hooks/_helpers/consume_override.py +51 -0
- core/hooks/_helpers/context_budget.py +77 -0
- core/hooks/_helpers/cos_say_json.py +103 -0
- core/hooks/_helpers/destructive_edit_check.py +163 -0
- core/hooks/_helpers/detect_status_transition.py +82 -0
- core/hooks/_helpers/digest_regen.py +56 -0
- core/hooks/_helpers/doc_sync_check.py +498 -0
- core/hooks/_helpers/drain_embedding_outbox.py +52 -0
- core/hooks/_helpers/extract_additional_context.py +51 -0
- core/hooks/_helpers/extract_commit_msg_arg.py +74 -0
- core/hooks/_helpers/git_command_parse.py +424 -0
- core/hooks/_helpers/git_settings_fields.py +47 -0
- core/hooks/_helpers/graph_context_match.py +37 -0
- core/hooks/_helpers/graph_marker_check.py +70 -0
- core/hooks/_helpers/jit_recall.py +56 -0
- core/hooks/_helpers/json_field.py +41 -0
- core/hooks/_helpers/narrative_signal.py +59 -0
- core/hooks/_helpers/observation_count.py +31 -0
- core/hooks/_helpers/pre_commit_batch.py +177 -0
- core/hooks/_helpers/pre_commit_fake_input.py +42 -0
- core/hooks/_helpers/presence_gc.py +102 -0
- core/hooks/_helpers/presence_write.py +167 -0
- core/hooks/_helpers/recover_indirect.py +35 -0
- core/hooks/_helpers/routing_evolution.py +104 -0
- core/hooks/_helpers/session_recap.py +72 -0
- core/hooks/_helpers/skill_primer.py +229 -0
- core/hooks/_helpers/task_sync.py +59 -0
- core/hooks/_helpers/tool_failure_capture.py +147 -0
- core/hooks/_helpers/trajectory_autosnap.py +278 -0
- core/hooks/_helpers/trajectory_startup.py +62 -0
- core/hooks/_helpers/turn_summary.py +82 -0
- core/hooks/_helpers/validate_task_frontmatter.py +98 -0
- core/hooks/_helpers/wip_limit_check.py +103 -0
- core/hooks/_helpers/wip_lines.py +53 -0
- core/hooks/_helpers/work_log_append.py +89 -0
- core/hooks/_helpers/wrap_dispatch_output.py +82 -0
- core/hooks/advance-role.sh +48 -0
- core/hooks/agent-presence.sh +179 -0
- core/hooks/auto-brain-decay.sh +184 -0
- core/hooks/auto-compose-roles.sh +83 -0
- core/hooks/auto-graph-reconcile-shell.sh +119 -0
- core/hooks/auto-regen-doc-index.sh +120 -0
- core/hooks/auto-reindex-docs.sh +130 -0
- core/hooks/auto-task-sync.sh +56 -0
- core/hooks/auto-trace-rotate.sh +88 -0
- core/hooks/block-bad-patterns.sh +212 -0
- core/hooks/block-dangerous-commands.sh +182 -0
- core/hooks/block-hardcoded-literals.sh +90 -0
- core/hooks/block-migration-conflict.sh +114 -0
- core/hooks/block-protected-files.sh +129 -0
- core/hooks/block-secrets.sh +185 -0
- core/hooks/block-shared-tree-edit.sh +75 -0
- core/hooks/block-uv-heredoc.sh +78 -0
- core/hooks/branch-guard.sh +122 -0
- core/hooks/capture-observation.sh +76 -0
- core/hooks/capture-tool-failure.sh +24 -0
- core/hooks/capture-work-log.sh +89 -0
- core/hooks/check-agents-md-refs.sh +75 -0
- core/hooks/check-agents-md-size.sh +49 -0
- core/hooks/check-capture-worked.sh +148 -0
- core/hooks/check-doc-size.sh +61 -0
- core/hooks/check-mcp-extras.sh +92 -0
- core/hooks/check-state.sh +87 -0
- core/hooks/classify-task-mode.sh +103 -0
- core/hooks/cos-env.sh +1301 -0
- core/hooks/drain-embedding-outbox.sh +24 -0
- core/hooks/enforce-anti-ambiguity.sh +74 -0
- core/hooks/enforce-commit-message.sh +73 -0
- core/hooks/enforce-doc-anchor.sh +224 -0
- core/hooks/enforce-doc-sync.sh +206 -0
- core/hooks/enforce-graph-context.sh +89 -0
- core/hooks/enforce-graph-first-read.sh +94 -0
- core/hooks/enforce-memory-check.sh +128 -0
- core/hooks/enforce-rename-plan.sh +78 -0
- core/hooks/enforce-scaffold-boundary.sh +68 -0
- core/hooks/enforce-skill.sh +125 -0
- core/hooks/enforce-task-body.sh +52 -0
- core/hooks/enforce-task-start.sh +81 -0
- core/hooks/enforce-task-transition.sh +75 -0
- core/hooks/enforce-template.sh +143 -0
- core/hooks/enforce-verify.sh +112 -0
- core/hooks/enforce-wip-limit.sh +43 -0
- core/hooks/enforce-zoom.sh +69 -0
- core/hooks/ensure-hub-up.sh +67 -0
- core/hooks/inject-mcp-caller-session.sh +70 -0
- core/hooks/jit-recall.sh +65 -0
- core/hooks/link-commit-to-task.sh +143 -0
- core/hooks/lint-task.sh +40 -0
- core/hooks/nudge-docs-first.sh +71 -0
- core/hooks/nudge-git-mode.sh +29 -0
- core/hooks/nudge-graph-os.sh +118 -0
- core/hooks/nudge-learn-narrative.sh +36 -0
- core/hooks/nudge-model-routing.sh +32 -0
- core/hooks/nudge-reentry.sh +101 -0
- core/hooks/nudge-reuse-first.sh +68 -0
- core/hooks/nudge-task-discovery.sh +81 -0
- core/hooks/nudge-thinking-os.sh +109 -0
- core/hooks/pr-reap.sh +23 -0
- core/hooks/reclaim-sweep.sh +58 -0
- core/hooks/record-verify-auto.sh +77 -0
- core/hooks/record-verify.sh +74 -0
- core/hooks/regen-reminder.sh +104 -0
- core/hooks/registry.yaml +1262 -0
- core/hooks/remind-daily.sh +27 -0
- core/hooks/remind-dogfood.sh +70 -0
- core/hooks/remind-learn-validate.sh +94 -0
- core/hooks/rules-primer.sh +50 -0
- core/hooks/search-enforce-inventory.sh +108 -0
- core/hooks/search-verify-remaining.sh +132 -0
- core/hooks/session-context.sh +729 -0
- core/hooks/session-end.sh +145 -0
- core/hooks/session-skill-primer.sh +43 -0
- core/hooks/snapshot-transcript.sh +56 -0
- core/hooks/sync-task-current.sh +85 -0
- core/hooks/test-first-reminder.sh +120 -0
- core/hooks/test-governor.sh +173 -0
- core/hooks/thinking_os-gate.sh +52 -0
- core/hooks/track-backtrack.sh +35 -0
- core/hooks/track-discovery.sh +121 -0
- core/hooks/track-skill.sh +53 -0
- core/hooks/validate-task-frontmatter.sh +49 -0
- core/hooks/verify-rename-callers.sh +119 -0
- core/hooks/warn-abandoned-task.sh +99 -0
- core/hooks/warn-destructive-edit.sh +64 -0
- core/hooks/warn-diff-size.sh +43 -0
- core/hooks/warn-graph-empty.sh +81 -0
- core/hooks/warn-mcp-down.sh +190 -0
- core/hooks/write-state.sh +55 -0
- core/logging_os/__init__.py +33 -0
- core/logging_os/api.py +127 -0
- core/logging_os/bridge.py +80 -0
- core/logging_os/config.py +172 -0
- core/logging_os/fingerprint.py +25 -0
- core/logging_os/redact.py +53 -0
- core/logging_os/render.py +83 -0
- core/logging_os/sinks.py +164 -0
- core/rules/anti-overengineering.md +44 -0
- core/rules/api-contract-discipline.md +41 -0
- core/rules/dimension-registry.md +155 -0
- core/rules/git-workflow.md +57 -0
- core/rules/memory.md +46 -0
- core/rules/model-routing.md +22 -0
- core/rules/skill-enforcement.md +75 -0
- core/rules/test-discipline.md +38 -0
- core/rules/thinking_os.md +48 -0
- core/rules/transparency-banner.md +37 -0
- core/runtime_paths.yaml +36 -0
- core/scaffold_manifest.json +14430 -0
- core/scheduled/__init__.py +0 -0
- core/scheduled/_activity.py +126 -0
- core/scheduled/_state.py +113 -0
- core/scheduled/config.py +86 -0
- core/scheduled/dep_reconcile.py +135 -0
- core/scheduled/error_sweep.py +137 -0
- core/scheduled/nightly.py +930 -0
- core/scheduled/responsive_extract.py +65 -0
- core/schemas/adapter.schema.json +269 -0
- core/schemas/preset.schema.json +50 -0
- core/schemas/skill.schema.json +81 -0
- core/schemas/stack.schema.json +404 -0
- core/scripts/_lib.sh +9 -0
- core/scripts/docs-lint.sh +228 -0
- core/scripts/docs-nav-fix.sh +133 -0
- core/scripts/docs-staleness-check.sh +154 -0
- core/scripts/install-adapter.sh +266 -0
- core/scripts/link-stack-skills.sh +52 -0
- core/scripts/log-latest.sh +106 -0
- core/scripts/log-search.sh +89 -0
- core/scripts/log-write.sh +134 -0
- core/scripts/ref-resolve.sh +71 -0
- core/skills/a11y/SKILL.md +305 -0
- core/skills/a11y/assets/a11y-checklist.md +137 -0
- core/skills/a11y/references/aria-and-focus.md +247 -0
- core/skills/a11y/references/rn-accessibility.md +343 -0
- core/skills/a11y/references/screen-reader-testing.md +190 -0
- core/skills/agent-memory/SKILL.md +191 -0
- core/skills/agent-memory/assets/memory-checklist.md +21 -0
- core/skills/agent-memory/references/memory-recipes.md +57 -0
- core/skills/api-design/SKILL.md +232 -0
- core/skills/api-design/assets/api-design-checklist.md +110 -0
- core/skills/api-design/references/error-envelope.md +381 -0
- core/skills/api-design/references/idempotency-pagination.md +312 -0
- core/skills/api-design/references/rest-contracts.md +426 -0
- core/skills/auth-patterns/SKILL.md +352 -0
- core/skills/auth-patterns/assets/auth-checklist.md +118 -0
- core/skills/auth-patterns/references/jwt-and-service-tokens.md +343 -0
- core/skills/auth-patterns/references/passkeys-2fa.md +289 -0
- core/skills/auth-patterns/references/sessions-vs-jwt.md +230 -0
- core/skills/auth-patterns/scripts/cookie-flag-check.py +146 -0
- core/skills/backend-fundamentals/SKILL.md +238 -0
- core/skills/backend-fundamentals/assets/backend-checklist.md +27 -0
- core/skills/backend-fundamentals/references/backend-patterns.md +56 -0
- core/skills/backend-fundamentals/scripts/check_layering.py +83 -0
- core/skills/clean-code/SKILL.md +642 -0
- core/skills/clean-code/scripts/audit-fail-closed.py +167 -0
- core/skills/codebase-explorer/SKILL.md +89 -0
- core/skills/codebase-explorer/assets/reading-checklist.md +24 -0
- core/skills/codebase-explorer/references/reading-strategies.md +52 -0
- core/skills/codebase-explorer/scripts/outline.py +99 -0
- core/skills/db-design/SKILL.md +327 -0
- core/skills/db-design/assets/migration-template.sql +49 -0
- core/skills/db-design/references/migration-discipline.md +290 -0
- core/skills/db-design/references/postgres-patterns.md +340 -0
- core/skills/db-design/scripts/migration-safety.sh +150 -0
- core/skills/deployment-cicd/SKILL.md +260 -0
- core/skills/deployment-cicd/assets/deploy-checklist.md +26 -0
- core/skills/deployment-cicd/references/pipeline-and-release.md +54 -0
- core/skills/deployment-cicd/scripts/lint_workflow.py +79 -0
- core/skills/docker/SKILL.md +114 -0
- core/skills/docker/assets/dockerfile-checklist.md +31 -0
- core/skills/docker/references/compose-patterns.md +66 -0
- core/skills/docker/references/dockerfile-optimization.md +64 -0
- core/skills/docker/scripts/lint_dockerfile.sh +48 -0
- core/skills/docker/versions.json +16 -0
- core/skills/end-to-end-testing/SKILL.md +101 -0
- core/skills/end-to-end-testing/assets/e2e-checklist.md +23 -0
- core/skills/end-to-end-testing/references/maestro.md +63 -0
- core/skills/end-to-end-testing/references/playwright.md +68 -0
- core/skills/end-to-end-testing/scripts/lint_e2e.py +88 -0
- core/skills/end-to-end-testing/versions.json +16 -0
- core/skills/frontend-design/SKILL.md +76 -0
- core/skills/frontend-design/assets/design-checklist.md +29 -0
- core/skills/frontend-design/references/design-principles.md +65 -0
- core/skills/frontend-design/scripts/check_contrast.py +89 -0
- core/skills/frontend-fundamentals/SKILL.md +213 -0
- core/skills/frontend-fundamentals/assets/frontend-checklist.md +25 -0
- core/skills/frontend-fundamentals/references/rendering-and-state.md +66 -0
- core/skills/frontend-fundamentals/scripts/check_frontend.py +86 -0
- core/skills/graph-explorer/SKILL.md +215 -0
- core/skills/graph-explorer/scripts/explain-impact.sh +64 -0
- core/skills/graphql/SKILL.md +187 -0
- core/skills/grpc-microservices/SKILL.md +174 -0
- core/skills/hexagonal-architecture/SKILL.md +199 -0
- core/skills/hexagonal-architecture/assets/folder-scaffold.md +233 -0
- core/skills/hexagonal-architecture/references/anti-patterns.md +129 -0
- core/skills/hexagonal-architecture/references/go-fiber-layout.md +429 -0
- core/skills/hexagonal-architecture/references/python-fastapi-layout.md +453 -0
- core/skills/hexagonal-architecture/references/react-native-layout.md +428 -0
- core/skills/i18n/SKILL.md +126 -0
- core/skills/incident-response/SKILL.md +225 -0
- core/skills/incident-response/assets/incident-checklist.md +29 -0
- core/skills/incident-response/references/severity-and-runbook.md +53 -0
- core/skills/incident-response/scripts/classify_severity.py +86 -0
- core/skills/linux-sysadmin/SKILL.md +115 -0
- core/skills/linux-sysadmin/assets/hardening-checklist.md +29 -0
- core/skills/linux-sysadmin/references/ssh-hardening.md +62 -0
- core/skills/linux-sysadmin/references/systemd-and-services.md +77 -0
- core/skills/linux-sysadmin/scripts/triage.sh +50 -0
- core/skills/linux-sysadmin/versions.json +17 -0
- core/skills/llm-patterns/SKILL.md +410 -0
- core/skills/llm-patterns/assets/llm-feature-checklist.md +26 -0
- core/skills/llm-patterns/references/rag-and-evals.md +59 -0
- core/skills/llm-patterns/scripts/estimate_tokens.py +75 -0
- core/skills/messaging-queues/SKILL.md +142 -0
- core/skills/mobile-fundamentals/SKILL.md +406 -0
- core/skills/mobile-fundamentals/assets/mobile-launch-checklist.md +130 -0
- core/skills/mobile-fundamentals/references/navigation-and-deep-links.md +337 -0
- core/skills/mobile-fundamentals/references/offline-sync.md +339 -0
- core/skills/node-backend/SKILL.md +114 -0
- core/skills/node-backend/assets/node-checklist.md +25 -0
- core/skills/node-backend/references/async-and-errors.md +67 -0
- core/skills/node-backend/references/event-loop.md +65 -0
- core/skills/node-backend/scripts/check_package.py +78 -0
- core/skills/node-backend/versions.json +17 -0
- core/skills/observability/SKILL.md +289 -0
- core/skills/observability/assets/observability-checklist.md +27 -0
- core/skills/observability/references/instrumentation.md +57 -0
- core/skills/observability/scripts/lint_logging.py +77 -0
- core/skills/payments/SKILL.md +102 -0
- core/skills/performance/SKILL.md +305 -0
- core/skills/performance/assets/perf-checklist.md +143 -0
- core/skills/performance/references/mobile-performance.md +249 -0
- core/skills/performance/references/web-vitals.md +209 -0
- core/skills/php/SKILL.md +116 -0
- core/skills/php/assets/php-checklist.md +26 -0
- core/skills/php/references/modern-php.md +64 -0
- core/skills/php/references/security.md +72 -0
- core/skills/php/scripts/scan_php_smells.py +99 -0
- core/skills/php/versions.json +9 -0
- core/skills/pr-mode-driver/SKILL.md +65 -0
- core/skills/realtime-websockets/SKILL.md +152 -0
- core/skills/redis/SKILL.md +105 -0
- core/skills/redis/assets/redis-checklist.md +27 -0
- core/skills/redis/references/operations.md +66 -0
- core/skills/redis/references/patterns.md +62 -0
- core/skills/redis/scripts/analyze_info.py +101 -0
- core/skills/redis/versions.json +9 -0
- core/skills/search/SKILL.md +91 -0
- core/skills/search/references/grep.md +76 -0
- core/skills/search/scripts/verify-count.sh +73 -0
- core/skills/search-infra/SKILL.md +109 -0
- core/skills/security-mobile/SKILL.md +394 -0
- core/skills/security-mobile/assets/mobile-security-checklist.md +117 -0
- core/skills/security-mobile/references/masvs-l1-checklist.md +127 -0
- core/skills/security-web/SKILL.md +217 -0
- core/skills/security-web/assets/security-web-checklist.md +167 -0
- core/skills/security-web/references/owasp-top-10.md +551 -0
- core/skills/security-web/references/supply-chain.md +179 -0
- core/skills/security-web/scripts/csp-check.sh +153 -0
- core/skills/shell-scripting/SKILL.md +128 -0
- core/skills/shell-scripting/assets/script-checklist.md +33 -0
- core/skills/shell-scripting/references/argument-parsing.md +84 -0
- core/skills/shell-scripting/references/bash-robustness.md +78 -0
- core/skills/shell-scripting/scripts/lint_script.sh +53 -0
- core/skills/shell-scripting/scripts/new_script.py +131 -0
- core/skills/shell-scripting/versions.json +23 -0
- core/skills/sql-authoring/SKILL.md +104 -0
- core/skills/sql-authoring/assets/query-review-checklist.md +26 -0
- core/skills/sql-authoring/references/query-patterns.md +89 -0
- core/skills/sql-authoring/references/reading-explain.md +52 -0
- core/skills/sql-authoring/scripts/analyze_plan.py +100 -0
- core/skills/sql-authoring/versions.json +17 -0
- core/skills/state-management/SKILL.md +428 -0
- core/skills/state-management/references/tanstack-query-recipes.md +301 -0
- core/skills/state-management/references/zustand-recipes.md +364 -0
- core/skills/supabase/SKILL.md +108 -0
- core/skills/supabase/assets/supabase-checklist.md +26 -0
- core/skills/supabase/references/realtime-and-storage.md +55 -0
- core/skills/supabase/references/rls-and-auth.md +65 -0
- core/skills/supabase/scripts/check_rls.py +88 -0
- core/skills/supabase/versions.json +9 -0
- core/skills/task-driver/SKILL.md +272 -0
- core/skills/task-driver/scripts/task-lint.sh +163 -0
- core/skills/technical-writing/SKILL.md +85 -0
- core/skills/technical-writing/assets/doc-checklist.md +29 -0
- core/skills/technical-writing/references/doc-anatomy.md +53 -0
- core/skills/technical-writing/references/writing-craft.md +59 -0
- core/skills/technical-writing/scripts/new_doc.py +81 -0
- core/skills/terraform-k8s/SKILL.md +133 -0
- core/skills/testing-strategy/SKILL.md +266 -0
- core/skills/testing-strategy/assets/test-review-checklist.md +25 -0
- core/skills/testing-strategy/references/test-types.md +52 -0
- core/skills/testing-strategy/scripts/coverage_gate.py +79 -0
- core/skills/thinking_os/SKILL.md +288 -0
- core/skills/thinking_os/scripts/classify.sh +122 -0
- core/skills/typescript/SKILL.md +110 -0
- core/skills/typescript/assets/typescript-checklist.md +24 -0
- core/skills/typescript/references/strictness.md +53 -0
- core/skills/typescript/references/type-system.md +86 -0
- core/skills/typescript/scripts/check_tsconfig.py +92 -0
- core/skills/typescript/versions.json +9 -0
- core/subsystems.yaml +202 -0
- core/thinking_os/__init__.py +1 -0
- core/thinking_os/_agent_markers.py +32 -0
- core/thinking_os/agents/README.md +71 -0
- core/thinking_os/agents/analyst.md +139 -0
- core/thinking_os/agents/architect.md +127 -0
- core/thinking_os/agents/debugger.md +111 -0
- core/thinking_os/agents/deployer.md +112 -0
- core/thinking_os/agents/distiller.md +28 -0
- core/thinking_os/agents/documenter.md +101 -0
- core/thinking_os/agents/implementer.md +135 -0
- core/thinking_os/agents/internal/session_observer.md +39 -0
- core/thinking_os/agents/observer.md +100 -0
- core/thinking_os/agents/onboarder.md +81 -0
- core/thinking_os/agents/refactorer.md +123 -0
- core/thinking_os/agents/repairer.md +47 -0
- core/thinking_os/agents/researcher.md +129 -0
- core/thinking_os/agents/reviewer.md +147 -0
- core/thinking_os/agents/security_auditor.md +133 -0
- core/thinking_os/background.py +405 -0
- core/thinking_os/bootstrap_outcomes.py +200 -0
- core/thinking_os/budget.py +302 -0
- core/thinking_os/capture.py +495 -0
- core/thinking_os/cognition.py +516 -0
- core/thinking_os/cognition_schemas.py +517 -0
- core/thinking_os/compress.py +192 -0
- core/thinking_os/concepts.py +233 -0
- core/thinking_os/dashboard.py +159 -0
- core/thinking_os/database.py +2883 -0
- core/thinking_os/decay.py +393 -0
- core/thinking_os/digest.py +295 -0
- core/thinking_os/dispatcher.py +192 -0
- core/thinking_os/dispatcher_helpers.py +48 -0
- core/thinking_os/dispatchers/__init__.py +3 -0
- core/thinking_os/dispatchers/default.py +47 -0
- core/thinking_os/distill.py +192 -0
- core/thinking_os/doc_indexer.py +905 -0
- core/thinking_os/embeddings.py +943 -0
- core/thinking_os/formula_composer.py +556 -0
- core/thinking_os/gate_marker.py +75 -0
- core/thinking_os/graph.py +296 -0
- core/thinking_os/graph_indexer.py +360 -0
- core/thinking_os/health_check.py +517 -0
- core/thinking_os/impact.py +119 -0
- core/thinking_os/memory_gc.py +356 -0
- core/thinking_os/migrator_embeddings.py +318 -0
- core/thinking_os/precision.py +194 -0
- core/thinking_os/presets/registry.yaml +127 -0
- core/thinking_os/record_outcome.py +399 -0
- core/thinking_os/repair.py +105 -0
- core/thinking_os/retrieval_quality.py +239 -0
- core/thinking_os/roles/analyst.yaml +83 -0
- core/thinking_os/roles/architect.yaml +88 -0
- core/thinking_os/roles/debugger.yaml +71 -0
- core/thinking_os/roles/deployer.yaml +71 -0
- core/thinking_os/roles/documenter.yaml +72 -0
- core/thinking_os/roles/implementer.yaml +83 -0
- core/thinking_os/roles/observer.yaml +70 -0
- core/thinking_os/roles/refactorer.yaml +71 -0
- core/thinking_os/roles/researcher.yaml +72 -0
- core/thinking_os/roles/reviewer.yaml +79 -0
- core/thinking_os/roles/security_auditor.yaml +83 -0
- core/thinking_os/roles_state.py +168 -0
- core/thinking_os/sanitizer.py +320 -0
- core/thinking_os/server.py +3160 -0
- core/thinking_os/session_enrich.py +272 -0
- core/thinking_os/session_observe_worker.py +111 -0
- core/thinking_os/session_startup.py +74 -0
- core/thinking_os/session_summary.py +245 -0
- core/thinking_os/situations/registry.yaml +99 -0
- core/thinking_os/task_analyzer.py +462 -0
- core/thinking_os/task_parser.py +342 -0
- core/thinking_os/task_sync.py +73 -0
- core/thinking_os/tools/__init__.py +6 -0
- core/thinking_os/tools/_shared.py +947 -0
- core/thinking_os/tools/cognition.py +1867 -0
- core/thinking_os/tools/docs.py +770 -0
- core/thinking_os/tools/learning.py +2078 -0
- core/thinking_os/tools/logs.py +79 -0
- core/thinking_os/tools/memory.py +840 -0
- core/thinking_os/tools/metrics.py +200 -0
- core/thinking_os/tools/retrieve.py +415 -0
- core/thinking_os/tools/routing.py +658 -0
- core/thinking_os/tools/tasks.py +449 -0
- core/thinking_os/tools/trajectory.py +181 -0
- core/thinking_os/tracing.py +235 -0
- core/web/__init__.py +5 -0
- core/web/_cache.py +118 -0
- core/web/_deps.py +56 -0
- core/web/_envelope.py +85 -0
- core/web/_project_context.py +140 -0
- core/web/chat_providers.py +108 -0
- core/web/init_jobs.py +216 -0
- core/web/routes/__init__.py +25 -0
- core/web/routes/_bounded_read.py +76 -0
- core/web/routes/board.py +1089 -0
- core/web/routes/cognition.py +1838 -0
- core/web/routes/config.py +635 -0
- core/web/routes/graph.py +513 -0
- core/web/routes/health.py +180 -0
- core/web/routes/hooks.py +288 -0
- core/web/routes/hub.py +1219 -0
- core/web/routes/logs.py +374 -0
- core/web/routes/metrics.py +43 -0
- core/web/routes/observability.py +400 -0
- core/web/routes/patterns.py +227 -0
- core/web/routes/presence.py +609 -0
- core/web/routes/roles.py +446 -0
- core/web/routes/scheduled.py +261 -0
- core/web/routes/search.py +238 -0
- core/web/routes/sessions.py +220 -0
- core/web/routes/settings.py +363 -0
- core/web/routes/stream.py +547 -0
- core/web/security.py +157 -0
- core/web/server.py +283 -0
- graph_os/__init__.py +29 -0
- graph_os/backend.py +233 -0
- graph_os/backends/__init__.py +13 -0
- graph_os/backends/sqlite_backend.py +1053 -0
- graph_os/communities.py +410 -0
- graph_os/enterprise.py +218 -0
- graph_os/entry_points.py +226 -0
- graph_os/extractors/__init__.py +25 -0
- graph_os/extractors/code_generic.py +914 -0
- graph_os/extractors/code_go.py +1422 -0
- graph_os/extractors/code_json.py +340 -0
- graph_os/extractors/code_php.py +979 -0
- graph_os/extractors/code_python.py +1454 -0
- graph_os/extractors/code_shell.py +538 -0
- graph_os/extractors/code_toml.py +302 -0
- graph_os/extractors/code_ts.py +1665 -0
- graph_os/extractors/code_yaml.py +394 -0
- graph_os/extractors/contracts.py +1592 -0
- graph_os/extractors/md_links.py +890 -0
- graph_os/extractors/task_deps.py +345 -0
- graph_os/groups/__init__.py +22 -0
- graph_os/groups/cross_repo.py +156 -0
- graph_os/groups/manifest.py +141 -0
- graph_os/ingest/__init__.py +19 -0
- graph_os/ingest/base.py +306 -0
- graph_os/ingest/github.py +112 -0
- graph_os/ingest/zip.py +95 -0
- graph_os/toolchain.py +393 -0
- graph_os/tools/__init__.py +9 -0
- graph_os/tools/graph.py +5573 -0
- graph_os/tools/reindex_dispatch.py +730 -0
- graph_os/tree_sitter_overlay.py +235 -0
- graph_os/types.py +252 -0
- graph_os/vec_index.py +277 -0
- graph_os/viewer/__init__.py +12 -0
- graph_os/viewer/exporter.py +93 -0
- graph_os/viewer/template.py +189 -0
- scheduled/__init__.py +0 -0
- scheduled/_activity.py +126 -0
- scheduled/_state.py +113 -0
- scheduled/config.py +86 -0
- scheduled/dep_reconcile.py +135 -0
- scheduled/error_sweep.py +137 -0
- scheduled/nightly.py +930 -0
- scheduled/responsive_extract.py +65 -0
- scripts/__init__.py +4 -0
- scripts/_commit_msg_body.sh +31 -0
- scripts/_post_commit_body.sh +49 -0
- scripts/_pre_commit_body.sh +121 -0
- scripts/_prepare_commit_msg_body.sh +53 -0
- scripts/audit_mcp_tools.py +693 -0
- scripts/bench_sdk_dispatcher.py +177 -0
- scripts/capture_golden.py +169 -0
- scripts/check_graph_phantoms.py +75 -0
- scripts/dev/audit_doc_links.py +359 -0
- scripts/dev/audit_scaffold_module_tags.py +107 -0
- scripts/dev/backfill_doc_headers.py +328 -0
- scripts/dev/backfill_nav_lines.py +119 -0
- scripts/dev/fix_nav_placement.py +106 -0
- scripts/dev/inspect_sdk_options.py +45 -0
- scripts/dev/migrate_check_ids.py +170 -0
- scripts/dev/strip_purpose_blocks.py +154 -0
- scripts/dump_openapi.py +66 -0
- scripts/e2e_dispatch_tool.py +195 -0
- scripts/generate_manifest.py +166 -0
- scripts/golden_sections.py +20 -0
- scripts/graph_demo.py +161 -0
- scripts/install-git-hooks.sh +47 -0
- scripts/migrate_embeddings_minilm_to_bge_m3.py +84 -0
- scripts/operational_eval.py +445 -0
- scripts/probe_agent_session_resolver.py +59 -0
- scripts/prune_deleted_path.py +127 -0
- scripts/refactor_agent_dual_mode.py +171 -0
- scripts/refresh_skill_versions.py +302 -0
- scripts/regen_doc_index.py +209 -0
- scripts/regen_doctor_schema.py +62 -0
- scripts/regen_rules.py +94 -0
- scripts/rename_formulas_to_semantic.py +241 -0
- scripts/smoke_db_connections.py +183 -0
- scripts/smoke_doc_header.py +72 -0
- scripts/smoke_graph_e2e.py +374 -0
- scripts/smoke_sdk_dispatch.py +84 -0
- scripts/smoke_uid_resolver.py +164 -0
- scripts/verify_dispatchers.py +244 -0
- scripts/verify_phase_c_e2e.py +436 -0
- templates/__init__.py +6 -0
- templates/_base/Makefile.base +353 -0
- templates/_base/base.yaml +59 -0
- templates/_base/coding-os.yaml.template +39 -0
- templates/_base/dimension-registry.template.md +68 -0
- templates/_base/domain-config.template.json +46 -0
- templates/_base/fragments/anatomy-map.md.tmpl +11 -0
- templates/_base/fragments/context-discipline.md.tmpl +3 -0
- templates/_base/fragments/core-loop.md.tmpl +52 -0
- templates/_base/fragments/engineering-routing.md.tmpl +3 -0
- templates/_base/fragments/header.md.tmpl +6 -0
- templates/_base/fragments/identity.md.tmpl +3 -0
- templates/_base/fragments/principles.md.tmpl +3 -0
- templates/_base/fragments/retrieval-routing.md.tmpl +23 -0
- templates/_base/fragments/session-handoff.md.tmpl +3 -0
- templates/_base/fragments/skills.md.tmpl +3 -0
- templates/_base/fragments/ssot-map.md.tmpl +3 -0
- templates/_base/fragments/stop-conditions.md.tmpl +3 -0
- templates/_base/fragments/subagent-dispatch.md.tmpl +3 -0
- templates/_base/fragments/task-authoring.md.tmpl +69 -0
- templates/_base/fragments/task-logging.md.tmpl +8 -0
- templates/_base/fragments/tool-routing.md.tmpl +9 -0
- templates/_base/fragments/verification-matrix.md.tmpl +12 -0
- templates/_base/lang/dart/analysis_options.yaml +7 -0
- templates/_base/lang/php/phpcs.xml.dist +10 -0
- templates/_base/lang/python/pyproject.toml +19 -0
- templates/_base/lang/rust/clippy.toml +5 -0
- templates/_base/lang/rust/rustfmt.toml +3 -0
- templates/_base/lang/typescript/eslint.config.js +26 -0
- templates/_base/lang/typescript/tsconfig.json +15 -0
- templates/_base/lang/typescript/vitest.config.ts +10 -0
- templates/_base/scaffold/changes.log +1 -0
- templates/_base/scaffold/docs/00-index.md +55 -0
- templates/_base/scaffold/docs/_meta/feature-dependency-tree.md +30 -0
- templates/_base/scaffold/docs/_meta/foundation-map.md +56 -0
- templates/_base/scaffold/docs/_meta/questions.md +8 -0
- templates/_base/scaffold/docs/_meta/roadmap.md +33 -0
- templates/_base/scaffold/docs/api-contracts/00-index.md +58 -0
- templates/_base/scaffold/docs/api-contracts/error-format.md +58 -0
- templates/_base/scaffold/docs/architecture/00-index.md +42 -0
- templates/_base/scaffold/docs/architecture/adr/00-index.md +39 -0
- templates/_base/scaffold/docs/engineering/00-index.md +9 -0
- templates/_base/scaffold/docs/governance/00-index.md +55 -0
- templates/_base/scaffold/docs/governance/_templates/doc-cheat-sheet.md +202 -0
- templates/_base/scaffold/docs/governance/_templates/playbook-template.md +81 -0
- templates/_base/scaffold/docs/governance/_templates/post-mortem-template.md +85 -0
- templates/_base/scaffold/docs/governance/_templates/runbook-template.md +88 -0
- templates/_base/scaffold/docs/governance/_templates/security-review-template.md +111 -0
- templates/_base/scaffold/docs/governance/_templates/task-detail.md +61 -0
- templates/_base/scaffold/docs/governance/agent-workflow.md +101 -0
- templates/_base/scaffold/docs/governance/anatomy-contract.md +150 -0
- templates/_base/scaffold/docs/governance/critical-rules.md +224 -0
- templates/_base/scaffold/docs/governance/decision-records.md +58 -0
- templates/_base/scaffold/docs/governance/docs-first-protocol.md +157 -0
- templates/_base/scaffold/docs/governance/docs-system.md +151 -0
- templates/_base/scaffold/docs/governance/gdpr-compliance.md +66 -0
- templates/_base/scaffold/docs/governance/mcp-tool-inventory.md +112 -0
- templates/_base/scaffold/docs/governance/risk-register.md +26 -0
- templates/_base/scaffold/docs/governance/scaffold-boundary-contract.md +161 -0
- templates/_base/scaffold/docs/governance/task-lifecycle.md +125 -0
- templates/_base/scaffold/docs/governance/wrapper-derivation.md +50 -0
- templates/_base/scaffold/docs/insights/00-index.md +17 -0
- templates/_base/scaffold/docs/ops/00-index.md +59 -0
- templates/_base/scaffold/docs/ops/runbooks/00-index.md +9 -0
- templates/_base/scaffold/docs/playbooks/00-index.md +12 -0
- templates/_base/scaffold/docs/playbooks/research-validation.md +29 -0
- templates/_base/scaffold/docs/playbooks/security-review.md +41 -0
- templates/_base/scaffold/docs/prd/00-index.md +43 -0
- templates/_base/scaffold/docs/prd/01-snapshot-vision.md +56 -0
- templates/_base/scaffold/docs/workflow/workflow-guide.md +138 -0
- templates/_base/scaffold/src/shared/README.md +23 -0
- templates/_base/skill-enforcement.template.md +14 -0
- templates/_base/task-detail.template.md +61 -0
- templates/_presets/ai-saas.yaml +9 -0
- templates/_presets/django-next.yaml +8 -0
- templates/_presets/dotnet-react.yaml +8 -0
- templates/_presets/flutter-baas.yaml +8 -0
- templates/_presets/go-react.yaml +8 -0
- templates/_presets/hexagonal-product.yaml +16 -0
- templates/_presets/jamstack.yaml +8 -0
- templates/_presets/laravel-vue.yaml +8 -0
- templates/_presets/mean.yaml +8 -0
- templates/_presets/mern.yaml +9 -0
- templates/_presets/nest-angular.yaml +8 -0
- templates/_presets/nextjs-fastapi.yaml +10 -0
- templates/_presets/nuxt-fullstack.yaml +9 -0
- templates/_presets/pern.yaml +9 -0
- templates/_presets/rails-react.yaml +8 -0
- templates/_presets/rn-api.yaml +8 -0
- templates/_presets/rust-svelte.yaml +8 -0
- templates/_presets/spring-react.yaml +8 -0
- templates/_presets/t3-style.yaml +9 -0
- templates/_presets/tall.yaml +8 -0
- templates/_presets/wordpress-cms.yaml +8 -0
- templates/angular/rules/frontend.md +19 -0
- templates/angular/scaffold/docs/engineering/accessibility.md +46 -0
- templates/angular/scaffold/docs/engineering/angular-rules.md +35 -0
- templates/angular/scaffold/docs/playbooks/angular-app.md +42 -0
- templates/angular/scaffold/src/frontend/angular.json +52 -0
- templates/angular/scaffold/src/frontend/package.json +28 -0
- templates/angular/scaffold/src/frontend/src/app/app.component.ts +19 -0
- templates/angular/scaffold/src/frontend/src/app/app.config.ts +22 -0
- templates/angular/scaffold/src/frontend/src/app/app.routes.ts +8 -0
- templates/angular/scaffold/src/frontend/src/app/core/global-error-handler.ts +12 -0
- templates/angular/scaffold/src/frontend/src/app/health/health.component.ts +14 -0
- templates/angular/scaffold/src/frontend/src/app/health/health.service.spec.ts +16 -0
- templates/angular/scaffold/src/frontend/src/app/health/health.service.ts +10 -0
- templates/angular/scaffold/src/frontend/src/index.html +11 -0
- templates/angular/scaffold/src/frontend/src/main.ts +9 -0
- templates/angular/scaffold/src/frontend/src/styles.css +14 -0
- templates/angular/scaffold/src/frontend/tsconfig.app.json +8 -0
- templates/angular/scaffold/src/frontend/tsconfig.json +27 -0
- templates/angular/scaffold/src/frontend/tsconfig.spec.json +8 -0
- templates/angular/scaffold-boundary.yaml +27 -0
- templates/angular/skills/angular/SKILL.md +79 -0
- templates/angular/skills/angular/references/anatomy.md +69 -0
- templates/angular/stack.yaml +75 -0
- templates/aspnet-core/rules/backend.md +20 -0
- templates/aspnet-core/scaffold/docs/engineering/aspnet-core-rules.md +36 -0
- templates/aspnet-core/scaffold/docs/playbooks/aspnet-core-service.md +40 -0
- templates/aspnet-core/scaffold/src/backend/Backend.csproj +11 -0
- templates/aspnet-core/scaffold/src/backend/Backend.sln +27 -0
- templates/aspnet-core/scaffold/src/backend/Common/ExceptionHandlingMiddleware.cs +35 -0
- templates/aspnet-core/scaffold/src/backend/Features/Health/HealthEndpoints.cs +10 -0
- templates/aspnet-core/scaffold/src/backend/Features/Health/HealthService.cs +9 -0
- templates/aspnet-core/scaffold/src/backend/Program.cs +22 -0
- templates/aspnet-core/scaffold/src/backend/tests/Backend.Tests/Backend.Tests.csproj +22 -0
- templates/aspnet-core/scaffold/src/backend/tests/Backend.Tests/HealthServiceTests.cs +15 -0
- templates/aspnet-core/scaffold-boundary.yaml +24 -0
- templates/aspnet-core/skills/aspnet-core/SKILL.md +80 -0
- templates/aspnet-core/skills/aspnet-core/references/anatomy.md +65 -0
- templates/aspnet-core/stack.yaml +68 -0
- templates/astro/rules/frontend.md +20 -0
- templates/astro/scaffold/docs/engineering/astro-rules.md +40 -0
- templates/astro/scaffold/docs/playbooks/astro-app.md +57 -0
- templates/astro/scaffold/docs/playbooks/content-seo.md +37 -0
- templates/astro/scaffold/src/frontend/astro.config.mjs +10 -0
- templates/astro/scaffold/src/frontend/package.json +23 -0
- templates/astro/scaffold/src/frontend/src/components/Greeting.astro +13 -0
- templates/astro/scaffold/src/frontend/src/content/posts/hello.md +10 -0
- templates/astro/scaffold/src/frontend/src/content.config.ts +19 -0
- templates/astro/scaffold/src/frontend/src/lib/problem.test.ts +39 -0
- templates/astro/scaffold/src/frontend/src/lib/problem.ts +30 -0
- templates/astro/scaffold/src/frontend/src/pages/api/health.ts +13 -0
- templates/astro/scaffold/src/frontend/src/pages/index.astro +21 -0
- templates/astro/scaffold/src/frontend/tsconfig.json +9 -0
- templates/astro/scaffold/src/frontend/vitest.config.ts +10 -0
- templates/astro/scaffold-boundary.yaml +28 -0
- templates/astro/skills/astro/SKILL.md +71 -0
- templates/astro/skills/astro/references/anatomy.md +66 -0
- templates/astro/stack.yaml +75 -0
- templates/csharp-plain/scaffold/src/backend/Backend.csproj +12 -0
- templates/csharp-plain/scaffold/src/backend/Program.cs +1 -0
- templates/csharp-plain/scaffold-boundary.yaml +23 -0
- templates/csharp-plain/stack.yaml +50 -0
- templates/django/rules/backend.md +18 -0
- templates/django/scaffold/docs/engineering/anti-ambiguity.md +74 -0
- templates/django/scaffold/docs/engineering/backend-rules.md +133 -0
- templates/django/scaffold/docs/engineering/glossary.md +51 -0
- templates/django/scaffold/docs/engineering/logging-standards.md +107 -0
- templates/django/scaffold/docs/engineering/naming-conventions.md +68 -0
- templates/django/scaffold/docs/engineering/secrets-rotation-runbook.md +142 -0
- templates/django/scaffold/docs/playbooks/backend-api.md +119 -0
- templates/django/scaffold/src/backend/config/__init__.py +0 -0
- templates/django/scaffold/src/backend/config/settings.py +31 -0
- templates/django/scaffold/src/backend/config/urls.py +11 -0
- templates/django/scaffold/src/backend/config/wsgi.py +6 -0
- templates/django/scaffold/src/backend/manage.py +14 -0
- templates/django/scaffold/src/backend/pyproject.toml +37 -0
- templates/django/scaffold/src/backend/tests/test_health.py +4 -0
- templates/django/scaffold-boundary.yaml +25 -0
- templates/django/skills/python-django/SKILL.md +450 -0
- templates/django/skills/python-django/references/anatomy.md +117 -0
- templates/django/skills/python-django/scripts/new_endpoint.py +89 -0
- templates/django/stack.yaml +73 -0
- templates/fastapi/rules/backend.md +18 -0
- templates/fastapi/scaffold/docs/engineering/fastapi-rules.md +37 -0
- templates/fastapi/scaffold/docs/playbooks/fastapi-service.md +30 -0
- templates/fastapi/scaffold/src/backend/app/__init__.py +0 -0
- templates/fastapi/scaffold/src/backend/app/main.py +8 -0
- templates/fastapi/scaffold/src/backend/pyproject.toml +39 -0
- templates/fastapi/scaffold/src/backend/tests/test_health.py +11 -0
- templates/fastapi/scaffold-boundary.yaml +25 -0
- templates/fastapi/skills/python-fastapi/SKILL.md +75 -0
- templates/fastapi/skills/python-fastapi/references/anatomy.md +117 -0
- templates/fastapi/skills/python-fastapi/scripts/new_endpoint.py +101 -0
- templates/fastapi/stack.yaml +57 -0
- templates/flutter/rules/mobile.md +26 -0
- templates/flutter/scaffold/docs/engineering/flutter-rules.md +35 -0
- templates/flutter/scaffold/docs/playbooks/flutter-app.md +45 -0
- templates/flutter/scaffold/src/mobile/lib/core/error_mapper.dart +17 -0
- templates/flutter/scaffold/src/mobile/lib/core/router.dart +13 -0
- templates/flutter/scaffold/src/mobile/lib/main.dart +22 -0
- templates/flutter/scaffold/src/mobile/lib/screens/health_screen.dart +32 -0
- templates/flutter/scaffold/src/mobile/lib/services/health_service.dart +11 -0
- templates/flutter/scaffold/src/mobile/lib/state/health_provider.dart +13 -0
- templates/flutter/scaffold/src/mobile/pubspec.yaml +22 -0
- templates/flutter/scaffold/src/mobile/test/health_provider_test.dart +90 -0
- templates/flutter/scaffold-boundary.yaml +28 -0
- templates/flutter/skills/flutter/SKILL.md +76 -0
- templates/flutter/skills/flutter/references/anatomy.md +64 -0
- templates/flutter/stack.yaml +70 -0
- templates/go/rules/backend.md +19 -0
- templates/go/scaffold/docs/engineering/go-rules.md +45 -0
- templates/go/scaffold/docs/playbooks/go-service.md +30 -0
- templates/go/scaffold/src/backend/cmd/api/main.go +22 -0
- templates/go/scaffold/src/backend/cmd/api/main_test.go +20 -0
- templates/go/scaffold/src/backend/go.mod +3 -0
- templates/go/scaffold-boundary.yaml +25 -0
- templates/go/skills/go-patterns/SKILL.md +68 -0
- templates/go/skills/go-patterns/assets/go-checklist.md +29 -0
- templates/go/skills/go-patterns/references/anatomy.md +115 -0
- templates/go/skills/go-patterns/references/go-2026-idioms.md +92 -0
- templates/go/skills/go-patterns/scripts/new_endpoint.py +117 -0
- templates/go/skills/go-patterns/versions.json +16 -0
- templates/go/stack.yaml +54 -0
- templates/go-fiber/rules/backend.md +20 -0
- templates/go-fiber/scaffold/docs/engineering/fiber-rules.md +97 -0
- templates/go-fiber/scaffold/docs/playbooks/fiber-service.md +149 -0
- templates/go-fiber/scaffold/src/backend/cmd/api/main.go +21 -0
- templates/go-fiber/scaffold/src/backend/cmd/api/main_test.go +17 -0
- templates/go-fiber/scaffold/src/backend/go.mod +23 -0
- templates/go-fiber/scaffold/src/backend/go.sum +49 -0
- templates/go-fiber/scaffold-boundary.yaml +26 -0
- templates/go-fiber/skills/go-fiber/SKILL.md +203 -0
- templates/go-fiber/skills/go-fiber/assets/fiber-checklist.md +26 -0
- templates/go-fiber/skills/go-fiber/references/anatomy.md +116 -0
- templates/go-fiber/skills/go-fiber/references/fiber-v3-patterns.md +89 -0
- templates/go-fiber/skills/go-fiber/scripts/new_endpoint.py +107 -0
- templates/go-fiber/skills/go-fiber/versions.json +16 -0
- templates/go-fiber/stack.yaml +59 -0
- templates/go-plain/scaffold/src/backend/go.mod +3 -0
- templates/go-plain/scaffold/src/backend/main.go +7 -0
- templates/go-plain/scaffold-boundary.yaml +22 -0
- templates/go-plain/stack.yaml +51 -0
- templates/java-plain/scaffold/src/backend/mvnw +302 -0
- templates/java-plain/scaffold/src/backend/pom.xml +41 -0
- templates/java-plain/scaffold/src/backend/src/main/java/com/example/app/Main.java +10 -0
- templates/java-plain/scaffold-boundary.yaml +23 -0
- templates/java-plain/stack.yaml +51 -0
- templates/laravel/rules/backend.md +20 -0
- templates/laravel/scaffold/docs/engineering/laravel-rules.md +28 -0
- templates/laravel/scaffold/docs/playbooks/laravel-service.md +28 -0
- templates/laravel/scaffold/src/backend/app/Exceptions/Handler.php +27 -0
- templates/laravel/scaffold/src/backend/app/Http/Controllers/HealthController.php +15 -0
- templates/laravel/scaffold/src/backend/app/Support/HealthStatus.php +14 -0
- templates/laravel/scaffold/src/backend/composer.json +24 -0
- templates/laravel/scaffold/src/backend/phpunit.xml +10 -0
- templates/laravel/scaffold/src/backend/public/index.php +8 -0
- templates/laravel/scaffold/src/backend/routes/api.php +7 -0
- templates/laravel/scaffold/src/backend/tests/Unit/HealthStatusTest.php +16 -0
- templates/laravel/scaffold-boundary.yaml +23 -0
- templates/laravel/skills/laravel/SKILL.md +56 -0
- templates/laravel/skills/laravel/references/anatomy.md +65 -0
- templates/laravel/stack.yaml +66 -0
- templates/meta/rules/graph-first.md +27 -0
- templates/meta/rules/hook-author.md +19 -0
- templates/meta/rules/mcp-tool-author.md +18 -0
- templates/meta/rules/meta-engineering.md +17 -0
- templates/meta/scaffold-boundary.yaml +55 -0
- templates/meta/skills/claude-sdk-integration/SKILL.md +163 -0
- templates/meta/skills/claude-sdk-integration/assets/sdk-checklist.md +27 -0
- templates/meta/skills/claude-sdk-integration/scripts/check_model_ids.py +97 -0
- templates/meta/skills/graph-os-authoring/SKILL.md +278 -0
- templates/meta/skills/graph-os-authoring/assets/graph-os-checklist.md +25 -0
- templates/meta/skills/graph-os-authoring/scripts/new_extractor.py +76 -0
- templates/meta/skills/hook-authoring/SKILL.md +292 -0
- templates/meta/skills/hook-authoring/assets/hook-checklist.md +30 -0
- templates/meta/skills/hook-authoring/scripts/new_hook.sh +75 -0
- templates/meta/skills/mcp-tool-authoring/SKILL.md +301 -0
- templates/meta/skills/mcp-tool-authoring/assets/mcp-tool-checklist.md +29 -0
- templates/meta/skills/mcp-tool-authoring/scripts/new_tool.py +74 -0
- templates/meta/skills/meta-engineering/SKILL.md +151 -0
- templates/meta/skills/meta-engineering/assets/meta-edit-checklist.md +28 -0
- templates/meta/skills/meta-engineering/scripts/which_layer.py +61 -0
- templates/meta/skills/python-meta-server/SKILL.md +162 -0
- templates/meta/skills/python-meta-server/assets/meta-server-checklist.md +28 -0
- templates/meta/skills/python-meta-server/scripts/check_envelope.py +91 -0
- templates/meta/skills/react-vite-hub/SKILL.md +140 -0
- templates/meta/skills/react-vite-hub/assets/hub-ui-checklist.md +23 -0
- templates/meta/skills/react-vite-hub/scripts/check_vite_env.py +73 -0
- templates/meta/stack.yaml +119 -0
- templates/nestjs/rules/backend.md +20 -0
- templates/nestjs/scaffold/docs/engineering/nestjs-rules.md +33 -0
- templates/nestjs/scaffold/docs/playbooks/nestjs-service.md +39 -0
- templates/nestjs/scaffold/src/backend/nest-cli.json +5 -0
- templates/nestjs/scaffold/src/backend/package.json +28 -0
- templates/nestjs/scaffold/src/backend/src/app.module.ts +9 -0
- templates/nestjs/scaffold/src/backend/src/common/all-exceptions.filter.ts +59 -0
- templates/nestjs/scaffold/src/backend/src/health/health.controller.ts +14 -0
- templates/nestjs/scaffold/src/backend/src/health/health.module.ts +10 -0
- templates/nestjs/scaffold/src/backend/src/health/health.service.spec.ts +21 -0
- templates/nestjs/scaffold/src/backend/src/health/health.service.ts +9 -0
- templates/nestjs/scaffold/src/backend/src/main.ts +26 -0
- templates/nestjs/scaffold/src/backend/tsconfig.json +16 -0
- templates/nestjs/scaffold/src/backend/vitest.config.ts +9 -0
- templates/nestjs/scaffold-boundary.yaml +25 -0
- templates/nestjs/skills/nestjs/SKILL.md +67 -0
- templates/nestjs/skills/nestjs/references/anatomy.md +65 -0
- templates/nestjs/stack.yaml +68 -0
- templates/nextjs/rules/frontend.md +18 -0
- templates/nextjs/scaffold/docs/design/00-index.md +23 -0
- templates/nextjs/scaffold/docs/design/colors-tokens.md +141 -0
- templates/nextjs/scaffold/docs/design/components-patterns.md +159 -0
- templates/nextjs/scaffold/docs/design/motion-accessibility.md +137 -0
- templates/nextjs/scaffold/docs/design/typography-spacing.md +107 -0
- templates/nextjs/scaffold/docs/engineering/accessibility-web.md +56 -0
- templates/nextjs/scaffold/docs/engineering/copywriting-standard.md +102 -0
- templates/nextjs/scaffold/docs/engineering/formatting-rules.md +89 -0
- templates/nextjs/scaffold/docs/engineering/frontend-rendering-rules.md +80 -0
- templates/nextjs/scaffold/docs/engineering/frontend-rules.md +183 -0
- templates/nextjs/scaffold/docs/engineering/i18n-policy.md +99 -0
- templates/nextjs/scaffold/docs/pages-content-spec/00-index.md +78 -0
- templates/nextjs/scaffold/docs/playbooks/content-seo.md +55 -0
- templates/nextjs/scaffold/docs/playbooks/docs-governance.md +51 -0
- templates/nextjs/scaffold/docs/playbooks/frontend-ui.md +63 -0
- templates/nextjs/scaffold/src/frontend/app/layout.tsx +14 -0
- templates/nextjs/scaffold/src/frontend/app/page.tsx +3 -0
- templates/nextjs/scaffold/src/frontend/eslint.config.js +17 -0
- templates/nextjs/scaffold/src/frontend/lib/greeting.test.ts +9 -0
- templates/nextjs/scaffold/src/frontend/lib/greeting.ts +3 -0
- templates/nextjs/scaffold/src/frontend/package.json +28 -0
- templates/nextjs/scaffold/src/frontend/tsconfig.json +18 -0
- templates/nextjs/scaffold/src/frontend/vitest.config.ts +10 -0
- templates/nextjs/scaffold-boundary.yaml +30 -0
- templates/nextjs/skills/nextjs-react/SKILL.md +485 -0
- templates/nextjs/skills/nextjs-react/references/anatomy.md +116 -0
- templates/nextjs/skills/nextjs-react/scripts/new_component.py +72 -0
- templates/nextjs/stack.yaml +81 -0
- templates/node-express/rules/backend.md +20 -0
- templates/node-express/scaffold/docs/engineering/express-rules.md +30 -0
- templates/node-express/scaffold/docs/playbooks/express-service.md +35 -0
- templates/node-express/scaffold/src/backend/package.json +24 -0
- templates/node-express/scaffold/src/backend/src/index.ts +17 -0
- templates/node-express/scaffold/src/backend/src/middleware/error-handler.ts +12 -0
- templates/node-express/scaffold/src/backend/src/routes/health.test.ts +33 -0
- templates/node-express/scaffold/src/backend/src/routes/health.ts +7 -0
- templates/node-express/scaffold/src/backend/tsconfig.json +15 -0
- templates/node-express/scaffold/src/backend/types/express-bootstrap.d.ts +21 -0
- templates/node-express/scaffold-boundary.yaml +25 -0
- templates/node-express/skills/node-express/SKILL.md +70 -0
- templates/node-express/skills/node-express/references/anatomy.md +63 -0
- templates/node-express/stack.yaml +63 -0
- templates/python/scaffold/docs/engineering/python-rules.md +27 -0
- templates/python/scaffold/docs/playbooks/python-library.md +35 -0
- templates/python/stack.yaml +60 -0
- templates/rails/rules/backend.md +10 -0
- templates/rails/scaffold/docs/engineering/rails-rules.md +33 -0
- templates/rails/scaffold/docs/playbooks/rails-service.md +42 -0
- templates/rails/scaffold/src/backend/Gemfile +12 -0
- templates/rails/scaffold/src/backend/app/controllers/application_controller.rb +26 -0
- templates/rails/scaffold/src/backend/app/controllers/health_controller.rb +6 -0
- templates/rails/scaffold/src/backend/app/models/health.rb +6 -0
- templates/rails/scaffold/src/backend/config/application.rb +12 -0
- templates/rails/scaffold/src/backend/config/boot.rb +3 -0
- templates/rails/scaffold/src/backend/config/routes.rb +4 -0
- templates/rails/scaffold/src/backend/config.ru +5 -0
- templates/rails/scaffold/src/backend/spec/rails_helper.rb +18 -0
- templates/rails/scaffold/src/backend/spec/requests/health_spec.rb +24 -0
- templates/rails/scaffold-boundary.yaml +25 -0
- templates/rails/skills/rails/SKILL.md +62 -0
- templates/rails/skills/rails/references/anatomy.md +71 -0
- templates/rails/stack.yaml +72 -0
- templates/react-native/rules/mobile.md +26 -0
- templates/react-native/scaffold/docs/engineering/accessibility-mobile.md +95 -0
- templates/react-native/scaffold/docs/engineering/mobile-rules.md +56 -0
- templates/react-native/scaffold/docs/engineering/offline-first.md +61 -0
- templates/react-native/scaffold/docs/playbooks/mobile-app.md +49 -0
- templates/react-native/scaffold/src/mobile/App.tsx +9 -0
- templates/react-native/scaffold/src/mobile/eslint.config.js +17 -0
- templates/react-native/scaffold/src/mobile/package.json +23 -0
- templates/react-native/scaffold/src/mobile/src/greeting.test.ts +9 -0
- templates/react-native/scaffold/src/mobile/src/greeting.ts +3 -0
- templates/react-native/scaffold/src/mobile/tsconfig.json +17 -0
- templates/react-native/scaffold/src/mobile/vitest.config.ts +10 -0
- templates/react-native/scaffold-boundary.yaml +30 -0
- templates/react-native/skills/react-native-mobile/SKILL.md +119 -0
- templates/react-native/skills/react-native-mobile/assets/rn-mobile-checklist.md +28 -0
- templates/react-native/skills/react-native-mobile/references/anatomy.md +140 -0
- templates/react-native/skills/react-native-mobile/references/rn-2026-practices.md +54 -0
- templates/react-native/skills/react-native-mobile/scripts/new_screen.py +73 -0
- templates/react-native/skills/react-native-mobile/versions.json +16 -0
- templates/react-native/skills/react-native-patterns/SKILL.md +512 -0
- templates/react-native/skills/react-native-patterns/assets/rn-review-checklist.md +26 -0
- templates/react-native/skills/react-native-patterns/references/anatomy.md +62 -0
- templates/react-native/skills/react-native-patterns/references/list-performance.md +70 -0
- templates/react-native/skills/react-native-patterns/scripts/scan_rn_perf.py +77 -0
- templates/react-native/stack.yaml +70 -0
- templates/ruby-plain/scaffold/src/backend/Gemfile +8 -0
- templates/ruby-plain/scaffold/src/backend/main.rb +3 -0
- templates/ruby-plain/scaffold-boundary.yaml +23 -0
- templates/ruby-plain/stack.yaml +50 -0
- templates/rust-axum/rules/backend.md +20 -0
- templates/rust-axum/scaffold/docs/engineering/rust-axum-rules.md +35 -0
- templates/rust-axum/scaffold/docs/playbooks/rust-axum-service.md +43 -0
- templates/rust-axum/scaffold/src/backend/Cargo.toml +20 -0
- templates/rust-axum/scaffold/src/backend/src/app.rs +12 -0
- templates/rust-axum/scaffold/src/backend/src/error.rs +48 -0
- templates/rust-axum/scaffold/src/backend/src/main.rs +24 -0
- templates/rust-axum/scaffold/src/backend/src/routes/health.rs +37 -0
- templates/rust-axum/scaffold/src/backend/src/routes/mod.rs +2 -0
- templates/rust-axum/scaffold-boundary.yaml +25 -0
- templates/rust-axum/skills/rust/SKILL.md +73 -0
- templates/rust-axum/skills/rust/references/anatomy.md +63 -0
- templates/rust-axum/stack.yaml +65 -0
- templates/rust-plain/scaffold/src/backend/Cargo.toml +6 -0
- templates/rust-plain/scaffold/src/backend/src/main.rs +3 -0
- templates/rust-plain/scaffold-boundary.yaml +23 -0
- templates/rust-plain/stack.yaml +51 -0
- templates/spring-boot/rules/backend.md +20 -0
- templates/spring-boot/scaffold/docs/engineering/spring-boot-rules.md +35 -0
- templates/spring-boot/scaffold/docs/playbooks/spring-boot-service.md +45 -0
- templates/spring-boot/scaffold/src/backend/mvnw +302 -0
- templates/spring-boot/scaffold/src/backend/pom.xml +69 -0
- templates/spring-boot/scaffold/src/backend/src/main/java/com/example/app/Application.java +16 -0
- templates/spring-boot/scaffold/src/backend/src/main/java/com/example/app/common/GlobalExceptionHandler.java +31 -0
- templates/spring-boot/scaffold/src/backend/src/main/java/com/example/app/health/HealthController.java +22 -0
- templates/spring-boot/scaffold/src/backend/src/main/java/com/example/app/health/HealthService.java +12 -0
- templates/spring-boot/scaffold/src/backend/src/main/java/com/example/app/health/HealthStatus.java +4 -0
- templates/spring-boot/scaffold/src/backend/src/test/java/com/example/app/health/HealthServiceTest.java +15 -0
- templates/spring-boot/scaffold-boundary.yaml +26 -0
- templates/spring-boot/skills/spring-boot/SKILL.md +84 -0
- templates/spring-boot/skills/spring-boot/references/anatomy.md +63 -0
- templates/spring-boot/stack.yaml +65 -0
- templates/svelte-sveltekit/rules/frontend.md +20 -0
- templates/svelte-sveltekit/scaffold/docs/engineering/svelte-sveltekit-rules.md +37 -0
- templates/svelte-sveltekit/scaffold/docs/playbooks/svelte-sveltekit-app.md +36 -0
- templates/svelte-sveltekit/scaffold/src/frontend/package.json +22 -0
- templates/svelte-sveltekit/scaffold/src/frontend/src/app.html +12 -0
- templates/svelte-sveltekit/scaffold/src/frontend/src/hooks.server.ts +14 -0
- templates/svelte-sveltekit/scaffold/src/frontend/src/lib/components/Greeting.svelte +6 -0
- templates/svelte-sveltekit/scaffold/src/frontend/src/lib/stores/count.test.ts +26 -0
- templates/svelte-sveltekit/scaffold/src/frontend/src/lib/stores/count.ts +4 -0
- templates/svelte-sveltekit/scaffold/src/frontend/src/routes/+layout.svelte +24 -0
- templates/svelte-sveltekit/scaffold/src/frontend/src/routes/+page.svelte +9 -0
- templates/svelte-sveltekit/scaffold/src/frontend/src/routes/+page.ts +7 -0
- templates/svelte-sveltekit/scaffold/src/frontend/src/routes/health/+server.ts +7 -0
- templates/svelte-sveltekit/scaffold/src/frontend/svelte.config.js +10 -0
- templates/svelte-sveltekit/scaffold/src/frontend/tsconfig.json +7 -0
- templates/svelte-sveltekit/scaffold/src/frontend/vite.config.ts +7 -0
- templates/svelte-sveltekit/scaffold/src/frontend/vitest.config.ts +11 -0
- templates/svelte-sveltekit/scaffold-boundary.yaml +29 -0
- templates/svelte-sveltekit/skills/svelte/SKILL.md +90 -0
- templates/svelte-sveltekit/skills/svelte/references/anatomy.md +62 -0
- templates/svelte-sveltekit/stack.yaml +70 -0
- templates/typescript-plain/scaffold/src/index.ts +3 -0
- templates/typescript-plain/scaffold/tsconfig.json +13 -0
- templates/typescript-plain/scaffold-boundary.yaml +22 -0
- templates/typescript-plain/stack.yaml +44 -0
- templates/vue-nuxt/rules/frontend.md +19 -0
- templates/vue-nuxt/scaffold/docs/engineering/nuxt-rules.md +30 -0
- templates/vue-nuxt/scaffold/docs/playbooks/nuxt-app.md +29 -0
- templates/vue-nuxt/scaffold/src/frontend/app.vue +3 -0
- templates/vue-nuxt/scaffold/src/frontend/nuxt.config.ts +11 -0
- templates/vue-nuxt/scaffold/src/frontend/package.json +20 -0
- templates/vue-nuxt/scaffold/src/frontend/pages/index.test.ts +19 -0
- templates/vue-nuxt/scaffold/src/frontend/pages/index.vue +11 -0
- templates/vue-nuxt/scaffold/src/frontend/vitest.config.ts +12 -0
- templates/vue-nuxt/scaffold-boundary.yaml +26 -0
- templates/vue-nuxt/skills/vue-nuxt/SKILL.md +57 -0
- templates/vue-nuxt/skills/vue-nuxt/references/anatomy.md +60 -0
- templates/vue-nuxt/stack.yaml +61 -0
- templates/wordpress/rules/backend.md +19 -0
- templates/wordpress/scaffold/docs/engineering/wordpress-rules.md +28 -0
- templates/wordpress/scaffold/docs/playbooks/wordpress-service.md +29 -0
- templates/wordpress/scaffold/src/backend/composer.json +17 -0
- templates/wordpress/scaffold/src/backend/phpcs.xml.dist +11 -0
- templates/wordpress/scaffold/src/backend/phpunit.xml +10 -0
- templates/wordpress/scaffold/src/backend/plugin/inc/health.php +8 -0
- templates/wordpress/scaffold/src/backend/plugin/plugin.php +28 -0
- templates/wordpress/scaffold/src/backend/tests/HealthStatusTest.php +15 -0
- templates/wordpress/scaffold/src/backend/theme/functions.php +18 -0
- templates/wordpress/scaffold/src/backend/theme/style.css +11 -0
- templates/wordpress/scaffold-boundary.yaml +23 -0
- templates/wordpress/skills/wordpress/SKILL.md +110 -0
- templates/wordpress/skills/wordpress/assets/wp-checklist.md +28 -0
- templates/wordpress/skills/wordpress/references/wp-development.md +75 -0
- templates/wordpress/skills/wordpress/references/wp-security.md +68 -0
- templates/wordpress/skills/wordpress/scripts/scan_wp_smells.py +91 -0
- templates/wordpress/skills/wordpress/versions.json +16 -0
- templates/wordpress/stack.yaml +60 -0
- thinking_os/__init__.py +1 -0
- thinking_os/_agent_markers.py +32 -0
- thinking_os/background.py +405 -0
- thinking_os/bootstrap_outcomes.py +200 -0
- thinking_os/budget.py +302 -0
- thinking_os/capture.py +495 -0
- thinking_os/cognition.py +516 -0
- thinking_os/cognition_schemas.py +517 -0
- thinking_os/compress.py +192 -0
- thinking_os/concepts.py +233 -0
- thinking_os/dashboard.py +159 -0
- thinking_os/database.py +2883 -0
- thinking_os/decay.py +393 -0
- thinking_os/digest.py +295 -0
- thinking_os/dispatcher.py +192 -0
- thinking_os/dispatcher_helpers.py +48 -0
- thinking_os/dispatchers/__init__.py +3 -0
- thinking_os/dispatchers/default.py +47 -0
- thinking_os/distill.py +192 -0
- thinking_os/doc_indexer.py +905 -0
- thinking_os/embeddings.py +943 -0
- thinking_os/formula_composer.py +556 -0
- thinking_os/gate_marker.py +75 -0
- thinking_os/graph.py +296 -0
- thinking_os/graph_indexer.py +360 -0
- thinking_os/health_check.py +517 -0
- thinking_os/impact.py +119 -0
- thinking_os/memory_gc.py +356 -0
- thinking_os/migrator_embeddings.py +318 -0
- thinking_os/precision.py +194 -0
- thinking_os/record_outcome.py +399 -0
- thinking_os/repair.py +105 -0
- thinking_os/retrieval_quality.py +239 -0
- thinking_os/roles_state.py +168 -0
- thinking_os/sanitizer.py +320 -0
- thinking_os/server.py +3160 -0
- thinking_os/session_enrich.py +272 -0
- thinking_os/session_observe_worker.py +111 -0
- thinking_os/session_startup.py +74 -0
- thinking_os/session_summary.py +245 -0
- thinking_os/task_analyzer.py +462 -0
- thinking_os/task_parser.py +342 -0
- thinking_os/task_sync.py +73 -0
- thinking_os/tools/__init__.py +6 -0
- thinking_os/tools/_shared.py +947 -0
- thinking_os/tools/cognition.py +1867 -0
- thinking_os/tools/docs.py +770 -0
- thinking_os/tools/learning.py +2078 -0
- thinking_os/tools/logs.py +79 -0
- thinking_os/tools/memory.py +840 -0
- thinking_os/tools/metrics.py +200 -0
- thinking_os/tools/retrieve.py +415 -0
- thinking_os/tools/routing.py +658 -0
- thinking_os/tools/tasks.py +449 -0
- thinking_os/tools/trajectory.py +181 -0
- thinking_os/tracing.py +235 -0
- web/__init__.py +5 -0
- web/_cache.py +118 -0
- web/_deps.py +56 -0
- web/_envelope.py +85 -0
- web/_project_context.py +140 -0
- web/chat_providers.py +108 -0
- web/init_jobs.py +216 -0
- web/routes/__init__.py +25 -0
- web/routes/_bounded_read.py +76 -0
- web/routes/board.py +1089 -0
- web/routes/cognition.py +1838 -0
- web/routes/config.py +635 -0
- web/routes/graph.py +513 -0
- web/routes/health.py +180 -0
- web/routes/hooks.py +288 -0
- web/routes/hub.py +1219 -0
- web/routes/logs.py +374 -0
- web/routes/metrics.py +43 -0
- web/routes/observability.py +400 -0
- web/routes/patterns.py +227 -0
- web/routes/presence.py +609 -0
- web/routes/roles.py +446 -0
- web/routes/scheduled.py +261 -0
- web/routes/search.py +238 -0
- web/routes/sessions.py +220 -0
- web/routes/settings.py +363 -0
- web/routes/stream.py +547 -0
- web/security.py +157 -0
- web/server.py +283 -0
|
@@ -0,0 +1,2883 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Coding OS — SQLite database module with auto-migration.
|
|
3
|
+
|
|
4
|
+
Provides connection management (WAL mode), schema versioning,
|
|
5
|
+
and migration execution for the thinking_os self-learning system.
|
|
6
|
+
|
|
7
|
+
Agent-agnostic: DB path is configurable via COS_DB_PATH env var.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import logging
|
|
14
|
+
import os
|
|
15
|
+
import sqlite3
|
|
16
|
+
from collections.abc import Callable, Generator
|
|
17
|
+
from contextlib import contextmanager
|
|
18
|
+
from contextvars import ContextVar
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import Union
|
|
21
|
+
|
|
22
|
+
# Per-request project scope hook. The web layer (Hub) binds this CV in
|
|
23
|
+
# `ProjectScopeMiddleware` so every downstream lookup that goes through
|
|
24
|
+
# `resolve_db_path()` lands on the right project's `coding-os.db` —
|
|
25
|
+
# without graph_os / thinking_os having to import from `core.web`.
|
|
26
|
+
#
|
|
27
|
+
# When the CV is None (MCP server, CLI, hooks), `resolve_db_path()`
|
|
28
|
+
# falls through to its legacy behaviour (explicit project_root arg or
|
|
29
|
+
# DEFAULT_DB_PATH). This keeps single-project callers unaffected.
|
|
30
|
+
_active_project_root: ContextVar[Path | None] = ContextVar(
|
|
31
|
+
"cos_active_project_root",
|
|
32
|
+
default=None,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def set_active_project_root(root: Path | None) -> object:
|
|
37
|
+
"""Bind the current request's project root; returns a reset token."""
|
|
38
|
+
return _active_project_root.set(root)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def reset_active_project_root(token: object) -> None:
|
|
42
|
+
"""Release a binding made by :func:`set_active_project_root`."""
|
|
43
|
+
_active_project_root.reset(token) # type: ignore[arg-type]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def get_active_project_root() -> Path | None:
|
|
47
|
+
"""Return the currently-bound project root (or None when unset)."""
|
|
48
|
+
return _active_project_root.get()
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
logger = logging.getLogger("coding_os.db")
|
|
52
|
+
|
|
53
|
+
# Default DB path — configurable via COS_DB_PATH env var
|
|
54
|
+
# Falls back to .coding-os/coding-os.db in current working directory.
|
|
55
|
+
# Canonical filename, single source of truth for every consumer (MCP server,
|
|
56
|
+
# Hub web, every CLI subcommand, every hook).
|
|
57
|
+
DB_FILENAME = "coding-os.db"
|
|
58
|
+
LEGACY_DB_FILENAME = "thinking_os.db" # rename target for migrate_legacy_db_filename()
|
|
59
|
+
STATE_DIRNAME = ".coding-os"
|
|
60
|
+
|
|
61
|
+
# A true project root co-locates its `.coding-os/` with at least one of these
|
|
62
|
+
# markers. A stray nested `.coding-os/` (lazy-created from a subdir like
|
|
63
|
+
# src/cli/) has none of them — so preferring a marked ancestor lets us skip
|
|
64
|
+
# strays and anchor on the real root.
|
|
65
|
+
_ROOT_MARKERS = (
|
|
66
|
+
".git",
|
|
67
|
+
".coding-os.yaml",
|
|
68
|
+
"pyproject.toml",
|
|
69
|
+
"package.json",
|
|
70
|
+
"go.mod",
|
|
71
|
+
"AGENTS.md",
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _find_project_root_from_cwd(start: Path | None = None) -> Path | None:
|
|
76
|
+
"""Walk up from cwd to find the enclosing coding-os project root.
|
|
77
|
+
|
|
78
|
+
.coding-os/ lives ONLY at the project root. Anywhere we land —
|
|
79
|
+
src/core/web/, tests/, src/cli/, … — we must walk parents and
|
|
80
|
+
anchor on the first .coding-os/ we find. Without this walk, lazy
|
|
81
|
+
init_db() calls from a subdirectory would CREATE a new stray
|
|
82
|
+
.coding-os/ at cwd, which then surfaces in the Hub as a phantom
|
|
83
|
+
project (the exact bug TASK-117 traced to nested .coding-os/).
|
|
84
|
+
"""
|
|
85
|
+
cur = (start or Path.cwd()).resolve()
|
|
86
|
+
try:
|
|
87
|
+
home = Path.home().resolve()
|
|
88
|
+
except (OSError, RuntimeError):
|
|
89
|
+
home = None
|
|
90
|
+
first_with_state: Path | None = None
|
|
91
|
+
for parent in [cur, *cur.parents]:
|
|
92
|
+
# $HOME hard-stop: never inspect or accept $HOME/.coding-os (the global
|
|
93
|
+
# hub state, not a project root). Mirrors the boundary in
|
|
94
|
+
# cos-env.sh::_cos_find_project_root (TASK-498).
|
|
95
|
+
if home is not None and parent == home:
|
|
96
|
+
break
|
|
97
|
+
try:
|
|
98
|
+
if not (parent / STATE_DIRNAME).is_dir():
|
|
99
|
+
continue
|
|
100
|
+
except OSError:
|
|
101
|
+
continue
|
|
102
|
+
if first_with_state is None:
|
|
103
|
+
first_with_state = parent
|
|
104
|
+
# Prefer a `.coding-os/` that co-locates with a project-root marker:
|
|
105
|
+
# this skips a stray nested `.coding-os/` (e.g. src/cli/.coding-os/
|
|
106
|
+
# lazy-created from a subdir) and anchors on the true root. The walk
|
|
107
|
+
# stops at the first MARKED root, so a legitimate checkout is never
|
|
108
|
+
# overridden by an unmarked outer stray (e.g. ~/.coding-os).
|
|
109
|
+
try:
|
|
110
|
+
if any((parent / marker).exists() for marker in _ROOT_MARKERS):
|
|
111
|
+
return parent
|
|
112
|
+
except OSError:
|
|
113
|
+
continue
|
|
114
|
+
if first_with_state is not None:
|
|
115
|
+
return first_with_state
|
|
116
|
+
# No project `.coding-os/` below the $HOME boundary. A real subdir returns
|
|
117
|
+
# cwd so it can lazy-create locally — but $HOME itself is refused: its
|
|
118
|
+
# `.coding-os/` is the global hub state dir, and anchoring there mints a
|
|
119
|
+
# phantom project DB inside it. At bare $HOME there is no project → None.
|
|
120
|
+
if home is not None and cur == home:
|
|
121
|
+
return None
|
|
122
|
+
return cur
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
DEFAULT_DB_PATH = Path(
|
|
126
|
+
os.environ.get("COS_DB_PATH", "")
|
|
127
|
+
or str((_find_project_root_from_cwd() or Path.cwd()) / STATE_DIRNAME / DB_FILENAME)
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def resolve_db_path(project_root: Path | str | None = None) -> Path:
|
|
132
|
+
"""Single source of truth for the canonical SQLite DB path.
|
|
133
|
+
|
|
134
|
+
Resolution priority:
|
|
135
|
+
1. ``<bound_root>/.coding-os/coding-os.db`` when a ProjectScopeMiddleware
|
|
136
|
+
request has bound a per-request project scope. This wins over
|
|
137
|
+
``$COS_DB_PATH`` because the Hub inherits that env var from the
|
|
138
|
+
directory it was launched in, so a scoped ``/api/p/<slug>/*`` request
|
|
139
|
+
must reach the slug's DB, not the launch project's.
|
|
140
|
+
2. ``$COS_DB_PATH`` env var, when set (the CLI / MCP default override).
|
|
141
|
+
3. ``<project_root>/.coding-os/coding-os.db`` when project_root given.
|
|
142
|
+
4. Walk up from cwd to find the enclosing ``.coding-os/``. RAISES at the
|
|
143
|
+
bare ``$HOME`` boundary (no project below it): ``~/.coding-os/`` is the
|
|
144
|
+
global hub state dir, and every DB-open path funnels through this
|
|
145
|
+
resolver, so raising here is the ONE complete guard against minting a
|
|
146
|
+
phantom ``$HOME/.coding-os/coding-os.db`` — the graph ``SqliteBackend``
|
|
147
|
+
and cognition route ``sqlite3.connect`` directly to this path and would
|
|
148
|
+
otherwise bypass ``init_db``'s guard. Fail-loud at a bare-``$HOME``
|
|
149
|
+
misconfiguration beats a silent phantom; set ``$COS_DB_PATH`` or run
|
|
150
|
+
inside a project. Do NOT weaken this to a cwd fallback.
|
|
151
|
+
|
|
152
|
+
Only the Hub's ProjectScopeMiddleware binds ``_active_project_root``, so
|
|
153
|
+
CLI / MCP callers skip step 1 and keep the prior ``$COS_DB_PATH`` behavior.
|
|
154
|
+
|
|
155
|
+
Use this helper instead of inlining the same fallback formula in
|
|
156
|
+
~30 different sites — a future filename change becomes one edit
|
|
157
|
+
here, not a sweep across `core/`, `cli/`, `adapters/`, and hooks.
|
|
158
|
+
|
|
159
|
+
The path is returned even if the file does not exist yet — callers
|
|
160
|
+
that need the file present should follow with ``init_db(path)``.
|
|
161
|
+
"""
|
|
162
|
+
bound = _active_project_root.get()
|
|
163
|
+
if bound is not None:
|
|
164
|
+
return Path(bound) / STATE_DIRNAME / DB_FILENAME
|
|
165
|
+
explicit = os.environ.get("COS_DB_PATH")
|
|
166
|
+
if explicit:
|
|
167
|
+
return Path(explicit)
|
|
168
|
+
if project_root is not None:
|
|
169
|
+
return Path(project_root) / STATE_DIRNAME / DB_FILENAME
|
|
170
|
+
root = _find_project_root_from_cwd()
|
|
171
|
+
if root is None:
|
|
172
|
+
# Bare $HOME, no project below (see step 4). Every DB-open path resolves
|
|
173
|
+
# through here, so raising is the complete guard — direct-connect
|
|
174
|
+
# callers (graph SqliteBackend, cognition route) bypass init_db.
|
|
175
|
+
raise RuntimeError(
|
|
176
|
+
"no coding-os project found from cwd; set $COS_DB_PATH or run "
|
|
177
|
+
"inside a project — $HOME/.coding-os is the global hub state dir, "
|
|
178
|
+
"not a project DB"
|
|
179
|
+
)
|
|
180
|
+
return root / STATE_DIRNAME / DB_FILENAME
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def project_root(start: Path | str | None = None) -> Path:
|
|
184
|
+
"""Single source of truth for the project root directory (holds .coding-os/).
|
|
185
|
+
|
|
186
|
+
Precedence:
|
|
187
|
+
1. ``$COS_PROJECT_ROOT`` env var, when set (explicit override).
|
|
188
|
+
2. Parent of an absolute ``$COS_STATE_DIR`` — already resolved by
|
|
189
|
+
cos-env.sh, so honoring it means the shell's one resolution is reused
|
|
190
|
+
instead of re-walking.
|
|
191
|
+
3. Upward marker-walk from cwd (``_find_project_root_from_cwd``), which has
|
|
192
|
+
the $HOME hard-stop so the global hub at $HOME/.coding-os is never bound.
|
|
193
|
+
|
|
194
|
+
Use this instead of the ``os.environ.get("COS_PROJECT_ROOT") or os.getcwd()``
|
|
195
|
+
idiom that was duplicated across the CLI, board, web, background, and hook
|
|
196
|
+
helpers — that idiom mis-resolves from a subdirectory (TASK-498).
|
|
197
|
+
"""
|
|
198
|
+
explicit = os.environ.get("COS_PROJECT_ROOT")
|
|
199
|
+
if explicit:
|
|
200
|
+
return Path(explicit).resolve()
|
|
201
|
+
state = os.environ.get("COS_STATE_DIR")
|
|
202
|
+
if state:
|
|
203
|
+
state_path = Path(state)
|
|
204
|
+
if state_path.is_absolute():
|
|
205
|
+
parent = state_path.resolve().parent
|
|
206
|
+
# $HOME hard-stop: COS_STATE_DIR == $HOME/.coding-os is the global
|
|
207
|
+
# hub (set by `cos hub`), not a project root — its parent is $HOME.
|
|
208
|
+
# Reuse the shell's boundary instead of binding $HOME; fall through
|
|
209
|
+
# to the marker-walk (which has its own $HOME hard-stop).
|
|
210
|
+
try:
|
|
211
|
+
home = Path.home().resolve()
|
|
212
|
+
except (OSError, RuntimeError):
|
|
213
|
+
home = None
|
|
214
|
+
if home is None or parent != home:
|
|
215
|
+
return parent
|
|
216
|
+
start_path = Path(start) if start else None
|
|
217
|
+
root = _find_project_root_from_cwd(start_path)
|
|
218
|
+
return root if root is not None else (start_path or Path.cwd()).resolve()
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def migrate_legacy_db_filename(target: Path) -> bool:
|
|
222
|
+
"""Rename `<dir>/thinking_os.db` → `<dir>/coding-os.db` once, in place."""
|
|
223
|
+
if target.exists():
|
|
224
|
+
return False
|
|
225
|
+
legacy = target.with_name(LEGACY_DB_FILENAME)
|
|
226
|
+
if not legacy.exists():
|
|
227
|
+
return False
|
|
228
|
+
legacy.rename(target)
|
|
229
|
+
for ext in ("-shm", "-wal"):
|
|
230
|
+
legacy_aux = legacy.with_name(legacy.name + ext)
|
|
231
|
+
if legacy_aux.exists():
|
|
232
|
+
legacy_aux.rename(target.with_name(target.name + ext))
|
|
233
|
+
logger.info("Migrated legacy DB filename: %s -> %s", legacy.name, target.name)
|
|
234
|
+
return True
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
# ---------------------------------------------------------------------------
|
|
238
|
+
# FTS5 detection (must be defined before migrations that use it)
|
|
239
|
+
# ---------------------------------------------------------------------------
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def has_fts5(conn: sqlite3.Connection) -> bool:
|
|
243
|
+
"""Check whether the current SQLite build supports FTS5."""
|
|
244
|
+
try:
|
|
245
|
+
conn.execute("CREATE VIRTUAL TABLE _fts5_probe USING fts5(x)")
|
|
246
|
+
conn.execute("DROP TABLE _fts5_probe")
|
|
247
|
+
return True
|
|
248
|
+
except sqlite3.OperationalError:
|
|
249
|
+
return False
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def has_fts5_table(conn: sqlite3.Connection) -> bool:
|
|
253
|
+
"""Check whether the observations_fts table exists (FTS5 was successfully created)."""
|
|
254
|
+
row = conn.execute(
|
|
255
|
+
"SELECT name FROM sqlite_master WHERE type='table' AND name='observations_fts'"
|
|
256
|
+
).fetchone()
|
|
257
|
+
return row is not None
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
# ---------------------------------------------------------------------------
|
|
261
|
+
# Callable migrations (defined before MIGRATIONS list so they can be referenced directly)
|
|
262
|
+
# ---------------------------------------------------------------------------
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def _migrate_v2_fts5(conn: sqlite3.Connection) -> None:
|
|
266
|
+
"""Migration v2: create FTS5 virtual table and sync triggers.
|
|
267
|
+
|
|
268
|
+
Gracefully degrades if FTS5 is not available — logs a warning and skips.
|
|
269
|
+
"""
|
|
270
|
+
if not has_fts5(conn):
|
|
271
|
+
logger.warning(
|
|
272
|
+
"FTS5 not available in this SQLite build — skipping FTS5 table creation. "
|
|
273
|
+
"Search will fall back to LIKE queries."
|
|
274
|
+
)
|
|
275
|
+
return
|
|
276
|
+
|
|
277
|
+
conn.executescript("""\
|
|
278
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS observations_fts USING fts5(
|
|
279
|
+
title, narrative, concepts,
|
|
280
|
+
content='observations', content_rowid='id'
|
|
281
|
+
);
|
|
282
|
+
|
|
283
|
+
-- Auto-populate on INSERT
|
|
284
|
+
CREATE TRIGGER IF NOT EXISTS observations_ai AFTER INSERT ON observations BEGIN
|
|
285
|
+
INSERT INTO observations_fts(rowid, title, narrative, concepts)
|
|
286
|
+
VALUES (new.id, new.title, new.narrative, new.concepts);
|
|
287
|
+
END;
|
|
288
|
+
|
|
289
|
+
-- Re-sync on UPDATE (needed for TASK-155 compression)
|
|
290
|
+
CREATE TRIGGER IF NOT EXISTS observations_au AFTER UPDATE ON observations BEGIN
|
|
291
|
+
INSERT INTO observations_fts(observations_fts, rowid, title, narrative, concepts)
|
|
292
|
+
VALUES ('delete', old.id, old.title, old.narrative, old.concepts);
|
|
293
|
+
INSERT INTO observations_fts(rowid, title, narrative, concepts)
|
|
294
|
+
VALUES (new.id, new.title, new.narrative, new.concepts);
|
|
295
|
+
END;
|
|
296
|
+
|
|
297
|
+
-- Auto-cleanup on DELETE
|
|
298
|
+
CREATE TRIGGER IF NOT EXISTS observations_ad AFTER DELETE ON observations BEGIN
|
|
299
|
+
INSERT INTO observations_fts(observations_fts, rowid, title, narrative, concepts)
|
|
300
|
+
VALUES ('delete', old.id, old.title, old.narrative, old.concepts);
|
|
301
|
+
END;
|
|
302
|
+
""")
|
|
303
|
+
logger.info("FTS5 observations_fts table and triggers created successfully")
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
# ---------------------------------------------------------------------------
|
|
307
|
+
# Migration registry
|
|
308
|
+
# ---------------------------------------------------------------------------
|
|
309
|
+
# Each migration is a (version, description, sql_or_callable) tuple.
|
|
310
|
+
# sql_or_callable is either a SQL string (executed via executescript)
|
|
311
|
+
# or a callable(conn) for migrations needing runtime logic (e.g. FTS5 check).
|
|
312
|
+
# Migrations MUST be append-only — never edit an applied migration.
|
|
313
|
+
MigrationAction = Union[str, Callable[[sqlite3.Connection], None]]
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def _migrate_v4_brain_features(conn: sqlite3.Connection) -> None:
|
|
317
|
+
"""Migration v4: outcome_history, concept_graph, session_summaries enrichment."""
|
|
318
|
+
|
|
319
|
+
# 1. outcome_history — append-only log of every outcome transition
|
|
320
|
+
conn.executescript("""\
|
|
321
|
+
CREATE TABLE IF NOT EXISTS outcome_history (
|
|
322
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
323
|
+
task_id TEXT NOT NULL,
|
|
324
|
+
outcome TEXT NOT NULL,
|
|
325
|
+
previous_outcome TEXT,
|
|
326
|
+
is_breakthrough INTEGER DEFAULT 0,
|
|
327
|
+
narrative_what_failed TEXT,
|
|
328
|
+
narrative_what_worked TEXT,
|
|
329
|
+
narrative_key_insight TEXT,
|
|
330
|
+
triggered_by TEXT,
|
|
331
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
332
|
+
);
|
|
333
|
+
|
|
334
|
+
CREATE INDEX IF NOT EXISTS idx_outcome_history_task
|
|
335
|
+
ON outcome_history(task_id);
|
|
336
|
+
CREATE INDEX IF NOT EXISTS idx_outcome_history_breakthrough
|
|
337
|
+
ON outcome_history(is_breakthrough) WHERE is_breakthrough = 1;
|
|
338
|
+
|
|
339
|
+
-- 2. concept_graph — lightweight adjacency list for file/concept relationships
|
|
340
|
+
CREATE TABLE IF NOT EXISTS concept_graph (
|
|
341
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
342
|
+
source TEXT NOT NULL,
|
|
343
|
+
target TEXT NOT NULL,
|
|
344
|
+
edge_type TEXT NOT NULL,
|
|
345
|
+
weight REAL DEFAULT 1.0,
|
|
346
|
+
evidence TEXT,
|
|
347
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
348
|
+
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
349
|
+
UNIQUE(source, target, edge_type)
|
|
350
|
+
);
|
|
351
|
+
|
|
352
|
+
CREATE INDEX IF NOT EXISTS idx_concept_graph_source ON concept_graph(source);
|
|
353
|
+
CREATE INDEX IF NOT EXISTS idx_concept_graph_target ON concept_graph(target);
|
|
354
|
+
CREATE INDEX IF NOT EXISTS idx_concept_graph_type ON concept_graph(edge_type);
|
|
355
|
+
""")
|
|
356
|
+
|
|
357
|
+
# 3. session_summaries enrichment — add columns if missing
|
|
358
|
+
existing_columns = {
|
|
359
|
+
row[1] for row in conn.execute("PRAGMA table_info(session_summaries)").fetchall()
|
|
360
|
+
}
|
|
361
|
+
new_columns = [
|
|
362
|
+
("previous_session_id", "TEXT"),
|
|
363
|
+
("duration_minutes", "INTEGER"),
|
|
364
|
+
("files_touched", "TEXT"),
|
|
365
|
+
("observations_count", "INTEGER DEFAULT 0"),
|
|
366
|
+
("breakthrough_ids", "TEXT"),
|
|
367
|
+
]
|
|
368
|
+
for col_name, col_type in new_columns:
|
|
369
|
+
if col_name not in existing_columns:
|
|
370
|
+
conn.execute(f"ALTER TABLE session_summaries ADD COLUMN {col_name} {col_type}")
|
|
371
|
+
|
|
372
|
+
logger.info(
|
|
373
|
+
"Brain features migration v4 applied: outcome_history, concept_graph, session_summaries enrichment"
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def _migrate_v5_rag(conn: sqlite3.Connection) -> None:
|
|
378
|
+
"""Migration v5: embeddings + document_chunks for RAG.
|
|
379
|
+
|
|
380
|
+
Adds two new tables:
|
|
381
|
+
- embeddings: vector storage for any source row (observations,
|
|
382
|
+
learned_patterns, outcome_history, document_chunks, tasks).
|
|
383
|
+
BLOB column holds float32 bytes (1536 bytes for 384-dim model).
|
|
384
|
+
- document_chunks: heading-aware chunks of project docs/ for the
|
|
385
|
+
document RAG knowledge base.
|
|
386
|
+
|
|
387
|
+
Both tables are additive — no existing tables are modified. Embeddings
|
|
388
|
+
are populated lazily by the embeddings module, so this migration is
|
|
389
|
+
safe even when sentence-transformers is not installed.
|
|
390
|
+
"""
|
|
391
|
+
conn.executescript("""\
|
|
392
|
+
CREATE TABLE IF NOT EXISTS embeddings (
|
|
393
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
394
|
+
source_table TEXT NOT NULL,
|
|
395
|
+
source_id INTEGER NOT NULL,
|
|
396
|
+
text_hash TEXT NOT NULL,
|
|
397
|
+
embedding BLOB NOT NULL,
|
|
398
|
+
model_name TEXT DEFAULT 'BAAI/bge-m3',
|
|
399
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
400
|
+
UNIQUE(source_table, source_id)
|
|
401
|
+
);
|
|
402
|
+
|
|
403
|
+
CREATE INDEX IF NOT EXISTS idx_embeddings_source
|
|
404
|
+
ON embeddings(source_table, source_id);
|
|
405
|
+
CREATE INDEX IF NOT EXISTS idx_embeddings_model
|
|
406
|
+
ON embeddings(model_name);
|
|
407
|
+
|
|
408
|
+
CREATE TABLE IF NOT EXISTS document_chunks (
|
|
409
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
410
|
+
source_path TEXT NOT NULL,
|
|
411
|
+
source_type TEXT NOT NULL,
|
|
412
|
+
chunk_index INTEGER NOT NULL,
|
|
413
|
+
heading_path TEXT,
|
|
414
|
+
content TEXT NOT NULL,
|
|
415
|
+
content_hash TEXT NOT NULL,
|
|
416
|
+
priority REAL DEFAULT 0.5,
|
|
417
|
+
mtime INTEGER NOT NULL,
|
|
418
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
419
|
+
UNIQUE(source_path, chunk_index)
|
|
420
|
+
);
|
|
421
|
+
|
|
422
|
+
CREATE INDEX IF NOT EXISTS idx_doc_chunks_path
|
|
423
|
+
ON document_chunks(source_path);
|
|
424
|
+
CREATE INDEX IF NOT EXISTS idx_doc_chunks_type
|
|
425
|
+
ON document_chunks(source_type);
|
|
426
|
+
""")
|
|
427
|
+
logger.info("RAG migration v5 applied: embeddings + document_chunks tables created")
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
def has_embeddings_table(conn: sqlite3.Connection) -> bool:
|
|
431
|
+
"""Check whether the embeddings table exists (migration v5 applied).
|
|
432
|
+
|
|
433
|
+
Mirrors `has_fts5_table` — used by callers that need to know whether
|
|
434
|
+
semantic search is structurally available before attempting it.
|
|
435
|
+
"""
|
|
436
|
+
row = conn.execute(
|
|
437
|
+
"SELECT name FROM sqlite_master WHERE type='table' AND name='embeddings'"
|
|
438
|
+
).fetchone()
|
|
439
|
+
return row is not None
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
def _migrate_v6_tasks(conn: sqlite3.Connection) -> None:
|
|
443
|
+
"""Migration v6: tasks table for hybrid task store.
|
|
444
|
+
|
|
445
|
+
Mirrors the structure of `docs/tasks/TASK-###-slug.md` files as a
|
|
446
|
+
queryable index. Files remain SSOT — the table is a derived cache
|
|
447
|
+
populated by `board_os/sync.py` (sole writer since TASK-398).
|
|
448
|
+
Dependencies are stored as a JSON-encoded
|
|
449
|
+
list so we can do `LIKE '%"TASK-195"%'` lookups for `cos_task_dependents`
|
|
450
|
+
without false-positive substring matches (TASK-19 vs TASK-195).
|
|
451
|
+
"""
|
|
452
|
+
conn.executescript("""\
|
|
453
|
+
CREATE TABLE IF NOT EXISTS tasks (
|
|
454
|
+
task_id TEXT PRIMARY KEY,
|
|
455
|
+
title TEXT NOT NULL,
|
|
456
|
+
domain TEXT,
|
|
457
|
+
status TEXT NOT NULL DEFAULT 'open',
|
|
458
|
+
file_path TEXT NOT NULL,
|
|
459
|
+
content_hash TEXT NOT NULL,
|
|
460
|
+
mtime INTEGER NOT NULL,
|
|
461
|
+
goal_text TEXT,
|
|
462
|
+
scope_in TEXT,
|
|
463
|
+
scope_out TEXT,
|
|
464
|
+
requirements TEXT,
|
|
465
|
+
dependencies TEXT,
|
|
466
|
+
source_of_truth TEXT,
|
|
467
|
+
read_first TEXT,
|
|
468
|
+
open_questions TEXT,
|
|
469
|
+
rabbit_holes TEXT,
|
|
470
|
+
verification TEXT,
|
|
471
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
472
|
+
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
473
|
+
);
|
|
474
|
+
|
|
475
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status);
|
|
476
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_domain ON tasks(domain);
|
|
477
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_file_path ON tasks(file_path);
|
|
478
|
+
""")
|
|
479
|
+
logger.info("Tasks migration v6 applied: tasks table created")
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
def has_tasks_table(conn: sqlite3.Connection) -> bool:
|
|
483
|
+
"""Check whether the tasks table exists (migration v6 applied).
|
|
484
|
+
|
|
485
|
+
Mirrors `has_fts5_table` / `has_embeddings_table` — callers can guard
|
|
486
|
+
task-related queries when running against an older DB.
|
|
487
|
+
"""
|
|
488
|
+
row = conn.execute(
|
|
489
|
+
"SELECT name FROM sqlite_master WHERE type='table' AND name='tasks'"
|
|
490
|
+
).fetchone()
|
|
491
|
+
return row is not None
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
# ---------------------------------------------------------------------------
|
|
495
|
+
# Brain-hardening validators (shared constants)
|
|
496
|
+
# ---------------------------------------------------------------------------
|
|
497
|
+
|
|
498
|
+
VALID_TRUST_TIERS: frozenset[str] = frozenset({"volatile", "validated", "locked", "core"})
|
|
499
|
+
PROTECTED_TRUST_TIERS: frozenset[str] = frozenset({"locked", "core"})
|
|
500
|
+
VALID_PROVENANCE: frozenset[str] = frozenset(
|
|
501
|
+
{
|
|
502
|
+
"agent_self",
|
|
503
|
+
"user_directive",
|
|
504
|
+
"extracted_from_outcome",
|
|
505
|
+
"promoted_from_rule",
|
|
506
|
+
"imported",
|
|
507
|
+
}
|
|
508
|
+
)
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
def _column_exists(conn: sqlite3.Connection, table: str, column: str) -> bool:
|
|
512
|
+
"""Return True if `column` is present in `table` per PRAGMA table_info."""
|
|
513
|
+
rows = conn.execute(f"PRAGMA table_info({table})").fetchall()
|
|
514
|
+
return any(r[1] == column for r in rows)
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
def _migrate_v7_brain_hardening(conn: sqlite3.Connection) -> None:
|
|
518
|
+
"""Migration v7: trust_tier + provenance + memory_audit."""
|
|
519
|
+
# 1. Add trust_tier + provenance to learned_patterns (idempotent per column)
|
|
520
|
+
if not _column_exists(conn, "learned_patterns", "trust_tier"):
|
|
521
|
+
conn.execute(
|
|
522
|
+
"ALTER TABLE learned_patterns ADD COLUMN trust_tier TEXT NOT NULL DEFAULT 'volatile'"
|
|
523
|
+
)
|
|
524
|
+
if not _column_exists(conn, "learned_patterns", "provenance"):
|
|
525
|
+
conn.execute(
|
|
526
|
+
"ALTER TABLE learned_patterns ADD COLUMN provenance TEXT NOT NULL DEFAULT 'agent_self'"
|
|
527
|
+
)
|
|
528
|
+
|
|
529
|
+
# 2. Add provenance to observations
|
|
530
|
+
if not _column_exists(conn, "observations", "provenance"):
|
|
531
|
+
conn.execute(
|
|
532
|
+
"ALTER TABLE observations ADD COLUMN provenance TEXT NOT NULL DEFAULT 'agent_self'"
|
|
533
|
+
)
|
|
534
|
+
|
|
535
|
+
# 3. memory_audit — append-only audit log
|
|
536
|
+
conn.executescript("""\
|
|
537
|
+
CREATE TABLE IF NOT EXISTS memory_audit (
|
|
538
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
539
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
540
|
+
actor TEXT NOT NULL,
|
|
541
|
+
action TEXT NOT NULL,
|
|
542
|
+
source_table TEXT NOT NULL,
|
|
543
|
+
source_id INTEGER,
|
|
544
|
+
old_value TEXT,
|
|
545
|
+
new_value TEXT,
|
|
546
|
+
reason TEXT
|
|
547
|
+
);
|
|
548
|
+
|
|
549
|
+
CREATE INDEX IF NOT EXISTS idx_memory_audit_table
|
|
550
|
+
ON memory_audit(source_table, source_id);
|
|
551
|
+
CREATE INDEX IF NOT EXISTS idx_memory_audit_created
|
|
552
|
+
ON memory_audit(created_at);
|
|
553
|
+
|
|
554
|
+
-- memory_audit is append-only: block any UPDATE or DELETE
|
|
555
|
+
CREATE TRIGGER IF NOT EXISTS trg_memory_audit_no_update
|
|
556
|
+
BEFORE UPDATE ON memory_audit
|
|
557
|
+
BEGIN
|
|
558
|
+
SELECT RAISE(ABORT, 'memory_audit is append-only');
|
|
559
|
+
END;
|
|
560
|
+
|
|
561
|
+
CREATE TRIGGER IF NOT EXISTS trg_memory_audit_no_delete
|
|
562
|
+
BEFORE DELETE ON memory_audit
|
|
563
|
+
BEGIN
|
|
564
|
+
SELECT RAISE(ABORT, 'memory_audit is append-only');
|
|
565
|
+
END;
|
|
566
|
+
|
|
567
|
+
-- Protect locked/core patterns from UPDATE
|
|
568
|
+
CREATE TRIGGER IF NOT EXISTS trg_learned_patterns_protect_update
|
|
569
|
+
BEFORE UPDATE ON learned_patterns
|
|
570
|
+
WHEN OLD.trust_tier IN ('locked', 'core')
|
|
571
|
+
BEGIN
|
|
572
|
+
SELECT RAISE(ABORT, 'learned_patterns: trust_tier locked/core is immutable via standard path');
|
|
573
|
+
END;
|
|
574
|
+
|
|
575
|
+
-- Protect locked/core patterns from DELETE
|
|
576
|
+
CREATE TRIGGER IF NOT EXISTS trg_learned_patterns_protect_delete
|
|
577
|
+
BEFORE DELETE ON learned_patterns
|
|
578
|
+
WHEN OLD.trust_tier IN ('locked', 'core')
|
|
579
|
+
BEGIN
|
|
580
|
+
SELECT RAISE(ABORT, 'learned_patterns: trust_tier locked/core cannot be deleted via standard path');
|
|
581
|
+
END;
|
|
582
|
+
""")
|
|
583
|
+
logger.info("Brain-hardening migration v7 applied: trust_tier, provenance, memory_audit")
|
|
584
|
+
|
|
585
|
+
|
|
586
|
+
def has_memory_audit_table(conn: sqlite3.Connection) -> bool:
|
|
587
|
+
"""Check whether the memory_audit table exists (migration v7 applied)."""
|
|
588
|
+
row = conn.execute(
|
|
589
|
+
"SELECT name FROM sqlite_master WHERE type='table' AND name='memory_audit'"
|
|
590
|
+
).fetchone()
|
|
591
|
+
return row is not None
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
def is_pattern_protected(conn: sqlite3.Connection, pattern_id: int) -> bool:
|
|
595
|
+
"""Return True if the pattern's trust_tier is in PROTECTED_TRUST_TIERS."""
|
|
596
|
+
if not _column_exists(conn, "learned_patterns", "trust_tier"):
|
|
597
|
+
return False # pre-v7 DB has no concept of protection
|
|
598
|
+
row = conn.execute(
|
|
599
|
+
"SELECT trust_tier FROM learned_patterns WHERE id = ?",
|
|
600
|
+
(pattern_id,),
|
|
601
|
+
).fetchone()
|
|
602
|
+
if row is None:
|
|
603
|
+
return False
|
|
604
|
+
return row[0] in PROTECTED_TRUST_TIERS
|
|
605
|
+
|
|
606
|
+
|
|
607
|
+
def _migrate_v8_validation_throttle(conn: sqlite3.Connection) -> None:
|
|
608
|
+
"""Migration v8: pattern_validations table for anti-sycophancy."""
|
|
609
|
+
conn.executescript("""\
|
|
610
|
+
CREATE TABLE IF NOT EXISTS pattern_validations (
|
|
611
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
612
|
+
session_id TEXT NOT NULL,
|
|
613
|
+
pattern_id INTEGER NOT NULL,
|
|
614
|
+
was_helpful INTEGER NOT NULL,
|
|
615
|
+
was_throttled INTEGER NOT NULL DEFAULT 0,
|
|
616
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
617
|
+
);
|
|
618
|
+
|
|
619
|
+
CREATE INDEX IF NOT EXISTS idx_pattern_validations_session_pattern
|
|
620
|
+
ON pattern_validations(session_id, pattern_id);
|
|
621
|
+
CREATE INDEX IF NOT EXISTS idx_pattern_validations_created
|
|
622
|
+
ON pattern_validations(created_at);
|
|
623
|
+
""")
|
|
624
|
+
logger.info("Validation-throttle migration v8 applied: pattern_validations")
|
|
625
|
+
|
|
626
|
+
|
|
627
|
+
def has_pattern_validations_table(conn: sqlite3.Connection) -> bool:
|
|
628
|
+
"""Check whether the pattern_validations table exists (migration v8)."""
|
|
629
|
+
row = conn.execute(
|
|
630
|
+
"SELECT name FROM sqlite_master WHERE type='table' AND name='pattern_validations'"
|
|
631
|
+
).fetchone()
|
|
632
|
+
return row is not None
|
|
633
|
+
|
|
634
|
+
|
|
635
|
+
def _migrate_v9_docs_fts(conn: sqlite3.Connection) -> None:
|
|
636
|
+
"""Migration v9: FTS5 virtual table over document_chunks."""
|
|
637
|
+
if not has_fts5(conn):
|
|
638
|
+
logger.warning(
|
|
639
|
+
"FTS5 unavailable — skipping document_chunks_fts. doc_search "
|
|
640
|
+
"lexical fallback will degrade to LIKE."
|
|
641
|
+
)
|
|
642
|
+
return
|
|
643
|
+
|
|
644
|
+
conn.executescript("""\
|
|
645
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS document_chunks_fts USING fts5(
|
|
646
|
+
heading_path, content,
|
|
647
|
+
content='document_chunks', content_rowid='id'
|
|
648
|
+
);
|
|
649
|
+
|
|
650
|
+
CREATE TRIGGER IF NOT EXISTS document_chunks_ai AFTER INSERT ON document_chunks BEGIN
|
|
651
|
+
INSERT INTO document_chunks_fts(rowid, heading_path, content)
|
|
652
|
+
VALUES (new.id, new.heading_path, new.content);
|
|
653
|
+
END;
|
|
654
|
+
|
|
655
|
+
CREATE TRIGGER IF NOT EXISTS document_chunks_au AFTER UPDATE ON document_chunks BEGIN
|
|
656
|
+
INSERT INTO document_chunks_fts(document_chunks_fts, rowid, heading_path, content)
|
|
657
|
+
VALUES ('delete', old.id, old.heading_path, old.content);
|
|
658
|
+
INSERT INTO document_chunks_fts(rowid, heading_path, content)
|
|
659
|
+
VALUES (new.id, new.heading_path, new.content);
|
|
660
|
+
END;
|
|
661
|
+
|
|
662
|
+
CREATE TRIGGER IF NOT EXISTS document_chunks_ad AFTER DELETE ON document_chunks BEGIN
|
|
663
|
+
INSERT INTO document_chunks_fts(document_chunks_fts, rowid, heading_path, content)
|
|
664
|
+
VALUES ('delete', old.id, old.heading_path, old.content);
|
|
665
|
+
END;
|
|
666
|
+
""")
|
|
667
|
+
|
|
668
|
+
conn.execute(
|
|
669
|
+
"INSERT INTO document_chunks_fts(rowid, heading_path, content) "
|
|
670
|
+
"SELECT id, heading_path, content FROM document_chunks "
|
|
671
|
+
"WHERE id NOT IN (SELECT rowid FROM document_chunks_fts)"
|
|
672
|
+
)
|
|
673
|
+
logger.info("FTS5 docs migration v9 applied: document_chunks_fts")
|
|
674
|
+
|
|
675
|
+
|
|
676
|
+
def has_document_chunks_fts(conn: sqlite3.Connection) -> bool:
|
|
677
|
+
"""Check whether document_chunks_fts exists (v9 + FTS5 available)."""
|
|
678
|
+
row = conn.execute(
|
|
679
|
+
"SELECT name FROM sqlite_master WHERE type='table' AND name='document_chunks_fts'"
|
|
680
|
+
).fetchone()
|
|
681
|
+
return row is not None
|
|
682
|
+
|
|
683
|
+
|
|
684
|
+
def has_tasks_fts(conn: sqlite3.Connection) -> bool:
|
|
685
|
+
"""Check whether tasks_fts exists (v35 + FTS5 available)."""
|
|
686
|
+
row = conn.execute(
|
|
687
|
+
"SELECT name FROM sqlite_master WHERE type='table' AND name='tasks_fts'"
|
|
688
|
+
).fetchone()
|
|
689
|
+
return row is not None
|
|
690
|
+
|
|
691
|
+
|
|
692
|
+
def has_task_dependencies_table(conn: sqlite3.Connection) -> bool:
|
|
693
|
+
"""Check whether the task_dependencies junction exists (v35 applied)."""
|
|
694
|
+
row = conn.execute(
|
|
695
|
+
"SELECT name FROM sqlite_master WHERE type='table' AND name='task_dependencies'"
|
|
696
|
+
).fetchone()
|
|
697
|
+
return row is not None
|
|
698
|
+
|
|
699
|
+
|
|
700
|
+
def _migrate_v10_retrievals(conn: sqlite3.Connection) -> None:
|
|
701
|
+
"""Migration v10: retrievals table for outcome-driven priority."""
|
|
702
|
+
conn.executescript("""\
|
|
703
|
+
CREATE TABLE IF NOT EXISTS retrievals (
|
|
704
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
705
|
+
session_id TEXT NOT NULL,
|
|
706
|
+
task_id TEXT,
|
|
707
|
+
layer TEXT NOT NULL,
|
|
708
|
+
query TEXT NOT NULL,
|
|
709
|
+
source_table TEXT NOT NULL,
|
|
710
|
+
source_id INTEGER NOT NULL,
|
|
711
|
+
score REAL,
|
|
712
|
+
was_cited INTEGER NOT NULL DEFAULT 0,
|
|
713
|
+
outcome TEXT,
|
|
714
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
715
|
+
outcome_at DATETIME
|
|
716
|
+
);
|
|
717
|
+
|
|
718
|
+
CREATE INDEX IF NOT EXISTS idx_retrievals_task ON retrievals(task_id);
|
|
719
|
+
CREATE INDEX IF NOT EXISTS idx_retrievals_session ON retrievals(session_id);
|
|
720
|
+
CREATE INDEX IF NOT EXISTS idx_retrievals_source ON retrievals(source_table, source_id);
|
|
721
|
+
CREATE INDEX IF NOT EXISTS idx_retrievals_outcome ON retrievals(outcome);
|
|
722
|
+
""")
|
|
723
|
+
logger.info("Retrievals migration v10 applied")
|
|
724
|
+
|
|
725
|
+
|
|
726
|
+
def has_retrievals_table(conn: sqlite3.Connection) -> bool:
|
|
727
|
+
"""Check whether the retrievals table exists (migration v10)."""
|
|
728
|
+
row = conn.execute(
|
|
729
|
+
"SELECT name FROM sqlite_master WHERE type='table' AND name='retrievals'"
|
|
730
|
+
).fetchone()
|
|
731
|
+
return row is not None
|
|
732
|
+
|
|
733
|
+
|
|
734
|
+
def _migrate_v11_retrieval_quality(conn: sqlite3.Connection) -> None:
|
|
735
|
+
"""Migration v11: retrieval precision tracking."""
|
|
736
|
+
conn.executescript("""\
|
|
737
|
+
CREATE TABLE IF NOT EXISTS retrieval_quality (
|
|
738
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
739
|
+
retrieval_id INTEGER NOT NULL,
|
|
740
|
+
task_id TEXT,
|
|
741
|
+
layer TEXT NOT NULL,
|
|
742
|
+
query TEXT,
|
|
743
|
+
precision REAL,
|
|
744
|
+
signal_source TEXT NOT NULL,
|
|
745
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
746
|
+
);
|
|
747
|
+
|
|
748
|
+
CREATE INDEX IF NOT EXISTS idx_retrieval_quality_task
|
|
749
|
+
ON retrieval_quality(task_id);
|
|
750
|
+
CREATE INDEX IF NOT EXISTS idx_retrieval_quality_layer
|
|
751
|
+
ON retrieval_quality(layer);
|
|
752
|
+
CREATE INDEX IF NOT EXISTS idx_retrieval_quality_created
|
|
753
|
+
ON retrieval_quality(created_at);
|
|
754
|
+
""")
|
|
755
|
+
|
|
756
|
+
# Contextual chunk text — LLM-generated situating sentence prepended at
|
|
757
|
+
# embed time. Column is nullable until G.11 enrichment runs; retrieval
|
|
758
|
+
# stays on the plain heading-path prefix meanwhile.
|
|
759
|
+
if not _column_exists(conn, "document_chunks", "contextual_prefix"):
|
|
760
|
+
conn.execute("ALTER TABLE document_chunks ADD COLUMN contextual_prefix TEXT")
|
|
761
|
+
if not _column_exists(conn, "document_chunks", "context_model"):
|
|
762
|
+
conn.execute("ALTER TABLE document_chunks ADD COLUMN context_model TEXT")
|
|
763
|
+
logger.info("Migration v11 applied: retrieval_quality + contextual chunk columns")
|
|
764
|
+
|
|
765
|
+
|
|
766
|
+
def has_retrieval_quality_table(conn: sqlite3.Connection) -> bool:
|
|
767
|
+
"""Check whether the retrieval_quality table exists (migration v11)."""
|
|
768
|
+
row = conn.execute(
|
|
769
|
+
"SELECT name FROM sqlite_master WHERE type='table' AND name='retrieval_quality'"
|
|
770
|
+
).fetchone()
|
|
771
|
+
return row is not None
|
|
772
|
+
|
|
773
|
+
|
|
774
|
+
def _migrate_v12_graph_os(conn: sqlite3.Connection) -> None:
|
|
775
|
+
"""Migration v12: graph_os knowledge-graph tables."""
|
|
776
|
+
conn.executescript("""\
|
|
777
|
+
CREATE TABLE IF NOT EXISTS graph_nodes (
|
|
778
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
779
|
+
kind TEXT NOT NULL,
|
|
780
|
+
label TEXT NOT NULL,
|
|
781
|
+
uid TEXT NOT NULL UNIQUE,
|
|
782
|
+
file_path TEXT,
|
|
783
|
+
start_line INTEGER,
|
|
784
|
+
end_line INTEGER,
|
|
785
|
+
signature TEXT,
|
|
786
|
+
lang TEXT,
|
|
787
|
+
doc_blob TEXT,
|
|
788
|
+
ast_hash TEXT,
|
|
789
|
+
content_hash TEXT,
|
|
790
|
+
metadata_json TEXT DEFAULT '{}',
|
|
791
|
+
created_at INTEGER NOT NULL,
|
|
792
|
+
updated_at INTEGER NOT NULL
|
|
793
|
+
);
|
|
794
|
+
CREATE INDEX IF NOT EXISTS idx_graph_nodes_kind_lang ON graph_nodes(kind, lang);
|
|
795
|
+
CREATE INDEX IF NOT EXISTS idx_graph_nodes_file ON graph_nodes(file_path);
|
|
796
|
+
|
|
797
|
+
CREATE TABLE IF NOT EXISTS graph_edges_v12 (
|
|
798
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
799
|
+
source_id INTEGER NOT NULL,
|
|
800
|
+
target_id INTEGER NOT NULL,
|
|
801
|
+
edge_type TEXT NOT NULL,
|
|
802
|
+
confidence REAL NOT NULL DEFAULT 1.0,
|
|
803
|
+
extractor TEXT NOT NULL,
|
|
804
|
+
source_span TEXT,
|
|
805
|
+
created_at INTEGER NOT NULL,
|
|
806
|
+
updated_at INTEGER NOT NULL,
|
|
807
|
+
UNIQUE(source_id, target_id, edge_type, extractor),
|
|
808
|
+
FOREIGN KEY (source_id) REFERENCES graph_nodes(id) ON DELETE CASCADE,
|
|
809
|
+
FOREIGN KEY (target_id) REFERENCES graph_nodes(id) ON DELETE CASCADE
|
|
810
|
+
);
|
|
811
|
+
CREATE INDEX IF NOT EXISTS idx_graph_edges_source ON graph_edges_v12(source_id, edge_type);
|
|
812
|
+
CREATE INDEX IF NOT EXISTS idx_graph_edges_target ON graph_edges_v12(target_id, edge_type);
|
|
813
|
+
CREATE INDEX IF NOT EXISTS idx_graph_edges_type ON graph_edges_v12(edge_type);
|
|
814
|
+
|
|
815
|
+
CREATE TABLE IF NOT EXISTS graph_evidence_v12 (
|
|
816
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
817
|
+
edge_id INTEGER NOT NULL,
|
|
818
|
+
signal_name TEXT NOT NULL,
|
|
819
|
+
weight REAL NOT NULL,
|
|
820
|
+
note TEXT,
|
|
821
|
+
created_at INTEGER NOT NULL,
|
|
822
|
+
FOREIGN KEY (edge_id) REFERENCES graph_edges_v12(id) ON DELETE CASCADE
|
|
823
|
+
);
|
|
824
|
+
CREATE INDEX IF NOT EXISTS idx_graph_evidence_edge ON graph_evidence_v12(edge_id);
|
|
825
|
+
""")
|
|
826
|
+
|
|
827
|
+
if has_fts5(conn):
|
|
828
|
+
conn.executescript("""\
|
|
829
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS graph_nodes_fts USING fts5(
|
|
830
|
+
label, signature, doc_blob,
|
|
831
|
+
content=graph_nodes,
|
|
832
|
+
content_rowid=id,
|
|
833
|
+
tokenize='porter unicode61'
|
|
834
|
+
);
|
|
835
|
+
|
|
836
|
+
CREATE TRIGGER IF NOT EXISTS graph_nodes_fts_ai AFTER INSERT ON graph_nodes BEGIN
|
|
837
|
+
INSERT INTO graph_nodes_fts(rowid, label, signature, doc_blob)
|
|
838
|
+
VALUES (new.id, new.label, COALESCE(new.signature, ''), COALESCE(new.doc_blob, ''));
|
|
839
|
+
END;
|
|
840
|
+
CREATE TRIGGER IF NOT EXISTS graph_nodes_fts_ad AFTER DELETE ON graph_nodes BEGIN
|
|
841
|
+
INSERT INTO graph_nodes_fts(graph_nodes_fts, rowid, label, signature, doc_blob)
|
|
842
|
+
VALUES ('delete', old.id, old.label, COALESCE(old.signature, ''), COALESCE(old.doc_blob, ''));
|
|
843
|
+
END;
|
|
844
|
+
CREATE TRIGGER IF NOT EXISTS graph_nodes_fts_au AFTER UPDATE ON graph_nodes BEGIN
|
|
845
|
+
INSERT INTO graph_nodes_fts(graph_nodes_fts, rowid, label, signature, doc_blob)
|
|
846
|
+
VALUES ('delete', old.id, old.label, COALESCE(old.signature, ''), COALESCE(old.doc_blob, ''));
|
|
847
|
+
INSERT INTO graph_nodes_fts(rowid, label, signature, doc_blob)
|
|
848
|
+
VALUES (new.id, new.label, COALESCE(new.signature, ''), COALESCE(new.doc_blob, ''));
|
|
849
|
+
END;
|
|
850
|
+
""")
|
|
851
|
+
|
|
852
|
+
if has_embeddings_table(conn) and not _column_exists(conn, "embeddings", "embedding_dim"):
|
|
853
|
+
conn.execute("ALTER TABLE embeddings ADD COLUMN embedding_dim INTEGER DEFAULT 384")
|
|
854
|
+
|
|
855
|
+
logger.info(
|
|
856
|
+
"Migration v12 applied: graph_nodes + graph_edges_v12 + "
|
|
857
|
+
"graph_evidence_v12 + graph_nodes_fts; embeddings.embedding_dim added"
|
|
858
|
+
)
|
|
859
|
+
|
|
860
|
+
|
|
861
|
+
def has_graph_nodes_table(conn: sqlite3.Connection) -> bool:
|
|
862
|
+
"""Check whether the graph_nodes table exists (migration v12)."""
|
|
863
|
+
row = conn.execute(
|
|
864
|
+
"SELECT name FROM sqlite_master WHERE type='table' AND name='graph_nodes'"
|
|
865
|
+
).fetchone()
|
|
866
|
+
return row is not None
|
|
867
|
+
|
|
868
|
+
|
|
869
|
+
def has_graph_edges_table(conn: sqlite3.Connection) -> bool:
|
|
870
|
+
"""Check whether the graph_edges_v12 table exists (migration v12)."""
|
|
871
|
+
row = conn.execute(
|
|
872
|
+
"SELECT name FROM sqlite_master WHERE type='table' AND name='graph_edges_v12'"
|
|
873
|
+
).fetchone()
|
|
874
|
+
return row is not None
|
|
875
|
+
|
|
876
|
+
|
|
877
|
+
def has_graph_evidence_table(conn: sqlite3.Connection) -> bool:
|
|
878
|
+
"""Check whether the graph_evidence_v12 table exists (migration v12)."""
|
|
879
|
+
row = conn.execute(
|
|
880
|
+
"SELECT name FROM sqlite_master WHERE type='table' AND name='graph_evidence_v12'"
|
|
881
|
+
).fetchone()
|
|
882
|
+
return row is not None
|
|
883
|
+
|
|
884
|
+
|
|
885
|
+
def has_graph_nodes_fts(conn: sqlite3.Connection) -> bool:
|
|
886
|
+
"""Check whether the graph_nodes_fts virtual table exists (v12)."""
|
|
887
|
+
row = conn.execute(
|
|
888
|
+
"SELECT name FROM sqlite_master WHERE type='table' AND name='graph_nodes_fts'"
|
|
889
|
+
).fetchone()
|
|
890
|
+
return row is not None
|
|
891
|
+
|
|
892
|
+
|
|
893
|
+
def _migrate_v13_board_os(conn: sqlite3.Connection) -> None:
|
|
894
|
+
"""Migration v13: board_os Scrumban task workflow extensions."""
|
|
895
|
+
# Idempotent ADD COLUMN (re-running is safe).
|
|
896
|
+
if not _column_exists(conn, "tasks", "swimlane"):
|
|
897
|
+
conn.execute("ALTER TABLE tasks ADD COLUMN swimlane TEXT")
|
|
898
|
+
if not _column_exists(conn, "tasks", "kind"):
|
|
899
|
+
conn.execute("ALTER TABLE tasks ADD COLUMN kind TEXT")
|
|
900
|
+
if not _column_exists(conn, "tasks", "epic"):
|
|
901
|
+
conn.execute("ALTER TABLE tasks ADD COLUMN epic TEXT")
|
|
902
|
+
if not _column_exists(conn, "tasks", "labels_json"):
|
|
903
|
+
conn.execute("ALTER TABLE tasks ADD COLUMN labels_json TEXT DEFAULT '[]'")
|
|
904
|
+
if not _column_exists(conn, "tasks", "priority"):
|
|
905
|
+
conn.execute("ALTER TABLE tasks ADD COLUMN priority TEXT")
|
|
906
|
+
if not _column_exists(conn, "tasks", "appetite"):
|
|
907
|
+
conn.execute("ALTER TABLE tasks ADD COLUMN appetite TEXT")
|
|
908
|
+
if not _column_exists(conn, "tasks", "started_at"):
|
|
909
|
+
conn.execute("ALTER TABLE tasks ADD COLUMN started_at INTEGER")
|
|
910
|
+
if not _column_exists(conn, "tasks", "completed_at"):
|
|
911
|
+
conn.execute("ALTER TABLE tasks ADD COLUMN completed_at INTEGER")
|
|
912
|
+
if not _column_exists(conn, "tasks", "agent_session"):
|
|
913
|
+
conn.execute("ALTER TABLE tasks ADD COLUMN agent_session TEXT")
|
|
914
|
+
if not _column_exists(conn, "tasks", "work_log_last_5"):
|
|
915
|
+
conn.execute("ALTER TABLE tasks ADD COLUMN work_log_last_5 TEXT DEFAULT '[]'")
|
|
916
|
+
|
|
917
|
+
conn.executescript("""\
|
|
918
|
+
CREATE TABLE IF NOT EXISTS task_status_history (
|
|
919
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
920
|
+
task_id TEXT NOT NULL,
|
|
921
|
+
old_status TEXT NOT NULL,
|
|
922
|
+
new_status TEXT NOT NULL,
|
|
923
|
+
agent_session TEXT,
|
|
924
|
+
reason TEXT,
|
|
925
|
+
transitioned_at INTEGER NOT NULL
|
|
926
|
+
);
|
|
927
|
+
CREATE INDEX IF NOT EXISTS idx_tsh_task
|
|
928
|
+
ON task_status_history(task_id, transitioned_at);
|
|
929
|
+
CREATE INDEX IF NOT EXISTS idx_tsh_session
|
|
930
|
+
ON task_status_history(agent_session, transitioned_at)
|
|
931
|
+
WHERE agent_session IS NOT NULL;
|
|
932
|
+
|
|
933
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_swimlane_status
|
|
934
|
+
ON tasks(swimlane, status);
|
|
935
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_kind_status
|
|
936
|
+
ON tasks(kind, status);
|
|
937
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_epic
|
|
938
|
+
ON tasks(epic) WHERE epic IS NOT NULL;
|
|
939
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_priority_status
|
|
940
|
+
ON tasks(priority, status)
|
|
941
|
+
WHERE status IN ('ready', 'in_progress', 'emergency');
|
|
942
|
+
""")
|
|
943
|
+
|
|
944
|
+
logger.info(
|
|
945
|
+
"Migration v13 applied: tasks +swimlane/kind/epic/"
|
|
946
|
+
"priority/appetite/started_at/completed_at/agent_session/"
|
|
947
|
+
"labels_json/work_log_last_5; task_status_history table; "
|
|
948
|
+
"5 new indices"
|
|
949
|
+
)
|
|
950
|
+
|
|
951
|
+
|
|
952
|
+
def _migrate_v14_cognition(conn: sqlite3.Connection) -> None:
|
|
953
|
+
"""Migration v14: formula-agent supervisor cognition tables."""
|
|
954
|
+
conn.executescript("""\
|
|
955
|
+
CREATE TABLE IF NOT EXISTS backtrack_events (
|
|
956
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
957
|
+
session_id TEXT NOT NULL,
|
|
958
|
+
from_formula TEXT NOT NULL,
|
|
959
|
+
to_formula TEXT NOT NULL,
|
|
960
|
+
reason TEXT NOT NULL,
|
|
961
|
+
ts TEXT NOT NULL DEFAULT (datetime('now'))
|
|
962
|
+
);
|
|
963
|
+
CREATE INDEX IF NOT EXISTS idx_backtrack_session
|
|
964
|
+
ON backtrack_events(session_id, ts);
|
|
965
|
+
|
|
966
|
+
CREATE TABLE IF NOT EXISTS persona_selections (
|
|
967
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
968
|
+
session_id TEXT NOT NULL,
|
|
969
|
+
task_marker TEXT,
|
|
970
|
+
persona_id TEXT NOT NULL,
|
|
971
|
+
confidence REAL NOT NULL,
|
|
972
|
+
reason TEXT,
|
|
973
|
+
intensity TEXT NOT NULL,
|
|
974
|
+
ts TEXT NOT NULL DEFAULT (datetime('now'))
|
|
975
|
+
);
|
|
976
|
+
CREATE INDEX IF NOT EXISTS idx_persona_session
|
|
977
|
+
ON persona_selections(session_id);
|
|
978
|
+
|
|
979
|
+
CREATE TABLE IF NOT EXISTS ambiguity_violations (
|
|
980
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
981
|
+
session_id TEXT NOT NULL,
|
|
982
|
+
formula_id TEXT NOT NULL,
|
|
983
|
+
step_id TEXT,
|
|
984
|
+
criterion TEXT NOT NULL,
|
|
985
|
+
detail TEXT,
|
|
986
|
+
ts TEXT NOT NULL DEFAULT (datetime('now'))
|
|
987
|
+
);
|
|
988
|
+
CREATE INDEX IF NOT EXISTS idx_ambiguity_session
|
|
989
|
+
ON ambiguity_violations(session_id);
|
|
990
|
+
|
|
991
|
+
CREATE TABLE IF NOT EXISTS formula_dispatches (
|
|
992
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
993
|
+
session_id TEXT NOT NULL,
|
|
994
|
+
task_marker TEXT,
|
|
995
|
+
persona_id TEXT NOT NULL,
|
|
996
|
+
formula_id TEXT NOT NULL,
|
|
997
|
+
input_hash TEXT NOT NULL,
|
|
998
|
+
output_hash TEXT,
|
|
999
|
+
latency_ms INTEGER,
|
|
1000
|
+
status TEXT NOT NULL,
|
|
1001
|
+
ts TEXT NOT NULL DEFAULT (datetime('now'))
|
|
1002
|
+
);
|
|
1003
|
+
CREATE INDEX IF NOT EXISTS idx_dispatches_session
|
|
1004
|
+
ON formula_dispatches(session_id, ts);
|
|
1005
|
+
""")
|
|
1006
|
+
logger.info(
|
|
1007
|
+
"Migration v14 applied: backtrack_events, persona_selections, "
|
|
1008
|
+
"ambiguity_violations, formula_dispatches + 4 indices"
|
|
1009
|
+
)
|
|
1010
|
+
|
|
1011
|
+
|
|
1012
|
+
def _migrate_v15_graph_edges_confidence_check(conn: sqlite3.Connection) -> None:
|
|
1013
|
+
"""Migration v15 (graph_os S1 / B17): CHECK (confidence BETWEEN 0 AND 1)."""
|
|
1014
|
+
conn.executescript("""\
|
|
1015
|
+
CREATE TRIGGER IF NOT EXISTS graph_edges_v12_confidence_ins
|
|
1016
|
+
BEFORE INSERT ON graph_edges_v12
|
|
1017
|
+
FOR EACH ROW
|
|
1018
|
+
WHEN NEW.confidence IS NULL
|
|
1019
|
+
OR NEW.confidence < 0.0
|
|
1020
|
+
OR NEW.confidence > 1.0
|
|
1021
|
+
BEGIN
|
|
1022
|
+
SELECT RAISE(ABORT, 'graph_edges_v12.confidence must lie in [0,1]');
|
|
1023
|
+
END;
|
|
1024
|
+
|
|
1025
|
+
CREATE TRIGGER IF NOT EXISTS graph_edges_v12_confidence_upd
|
|
1026
|
+
BEFORE UPDATE OF confidence ON graph_edges_v12
|
|
1027
|
+
FOR EACH ROW
|
|
1028
|
+
WHEN NEW.confidence IS NULL
|
|
1029
|
+
OR NEW.confidence < 0.0
|
|
1030
|
+
OR NEW.confidence > 1.0
|
|
1031
|
+
BEGIN
|
|
1032
|
+
SELECT RAISE(ABORT, 'graph_edges_v12.confidence must lie in [0,1]');
|
|
1033
|
+
END;
|
|
1034
|
+
""")
|
|
1035
|
+
logger.info("Migration v15 applied: graph_edges_v12 confidence CHECK triggers")
|
|
1036
|
+
|
|
1037
|
+
|
|
1038
|
+
def _migrate_v16_normalize_graph_node_kinds(conn: sqlite3.Connection) -> None:
|
|
1039
|
+
"""Migration v16 (graph_os S3): normalize graph_nodes.kind values."""
|
|
1040
|
+
row = conn.execute(
|
|
1041
|
+
"SELECT name FROM sqlite_master WHERE type='table' AND name='graph_nodes'"
|
|
1042
|
+
).fetchone()
|
|
1043
|
+
if row is None:
|
|
1044
|
+
logger.debug("Migration v16: graph_nodes table not present — skip")
|
|
1045
|
+
return
|
|
1046
|
+
|
|
1047
|
+
# Resolve ``normalize_kind`` via a sys.path-side-door so this
|
|
1048
|
+
# migration works both under the MCP server (which already has
|
|
1049
|
+
# ``core/`` on sys.path) and under test fixtures that only
|
|
1050
|
+
# pre-register ``core/thinking_os``.
|
|
1051
|
+
try:
|
|
1052
|
+
import sys as _sys
|
|
1053
|
+
from pathlib import Path as _Path
|
|
1054
|
+
|
|
1055
|
+
core_dir = _Path(__file__).resolve().parent.parent
|
|
1056
|
+
core_str = str(core_dir)
|
|
1057
|
+
if core_str not in _sys.path:
|
|
1058
|
+
_sys.path.insert(0, core_str)
|
|
1059
|
+
from graph_os.types import normalize_kind as _normalize # type: ignore
|
|
1060
|
+
except Exception as exc:
|
|
1061
|
+
logger.warning(
|
|
1062
|
+
"Migration v16 could not import normalize_kind (%s) — "
|
|
1063
|
+
"skipping normalization; rows remain in legacy form",
|
|
1064
|
+
exc,
|
|
1065
|
+
)
|
|
1066
|
+
return
|
|
1067
|
+
|
|
1068
|
+
rows = conn.execute("SELECT DISTINCT kind FROM graph_nodes").fetchall()
|
|
1069
|
+
rename_map: dict[str, str] = {}
|
|
1070
|
+
for r in rows:
|
|
1071
|
+
legacy = r[0]
|
|
1072
|
+
if legacy is None:
|
|
1073
|
+
continue
|
|
1074
|
+
try:
|
|
1075
|
+
canonical = _normalize(legacy).value
|
|
1076
|
+
except ValueError:
|
|
1077
|
+
# Unknown kind — leave as-is so we don't silently drop data.
|
|
1078
|
+
continue
|
|
1079
|
+
if canonical != legacy:
|
|
1080
|
+
rename_map[legacy] = canonical
|
|
1081
|
+
|
|
1082
|
+
total_updated = 0
|
|
1083
|
+
for legacy, canonical in rename_map.items():
|
|
1084
|
+
cur = conn.execute(
|
|
1085
|
+
"UPDATE graph_nodes SET kind = ? WHERE kind = ?",
|
|
1086
|
+
(canonical, legacy),
|
|
1087
|
+
)
|
|
1088
|
+
total_updated += cur.rowcount or 0
|
|
1089
|
+
conn.commit()
|
|
1090
|
+
logger.info(
|
|
1091
|
+
"Migration v16 applied: graph_nodes.kind normalized "
|
|
1092
|
+
"(%d kind(s) rewritten, %d row(s) updated)",
|
|
1093
|
+
len(rename_map),
|
|
1094
|
+
total_updated,
|
|
1095
|
+
)
|
|
1096
|
+
|
|
1097
|
+
|
|
1098
|
+
def _migrate_v17_file_index_state(conn: sqlite3.Connection) -> None:
|
|
1099
|
+
"""Migration v17 (graph_os V1): per-file content-hash cache."""
|
|
1100
|
+
conn.executescript(
|
|
1101
|
+
"""
|
|
1102
|
+
CREATE TABLE IF NOT EXISTS file_index_state (
|
|
1103
|
+
file_path TEXT NOT NULL,
|
|
1104
|
+
content_hash TEXT NOT NULL,
|
|
1105
|
+
extractor_chain TEXT NOT NULL,
|
|
1106
|
+
nodes_written INTEGER NOT NULL,
|
|
1107
|
+
edges_written INTEGER NOT NULL,
|
|
1108
|
+
parse_errors_count INTEGER NOT NULL DEFAULT 0,
|
|
1109
|
+
last_indexed_at INTEGER NOT NULL,
|
|
1110
|
+
last_error TEXT,
|
|
1111
|
+
PRIMARY KEY (file_path, extractor_chain)
|
|
1112
|
+
);
|
|
1113
|
+
CREATE INDEX IF NOT EXISTS idx_file_index_state_hash
|
|
1114
|
+
ON file_index_state(content_hash);
|
|
1115
|
+
"""
|
|
1116
|
+
)
|
|
1117
|
+
conn.commit()
|
|
1118
|
+
logger.info("Migration v17 applied: file_index_state table + hash index")
|
|
1119
|
+
|
|
1120
|
+
|
|
1121
|
+
def _migrate_v18_retrieval_router_log(conn: sqlite3.Connection) -> None:
|
|
1122
|
+
"""Migration v18: retrieval_router_log append-only table."""
|
|
1123
|
+
conn.executescript(
|
|
1124
|
+
"""
|
|
1125
|
+
CREATE TABLE IF NOT EXISTS retrieval_router_log (
|
|
1126
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1127
|
+
query_hash TEXT NOT NULL,
|
|
1128
|
+
query_shape TEXT NOT NULL,
|
|
1129
|
+
confidence REAL NOT NULL,
|
|
1130
|
+
chosen_layer TEXT,
|
|
1131
|
+
fanout_layers TEXT,
|
|
1132
|
+
bytes_returned INTEGER,
|
|
1133
|
+
truncated INTEGER DEFAULT 0,
|
|
1134
|
+
agent_override TEXT,
|
|
1135
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
1136
|
+
);
|
|
1137
|
+
CREATE INDEX IF NOT EXISTS idx_router_log_created
|
|
1138
|
+
ON retrieval_router_log(created_at);
|
|
1139
|
+
CREATE INDEX IF NOT EXISTS idx_router_log_shape
|
|
1140
|
+
ON retrieval_router_log(query_shape);
|
|
1141
|
+
"""
|
|
1142
|
+
)
|
|
1143
|
+
conn.commit()
|
|
1144
|
+
logger.info("Migration v18 applied: retrieval_router_log table + indexes")
|
|
1145
|
+
|
|
1146
|
+
|
|
1147
|
+
def _migrate_v19_drop_ready_status(conn: sqlite3.Connection) -> None:
|
|
1148
|
+
"""Migration v19: fold 'ready' status into icebox + 'ready' label."""
|
|
1149
|
+
rows = conn.execute(
|
|
1150
|
+
"SELECT task_id, labels_json FROM tasks WHERE status = 'ready'",
|
|
1151
|
+
).fetchall()
|
|
1152
|
+
|
|
1153
|
+
if not rows:
|
|
1154
|
+
logger.info("Migration v19 applied: no 'ready' rows to migrate (clean DB)")
|
|
1155
|
+
return
|
|
1156
|
+
|
|
1157
|
+
import json as _json
|
|
1158
|
+
import time as _time
|
|
1159
|
+
|
|
1160
|
+
now_epoch = int(_time.time())
|
|
1161
|
+
|
|
1162
|
+
for task_id, labels_json in rows:
|
|
1163
|
+
try:
|
|
1164
|
+
labels = _json.loads(labels_json) if labels_json else []
|
|
1165
|
+
if not isinstance(labels, list):
|
|
1166
|
+
labels = []
|
|
1167
|
+
except (TypeError, ValueError):
|
|
1168
|
+
labels = []
|
|
1169
|
+
if "ready" not in labels:
|
|
1170
|
+
labels.append("ready")
|
|
1171
|
+
conn.execute(
|
|
1172
|
+
"UPDATE tasks SET status = 'icebox', labels_json = ? WHERE task_id = ?",
|
|
1173
|
+
(_json.dumps(labels, ensure_ascii=False), task_id),
|
|
1174
|
+
)
|
|
1175
|
+
conn.execute(
|
|
1176
|
+
"""
|
|
1177
|
+
INSERT INTO task_status_history
|
|
1178
|
+
(task_id, old_status, new_status, agent_session,
|
|
1179
|
+
reason, transitioned_at)
|
|
1180
|
+
VALUES (?, 'ready', 'icebox', NULL, ?, ?)
|
|
1181
|
+
""",
|
|
1182
|
+
(task_id, "migrated from ready column (v19)", now_epoch),
|
|
1183
|
+
)
|
|
1184
|
+
conn.commit()
|
|
1185
|
+
logger.info(
|
|
1186
|
+
"Migration v19 applied: folded %d 'ready' task(s) into icebox + label",
|
|
1187
|
+
len(rows),
|
|
1188
|
+
)
|
|
1189
|
+
|
|
1190
|
+
|
|
1191
|
+
def _column_exists_table(conn: sqlite3.Connection, table: str, column: str) -> bool:
|
|
1192
|
+
"""Local helper — pragma table_info reads. Defined inline to keep
|
|
1193
|
+
the migration self-contained (the file already has _column_exists
|
|
1194
|
+
earlier; this is only used by v20)."""
|
|
1195
|
+
rows = conn.execute(f"PRAGMA table_info({table})").fetchall()
|
|
1196
|
+
return any(r[1] == column for r in rows)
|
|
1197
|
+
|
|
1198
|
+
|
|
1199
|
+
def _migrate_v20_override_audit(conn: sqlite3.Connection) -> None:
|
|
1200
|
+
"""Migration v20 — override audit columns on task_status_history."""
|
|
1201
|
+
if not has_task_status_history_table(conn):
|
|
1202
|
+
# Older DBs that never reached v13 don't have this table; the
|
|
1203
|
+
# v13 migration will create it with the modern shape via
|
|
1204
|
+
# _migrate_v13_board_os, but if a future re-run order is shuffled
|
|
1205
|
+
# we should be defensive.
|
|
1206
|
+
logger.info(
|
|
1207
|
+
"Migration v20 skipped: task_status_history not present yet "
|
|
1208
|
+
"(v13 will create it; v20 re-runs once v13 lands)"
|
|
1209
|
+
)
|
|
1210
|
+
return
|
|
1211
|
+
|
|
1212
|
+
if not _column_exists_table(conn, "task_status_history", "override_reason"):
|
|
1213
|
+
conn.execute("ALTER TABLE task_status_history ADD COLUMN override_reason TEXT")
|
|
1214
|
+
if not _column_exists_table(conn, "task_status_history", "override_actor"):
|
|
1215
|
+
conn.execute("ALTER TABLE task_status_history ADD COLUMN override_actor TEXT")
|
|
1216
|
+
|
|
1217
|
+
# Index lets retro/audit queries scan only override rows efficiently.
|
|
1218
|
+
conn.execute(
|
|
1219
|
+
"CREATE INDEX IF NOT EXISTS idx_tsh_override "
|
|
1220
|
+
"ON task_status_history(override_reason) "
|
|
1221
|
+
"WHERE override_reason IS NOT NULL"
|
|
1222
|
+
)
|
|
1223
|
+
conn.commit()
|
|
1224
|
+
logger.info("Migration v20 applied: override audit columns on task_status_history")
|
|
1225
|
+
|
|
1226
|
+
|
|
1227
|
+
def _migrate_v21_doc_audit_trail(conn: sqlite3.Connection) -> None:
|
|
1228
|
+
"""Migration v21 — append-only doc edit + decision-history log."""
|
|
1229
|
+
conn.executescript(
|
|
1230
|
+
"""
|
|
1231
|
+
CREATE TABLE IF NOT EXISTS doc_audit_trail (
|
|
1232
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1233
|
+
doc_path TEXT NOT NULL,
|
|
1234
|
+
session_id TEXT,
|
|
1235
|
+
agent TEXT,
|
|
1236
|
+
action TEXT NOT NULL CHECK (action IN (
|
|
1237
|
+
'created','updated','deleted','reverted','moved','renamed'
|
|
1238
|
+
)),
|
|
1239
|
+
old_frontmatter TEXT,
|
|
1240
|
+
new_frontmatter TEXT,
|
|
1241
|
+
old_content_hash TEXT,
|
|
1242
|
+
new_content_hash TEXT,
|
|
1243
|
+
reason TEXT,
|
|
1244
|
+
supersedes_id INTEGER REFERENCES doc_audit_trail(id),
|
|
1245
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
1246
|
+
);
|
|
1247
|
+
|
|
1248
|
+
CREATE INDEX IF NOT EXISTS idx_doc_audit_path_created
|
|
1249
|
+
ON doc_audit_trail(doc_path, created_at DESC);
|
|
1250
|
+
|
|
1251
|
+
CREATE INDEX IF NOT EXISTS idx_doc_audit_session
|
|
1252
|
+
ON doc_audit_trail(session_id);
|
|
1253
|
+
|
|
1254
|
+
CREATE INDEX IF NOT EXISTS idx_doc_audit_supersedes
|
|
1255
|
+
ON doc_audit_trail(supersedes_id)
|
|
1256
|
+
WHERE supersedes_id IS NOT NULL;
|
|
1257
|
+
|
|
1258
|
+
-- Append-only: forbid UPDATE / DELETE on this table. The audit
|
|
1259
|
+
-- log is meaningful only if it cannot be rewritten.
|
|
1260
|
+
CREATE TRIGGER IF NOT EXISTS doc_audit_trail_no_update
|
|
1261
|
+
BEFORE UPDATE ON doc_audit_trail
|
|
1262
|
+
BEGIN
|
|
1263
|
+
SELECT RAISE(FAIL, 'doc_audit_trail is append-only');
|
|
1264
|
+
END;
|
|
1265
|
+
|
|
1266
|
+
CREATE TRIGGER IF NOT EXISTS doc_audit_trail_no_delete
|
|
1267
|
+
BEFORE DELETE ON doc_audit_trail
|
|
1268
|
+
BEGIN
|
|
1269
|
+
SELECT RAISE(FAIL, 'doc_audit_trail is append-only');
|
|
1270
|
+
END;
|
|
1271
|
+
"""
|
|
1272
|
+
)
|
|
1273
|
+
conn.commit()
|
|
1274
|
+
logger.info("Migration v21 applied: doc_audit_trail (append-only)")
|
|
1275
|
+
|
|
1276
|
+
|
|
1277
|
+
def has_doc_audit_trail_table(conn: sqlite3.Connection) -> bool:
|
|
1278
|
+
"""Check whether doc_audit_trail exists (migration v21)."""
|
|
1279
|
+
row = conn.execute(
|
|
1280
|
+
"SELECT name FROM sqlite_master WHERE type='table' AND name='doc_audit_trail'"
|
|
1281
|
+
).fetchone()
|
|
1282
|
+
return row is not None
|
|
1283
|
+
|
|
1284
|
+
|
|
1285
|
+
def _migrate_v22_doc_chunks_metadata(conn: sqlite3.Connection) -> None:
|
|
1286
|
+
"""Migration v22 — frontmatter metadata columns on document_chunks."""
|
|
1287
|
+
if not _table_exists(conn, "document_chunks"):
|
|
1288
|
+
logger.info("Migration v22 skipped: document_chunks not present yet")
|
|
1289
|
+
return
|
|
1290
|
+
|
|
1291
|
+
cols = [
|
|
1292
|
+
("domain", "TEXT"),
|
|
1293
|
+
("layer", "TEXT"),
|
|
1294
|
+
("ssot", "TEXT"),
|
|
1295
|
+
("updated_iso", "TEXT"),
|
|
1296
|
+
("is_active", "INTEGER DEFAULT 1"),
|
|
1297
|
+
]
|
|
1298
|
+
for name, decl in cols:
|
|
1299
|
+
if not _column_exists_table(conn, "document_chunks", name):
|
|
1300
|
+
conn.execute(f"ALTER TABLE document_chunks ADD COLUMN {name} {decl}")
|
|
1301
|
+
|
|
1302
|
+
# Backfill is_active for rows that pre-date this migration.
|
|
1303
|
+
conn.execute("UPDATE document_chunks SET is_active = 1 WHERE is_active IS NULL")
|
|
1304
|
+
|
|
1305
|
+
conn.executescript(
|
|
1306
|
+
"""
|
|
1307
|
+
CREATE INDEX IF NOT EXISTS idx_chunks_domain
|
|
1308
|
+
ON document_chunks(domain) WHERE domain IS NOT NULL;
|
|
1309
|
+
CREATE INDEX IF NOT EXISTS idx_chunks_layer
|
|
1310
|
+
ON document_chunks(layer) WHERE layer IS NOT NULL;
|
|
1311
|
+
CREATE INDEX IF NOT EXISTS idx_chunks_active
|
|
1312
|
+
ON document_chunks(is_active);
|
|
1313
|
+
CREATE INDEX IF NOT EXISTS idx_chunks_updated
|
|
1314
|
+
ON document_chunks(updated_iso) WHERE updated_iso IS NOT NULL;
|
|
1315
|
+
"""
|
|
1316
|
+
)
|
|
1317
|
+
conn.commit()
|
|
1318
|
+
logger.info(
|
|
1319
|
+
"Migration v22 applied: document_chunks gained domain/layer/ssot/updated_iso/is_active"
|
|
1320
|
+
)
|
|
1321
|
+
|
|
1322
|
+
|
|
1323
|
+
def _migrate_v23_dispatch_cost(conn: sqlite3.Connection) -> None:
|
|
1324
|
+
"""Migration v23 — formula_dispatches cost columns."""
|
|
1325
|
+
if not _table_exists(conn, "formula_dispatches"):
|
|
1326
|
+
logger.info("Migration v23 skipped: formula_dispatches not present yet")
|
|
1327
|
+
return
|
|
1328
|
+
|
|
1329
|
+
cols = [
|
|
1330
|
+
("cost_usd", "REAL"),
|
|
1331
|
+
("budget_usd", "REAL"),
|
|
1332
|
+
("usage_jsonb", "TEXT"),
|
|
1333
|
+
("model_usage_jsonb", "TEXT"),
|
|
1334
|
+
("tool_calls_jsonb", "TEXT"),
|
|
1335
|
+
("tool_failures_jsonb", "TEXT"),
|
|
1336
|
+
]
|
|
1337
|
+
for name, decl in cols:
|
|
1338
|
+
if not _column_exists_table(conn, "formula_dispatches", name):
|
|
1339
|
+
conn.execute(f"ALTER TABLE formula_dispatches ADD COLUMN {name} {decl}")
|
|
1340
|
+
|
|
1341
|
+
conn.executescript(
|
|
1342
|
+
"""
|
|
1343
|
+
CREATE INDEX IF NOT EXISTS idx_dispatches_cost
|
|
1344
|
+
ON formula_dispatches(cost_usd) WHERE cost_usd IS NOT NULL;
|
|
1345
|
+
"""
|
|
1346
|
+
)
|
|
1347
|
+
conn.commit()
|
|
1348
|
+
logger.info(
|
|
1349
|
+
"Migration v23 applied: formula_dispatches gained "
|
|
1350
|
+
"cost_usd / budget_usd / usage_jsonb / model_usage_jsonb / "
|
|
1351
|
+
"tool_calls_jsonb / tool_failures_jsonb"
|
|
1352
|
+
)
|
|
1353
|
+
|
|
1354
|
+
|
|
1355
|
+
def _migrate_v24_project_trajectory(conn: sqlite3.Connection) -> None:
|
|
1356
|
+
"""Migration v24 — project_trajectory table for long-term project intent."""
|
|
1357
|
+
conn.executescript("""\
|
|
1358
|
+
CREATE TABLE IF NOT EXISTS project_trajectory (
|
|
1359
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1360
|
+
session_id TEXT NOT NULL,
|
|
1361
|
+
phase TEXT,
|
|
1362
|
+
current_focus TEXT,
|
|
1363
|
+
architectural_decisions TEXT DEFAULT '[]',
|
|
1364
|
+
anti_patterns_discovered TEXT DEFAULT '[]',
|
|
1365
|
+
open_questions TEXT DEFAULT '[]',
|
|
1366
|
+
next_logical_step TEXT,
|
|
1367
|
+
confidence REAL DEFAULT 0.7,
|
|
1368
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
1369
|
+
supersedes_id INTEGER REFERENCES project_trajectory(id)
|
|
1370
|
+
);
|
|
1371
|
+
|
|
1372
|
+
CREATE INDEX IF NOT EXISTS idx_trajectory_session
|
|
1373
|
+
ON project_trajectory(session_id);
|
|
1374
|
+
CREATE INDEX IF NOT EXISTS idx_trajectory_created
|
|
1375
|
+
ON project_trajectory(created_at DESC);
|
|
1376
|
+
""")
|
|
1377
|
+
conn.commit()
|
|
1378
|
+
logger.info("Migration v24 applied: project_trajectory table created")
|
|
1379
|
+
|
|
1380
|
+
|
|
1381
|
+
def _migrate_v25_backtrack_failure_anatomy(conn: sqlite3.Connection) -> None:
|
|
1382
|
+
"""Migration v25 — structured failure anatomy columns on backtrack_events."""
|
|
1383
|
+
if not _table_exists(conn, "backtrack_events"):
|
|
1384
|
+
logger.info("Migration v25 skipped: backtrack_events not present yet")
|
|
1385
|
+
return
|
|
1386
|
+
|
|
1387
|
+
cols = [
|
|
1388
|
+
("hypothesis", "TEXT"),
|
|
1389
|
+
("failure_signal", "TEXT"),
|
|
1390
|
+
("root_cause", "TEXT"),
|
|
1391
|
+
("corrective_action", "TEXT"),
|
|
1392
|
+
]
|
|
1393
|
+
for name, decl in cols:
|
|
1394
|
+
if not _column_exists_table(conn, "backtrack_events", name):
|
|
1395
|
+
conn.execute(f"ALTER TABLE backtrack_events ADD COLUMN {name} {decl}")
|
|
1396
|
+
|
|
1397
|
+
conn.executescript("""\
|
|
1398
|
+
CREATE INDEX IF NOT EXISTS idx_backtrack_root_cause
|
|
1399
|
+
ON backtrack_events(root_cause) WHERE root_cause IS NOT NULL;
|
|
1400
|
+
""")
|
|
1401
|
+
conn.commit()
|
|
1402
|
+
logger.info(
|
|
1403
|
+
"Migration v25 applied: backtrack_events gained "
|
|
1404
|
+
"hypothesis / failure_signal / root_cause / corrective_action"
|
|
1405
|
+
)
|
|
1406
|
+
|
|
1407
|
+
|
|
1408
|
+
def _migrate_v26_routing_evolution(conn: sqlite3.Connection) -> None:
|
|
1409
|
+
"""Migration v26 — routing_weights staleness tracking for autonomous refresh."""
|
|
1410
|
+
if not _table_exists(conn, "routing_weights"):
|
|
1411
|
+
logger.info("Migration v26 skipped: routing_weights not present yet")
|
|
1412
|
+
return
|
|
1413
|
+
|
|
1414
|
+
for name, decl in [("last_recalc_at", "TEXT"), ("outcomes_at_recalc", "INTEGER")]:
|
|
1415
|
+
if not _column_exists_table(conn, "routing_weights", name):
|
|
1416
|
+
conn.execute(f"ALTER TABLE routing_weights ADD COLUMN {name} {decl}")
|
|
1417
|
+
conn.commit()
|
|
1418
|
+
logger.info("Migration v26 applied: routing_weights gained last_recalc_at / outcomes_at_recalc")
|
|
1419
|
+
|
|
1420
|
+
|
|
1421
|
+
def _migrate_v27_dispatch_sdk_columns(conn: sqlite3.Connection) -> None:
|
|
1422
|
+
"""Migration v27 — full SDK telemetry on formula_dispatches."""
|
|
1423
|
+
if not _table_exists(conn, "formula_dispatches"):
|
|
1424
|
+
logger.info("Migration v27 skipped: formula_dispatches not present yet")
|
|
1425
|
+
return
|
|
1426
|
+
|
|
1427
|
+
cols = [
|
|
1428
|
+
("sub_session_id", "TEXT"),
|
|
1429
|
+
("model", "TEXT"),
|
|
1430
|
+
("checkpoints_jsonb", "TEXT"),
|
|
1431
|
+
]
|
|
1432
|
+
for name, decl in cols:
|
|
1433
|
+
if not _column_exists_table(conn, "formula_dispatches", name):
|
|
1434
|
+
conn.execute(f"ALTER TABLE formula_dispatches ADD COLUMN {name} {decl}")
|
|
1435
|
+
|
|
1436
|
+
conn.executescript(
|
|
1437
|
+
"""
|
|
1438
|
+
CREATE INDEX IF NOT EXISTS idx_dispatches_sub_session
|
|
1439
|
+
ON formula_dispatches(sub_session_id) WHERE sub_session_id IS NOT NULL;
|
|
1440
|
+
CREATE INDEX IF NOT EXISTS idx_dispatches_model
|
|
1441
|
+
ON formula_dispatches(model) WHERE model IS NOT NULL;
|
|
1442
|
+
"""
|
|
1443
|
+
)
|
|
1444
|
+
conn.commit()
|
|
1445
|
+
logger.info(
|
|
1446
|
+
"Migration v27 applied: formula_dispatches gained "
|
|
1447
|
+
"sub_session_id / model / checkpoints_jsonb"
|
|
1448
|
+
)
|
|
1449
|
+
|
|
1450
|
+
|
|
1451
|
+
def _migrate_v28_file_index_duration(conn: sqlite3.Connection) -> None:
|
|
1452
|
+
"""Migration v28 — per-extractor timing on file_index_state."""
|
|
1453
|
+
if not _table_exists(conn, "file_index_state"):
|
|
1454
|
+
logger.info("Migration v28 skipped: file_index_state not present yet")
|
|
1455
|
+
return
|
|
1456
|
+
if not _column_exists_table(conn, "file_index_state", "duration_ms"):
|
|
1457
|
+
conn.execute("ALTER TABLE file_index_state ADD COLUMN duration_ms INTEGER")
|
|
1458
|
+
conn.commit()
|
|
1459
|
+
logger.info("Migration v28 applied: file_index_state gained duration_ms")
|
|
1460
|
+
|
|
1461
|
+
|
|
1462
|
+
def _migrate_v29_fts5_unicode_tokenizer(conn: sqlite3.Connection) -> None:
|
|
1463
|
+
"""Migration v29 (G14) — drop `porter` from FTS5 so non-English tokens
|
|
1464
|
+
(Persian, Arabic, Chinese) are actually indexed. porter is an
|
|
1465
|
+
English-only stemmer that strips Persian body content silently;
|
|
1466
|
+
unicode61 alone normalises + tokenises by Unicode letter classes.
|
|
1467
|
+
"""
|
|
1468
|
+
if not _table_exists(conn, "graph_nodes"):
|
|
1469
|
+
logger.info("Migration v29 skipped: graph_nodes not present yet")
|
|
1470
|
+
return
|
|
1471
|
+
# Detect current tokenizer; only act when porter is still configured.
|
|
1472
|
+
row = conn.execute(
|
|
1473
|
+
"SELECT sql FROM sqlite_master WHERE type='table' AND name='graph_nodes_fts'"
|
|
1474
|
+
).fetchone()
|
|
1475
|
+
if row is None:
|
|
1476
|
+
logger.info("Migration v29 skipped: graph_nodes_fts not present")
|
|
1477
|
+
return
|
|
1478
|
+
current_sql = row[0] or ""
|
|
1479
|
+
if "porter" not in current_sql:
|
|
1480
|
+
logger.info("Migration v29 skipped: FTS5 already on unicode61-only tokenizer")
|
|
1481
|
+
return
|
|
1482
|
+
conn.executescript(
|
|
1483
|
+
"""
|
|
1484
|
+
DROP TRIGGER IF EXISTS graph_nodes_fts_ai;
|
|
1485
|
+
DROP TRIGGER IF EXISTS graph_nodes_fts_ad;
|
|
1486
|
+
DROP TRIGGER IF EXISTS graph_nodes_fts_au;
|
|
1487
|
+
DROP TABLE IF EXISTS graph_nodes_fts;
|
|
1488
|
+
CREATE VIRTUAL TABLE graph_nodes_fts USING fts5(
|
|
1489
|
+
label, signature, doc_blob,
|
|
1490
|
+
content=graph_nodes,
|
|
1491
|
+
content_rowid=id,
|
|
1492
|
+
tokenize='unicode61 remove_diacritics 2'
|
|
1493
|
+
);
|
|
1494
|
+
INSERT INTO graph_nodes_fts(rowid, label, signature, doc_blob)
|
|
1495
|
+
SELECT id, label, COALESCE(signature, ''), COALESCE(doc_blob, '')
|
|
1496
|
+
FROM graph_nodes;
|
|
1497
|
+
CREATE TRIGGER graph_nodes_fts_ai AFTER INSERT ON graph_nodes BEGIN
|
|
1498
|
+
INSERT INTO graph_nodes_fts(rowid, label, signature, doc_blob)
|
|
1499
|
+
VALUES (new.id, new.label, COALESCE(new.signature, ''), COALESCE(new.doc_blob, ''));
|
|
1500
|
+
END;
|
|
1501
|
+
CREATE TRIGGER graph_nodes_fts_ad AFTER DELETE ON graph_nodes BEGIN
|
|
1502
|
+
INSERT INTO graph_nodes_fts(graph_nodes_fts, rowid, label, signature, doc_blob)
|
|
1503
|
+
VALUES ('delete', old.id, old.label, COALESCE(old.signature, ''), COALESCE(old.doc_blob, ''));
|
|
1504
|
+
END;
|
|
1505
|
+
CREATE TRIGGER graph_nodes_fts_au AFTER UPDATE ON graph_nodes BEGIN
|
|
1506
|
+
INSERT INTO graph_nodes_fts(graph_nodes_fts, rowid, label, signature, doc_blob)
|
|
1507
|
+
VALUES ('delete', old.id, old.label, COALESCE(old.signature, ''), COALESCE(old.doc_blob, ''));
|
|
1508
|
+
INSERT INTO graph_nodes_fts(rowid, label, signature, doc_blob)
|
|
1509
|
+
VALUES (new.id, new.label, COALESCE(new.signature, ''), COALESCE(new.doc_blob, ''));
|
|
1510
|
+
END;
|
|
1511
|
+
"""
|
|
1512
|
+
)
|
|
1513
|
+
conn.commit()
|
|
1514
|
+
logger.info(
|
|
1515
|
+
"Migration v29 applied: FTS5 tokenizer porter→unicode61 (Persian/Arabic/CJK now indexed)"
|
|
1516
|
+
)
|
|
1517
|
+
|
|
1518
|
+
|
|
1519
|
+
def _migrate_v30_observation_access_signal(conn: sqlite3.Connection) -> None:
|
|
1520
|
+
"""Migration v30 — observations gain access_count + last_accessed_at so the
|
|
1521
|
+
5-signal ranker's access + recency-on-use terms apply to raw observations,
|
|
1522
|
+
not just learned_patterns. Closes the ranking asymmetry where an
|
|
1523
|
+
often-retrieved observation could never accrue an access boost."""
|
|
1524
|
+
if not _table_exists(conn, "observations"):
|
|
1525
|
+
logger.info("Migration v30 skipped: observations not present yet")
|
|
1526
|
+
return
|
|
1527
|
+
if not _column_exists(conn, "observations", "access_count"):
|
|
1528
|
+
conn.execute("ALTER TABLE observations ADD COLUMN access_count INTEGER DEFAULT 0")
|
|
1529
|
+
if not _column_exists(conn, "observations", "last_accessed_at"):
|
|
1530
|
+
conn.execute("ALTER TABLE observations ADD COLUMN last_accessed_at DATETIME")
|
|
1531
|
+
conn.commit()
|
|
1532
|
+
logger.info("Migration v30 applied: observations gained access_count + last_accessed_at")
|
|
1533
|
+
|
|
1534
|
+
|
|
1535
|
+
def _table_exists(conn: sqlite3.Connection, name: str) -> bool:
|
|
1536
|
+
row = conn.execute(
|
|
1537
|
+
"SELECT name FROM sqlite_master WHERE type='table' AND name=?", (name,)
|
|
1538
|
+
).fetchone()
|
|
1539
|
+
return row is not None
|
|
1540
|
+
|
|
1541
|
+
|
|
1542
|
+
def has_file_index_state_table(conn: sqlite3.Connection) -> bool:
|
|
1543
|
+
"""Check whether the file_index_state table exists (migration v17)."""
|
|
1544
|
+
row = conn.execute(
|
|
1545
|
+
"SELECT name FROM sqlite_master WHERE type='table' AND name='file_index_state'"
|
|
1546
|
+
).fetchone()
|
|
1547
|
+
return row is not None
|
|
1548
|
+
|
|
1549
|
+
|
|
1550
|
+
def has_formula_dispatches_table(conn: sqlite3.Connection) -> bool:
|
|
1551
|
+
"""Check whether formula_dispatches exists (migration v14)."""
|
|
1552
|
+
row = conn.execute(
|
|
1553
|
+
"SELECT name FROM sqlite_master WHERE type='table' AND name='formula_dispatches'"
|
|
1554
|
+
).fetchone()
|
|
1555
|
+
return row is not None
|
|
1556
|
+
|
|
1557
|
+
|
|
1558
|
+
def has_backtrack_events_table(conn: sqlite3.Connection) -> bool:
|
|
1559
|
+
"""Check whether backtrack_events exists (migration v14)."""
|
|
1560
|
+
row = conn.execute(
|
|
1561
|
+
"SELECT name FROM sqlite_master WHERE type='table' AND name='backtrack_events'"
|
|
1562
|
+
).fetchone()
|
|
1563
|
+
return row is not None
|
|
1564
|
+
|
|
1565
|
+
|
|
1566
|
+
def has_persona_selections_table(conn: sqlite3.Connection) -> bool:
|
|
1567
|
+
"""Check whether persona_selections exists (migration v14)."""
|
|
1568
|
+
row = conn.execute(
|
|
1569
|
+
"SELECT name FROM sqlite_master WHERE type='table' AND name='persona_selections'"
|
|
1570
|
+
).fetchone()
|
|
1571
|
+
return row is not None
|
|
1572
|
+
|
|
1573
|
+
|
|
1574
|
+
def has_task_status_history_table(conn: sqlite3.Connection) -> bool:
|
|
1575
|
+
"""Check whether task_status_history exists (migration v13)."""
|
|
1576
|
+
row = conn.execute(
|
|
1577
|
+
"SELECT name FROM sqlite_master WHERE type='table' AND name='task_status_history'"
|
|
1578
|
+
).fetchone()
|
|
1579
|
+
return row is not None
|
|
1580
|
+
|
|
1581
|
+
|
|
1582
|
+
def has_tasks_v13_columns(conn: sqlite3.Connection) -> bool:
|
|
1583
|
+
"""Quick check whether the v13 columns are on the tasks table."""
|
|
1584
|
+
return _column_exists(conn, "tasks", "swimlane") and _column_exists(conn, "tasks", "kind")
|
|
1585
|
+
|
|
1586
|
+
|
|
1587
|
+
def record_audit(
|
|
1588
|
+
conn: sqlite3.Connection,
|
|
1589
|
+
*,
|
|
1590
|
+
actor: str,
|
|
1591
|
+
action: str,
|
|
1592
|
+
source_table: str,
|
|
1593
|
+
source_id: int | None = None,
|
|
1594
|
+
old_value: str | None = None,
|
|
1595
|
+
new_value: str | None = None,
|
|
1596
|
+
reason: str | None = None,
|
|
1597
|
+
) -> int | None:
|
|
1598
|
+
"""Append a row to memory_audit. Fire-and-forget — never raises."""
|
|
1599
|
+
if not has_memory_audit_table(conn):
|
|
1600
|
+
return None
|
|
1601
|
+
try:
|
|
1602
|
+
cursor = conn.execute(
|
|
1603
|
+
"INSERT INTO memory_audit "
|
|
1604
|
+
"(actor, action, source_table, source_id, old_value, new_value, reason) "
|
|
1605
|
+
"VALUES (?, ?, ?, ?, ?, ?, ?)",
|
|
1606
|
+
(actor, action, source_table, source_id, old_value, new_value, reason),
|
|
1607
|
+
)
|
|
1608
|
+
conn.commit()
|
|
1609
|
+
return cursor.lastrowid
|
|
1610
|
+
except sqlite3.OperationalError as exc:
|
|
1611
|
+
logger.debug("record_audit skipped: %s", exc)
|
|
1612
|
+
return None
|
|
1613
|
+
|
|
1614
|
+
|
|
1615
|
+
def _migrate_v31_task_edit_history(conn: sqlite3.Connection) -> None:
|
|
1616
|
+
"""Migration v31 — append-only, actor-attributed field-edit history for tasks."""
|
|
1617
|
+
conn.executescript(
|
|
1618
|
+
"""
|
|
1619
|
+
CREATE TABLE IF NOT EXISTS task_edit_history (
|
|
1620
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1621
|
+
task_id TEXT NOT NULL,
|
|
1622
|
+
field TEXT NOT NULL,
|
|
1623
|
+
old_value TEXT,
|
|
1624
|
+
new_value TEXT,
|
|
1625
|
+
actor_type TEXT NOT NULL DEFAULT 'agent',
|
|
1626
|
+
actor_id TEXT,
|
|
1627
|
+
source TEXT,
|
|
1628
|
+
edited_at INTEGER NOT NULL
|
|
1629
|
+
);
|
|
1630
|
+
CREATE INDEX IF NOT EXISTS idx_teh_task
|
|
1631
|
+
ON task_edit_history(task_id, edited_at);
|
|
1632
|
+
CREATE INDEX IF NOT EXISTS idx_teh_actor
|
|
1633
|
+
ON task_edit_history(actor_id, edited_at) WHERE actor_id IS NOT NULL;
|
|
1634
|
+
"""
|
|
1635
|
+
)
|
|
1636
|
+
conn.commit()
|
|
1637
|
+
logger.info("Migration v31 applied: task_edit_history (actor-attributed field edits)")
|
|
1638
|
+
|
|
1639
|
+
|
|
1640
|
+
def _migrate_v32_log_events(conn: sqlite3.Connection) -> None:
|
|
1641
|
+
"""Migration v32 — durable WARN+ error store + permanent fingerprint rollup (observability eye)."""
|
|
1642
|
+
conn.executescript(
|
|
1643
|
+
"""
|
|
1644
|
+
CREATE TABLE IF NOT EXISTS log_events (
|
|
1645
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1646
|
+
ts TEXT NOT NULL,
|
|
1647
|
+
lvl TEXT NOT NULL,
|
|
1648
|
+
scope TEXT NOT NULL,
|
|
1649
|
+
msg TEXT NOT NULL,
|
|
1650
|
+
kv TEXT,
|
|
1651
|
+
exc_type TEXT,
|
|
1652
|
+
stack TEXT,
|
|
1653
|
+
session_id TEXT,
|
|
1654
|
+
trace_id TEXT,
|
|
1655
|
+
fingerprint TEXT NOT NULL,
|
|
1656
|
+
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ','now'))
|
|
1657
|
+
);
|
|
1658
|
+
CREATE INDEX IF NOT EXISTS idx_log_events_fp ON log_events(fingerprint);
|
|
1659
|
+
CREATE INDEX IF NOT EXISTS idx_log_events_ts ON log_events(ts);
|
|
1660
|
+
CREATE INDEX IF NOT EXISTS idx_log_events_lvl ON log_events(lvl);
|
|
1661
|
+
|
|
1662
|
+
CREATE TABLE IF NOT EXISTS log_fingerprints (
|
|
1663
|
+
fingerprint TEXT PRIMARY KEY,
|
|
1664
|
+
scope TEXT NOT NULL,
|
|
1665
|
+
exc_type TEXT,
|
|
1666
|
+
sample_msg TEXT NOT NULL,
|
|
1667
|
+
max_lvl TEXT NOT NULL,
|
|
1668
|
+
first_seen TEXT NOT NULL,
|
|
1669
|
+
last_seen TEXT NOT NULL,
|
|
1670
|
+
count INTEGER NOT NULL DEFAULT 0,
|
|
1671
|
+
distinct_sessions INTEGER NOT NULL DEFAULT 0,
|
|
1672
|
+
task_id TEXT,
|
|
1673
|
+
status TEXT NOT NULL DEFAULT 'open'
|
|
1674
|
+
);
|
|
1675
|
+
CREATE INDEX IF NOT EXISTS idx_log_fp_status ON log_fingerprints(status, last_seen);
|
|
1676
|
+
"""
|
|
1677
|
+
)
|
|
1678
|
+
conn.commit()
|
|
1679
|
+
logger.info(
|
|
1680
|
+
"Migration v32 applied: log_events + log_fingerprints (observability eye durable store)"
|
|
1681
|
+
)
|
|
1682
|
+
|
|
1683
|
+
|
|
1684
|
+
def _migrate_v33_pattern_archived_at(conn: sqlite3.Connection) -> None:
|
|
1685
|
+
"""Migration v33 — learned_patterns.archived_at so the decay prune measures a
|
|
1686
|
+
real time-since-archived grace window (was COALESCE of access/validate/create)."""
|
|
1687
|
+
if not _column_exists(conn, "learned_patterns", "archived_at"):
|
|
1688
|
+
conn.execute("ALTER TABLE learned_patterns ADD COLUMN archived_at DATETIME")
|
|
1689
|
+
logger.info("Migration v33 applied: learned_patterns gained archived_at")
|
|
1690
|
+
|
|
1691
|
+
|
|
1692
|
+
def _migrate_v34_dispatch_error(conn: sqlite3.Connection) -> None:
|
|
1693
|
+
"""Migration v34 — formula_dispatches.error captures the dispatch failure reason."""
|
|
1694
|
+
if not _table_exists(conn, "formula_dispatches"):
|
|
1695
|
+
logger.info("Migration v34 skipped: formula_dispatches not present yet")
|
|
1696
|
+
return
|
|
1697
|
+
if not _column_exists_table(conn, "formula_dispatches", "error"):
|
|
1698
|
+
conn.execute("ALTER TABLE formula_dispatches ADD COLUMN error TEXT")
|
|
1699
|
+
conn.commit()
|
|
1700
|
+
logger.info("Migration v34 applied: formula_dispatches gained error")
|
|
1701
|
+
|
|
1702
|
+
|
|
1703
|
+
def _migrate_v35_scale_foundation(conn: sqlite3.Connection) -> None:
|
|
1704
|
+
"""Migration v35 — scale foundation for 100K+ tasks (TASK-226).
|
|
1705
|
+
|
|
1706
|
+
Adds the keyset/board indexes pagination + bounded queries need at
|
|
1707
|
+
scale, an FTS5 table so task_search stops full-scanning title/goal, and
|
|
1708
|
+
a task_dependencies junction that replaces the O(n) `dependencies LIKE
|
|
1709
|
+
'%"TASK-NNN"%'` scan with indexed lookups in both directions.
|
|
1710
|
+
"""
|
|
1711
|
+
if not has_tasks_table(conn):
|
|
1712
|
+
logger.info("Migration v35 skipped: tasks table not present yet")
|
|
1713
|
+
return
|
|
1714
|
+
|
|
1715
|
+
# 1. Keyset/board indexes. (task_status_history(task_id, transitioned_at)
|
|
1716
|
+
# already exists as idx_tsh_task from v13 — no duplicate needed.)
|
|
1717
|
+
conn.executescript(
|
|
1718
|
+
"""
|
|
1719
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_status_completed
|
|
1720
|
+
ON tasks(status, completed_at);
|
|
1721
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_swimlane_status_priority
|
|
1722
|
+
ON tasks(swimlane, status, priority);
|
|
1723
|
+
"""
|
|
1724
|
+
)
|
|
1725
|
+
|
|
1726
|
+
# 2. task_dependencies junction. PK(task_id, depends_on) indexes the
|
|
1727
|
+
# dependencies() direction; idx on depends_on indexes dependents().
|
|
1728
|
+
# Triggers derive it from the tasks.dependencies JSON column so EVERY
|
|
1729
|
+
# writer (board_os sync, orphan-delete) keeps it in
|
|
1730
|
+
# step with zero per-writer code. json_each is built into SQLite 3.38+
|
|
1731
|
+
# (the repo already floors at 3.27+ for FTS5 remove_diacritics 2).
|
|
1732
|
+
conn.executescript(
|
|
1733
|
+
"""
|
|
1734
|
+
CREATE TABLE IF NOT EXISTS task_dependencies (
|
|
1735
|
+
task_id TEXT NOT NULL,
|
|
1736
|
+
depends_on TEXT NOT NULL,
|
|
1737
|
+
PRIMARY KEY (task_id, depends_on)
|
|
1738
|
+
);
|
|
1739
|
+
CREATE INDEX IF NOT EXISTS idx_task_deps_depends_on
|
|
1740
|
+
ON task_dependencies(depends_on);
|
|
1741
|
+
|
|
1742
|
+
CREATE TRIGGER IF NOT EXISTS tasks_deps_ai AFTER INSERT ON tasks BEGIN
|
|
1743
|
+
INSERT OR IGNORE INTO task_dependencies(task_id, depends_on)
|
|
1744
|
+
SELECT new.task_id, je.value
|
|
1745
|
+
FROM json_each(COALESCE(NULLIF(new.dependencies, ''), '[]')) je;
|
|
1746
|
+
END;
|
|
1747
|
+
CREATE TRIGGER IF NOT EXISTS tasks_deps_au AFTER UPDATE OF dependencies ON tasks BEGIN
|
|
1748
|
+
DELETE FROM task_dependencies WHERE task_id = new.task_id;
|
|
1749
|
+
INSERT OR IGNORE INTO task_dependencies(task_id, depends_on)
|
|
1750
|
+
SELECT new.task_id, je.value
|
|
1751
|
+
FROM json_each(COALESCE(NULLIF(new.dependencies, ''), '[]')) je;
|
|
1752
|
+
END;
|
|
1753
|
+
CREATE TRIGGER IF NOT EXISTS tasks_deps_ad AFTER DELETE ON tasks BEGIN
|
|
1754
|
+
DELETE FROM task_dependencies WHERE task_id = old.task_id;
|
|
1755
|
+
END;
|
|
1756
|
+
"""
|
|
1757
|
+
)
|
|
1758
|
+
# Backfill from any populated dependencies JSON (idempotent; the live
|
|
1759
|
+
# column is normally empty — sync.py populates the junction going forward).
|
|
1760
|
+
for task_id, deps_json in conn.execute(
|
|
1761
|
+
"SELECT task_id, dependencies FROM tasks "
|
|
1762
|
+
"WHERE dependencies IS NOT NULL AND dependencies NOT IN ('', '[]')"
|
|
1763
|
+
).fetchall():
|
|
1764
|
+
try:
|
|
1765
|
+
dep_ids = json.loads(deps_json)
|
|
1766
|
+
except (json.JSONDecodeError, TypeError):
|
|
1767
|
+
continue
|
|
1768
|
+
for dep in dep_ids:
|
|
1769
|
+
if isinstance(dep, str) and dep:
|
|
1770
|
+
conn.execute(
|
|
1771
|
+
"INSERT OR IGNORE INTO task_dependencies (task_id, depends_on) VALUES (?, ?)",
|
|
1772
|
+
(task_id, dep),
|
|
1773
|
+
)
|
|
1774
|
+
|
|
1775
|
+
# 3. FTS5 over tasks(title, goal_text). A regular (own-content) table,
|
|
1776
|
+
# NOT external-content: external-content FTS5 raises "database disk
|
|
1777
|
+
# image is malformed" when its 'delete' trigger runs against rows that
|
|
1778
|
+
# were backfilled rather than trigger-inserted (the delete tokens can't
|
|
1779
|
+
# be reconciled). Own-content deletes by rowid and is corruption-safe.
|
|
1780
|
+
# DROP-first heals any partially-applied earlier build of this table.
|
|
1781
|
+
# unicode61 matches v29 so Persian/Arabic/CJK task text is indexed.
|
|
1782
|
+
if has_fts5(conn):
|
|
1783
|
+
conn.executescript(
|
|
1784
|
+
"""
|
|
1785
|
+
DROP TRIGGER IF EXISTS tasks_fts_ai;
|
|
1786
|
+
DROP TRIGGER IF EXISTS tasks_fts_ad;
|
|
1787
|
+
DROP TRIGGER IF EXISTS tasks_fts_au;
|
|
1788
|
+
DROP TABLE IF EXISTS tasks_fts;
|
|
1789
|
+
CREATE VIRTUAL TABLE tasks_fts USING fts5(
|
|
1790
|
+
title, goal_text,
|
|
1791
|
+
tokenize='unicode61 remove_diacritics 2'
|
|
1792
|
+
);
|
|
1793
|
+
CREATE TRIGGER tasks_fts_ai AFTER INSERT ON tasks BEGIN
|
|
1794
|
+
INSERT INTO tasks_fts(rowid, title, goal_text)
|
|
1795
|
+
VALUES (new.rowid, new.title, COALESCE(new.goal_text, ''));
|
|
1796
|
+
END;
|
|
1797
|
+
CREATE TRIGGER tasks_fts_ad AFTER DELETE ON tasks BEGIN
|
|
1798
|
+
DELETE FROM tasks_fts WHERE rowid = old.rowid;
|
|
1799
|
+
END;
|
|
1800
|
+
CREATE TRIGGER tasks_fts_au AFTER UPDATE ON tasks BEGIN
|
|
1801
|
+
DELETE FROM tasks_fts WHERE rowid = new.rowid;
|
|
1802
|
+
INSERT INTO tasks_fts(rowid, title, goal_text)
|
|
1803
|
+
VALUES (new.rowid, new.title, COALESCE(new.goal_text, ''));
|
|
1804
|
+
END;
|
|
1805
|
+
"""
|
|
1806
|
+
)
|
|
1807
|
+
conn.execute(
|
|
1808
|
+
"INSERT INTO tasks_fts(rowid, title, goal_text) "
|
|
1809
|
+
"SELECT rowid, title, COALESCE(goal_text, '') FROM tasks"
|
|
1810
|
+
)
|
|
1811
|
+
else:
|
|
1812
|
+
logger.warning("Migration v35: FTS5 unavailable — tasks_fts skipped (LIKE fallback active)")
|
|
1813
|
+
|
|
1814
|
+
conn.commit()
|
|
1815
|
+
logger.info(
|
|
1816
|
+
"Migration v35 applied: idx_tasks_status_completed + "
|
|
1817
|
+
"idx_tasks_swimlane_status_priority, task_dependencies junction, tasks_fts"
|
|
1818
|
+
)
|
|
1819
|
+
|
|
1820
|
+
|
|
1821
|
+
def _migrate_v36_scrub_username_from_observations(conn: sqlite3.Connection) -> None:
|
|
1822
|
+
"""Migration v36 — backfill: strip the local OS username from existing
|
|
1823
|
+
observations.files_modified + title (pre-fix rows leaked /Users/<name>/…,
|
|
1824
|
+
a PII exposure per memory.md § Privacy). Idempotent: re-running finds no
|
|
1825
|
+
home/root prefix to replace. The capture-time fix (_scrub_username) keeps
|
|
1826
|
+
new rows clean; this scrubs the historical corpus with no backfill before it."""
|
|
1827
|
+
import os
|
|
1828
|
+
|
|
1829
|
+
if not _table_exists(conn, "observations"):
|
|
1830
|
+
logger.info("Migration v36 skipped: observations not present yet")
|
|
1831
|
+
return
|
|
1832
|
+
|
|
1833
|
+
root = None
|
|
1834
|
+
for row in conn.execute("PRAGMA database_list").fetchall():
|
|
1835
|
+
db_str = row[2] if len(row) > 2 else None
|
|
1836
|
+
if db_str and db_str not in ("", ":memory:"):
|
|
1837
|
+
dbp = Path(db_str).resolve()
|
|
1838
|
+
if dbp.parent.name == ".coding-os":
|
|
1839
|
+
root = str(dbp.parent.parent)
|
|
1840
|
+
break
|
|
1841
|
+
home = os.path.expanduser("~")
|
|
1842
|
+
|
|
1843
|
+
def _scrub(text: str) -> str:
|
|
1844
|
+
out = text or ""
|
|
1845
|
+
if root:
|
|
1846
|
+
out = out.replace(root + "/", "")
|
|
1847
|
+
if home and home != "~":
|
|
1848
|
+
out = out.replace(home + "/", "~/")
|
|
1849
|
+
return out
|
|
1850
|
+
|
|
1851
|
+
scrubbed = 0
|
|
1852
|
+
for rid, title, fm in conn.execute(
|
|
1853
|
+
"SELECT id, title, files_modified FROM observations"
|
|
1854
|
+
).fetchall():
|
|
1855
|
+
new_title, new_fm = _scrub(title or ""), _scrub(fm or "")
|
|
1856
|
+
if new_title != (title or "") or new_fm != (fm or ""):
|
|
1857
|
+
conn.execute(
|
|
1858
|
+
"UPDATE observations SET title = ?, files_modified = ? WHERE id = ?",
|
|
1859
|
+
(new_title, new_fm, rid),
|
|
1860
|
+
)
|
|
1861
|
+
scrubbed += 1
|
|
1862
|
+
conn.commit()
|
|
1863
|
+
logger.info("Migration v36 applied: scrubbed username from %d observation row(s)", scrubbed)
|
|
1864
|
+
|
|
1865
|
+
|
|
1866
|
+
def _migrate_v37_scrub_narrative_and_dash(conn: sqlite3.Connection) -> None:
|
|
1867
|
+
"""Migration v37 — completes the v36 PII backfill: (1) scrub observations.narrative
|
|
1868
|
+
(v36 only touched title+files_modified), and (2) strip the dash-encoded username
|
|
1869
|
+
that survives inside agent project-dir slugs (~/.claude/projects/-Users-<name>-…)
|
|
1870
|
+
across title+narrative+files_modified. Idempotent. SSOT scrub: sanitizer.scrub_username."""
|
|
1871
|
+
import os
|
|
1872
|
+
|
|
1873
|
+
if not _table_exists(conn, "observations"):
|
|
1874
|
+
logger.info("Migration v37 skipped: observations not present yet")
|
|
1875
|
+
return
|
|
1876
|
+
|
|
1877
|
+
from sanitizer import scrub_username
|
|
1878
|
+
|
|
1879
|
+
root = None
|
|
1880
|
+
for row in conn.execute("PRAGMA database_list").fetchall():
|
|
1881
|
+
db_str = row[2] if len(row) > 2 else None
|
|
1882
|
+
if db_str and db_str not in ("", ":memory:"):
|
|
1883
|
+
dbp = Path(db_str).resolve()
|
|
1884
|
+
if dbp.parent.name == ".coding-os":
|
|
1885
|
+
root = str(dbp.parent.parent)
|
|
1886
|
+
break
|
|
1887
|
+
home = os.path.expanduser("~")
|
|
1888
|
+
|
|
1889
|
+
def _scrub(text: str) -> str:
|
|
1890
|
+
out = text or ""
|
|
1891
|
+
if root:
|
|
1892
|
+
out = out.replace(root + "/", "")
|
|
1893
|
+
return scrub_username(out, home=home)
|
|
1894
|
+
|
|
1895
|
+
scrubbed = 0
|
|
1896
|
+
for rid, title, narrative, fm in conn.execute(
|
|
1897
|
+
"SELECT id, title, narrative, files_modified FROM observations"
|
|
1898
|
+
).fetchall():
|
|
1899
|
+
nt, nn, nf = _scrub(title or ""), _scrub(narrative or ""), _scrub(fm or "")
|
|
1900
|
+
if nt != (title or "") or nn != (narrative or "") or nf != (fm or ""):
|
|
1901
|
+
conn.execute(
|
|
1902
|
+
"UPDATE observations SET title = ?, narrative = ?, files_modified = ? WHERE id = ?",
|
|
1903
|
+
(nt, nn, nf, rid),
|
|
1904
|
+
)
|
|
1905
|
+
scrubbed += 1
|
|
1906
|
+
conn.commit()
|
|
1907
|
+
logger.info("Migration v37 applied: scrubbed narrative/dash-username from %d row(s)", scrubbed)
|
|
1908
|
+
|
|
1909
|
+
|
|
1910
|
+
def _migrate_v38_backfill_rework_outcome(conn: sqlite3.Connection) -> None:
|
|
1911
|
+
"""Migration v38 — backfill honest outcomes for the historical corpus.
|
|
1912
|
+
Pre-fix every task_outcomes row was hardcoded 'success' (board_commands), so
|
|
1913
|
+
the variance gate suppressed every rework/stat extractor and learn_extract
|
|
1914
|
+
was starved (192 tasks → 4 patterns). Flip 'success' → 'rework' for any task
|
|
1915
|
+
whose task_status_history shows a backward move (reopened after testing/
|
|
1916
|
+
complete/review). Idempotent: only touches rows still 'success' with a reopen."""
|
|
1917
|
+
if not (_table_exists(conn, "task_outcomes") and _table_exists(conn, "task_status_history")):
|
|
1918
|
+
logger.info("Migration v38 skipped: outcome/history tables not present yet")
|
|
1919
|
+
return
|
|
1920
|
+
cur = conn.execute(
|
|
1921
|
+
"UPDATE task_outcomes SET outcome = 'rework' "
|
|
1922
|
+
"WHERE outcome = 'success' AND task_id IN ("
|
|
1923
|
+
" SELECT DISTINCT task_id FROM task_status_history "
|
|
1924
|
+
" WHERE old_status IN ('testing','complete','done','review') "
|
|
1925
|
+
" AND new_status IN ('in_progress','open','ready','icebox'))"
|
|
1926
|
+
)
|
|
1927
|
+
conn.commit()
|
|
1928
|
+
logger.info("Migration v38 applied: backfilled %d task(s) to rework", cur.rowcount)
|
|
1929
|
+
|
|
1930
|
+
|
|
1931
|
+
def _migrate_v39_observations_task_id(conn: sqlite3.Connection) -> None:
|
|
1932
|
+
"""Migration v39 — add observations.task_id so an observation can be linked
|
|
1933
|
+
to the task active when it was written. Until now observations carried only
|
|
1934
|
+
session_id, and a session spans many tasks, so NO per-task rework signal
|
|
1935
|
+
(file churn, in-task errors) could be derived — the root reason the learning
|
|
1936
|
+
loop is blind to mid-task rework (audit 2026-06-08). Forward-only: historical
|
|
1937
|
+
rows stay NULL (the linkage was never captured and cannot be recovered);
|
|
1938
|
+
capture.py stamps it going forward. Idempotent — skips if the column exists."""
|
|
1939
|
+
if not _table_exists(conn, "observations"):
|
|
1940
|
+
logger.info("Migration v39 skipped: observations not present yet")
|
|
1941
|
+
return
|
|
1942
|
+
cols = {row[1] for row in conn.execute("PRAGMA table_info(observations)").fetchall()}
|
|
1943
|
+
if "task_id" in cols:
|
|
1944
|
+
logger.info("Migration v39 skipped: observations.task_id already present")
|
|
1945
|
+
return
|
|
1946
|
+
conn.execute("ALTER TABLE observations ADD COLUMN task_id TEXT")
|
|
1947
|
+
conn.commit()
|
|
1948
|
+
logger.info("Migration v39 applied: observations.task_id added")
|
|
1949
|
+
|
|
1950
|
+
|
|
1951
|
+
def _migrate_v40_embedding_outbox(conn: sqlite3.Connection) -> None:
|
|
1952
|
+
"""Migration v40 — durable embedding_outbox so the capture hot path can defer
|
|
1953
|
+
embedding off the interactive path (Wave 4). The PostToolUse capture skips
|
|
1954
|
+
the model load (COS_CAPTURE_SKIP_EMBED) to keep Edits fast; without an outbox
|
|
1955
|
+
those rows had NO embedding until a manual reindex that might never run. This
|
|
1956
|
+
table records the backlog; a Stop-hook drains it. UNIQUE(source_table,
|
|
1957
|
+
source_id) makes enqueue idempotent. Idempotent — skips if the table exists."""
|
|
1958
|
+
conn.executescript("""\
|
|
1959
|
+
CREATE TABLE IF NOT EXISTS embedding_outbox (
|
|
1960
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1961
|
+
source_table TEXT NOT NULL,
|
|
1962
|
+
source_id INTEGER NOT NULL,
|
|
1963
|
+
enqueued_at INTEGER NOT NULL,
|
|
1964
|
+
attempts INTEGER NOT NULL DEFAULT 0,
|
|
1965
|
+
last_error TEXT,
|
|
1966
|
+
UNIQUE(source_table, source_id)
|
|
1967
|
+
);
|
|
1968
|
+
CREATE INDEX IF NOT EXISTS idx_embedding_outbox_pending
|
|
1969
|
+
ON embedding_outbox(attempts, enqueued_at);
|
|
1970
|
+
""")
|
|
1971
|
+
conn.commit()
|
|
1972
|
+
logger.info("Migration v40 applied: embedding_outbox table")
|
|
1973
|
+
|
|
1974
|
+
|
|
1975
|
+
def _migrate_v41_tasks_lean_columns(conn: sqlite3.Connection) -> None:
|
|
1976
|
+
"""Migration v41 — tasks table catches up with the lean card contract
|
|
1977
|
+
(TASK-398): adds blocked_by_json / references_json / external_ref
|
|
1978
|
+
(parsed since the Scrumban migration but never persisted) and drops the
|
|
1979
|
+
eight v6-era columns with no reader or writer after the legacy
|
|
1980
|
+
task_sync retirement. domain / goal_text / dependencies stay — the
|
|
1981
|
+
cos_task_* tools read them. Idempotent — guarded per column; DROP
|
|
1982
|
+
COLUMN is best-effort so a pre-3.35 SQLite degrades to harmless
|
|
1983
|
+
residue instead of a blocked init."""
|
|
1984
|
+
cols = {row[1] for row in conn.execute("PRAGMA table_info(tasks)")}
|
|
1985
|
+
for name in ("blocked_by_json", "references_json", "external_ref"):
|
|
1986
|
+
if name not in cols:
|
|
1987
|
+
conn.execute(f"ALTER TABLE tasks ADD COLUMN {name} TEXT")
|
|
1988
|
+
dead = (
|
|
1989
|
+
"scope_in",
|
|
1990
|
+
"scope_out",
|
|
1991
|
+
"requirements",
|
|
1992
|
+
"source_of_truth",
|
|
1993
|
+
"open_questions",
|
|
1994
|
+
"rabbit_holes",
|
|
1995
|
+
"verification",
|
|
1996
|
+
"read_first",
|
|
1997
|
+
)
|
|
1998
|
+
for name in dead:
|
|
1999
|
+
if name not in cols:
|
|
2000
|
+
continue
|
|
2001
|
+
try:
|
|
2002
|
+
conn.execute(f"ALTER TABLE tasks DROP COLUMN {name}")
|
|
2003
|
+
except sqlite3.OperationalError as exc:
|
|
2004
|
+
logger.warning(
|
|
2005
|
+
"Migration v41: could not drop tasks.%s (%s) — leaving in place", name, exc
|
|
2006
|
+
)
|
|
2007
|
+
conn.commit()
|
|
2008
|
+
logger.info("Migration v41 applied: lean task columns added, v6 dead columns dropped")
|
|
2009
|
+
|
|
2010
|
+
|
|
2011
|
+
def _migrate_v42_drop_doc_audit_trail(conn: sqlite3.Connection) -> None:
|
|
2012
|
+
"""Migration v42 — drop doc_audit_trail (TASK-401): the audit concept is
|
|
2013
|
+
retired project-wide; git is the forensic record for doc edits. Drops
|
|
2014
|
+
the append-only guard triggers first (they would block any future
|
|
2015
|
+
cleanup), then the table. Idempotent via IF EXISTS."""
|
|
2016
|
+
conn.executescript("""\
|
|
2017
|
+
DROP TRIGGER IF EXISTS doc_audit_trail_no_update;
|
|
2018
|
+
DROP TRIGGER IF EXISTS doc_audit_trail_no_delete;
|
|
2019
|
+
DROP TABLE IF EXISTS doc_audit_trail;
|
|
2020
|
+
""")
|
|
2021
|
+
conn.commit()
|
|
2022
|
+
logger.info("Migration v42 applied: doc_audit_trail dropped (audit retirement)")
|
|
2023
|
+
|
|
2024
|
+
|
|
2025
|
+
def _migrate_v43_drop_experiment_log(conn: sqlite3.Connection) -> None:
|
|
2026
|
+
"""Migration v43 — drop experiment_log (audit 2026-06-19, group B5). The table
|
|
2027
|
+
was created in the v1 schema for the Experiment Protocol but never got a write
|
|
2028
|
+
path (zero writers in 260 tasks); only a dashboard reader + stats count showed
|
|
2029
|
+
a perpetual 0. Speculative scaffolding for a feature with no demand → removed
|
|
2030
|
+
per anti-overengineering. If the Experiment Protocol later needs persistence, a
|
|
2031
|
+
task adds the table + its write tool together. Idempotent (IF EXISTS)."""
|
|
2032
|
+
conn.executescript("""\
|
|
2033
|
+
DROP TABLE IF EXISTS experiment_log;
|
|
2034
|
+
""")
|
|
2035
|
+
conn.commit()
|
|
2036
|
+
logger.info("Migration v43 applied: experiment_log dropped (no writer, B5)")
|
|
2037
|
+
|
|
2038
|
+
|
|
2039
|
+
def _migrate_v44_dispatch_transcript(conn: sqlite3.Connection) -> None:
|
|
2040
|
+
"""Migration v44 — formula_dispatches.raw_transcript so a dispatched
|
|
2041
|
+
sub-agent's chat/session is auditable, not just its summarized output."""
|
|
2042
|
+
if not _table_exists(conn, "formula_dispatches"):
|
|
2043
|
+
logger.info("Migration v44 skipped: formula_dispatches not present yet")
|
|
2044
|
+
return
|
|
2045
|
+
if not _column_exists_table(conn, "formula_dispatches", "raw_transcript"):
|
|
2046
|
+
conn.execute("ALTER TABLE formula_dispatches ADD COLUMN raw_transcript TEXT")
|
|
2047
|
+
conn.commit()
|
|
2048
|
+
logger.info("Migration v44 applied: formula_dispatches gained raw_transcript")
|
|
2049
|
+
|
|
2050
|
+
|
|
2051
|
+
def _migrate_v45_task_child_cascade(conn: sqlite3.Connection) -> None:
|
|
2052
|
+
"""Migration v45 — a deleted tasks row cascades to its child tables."""
|
|
2053
|
+
conn.executescript(
|
|
2054
|
+
"""
|
|
2055
|
+
CREATE TRIGGER IF NOT EXISTS task_status_history_ad AFTER DELETE ON tasks BEGIN
|
|
2056
|
+
DELETE FROM task_status_history WHERE task_id = old.task_id;
|
|
2057
|
+
END;
|
|
2058
|
+
CREATE TRIGGER IF NOT EXISTS task_outcomes_ad AFTER DELETE ON tasks BEGIN
|
|
2059
|
+
DELETE FROM task_outcomes WHERE task_id = old.task_id;
|
|
2060
|
+
END;
|
|
2061
|
+
CREATE TRIGGER IF NOT EXISTS task_edit_history_ad AFTER DELETE ON tasks BEGIN
|
|
2062
|
+
DELETE FROM task_edit_history WHERE task_id = old.task_id;
|
|
2063
|
+
END;
|
|
2064
|
+
"""
|
|
2065
|
+
)
|
|
2066
|
+
conn.commit()
|
|
2067
|
+
logger.info("Migration v45 applied: task child-table delete-cascade triggers")
|
|
2068
|
+
|
|
2069
|
+
|
|
2070
|
+
def _migrate_v46_log_event_class(conn: sqlite3.Connection) -> None:
|
|
2071
|
+
"""Migration v46 — log_events.event_class splits policy enforcement (hook
|
|
2072
|
+
BLOCKs) from faults so error_sweep files only genuine bugs."""
|
|
2073
|
+
if not _table_exists(conn, "log_events"):
|
|
2074
|
+
logger.info("Migration v46 skipped: log_events not present yet")
|
|
2075
|
+
return
|
|
2076
|
+
if not _column_exists_table(conn, "log_events", "event_class"):
|
|
2077
|
+
conn.execute("ALTER TABLE log_events ADD COLUMN event_class TEXT NOT NULL DEFAULT 'fault'")
|
|
2078
|
+
conn.execute(
|
|
2079
|
+
"UPDATE log_events SET event_class = 'policy' "
|
|
2080
|
+
"WHERE scope LIKE 'hook.%' AND kv LIKE '%\"action\": \"block\"%'"
|
|
2081
|
+
)
|
|
2082
|
+
conn.commit()
|
|
2083
|
+
logger.info("Migration v46 applied: log_events gained event_class")
|
|
2084
|
+
|
|
2085
|
+
|
|
2086
|
+
def _migrate_v47_distill_columns(conn: sqlite3.Connection) -> None:
|
|
2087
|
+
"""Migration v47 — learned_patterns gains distill_fingerprint (idempotent
|
|
2088
|
+
LLM distillation per friction cluster) and evidence_json (sanitized sample
|
|
2089
|
+
failures backing the lesson)."""
|
|
2090
|
+
if not _column_exists_table(conn, "learned_patterns", "distill_fingerprint"):
|
|
2091
|
+
conn.execute("ALTER TABLE learned_patterns ADD COLUMN distill_fingerprint TEXT")
|
|
2092
|
+
if not _column_exists_table(conn, "learned_patterns", "evidence_json"):
|
|
2093
|
+
conn.execute("ALTER TABLE learned_patterns ADD COLUMN evidence_json TEXT")
|
|
2094
|
+
conn.commit()
|
|
2095
|
+
logger.info("Migration v47 applied: learned_patterns gained distill columns")
|
|
2096
|
+
|
|
2097
|
+
|
|
2098
|
+
def _migrate_v48_reclassify_mechanical_observations(conn: sqlite3.Connection) -> None:
|
|
2099
|
+
cur = conn.execute(
|
|
2100
|
+
"UPDATE observations SET memory_type = 'changelog' "
|
|
2101
|
+
"WHERE observation_type IN ('write', 'edit', 'multiedit') "
|
|
2102
|
+
"AND COALESCE(memory_type, '') != 'changelog'"
|
|
2103
|
+
)
|
|
2104
|
+
conn.commit()
|
|
2105
|
+
logger.info(
|
|
2106
|
+
"Migration v48 applied: reclassified %d mechanical observation(s) to changelog",
|
|
2107
|
+
cur.rowcount,
|
|
2108
|
+
)
|
|
2109
|
+
|
|
2110
|
+
|
|
2111
|
+
def _migrate_v49_add_times_seen(conn: sqlite3.Connection) -> None:
|
|
2112
|
+
conn.execute("ALTER TABLE learned_patterns ADD COLUMN times_seen INTEGER DEFAULT 0")
|
|
2113
|
+
cur = conn.execute("UPDATE learned_patterns SET times_seen = COALESCE(times_validated, 0)")
|
|
2114
|
+
conn.commit()
|
|
2115
|
+
logger.info(
|
|
2116
|
+
"Migration v49 applied: learned_patterns gained times_seen; backfilled %d row(s) from times_validated",
|
|
2117
|
+
cur.rowcount,
|
|
2118
|
+
)
|
|
2119
|
+
|
|
2120
|
+
|
|
2121
|
+
def _migrate_v50_reset_times_validated_from_ledger(conn: sqlite3.Connection) -> None:
|
|
2122
|
+
# v49 moved occurrence counts to times_seen but left the historical
|
|
2123
|
+
# times_validated values inflated by the pre-split re-mine/dedup bumps (up to
|
|
2124
|
+
# 534, with zero real validations behind them). Rebuild the counter from the
|
|
2125
|
+
# pattern_validations ledger — the append-only record of genuine validations
|
|
2126
|
+
# (was_helpful, non-throttled) — so pattern_tier's "Trusted" reflects real
|
|
2127
|
+
# confirmation. An empty ledger resets every row to 0; trust is then re-earned
|
|
2128
|
+
# by the now-firing validation loop.
|
|
2129
|
+
try:
|
|
2130
|
+
cur = conn.execute(
|
|
2131
|
+
"UPDATE learned_patterns SET times_validated = ("
|
|
2132
|
+
" SELECT COUNT(*) FROM pattern_validations pv "
|
|
2133
|
+
" WHERE pv.pattern_id = learned_patterns.id "
|
|
2134
|
+
" AND pv.was_helpful = 1 AND COALESCE(pv.was_throttled, 0) = 0"
|
|
2135
|
+
")"
|
|
2136
|
+
)
|
|
2137
|
+
except sqlite3.OperationalError:
|
|
2138
|
+
# No ledger table → no real validations exist; honest baseline is 0.
|
|
2139
|
+
cur = conn.execute("UPDATE learned_patterns SET times_validated = 0")
|
|
2140
|
+
conn.commit()
|
|
2141
|
+
logger.info(
|
|
2142
|
+
"Migration v50 applied: rebuilt times_validated from pattern_validations ledger (%d row(s))",
|
|
2143
|
+
cur.rowcount,
|
|
2144
|
+
)
|
|
2145
|
+
|
|
2146
|
+
|
|
2147
|
+
def _migrate_v51_observations_dedup_unique(conn: sqlite3.Connection) -> None:
|
|
2148
|
+
# The write path deduped observations by (content_hash, session_id) with a
|
|
2149
|
+
# race-prone SELECT-then-INSERT: two concurrent captures both miss the SELECT
|
|
2150
|
+
# and both insert. Collapse existing duplicates (keep the earliest id) and add
|
|
2151
|
+
# a partial UNIQUE index so INSERT OR IGNORE enforces one row per group
|
|
2152
|
+
# atomically. NULL content_hash/session_id rows are exempt (not dedup targets;
|
|
2153
|
+
# SQLite already treats their NULLs as distinct). The observations_fts index
|
|
2154
|
+
# stays consistent via the AFTER DELETE trigger; no FK references observations.id.
|
|
2155
|
+
cur = conn.execute(
|
|
2156
|
+
"DELETE FROM observations "
|
|
2157
|
+
"WHERE content_hash IS NOT NULL AND session_id IS NOT NULL "
|
|
2158
|
+
" AND id NOT IN ("
|
|
2159
|
+
" SELECT MIN(id) FROM observations "
|
|
2160
|
+
" WHERE content_hash IS NOT NULL AND session_id IS NOT NULL "
|
|
2161
|
+
" GROUP BY content_hash, session_id"
|
|
2162
|
+
" )"
|
|
2163
|
+
)
|
|
2164
|
+
removed = cur.rowcount
|
|
2165
|
+
conn.execute(
|
|
2166
|
+
"CREATE UNIQUE INDEX IF NOT EXISTS idx_observations_content_session "
|
|
2167
|
+
"ON observations(content_hash, session_id) "
|
|
2168
|
+
"WHERE content_hash IS NOT NULL AND session_id IS NOT NULL"
|
|
2169
|
+
)
|
|
2170
|
+
conn.commit()
|
|
2171
|
+
logger.info(
|
|
2172
|
+
"Migration v51 applied: collapsed %d duplicate observation(s); "
|
|
2173
|
+
"added partial UNIQUE(content_hash, session_id)",
|
|
2174
|
+
removed,
|
|
2175
|
+
)
|
|
2176
|
+
|
|
2177
|
+
|
|
2178
|
+
def _migrate_v52_derived_outcome_columns(conn: sqlite3.Connection) -> None:
|
|
2179
|
+
if not _column_exists_table(conn, "task_outcomes", "derived_outcome"):
|
|
2180
|
+
conn.execute("ALTER TABLE task_outcomes ADD COLUMN derived_outcome TEXT")
|
|
2181
|
+
if not _column_exists_table(conn, "task_outcomes", "derived_provenance"):
|
|
2182
|
+
conn.execute("ALTER TABLE task_outcomes ADD COLUMN derived_provenance TEXT")
|
|
2183
|
+
conn.commit()
|
|
2184
|
+
logger.info("Migration v52 applied: task_outcomes gained derived_outcome columns")
|
|
2185
|
+
|
|
2186
|
+
|
|
2187
|
+
MIGRATIONS: list[tuple[int, str, MigrationAction]] = [
|
|
2188
|
+
(
|
|
2189
|
+
1,
|
|
2190
|
+
"TASK-141: initial schema — task_outcomes, agent_metrics, learned_patterns, experiment_log, observations, session_summaries",
|
|
2191
|
+
"""\
|
|
2192
|
+
-- task_outcomes: one row per completed task
|
|
2193
|
+
CREATE TABLE IF NOT EXISTS task_outcomes (
|
|
2194
|
+
task_id TEXT PRIMARY KEY,
|
|
2195
|
+
type TEXT NOT NULL,
|
|
2196
|
+
domain TEXT NOT NULL,
|
|
2197
|
+
complexity TEXT NOT NULL,
|
|
2198
|
+
dimensions INTEGER DEFAULT 1,
|
|
2199
|
+
outcome TEXT NOT NULL,
|
|
2200
|
+
duration_min INTEGER,
|
|
2201
|
+
model TEXT,
|
|
2202
|
+
skills_used TEXT,
|
|
2203
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
2204
|
+
);
|
|
2205
|
+
|
|
2206
|
+
-- agent_metrics: per-agent-invocation telemetry
|
|
2207
|
+
CREATE TABLE IF NOT EXISTS agent_metrics (
|
|
2208
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
2209
|
+
task_id TEXT,
|
|
2210
|
+
agent_type TEXT NOT NULL,
|
|
2211
|
+
model TEXT,
|
|
2212
|
+
duration_ms INTEGER,
|
|
2213
|
+
domain TEXT,
|
|
2214
|
+
complexity TEXT,
|
|
2215
|
+
outcome TEXT,
|
|
2216
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
2217
|
+
);
|
|
2218
|
+
|
|
2219
|
+
-- learned_patterns: extracted reusable patterns with confidence
|
|
2220
|
+
CREATE TABLE IF NOT EXISTS learned_patterns (
|
|
2221
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
2222
|
+
pattern TEXT NOT NULL,
|
|
2223
|
+
memory_type TEXT DEFAULT 'pattern',
|
|
2224
|
+
domain TEXT,
|
|
2225
|
+
source TEXT,
|
|
2226
|
+
confidence REAL DEFAULT 0.5,
|
|
2227
|
+
decay_rate REAL DEFAULT 0.1,
|
|
2228
|
+
impact_score REAL DEFAULT 0.5,
|
|
2229
|
+
concepts TEXT,
|
|
2230
|
+
times_validated INTEGER DEFAULT 0,
|
|
2231
|
+
times_violated INTEGER DEFAULT 0,
|
|
2232
|
+
access_count INTEGER DEFAULT 0,
|
|
2233
|
+
last_accessed_at DATETIME,
|
|
2234
|
+
promoted_to TEXT,
|
|
2235
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
2236
|
+
last_validated DATETIME,
|
|
2237
|
+
archived_at DATETIME
|
|
2238
|
+
);
|
|
2239
|
+
|
|
2240
|
+
-- experiment_log: hypothesis tracking per task
|
|
2241
|
+
CREATE TABLE IF NOT EXISTS experiment_log (
|
|
2242
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
2243
|
+
task_id TEXT,
|
|
2244
|
+
hypothesis TEXT NOT NULL,
|
|
2245
|
+
test_description TEXT,
|
|
2246
|
+
outcome TEXT,
|
|
2247
|
+
learning TEXT,
|
|
2248
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
2249
|
+
);
|
|
2250
|
+
|
|
2251
|
+
-- observations: raw captured observations from tool use
|
|
2252
|
+
CREATE TABLE IF NOT EXISTS observations (
|
|
2253
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
2254
|
+
session_id TEXT,
|
|
2255
|
+
tool_name TEXT,
|
|
2256
|
+
observation_type TEXT,
|
|
2257
|
+
memory_type TEXT DEFAULT 'discovery',
|
|
2258
|
+
impact_score REAL DEFAULT 0.5,
|
|
2259
|
+
title TEXT,
|
|
2260
|
+
narrative TEXT,
|
|
2261
|
+
facts TEXT,
|
|
2262
|
+
concepts TEXT,
|
|
2263
|
+
files_read TEXT,
|
|
2264
|
+
files_modified TEXT,
|
|
2265
|
+
content_hash TEXT,
|
|
2266
|
+
cost_tokens INTEGER DEFAULT 0,
|
|
2267
|
+
expires_at DATETIME,
|
|
2268
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
2269
|
+
);
|
|
2270
|
+
|
|
2271
|
+
-- session_summaries: end-of-session digests.
|
|
2272
|
+
--
|
|
2273
|
+
-- Writer matrix (2026-04):
|
|
2274
|
+
-- session_id, task_id, previous_session_id, files_touched,
|
|
2275
|
+
-- observations_count, breakthrough_ids, duration_minutes
|
|
2276
|
+
-- → filled by session_summary.build_session_summary on Stop hook.
|
|
2277
|
+
-- request, learned
|
|
2278
|
+
-- → filled by session_enrich.py from tool/outcome signal.
|
|
2279
|
+
-- investigated, completed, next_steps
|
|
2280
|
+
-- → RESERVED for narrative fields the agent populates via
|
|
2281
|
+
-- cos_learn_narrative on breakthrough + a future explicit
|
|
2282
|
+
-- retro tool. Nullable by design; NULL is not a bug.
|
|
2283
|
+
CREATE TABLE IF NOT EXISTS session_summaries (
|
|
2284
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
2285
|
+
session_id TEXT,
|
|
2286
|
+
task_id TEXT,
|
|
2287
|
+
request TEXT,
|
|
2288
|
+
investigated TEXT,
|
|
2289
|
+
learned TEXT,
|
|
2290
|
+
completed TEXT,
|
|
2291
|
+
next_steps TEXT,
|
|
2292
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
2293
|
+
);
|
|
2294
|
+
""",
|
|
2295
|
+
),
|
|
2296
|
+
# FTS5 full-text search layer (callable migration — needs runtime FTS5 check)
|
|
2297
|
+
(
|
|
2298
|
+
2,
|
|
2299
|
+
"TASK-152: FTS5 observations_fts virtual table + INSERT/UPDATE/DELETE triggers",
|
|
2300
|
+
_migrate_v2_fts5,
|
|
2301
|
+
),
|
|
2302
|
+
# routing_weights table for adaptive model/skill routing
|
|
2303
|
+
(
|
|
2304
|
+
3,
|
|
2305
|
+
"TASK-148: routing_weights table for adaptive routing",
|
|
2306
|
+
"""\
|
|
2307
|
+
CREATE TABLE IF NOT EXISTS routing_weights (
|
|
2308
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
2309
|
+
domain TEXT NOT NULL,
|
|
2310
|
+
complexity TEXT NOT NULL,
|
|
2311
|
+
model TEXT,
|
|
2312
|
+
skill TEXT,
|
|
2313
|
+
success_rate REAL DEFAULT 0.0,
|
|
2314
|
+
sample_count INTEGER DEFAULT 0,
|
|
2315
|
+
last_updated DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
2316
|
+
UNIQUE(domain, complexity, model, skill)
|
|
2317
|
+
);
|
|
2318
|
+
""",
|
|
2319
|
+
),
|
|
2320
|
+
# Brain features: outcome_history, concept_graph, session_summaries enrichment
|
|
2321
|
+
(
|
|
2322
|
+
4,
|
|
2323
|
+
"Brain features: outcome_history, concept_graph, session_summaries enrichment",
|
|
2324
|
+
_migrate_v4_brain_features,
|
|
2325
|
+
),
|
|
2326
|
+
# embeddings + document_chunks for RAG vector search
|
|
2327
|
+
(5, "RAG: embeddings + document_chunks tables", _migrate_v5_rag),
|
|
2328
|
+
# tasks table for hybrid task store
|
|
2329
|
+
(6, "Task store: tasks table indexing docs/tasks/*.md", _migrate_v6_tasks),
|
|
2330
|
+
# brain hardening — trust_tier, provenance, memory_audit
|
|
2331
|
+
(
|
|
2332
|
+
7,
|
|
2333
|
+
"Brain hardening: trust_tier + provenance + memory_audit",
|
|
2334
|
+
_migrate_v7_brain_hardening,
|
|
2335
|
+
),
|
|
2336
|
+
# self-validation throttle
|
|
2337
|
+
(
|
|
2338
|
+
8,
|
|
2339
|
+
"Validation throttle: pattern_validations table",
|
|
2340
|
+
_migrate_v8_validation_throttle,
|
|
2341
|
+
),
|
|
2342
|
+
# FTS5 over document_chunks for lexical doc fallback
|
|
2343
|
+
(9, "Docs FTS: document_chunks_fts + triggers", _migrate_v9_docs_fts),
|
|
2344
|
+
# retrievals table — audit + feedback loop
|
|
2345
|
+
(10, "Retrieval-outcome loop: retrievals table", _migrate_v10_retrievals),
|
|
2346
|
+
# retrieval quality tracker + contextual-chunk scaffolding
|
|
2347
|
+
(
|
|
2348
|
+
11,
|
|
2349
|
+
"Retrieval quality: retrieval_quality + contextual chunk columns",
|
|
2350
|
+
_migrate_v11_retrieval_quality,
|
|
2351
|
+
),
|
|
2352
|
+
# graph_os knowledge-graph tables + embedding_dim column
|
|
2353
|
+
(
|
|
2354
|
+
12,
|
|
2355
|
+
"graph_os: graph_nodes + graph_edges_v12 + graph_evidence_v12 + graph_nodes_fts + embeddings.embedding_dim",
|
|
2356
|
+
_migrate_v12_graph_os,
|
|
2357
|
+
),
|
|
2358
|
+
# board_os Scrumban — extend tasks + task_status_history
|
|
2359
|
+
(
|
|
2360
|
+
13,
|
|
2361
|
+
"board_os: tasks +swimlane/kind/epic/priority/appetite/started_at/completed_at/agent_session/labels_json/work_log_last_5; task_status_history",
|
|
2362
|
+
_migrate_v13_board_os,
|
|
2363
|
+
),
|
|
2364
|
+
# formula-agent supervisor — 4 cognition tables
|
|
2365
|
+
(
|
|
2366
|
+
14,
|
|
2367
|
+
"Formula-agents: backtrack_events + persona_selections + ambiguity_violations + formula_dispatches",
|
|
2368
|
+
_migrate_v14_cognition,
|
|
2369
|
+
),
|
|
2370
|
+
# graph_os S1 / B17: CHECK(confidence BETWEEN 0 AND 1) triggers on graph_edges_v12
|
|
2371
|
+
(
|
|
2372
|
+
15,
|
|
2373
|
+
"graph_os S1 B17: graph_edges_v12 confidence CHECK triggers (INSERT + UPDATE)",
|
|
2374
|
+
_migrate_v15_graph_edges_confidence_check,
|
|
2375
|
+
),
|
|
2376
|
+
# graph_os S3: data migration — normalize graph_nodes.kind legacy values
|
|
2377
|
+
(
|
|
2378
|
+
16,
|
|
2379
|
+
"graph_os S3: normalize graph_nodes.kind via NodeKind/normalize_kind",
|
|
2380
|
+
_migrate_v16_normalize_graph_node_kinds,
|
|
2381
|
+
),
|
|
2382
|
+
# graph_os V1: file-level incremental indexing — file_index_state cache
|
|
2383
|
+
(
|
|
2384
|
+
17,
|
|
2385
|
+
"graph_os V1: file_index_state cache table for incremental reindex",
|
|
2386
|
+
_migrate_v17_file_index_state,
|
|
2387
|
+
),
|
|
2388
|
+
# retrieval router telemetry table
|
|
2389
|
+
(
|
|
2390
|
+
18,
|
|
2391
|
+
"Retrieval router telemetry: retrieval_router_log table",
|
|
2392
|
+
_migrate_v18_retrieval_router_log,
|
|
2393
|
+
),
|
|
2394
|
+
# drop 'ready' column — fold into icebox + 'ready' label
|
|
2395
|
+
(
|
|
2396
|
+
19,
|
|
2397
|
+
"board_os: drop 'ready' status, migrate existing rows to icebox + 'ready' label",
|
|
2398
|
+
_migrate_v19_drop_ready_status,
|
|
2399
|
+
),
|
|
2400
|
+
# override audit — task_status_history.override_reason/actor
|
|
2401
|
+
(20, "Override audit columns on task_status_history", _migrate_v20_override_audit),
|
|
2402
|
+
# doc_audit_trail — append-only doc edit + decision history
|
|
2403
|
+
(
|
|
2404
|
+
21,
|
|
2405
|
+
"doc_audit_trail (append-only) for doc edits + decision history",
|
|
2406
|
+
_migrate_v21_doc_audit_trail,
|
|
2407
|
+
),
|
|
2408
|
+
# document_chunks frontmatter metadata for Stage-1 RAG pre-filter
|
|
2409
|
+
(
|
|
2410
|
+
22,
|
|
2411
|
+
"document_chunks frontmatter metadata (domain/layer/ssot/updated_iso/is_active)",
|
|
2412
|
+
_migrate_v22_doc_chunks_metadata,
|
|
2413
|
+
),
|
|
2414
|
+
(
|
|
2415
|
+
23,
|
|
2416
|
+
"formula_dispatches cost / budget / usage / tool_calls columns",
|
|
2417
|
+
_migrate_v23_dispatch_cost,
|
|
2418
|
+
),
|
|
2419
|
+
# v24: project_trajectory — long-term project intent across sessions
|
|
2420
|
+
(
|
|
2421
|
+
24,
|
|
2422
|
+
"v24: project_trajectory table for cross-session project intent",
|
|
2423
|
+
_migrate_v24_project_trajectory,
|
|
2424
|
+
),
|
|
2425
|
+
# v25: structured failure anatomy on backtrack_events
|
|
2426
|
+
(
|
|
2427
|
+
25,
|
|
2428
|
+
"v25: backtrack_events failure anatomy (hypothesis/failure_signal/root_cause/corrective_action)",
|
|
2429
|
+
_migrate_v25_backtrack_failure_anatomy,
|
|
2430
|
+
),
|
|
2431
|
+
# v26: routing_weights staleness tracking for autonomous refresh
|
|
2432
|
+
(
|
|
2433
|
+
26,
|
|
2434
|
+
"v26: routing_weights last_recalc_at + outcomes_at_recalc",
|
|
2435
|
+
_migrate_v26_routing_evolution,
|
|
2436
|
+
),
|
|
2437
|
+
# formula_dispatches gains sub_session_id (SDK key),
|
|
2438
|
+
# model (claude-opus-4-7 / claude-sonnet-4-6), checkpoints_jsonb (T9.2).
|
|
2439
|
+
(
|
|
2440
|
+
27,
|
|
2441
|
+
"v27: formula_dispatches sub_session_id / model / checkpoints_jsonb",
|
|
2442
|
+
_migrate_v27_dispatch_sdk_columns,
|
|
2443
|
+
),
|
|
2444
|
+
(
|
|
2445
|
+
28,
|
|
2446
|
+
"Polyglot v28: file_index_state.duration_ms for per-extractor latency telemetry",
|
|
2447
|
+
_migrate_v28_file_index_duration,
|
|
2448
|
+
),
|
|
2449
|
+
(
|
|
2450
|
+
29,
|
|
2451
|
+
"G14 FTS5 v29: porter→unicode61 tokenizer so Persian/Arabic body content is indexed",
|
|
2452
|
+
_migrate_v29_fts5_unicode_tokenizer,
|
|
2453
|
+
),
|
|
2454
|
+
(
|
|
2455
|
+
30,
|
|
2456
|
+
"Memory ranking symmetry v30: observations.access_count + last_accessed_at",
|
|
2457
|
+
_migrate_v30_observation_access_signal,
|
|
2458
|
+
),
|
|
2459
|
+
(
|
|
2460
|
+
31,
|
|
2461
|
+
"G4 v31: task_edit_history (append-only, actor-attributed field edits)",
|
|
2462
|
+
_migrate_v31_task_edit_history,
|
|
2463
|
+
),
|
|
2464
|
+
(
|
|
2465
|
+
32,
|
|
2466
|
+
"Observability eye v32: log_events + log_fingerprints durable error store",
|
|
2467
|
+
_migrate_v32_log_events,
|
|
2468
|
+
),
|
|
2469
|
+
(
|
|
2470
|
+
33,
|
|
2471
|
+
"Memory durability v33: learned_patterns.archived_at for prune grace window",
|
|
2472
|
+
_migrate_v33_pattern_archived_at,
|
|
2473
|
+
),
|
|
2474
|
+
(
|
|
2475
|
+
34,
|
|
2476
|
+
"formula_dispatches.error captures the dispatch failure reason",
|
|
2477
|
+
_migrate_v34_dispatch_error,
|
|
2478
|
+
),
|
|
2479
|
+
(
|
|
2480
|
+
35,
|
|
2481
|
+
"Scale foundation: keyset indexes + tasks_fts + task_dependencies junction",
|
|
2482
|
+
_migrate_v35_scale_foundation,
|
|
2483
|
+
),
|
|
2484
|
+
(
|
|
2485
|
+
36,
|
|
2486
|
+
"PII backfill v36: scrub local username from observations.files_modified + title",
|
|
2487
|
+
_migrate_v36_scrub_username_from_observations,
|
|
2488
|
+
),
|
|
2489
|
+
(
|
|
2490
|
+
37,
|
|
2491
|
+
"PII backfill v37: scrub observations.narrative + dash-encoded username in project slugs",
|
|
2492
|
+
_migrate_v37_scrub_narrative_and_dash,
|
|
2493
|
+
),
|
|
2494
|
+
(
|
|
2495
|
+
38,
|
|
2496
|
+
"Backfill honest rework outcomes from task_status_history reopen signal (unstarves learn_extract)",
|
|
2497
|
+
_migrate_v38_backfill_rework_outcome,
|
|
2498
|
+
),
|
|
2499
|
+
(
|
|
2500
|
+
39,
|
|
2501
|
+
"Add observations.task_id — per-task linkage so mid-task rework signals become derivable",
|
|
2502
|
+
_migrate_v39_observations_task_id,
|
|
2503
|
+
),
|
|
2504
|
+
(
|
|
2505
|
+
40,
|
|
2506
|
+
"Add embedding_outbox — durable backlog so hot-path-skipped embeddings drain off the interactive path",
|
|
2507
|
+
_migrate_v40_embedding_outbox,
|
|
2508
|
+
),
|
|
2509
|
+
(
|
|
2510
|
+
41,
|
|
2511
|
+
"Tasks lean columns — add blocked_by/references/external_ref, drop dead v6 columns (TASK-398)",
|
|
2512
|
+
_migrate_v41_tasks_lean_columns,
|
|
2513
|
+
),
|
|
2514
|
+
(
|
|
2515
|
+
42,
|
|
2516
|
+
"Drop doc_audit_trail — audit concept retired project-wide; git is the forensic record (TASK-401)",
|
|
2517
|
+
_migrate_v42_drop_doc_audit_trail,
|
|
2518
|
+
),
|
|
2519
|
+
(
|
|
2520
|
+
43,
|
|
2521
|
+
"Drop experiment_log — created in v1 but never wired (zero writers); speculative scaffolding removed (B5)",
|
|
2522
|
+
_migrate_v43_drop_experiment_log,
|
|
2523
|
+
),
|
|
2524
|
+
(
|
|
2525
|
+
44,
|
|
2526
|
+
"formula_dispatches.raw_transcript — persist dispatched sub-agent transcript for audit",
|
|
2527
|
+
_migrate_v44_dispatch_transcript,
|
|
2528
|
+
),
|
|
2529
|
+
(
|
|
2530
|
+
45,
|
|
2531
|
+
"Delete-cascade triggers: a pruned task removes its task_status_history / task_outcomes / task_edit_history rows (mirrors tasks_deps_ad)",
|
|
2532
|
+
_migrate_v45_task_child_cascade,
|
|
2533
|
+
),
|
|
2534
|
+
(
|
|
2535
|
+
46,
|
|
2536
|
+
"log_events.event_class (fault|policy|audit) — hook BLOCKs are policy, not faults, so error_sweep stops mis-filing them as bugs",
|
|
2537
|
+
_migrate_v46_log_event_class,
|
|
2538
|
+
),
|
|
2539
|
+
(
|
|
2540
|
+
47,
|
|
2541
|
+
"learned_patterns.distill_fingerprint + evidence_json — idempotent LLM lesson distillation with auditable evidence",
|
|
2542
|
+
_migrate_v47_distill_columns,
|
|
2543
|
+
),
|
|
2544
|
+
(
|
|
2545
|
+
48,
|
|
2546
|
+
"Reclassify mechanical auto-capture observations (write/edit/multiedit) to memory_type=changelog so recall exclusion is complete; forward-only, re-derivable from files_modified",
|
|
2547
|
+
_migrate_v48_reclassify_mechanical_observations,
|
|
2548
|
+
),
|
|
2549
|
+
(
|
|
2550
|
+
49,
|
|
2551
|
+
"learned_patterns.times_seen — split the conflated times_validated: occurrence re-mines / dedup folds move to times_seen; times_validated reserved for real validation events",
|
|
2552
|
+
_migrate_v49_add_times_seen,
|
|
2553
|
+
),
|
|
2554
|
+
(
|
|
2555
|
+
50,
|
|
2556
|
+
"Rebuild times_validated from the pattern_validations ledger — retire the pre-split inflated values so pattern_tier 'Trusted' reflects genuine validations; trust is re-earned by the firing loop",
|
|
2557
|
+
_migrate_v50_reset_times_validated_from_ledger,
|
|
2558
|
+
),
|
|
2559
|
+
(
|
|
2560
|
+
51,
|
|
2561
|
+
"Atomic write-path dedup: collapse duplicate observations and add a partial UNIQUE(content_hash, session_id) index so INSERT OR IGNORE enforces one-per-group without the race-prone SELECT-then-INSERT",
|
|
2562
|
+
_migrate_v51_observations_dedup_unique,
|
|
2563
|
+
),
|
|
2564
|
+
(
|
|
2565
|
+
52,
|
|
2566
|
+
"task_outcomes gains additive derived_outcome + derived_provenance — reward label sourced from the tree-keyed verify ledger, self-report fallback (ADR-0016 stage 1)",
|
|
2567
|
+
_migrate_v52_derived_outcome_columns,
|
|
2568
|
+
),
|
|
2569
|
+
]
|
|
2570
|
+
|
|
2571
|
+
|
|
2572
|
+
# ---------------------------------------------------------------------------
|
|
2573
|
+
# Connection helpers
|
|
2574
|
+
# ---------------------------------------------------------------------------
|
|
2575
|
+
|
|
2576
|
+
|
|
2577
|
+
def _apply_pragmas(conn: sqlite3.Connection) -> None:
|
|
2578
|
+
"""Apply performance and safety PRAGMAs.
|
|
2579
|
+
|
|
2580
|
+
Tuned for consumer repos up to ~10x meta-repo size (~400K graph nodes,
|
|
2581
|
+
~600MB DB). Trade-off chosen: durability >= NORMAL (WAL still crash-safe),
|
|
2582
|
+
throughput maximized via mmap + large cache.
|
|
2583
|
+
"""
|
|
2584
|
+
conn.execute("PRAGMA journal_mode = WAL")
|
|
2585
|
+
conn.execute("PRAGMA synchronous = NORMAL") # 3-5x faster writes; WAL still crash-safe
|
|
2586
|
+
conn.execute("PRAGMA foreign_keys = ON")
|
|
2587
|
+
conn.execute("PRAGMA temp_store = MEMORY") # sort/group spill to RAM, not disk
|
|
2588
|
+
conn.execute("PRAGMA cache_size = -65536") # 64 MB page cache (signed = KB)
|
|
2589
|
+
conn.execute("PRAGMA mmap_size = 268435456") # 256 MB memory-mapped I/O — skips read() syscalls
|
|
2590
|
+
conn.execute("PRAGMA wal_autocheckpoint = 1000") # checkpoint every ~4MB of WAL (4KB pages)
|
|
2591
|
+
conn.execute("PRAGMA busy_timeout = 5000") # 5s wait on locked DB instead of immediate fail
|
|
2592
|
+
|
|
2593
|
+
|
|
2594
|
+
def get_connection(db_path: str | Path | None = None) -> sqlite3.Connection:
|
|
2595
|
+
"""Open a connection with WAL mode and safety PRAGMAs.
|
|
2596
|
+
|
|
2597
|
+
Args:
|
|
2598
|
+
db_path: Path to the SQLite database file.
|
|
2599
|
+
Defaults to .coding-os/coding-os.db (via COS_DB_PATH env).
|
|
2600
|
+
|
|
2601
|
+
Returns:
|
|
2602
|
+
A configured sqlite3.Connection.
|
|
2603
|
+
"""
|
|
2604
|
+
path = str(db_path or DEFAULT_DB_PATH)
|
|
2605
|
+
# check_same_thread=False: single-writer model enforced by SqliteBackend's
|
|
2606
|
+
# RLock + WAL. Without this, any consumer that shares the connection
|
|
2607
|
+
# across threads (e.g. MCP server, web routes, test harness) hits
|
|
2608
|
+
# sqlite3.ProgrammingError. Matches get_pooled_conn above.
|
|
2609
|
+
conn = sqlite3.connect(path, timeout=10, check_same_thread=False)
|
|
2610
|
+
conn.row_factory = sqlite3.Row
|
|
2611
|
+
_apply_pragmas(conn)
|
|
2612
|
+
return conn
|
|
2613
|
+
|
|
2614
|
+
|
|
2615
|
+
# ---------------------------------------------------------------------------
|
|
2616
|
+
# Thread-local connection pool for multi-agent concurrency
|
|
2617
|
+
# Spec: docs/phase-n-role-based-routing-plan.md §7a-A
|
|
2618
|
+
# One cached connection per thread; WAL lets readers run concurrently;
|
|
2619
|
+
# busy_timeout=5000 handles writer contention gracefully.
|
|
2620
|
+
# ---------------------------------------------------------------------------
|
|
2621
|
+
|
|
2622
|
+
import threading # noqa: E402
|
|
2623
|
+
|
|
2624
|
+
_thread_local = threading.local()
|
|
2625
|
+
_pool_lock = threading.Lock()
|
|
2626
|
+
_pool_stats = {"hits": 0, "misses": 0, "active": 0}
|
|
2627
|
+
|
|
2628
|
+
|
|
2629
|
+
def get_pooled_conn(db_path: str | Path | None = None) -> sqlite3.Connection:
|
|
2630
|
+
path = str(db_path or DEFAULT_DB_PATH)
|
|
2631
|
+
existing = getattr(_thread_local, "conns", {}).get(path)
|
|
2632
|
+
if existing is not None:
|
|
2633
|
+
try:
|
|
2634
|
+
existing.execute("SELECT 1").fetchone()
|
|
2635
|
+
with _pool_lock:
|
|
2636
|
+
_pool_stats["hits"] += 1
|
|
2637
|
+
return existing
|
|
2638
|
+
except sqlite3.Error:
|
|
2639
|
+
pass # Dead connection, reopen below
|
|
2640
|
+
|
|
2641
|
+
conn = sqlite3.connect(path, timeout=10, check_same_thread=False)
|
|
2642
|
+
conn.row_factory = sqlite3.Row
|
|
2643
|
+
conn.execute("PRAGMA busy_timeout = 5000")
|
|
2644
|
+
_apply_pragmas(conn)
|
|
2645
|
+
if not hasattr(_thread_local, "conns"):
|
|
2646
|
+
_thread_local.conns = {}
|
|
2647
|
+
_thread_local.conns[path] = conn
|
|
2648
|
+
with _pool_lock:
|
|
2649
|
+
_pool_stats["misses"] += 1
|
|
2650
|
+
_pool_stats["active"] += 1
|
|
2651
|
+
return conn
|
|
2652
|
+
|
|
2653
|
+
|
|
2654
|
+
def close_pool() -> None:
|
|
2655
|
+
"""Close all pooled connections for the current thread. Safe to call repeatedly."""
|
|
2656
|
+
conns = getattr(_thread_local, "conns", {})
|
|
2657
|
+
for conn in conns.values():
|
|
2658
|
+
try:
|
|
2659
|
+
conn.close()
|
|
2660
|
+
except sqlite3.Error:
|
|
2661
|
+
pass
|
|
2662
|
+
_thread_local.conns = {}
|
|
2663
|
+
with _pool_lock:
|
|
2664
|
+
_pool_stats["active"] = max(0, _pool_stats["active"] - len(conns))
|
|
2665
|
+
|
|
2666
|
+
|
|
2667
|
+
def pool_stats() -> dict[str, int]:
|
|
2668
|
+
"""Return pool stats snapshot for observability (N.5-B)."""
|
|
2669
|
+
with _pool_lock:
|
|
2670
|
+
return dict(_pool_stats)
|
|
2671
|
+
|
|
2672
|
+
|
|
2673
|
+
@contextmanager
|
|
2674
|
+
def db_connection(db_path: str | Path | None = None) -> Generator[sqlite3.Connection, None, None]:
|
|
2675
|
+
"""Context manager that yields a connection and closes it on exit."""
|
|
2676
|
+
conn = get_connection(db_path)
|
|
2677
|
+
try:
|
|
2678
|
+
yield conn
|
|
2679
|
+
finally:
|
|
2680
|
+
conn.close()
|
|
2681
|
+
|
|
2682
|
+
|
|
2683
|
+
# ---------------------------------------------------------------------------
|
|
2684
|
+
# Schema versioning & migration
|
|
2685
|
+
# ---------------------------------------------------------------------------
|
|
2686
|
+
|
|
2687
|
+
|
|
2688
|
+
def _ensure_version_table(conn: sqlite3.Connection) -> None:
|
|
2689
|
+
"""Create the schema_version table if it doesn't exist."""
|
|
2690
|
+
conn.execute(
|
|
2691
|
+
"CREATE TABLE IF NOT EXISTS schema_version ("
|
|
2692
|
+
" version INTEGER PRIMARY KEY,"
|
|
2693
|
+
" description TEXT,"
|
|
2694
|
+
" applied_at DATETIME DEFAULT CURRENT_TIMESTAMP"
|
|
2695
|
+
")"
|
|
2696
|
+
)
|
|
2697
|
+
conn.commit()
|
|
2698
|
+
|
|
2699
|
+
|
|
2700
|
+
def get_schema_version(conn: sqlite3.Connection) -> int:
|
|
2701
|
+
"""Return the highest applied migration version, or 0 if none."""
|
|
2702
|
+
_ensure_version_table(conn)
|
|
2703
|
+
row = conn.execute("SELECT MAX(version) FROM schema_version").fetchone()
|
|
2704
|
+
return row[0] if row[0] is not None else 0
|
|
2705
|
+
|
|
2706
|
+
|
|
2707
|
+
def run_migrations(conn: sqlite3.Connection) -> list[int]:
|
|
2708
|
+
"""Apply any unapplied migrations in order.
|
|
2709
|
+
|
|
2710
|
+
Concurrency-safe: takes an EXCLUSIVE transaction on the version
|
|
2711
|
+
table so two simultaneously-opening connections don't both try to
|
|
2712
|
+
apply the same migration and trip the UNIQUE constraint. Idempotent
|
|
2713
|
+
via INSERT OR IGNORE on the version row.
|
|
2714
|
+
|
|
2715
|
+
Returns:
|
|
2716
|
+
List of migration versions that were applied.
|
|
2717
|
+
"""
|
|
2718
|
+
_ensure_version_table(conn)
|
|
2719
|
+
applied: list[int] = []
|
|
2720
|
+
|
|
2721
|
+
try:
|
|
2722
|
+
conn.execute("BEGIN IMMEDIATE")
|
|
2723
|
+
except sqlite3.OperationalError:
|
|
2724
|
+
# Another writer holds the lock — wait briefly and re-read; if
|
|
2725
|
+
# we're already at the target version, return without doing
|
|
2726
|
+
# anything. This is the common case under concurrent dispatcher
|
|
2727
|
+
# workers.
|
|
2728
|
+
pass
|
|
2729
|
+
try:
|
|
2730
|
+
current = get_schema_version(conn)
|
|
2731
|
+
for version, description, action in MIGRATIONS:
|
|
2732
|
+
if version <= current:
|
|
2733
|
+
continue
|
|
2734
|
+
logger.info("Applying migration v%d: %s", version, description)
|
|
2735
|
+
try:
|
|
2736
|
+
if callable(action):
|
|
2737
|
+
action(conn)
|
|
2738
|
+
else:
|
|
2739
|
+
conn.executescript(action)
|
|
2740
|
+
except sqlite3.OperationalError as exc:
|
|
2741
|
+
# ALTER TABLE under concurrent runners can race past the
|
|
2742
|
+
# column-exists guard — skip when the message explicitly
|
|
2743
|
+
# confirms the schema is already where we want it.
|
|
2744
|
+
if "duplicate column name" in str(exc).lower():
|
|
2745
|
+
logger.debug(
|
|
2746
|
+
"migration v%d ALTER race tolerated: %s",
|
|
2747
|
+
version,
|
|
2748
|
+
exc,
|
|
2749
|
+
)
|
|
2750
|
+
else:
|
|
2751
|
+
raise
|
|
2752
|
+
conn.execute(
|
|
2753
|
+
"INSERT OR IGNORE INTO schema_version (version, description) VALUES (?, ?)",
|
|
2754
|
+
(version, description),
|
|
2755
|
+
)
|
|
2756
|
+
applied.append(version)
|
|
2757
|
+
conn.commit()
|
|
2758
|
+
except sqlite3.IntegrityError as exc:
|
|
2759
|
+
logger.debug("migration race resolved: %s", exc)
|
|
2760
|
+
conn.rollback()
|
|
2761
|
+
applied = []
|
|
2762
|
+
|
|
2763
|
+
if applied:
|
|
2764
|
+
logger.info("Migrations applied: %s (now at v%d)", applied, applied[-1])
|
|
2765
|
+
else:
|
|
2766
|
+
logger.debug("Schema up-to-date at v%d", current)
|
|
2767
|
+
|
|
2768
|
+
return applied
|
|
2769
|
+
|
|
2770
|
+
|
|
2771
|
+
# ---------------------------------------------------------------------------
|
|
2772
|
+
# Stats helpers (used by health tool)
|
|
2773
|
+
# ---------------------------------------------------------------------------
|
|
2774
|
+
|
|
2775
|
+
_TABLES = [
|
|
2776
|
+
"task_outcomes",
|
|
2777
|
+
"agent_metrics",
|
|
2778
|
+
"learned_patterns",
|
|
2779
|
+
"observations",
|
|
2780
|
+
"session_summaries",
|
|
2781
|
+
"outcome_history",
|
|
2782
|
+
"concept_graph",
|
|
2783
|
+
"embeddings",
|
|
2784
|
+
"document_chunks",
|
|
2785
|
+
"tasks",
|
|
2786
|
+
"memory_audit",
|
|
2787
|
+
"pattern_validations",
|
|
2788
|
+
"retrievals",
|
|
2789
|
+
"retrieval_quality",
|
|
2790
|
+
"graph_nodes",
|
|
2791
|
+
"graph_edges_v12",
|
|
2792
|
+
"graph_evidence_v12",
|
|
2793
|
+
"file_index_state",
|
|
2794
|
+
"retrieval_router_log",
|
|
2795
|
+
]
|
|
2796
|
+
|
|
2797
|
+
|
|
2798
|
+
def get_db_stats(conn: sqlite3.Connection) -> dict:
|
|
2799
|
+
"""Collect row counts per table and DB file size."""
|
|
2800
|
+
stats: dict = {"tables": {}}
|
|
2801
|
+
|
|
2802
|
+
for table in _TABLES:
|
|
2803
|
+
try:
|
|
2804
|
+
row = conn.execute(f"SELECT COUNT(*) FROM {table}").fetchone()
|
|
2805
|
+
stats["tables"][table] = row[0]
|
|
2806
|
+
except sqlite3.OperationalError:
|
|
2807
|
+
stats["tables"][table] = None # table doesn't exist yet
|
|
2808
|
+
|
|
2809
|
+
stats["schema_version"] = get_schema_version(conn)
|
|
2810
|
+
stats["fts5_available"] = has_fts5(conn)
|
|
2811
|
+
|
|
2812
|
+
# File size
|
|
2813
|
+
db_path = conn.execute("PRAGMA database_list").fetchone()[2]
|
|
2814
|
+
if db_path and os.path.exists(db_path):
|
|
2815
|
+
stats["db_size_bytes"] = os.path.getsize(db_path)
|
|
2816
|
+
else:
|
|
2817
|
+
stats["db_size_bytes"] = 0
|
|
2818
|
+
|
|
2819
|
+
return stats
|
|
2820
|
+
|
|
2821
|
+
|
|
2822
|
+
# ---------------------------------------------------------------------------
|
|
2823
|
+
# Bootstrap (called on server start and by --test)
|
|
2824
|
+
# ---------------------------------------------------------------------------
|
|
2825
|
+
|
|
2826
|
+
|
|
2827
|
+
def _refuse_global_hub_db(target: Path) -> None:
|
|
2828
|
+
try:
|
|
2829
|
+
hub_state = (Path.home() / STATE_DIRNAME).resolve()
|
|
2830
|
+
except (OSError, RuntimeError):
|
|
2831
|
+
return
|
|
2832
|
+
if target.parent.resolve() == hub_state:
|
|
2833
|
+
raise RuntimeError(
|
|
2834
|
+
f"refusing to create a project DB inside the global hub state dir "
|
|
2835
|
+
f"({hub_state}) — set $COS_DB_PATH or run inside a project"
|
|
2836
|
+
)
|
|
2837
|
+
|
|
2838
|
+
|
|
2839
|
+
def init_db(db_path: str | Path | None = None) -> sqlite3.Connection:
|
|
2840
|
+
"""Open the DB, run migrations, return the live connection.
|
|
2841
|
+
|
|
2842
|
+
Renames a legacy `thinking_os.db` sibling to the canonical
|
|
2843
|
+
`coding-os.db` once, before opening — silent no-op when the
|
|
2844
|
+
rename has already happened or no legacy file exists.
|
|
2845
|
+
|
|
2846
|
+
Also asks SQLite to refresh its query-planner statistics
|
|
2847
|
+
(`PRAGMA optimize`) once per process. Without this, the planner
|
|
2848
|
+
falls back to heuristics and routinely picks the slower index for
|
|
2849
|
+
multi-table JOINs (observed: 14ms → 2ms on graph_nodes JOIN
|
|
2850
|
+
graph_edges_v12 after stats present). Cost is bounded — the pragma
|
|
2851
|
+
is a no-op when stats are current.
|
|
2852
|
+
"""
|
|
2853
|
+
target = Path(db_path) if db_path else DEFAULT_DB_PATH
|
|
2854
|
+
_refuse_global_hub_db(target)
|
|
2855
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
2856
|
+
migrate_legacy_db_filename(target)
|
|
2857
|
+
conn = get_connection(str(target))
|
|
2858
|
+
run_migrations(conn)
|
|
2859
|
+
_ensure_query_planner_stats(conn)
|
|
2860
|
+
return conn
|
|
2861
|
+
|
|
2862
|
+
|
|
2863
|
+
def _ensure_query_planner_stats(conn: sqlite3.Connection) -> None:
|
|
2864
|
+
"""Make sure SQLite has query-planner stats; refresh stale ones cheaply.
|
|
2865
|
+
|
|
2866
|
+
First call on a fresh DB runs a full ``ANALYZE`` (one-shot cost,
|
|
2867
|
+
~50ms even on 600MB DBs). Subsequent calls hit only ``PRAGMA optimize``
|
|
2868
|
+
which is a no-op when stats are current and very cheap otherwise.
|
|
2869
|
+
|
|
2870
|
+
Without this the planner picks the wrong index on graph JOINs
|
|
2871
|
+
(measured: 14ms vs 3ms on graph_nodes JOIN graph_edges_v12).
|
|
2872
|
+
"""
|
|
2873
|
+
try:
|
|
2874
|
+
row = conn.execute(
|
|
2875
|
+
"SELECT name FROM sqlite_master WHERE type='table' AND name='sqlite_stat1'"
|
|
2876
|
+
).fetchone()
|
|
2877
|
+
if row is None:
|
|
2878
|
+
conn.execute("ANALYZE")
|
|
2879
|
+
conn.commit()
|
|
2880
|
+
else:
|
|
2881
|
+
conn.execute("PRAGMA optimize")
|
|
2882
|
+
except sqlite3.Error as exc:
|
|
2883
|
+
logger.debug("query-planner stats refresh skipped: %s", exc)
|