@jungjaehoon/mama-core 2.4.1 → 4.1.0
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.
- package/README.md +76 -239
- package/db/migrations/030-case-first-memory-substrate.sql +13 -98
- package/db/migrations/034-add-connector-event-scope-columns.sql +9 -13
- package/db/migrations/039-add-connector-event-operator-ingest-seq.sql +9 -95
- package/db/migrations/062-refresh-connector-event-sequences.sql +9 -113
- package/db/migrations/063-refresh-legacy-connector-event-updates.sql +9 -32
- package/db/migrations/067-add-connector-event-source-entity-id.sql +9 -13
- package/db/migrations/069-create-registry-nodes.sql +52 -0
- package/db/migrations/070-record-identity.sql +35 -0
- package/db/migrations/071-service-operation-origins.sql +187 -0
- package/db/migrations/072-observation-versions.sql +29 -0
- package/db/migrations/073-registry-corrections.sql +45 -0
- package/db/migrations/074-work-graph-ref-kinds.sql +39 -0
- package/db/migrations/075-agent-judgments.sql +58 -0
- package/db/migrations/076-scoped-checkpoint-bindings.sql +15 -0
- package/db/migrations/077-legacy-record-kind.sql +6 -0
- package/db/migrations/078-source-ingest-commands.sql +18 -0
- package/db/migrations/079-twin-edge-relations.sql +43 -0
- package/db/migrations/080-restore-duration-days.sql +7 -0
- package/db/migrations/081-mailbox.sql +61 -0
- package/db/migrations/082-commitments-row-id.sql +12 -0
- package/db/migrations/083-observation-is-the-evidence.sql +49 -0
- package/db/migrations/084-scope-kind-is-not-the-cores-to-list.sql +34 -0
- package/db/migrations/085-a-migration-has-a-source.sql +37 -0
- package/db/migrations/086-the-mailbox-is-generic.sql +110 -0
- package/db/migrations/087-mailbox-payload.sql +5 -0
- package/db/migrations/088-native-input-journal.sql +14 -0
- package/db/migrations/089-native-turn-receipt-lookup.sql +3 -0
- package/db/migrations/090-model-run-native-inputs.sql +29 -0
- package/db/migrations/091-native-turn-results.sql +11 -0
- package/db/migrations/092-restore-decision-fts.sql +34 -0
- package/db/migrations/093-native-turn-primary-kind.sql +14 -0
- package/db/migrations/094-commitment-run-provenance.sql +9 -0
- package/db/migrations/095-model-run-native-input-identity.sql +39 -0
- package/db/migrations/096-drop-connector-event-index-fts.sql +9 -0
- package/db/migrations/097-native-input-json-validity.sql +37 -0
- package/db/migrations/098-workflow-memory-kind.sql +105 -0
- package/db/migrations/099-commitment-revision-graph.sql +2 -0
- package/dist/action-contracts.d.ts +212 -0
- package/dist/action-contracts.js +10 -0
- package/dist/api/catalog.d.ts +118 -0
- package/dist/api/catalog.js +1756 -0
- package/dist/api/dispatch.d.ts +76 -0
- package/dist/api/dispatch.js +372 -0
- package/dist/canonicalize.d.ts +2 -2
- package/dist/canonicalize.js +3 -3
- package/dist/client/client.d.ts +33 -0
- package/dist/client/client.js +123 -0
- package/dist/client/ipc.d.ts +81 -0
- package/dist/client/ipc.js +314 -0
- package/dist/db-adapter/base-adapter.d.ts +6 -66
- package/dist/db-adapter/base-adapter.js +6 -6
- package/dist/db-adapter/index.d.ts +5 -5
- package/dist/db-adapter/index.js +2 -5
- package/dist/db-adapter/node-sqlite-adapter.d.ts +85 -15
- package/dist/db-adapter/node-sqlite-adapter.js +1985 -570
- package/dist/db-manager.d.ts +53 -97
- package/dist/db-manager.js +121 -603
- package/dist/embedding/embedder.d.ts +132 -0
- package/dist/embedding/embedder.js +299 -0
- package/dist/identity/principal-repository.d.ts +2 -2
- package/dist/index.d.ts +43 -58
- package/dist/index.js +97 -145
- package/dist/{context-compile/channel-grant.d.ts → knowledge/access.d.ts} +12 -17
- package/dist/knowledge/access.js +663 -0
- package/dist/knowledge/case-errors.d.ts +31 -0
- package/dist/knowledge/case-errors.js +40 -0
- package/dist/{cases/search-rollup.d.ts → knowledge/case-search-rollup.d.ts} +5 -4
- package/dist/{cases/search-rollup.js → knowledge/case-search-rollup.js} +6 -8
- package/dist/{cases/store.d.ts → knowledge/case-store.d.ts} +3 -6
- package/dist/{cases/store.js → knowledge/case-store.js} +7 -42
- package/dist/{cases/timeline-range.d.ts → knowledge/case-timeline-range.d.ts} +3 -3
- package/dist/{cases/timeline-range.js → knowledge/case-timeline-range.js} +47 -25
- package/dist/{cases/types.d.ts → knowledge/case-types.d.ts} +8 -2
- package/dist/{cases/types.js → knowledge/case-types.js} +1 -1
- package/dist/knowledge/commitment-revision-migration.d.ts +4 -0
- package/dist/knowledge/commitment-revision-migration.js +50 -0
- package/dist/knowledge/commitments.d.ts +192 -0
- package/dist/knowledge/commitments.js +386 -0
- package/dist/knowledge/decision-edges.d.ts +70 -0
- package/dist/knowledge/decision-edges.js +123 -0
- package/dist/{agent-graph/types.d.ts → knowledge/graph-query.d.ts} +80 -70
- package/dist/knowledge/graph-query.js +1585 -0
- package/dist/knowledge/identity.d.ts +16 -0
- package/dist/knowledge/identity.js +22 -0
- package/dist/knowledge/index.d.ts +47 -0
- package/dist/knowledge/index.js +90 -0
- package/dist/knowledge/judgment-edge.d.ts +6 -0
- package/dist/knowledge/judgment-edge.js +22 -0
- package/dist/knowledge/judgments.d.ts +118 -0
- package/dist/knowledge/judgments.js +802 -0
- package/dist/knowledge/observations.d.ts +160 -0
- package/dist/knowledge/observations.js +449 -0
- package/dist/knowledge/question-type.d.ts +34 -0
- package/dist/knowledge/question-type.js +70 -0
- package/dist/{search → knowledge}/ranker-features.js +1 -1
- package/dist/{search → knowledge}/search-quality.d.ts +0 -1
- package/dist/knowledge/search.d.ts +38 -0
- package/dist/knowledge/search.js +91 -0
- package/dist/knowledge/source-ingest.d.ts +65 -0
- package/dist/knowledge/source-ingest.js +174 -0
- package/dist/{edges/types.d.ts → knowledge/twin-edge-types.d.ts} +31 -4
- package/dist/{edges/types.js → knowledge/twin-edge-types.js} +13 -2
- package/dist/knowledge/work-dates.d.ts +49 -0
- package/dist/knowledge/work-dates.js +120 -0
- package/dist/mama-api.d.ts +114 -778
- package/dist/mama-api.js +313 -2461
- package/dist/memory/api.d.ts +264 -24
- package/dist/memory/api.js +1633 -843
- package/dist/memory/bootstrap-builder.d.ts +2 -1
- package/dist/memory/bootstrap-builder.js +5 -5
- package/dist/memory/channel-summary-state-store.d.ts +3 -2
- package/dist/memory/channel-summary-state-store.js +53 -51
- package/dist/memory/channel-summary-store.d.ts +5 -3
- package/dist/memory/channel-summary-store.js +3 -8
- package/dist/memory/dashboard-read.d.ts +46 -0
- package/dist/memory/dashboard-read.js +94 -0
- package/dist/memory/event-store.d.ts +3 -3
- package/dist/memory/event-store.js +3 -10
- package/dist/memory/finding-store.d.ts +3 -2
- package/dist/memory/finding-store.js +2 -7
- package/dist/memory/graph-read.d.ts +52 -0
- package/dist/memory/graph-read.js +130 -0
- package/dist/memory/judgment-types.d.ts +394 -0
- package/dist/memory/judgment-types.js +16 -0
- package/dist/memory/provenance-audit.d.ts +5 -2
- package/dist/memory/provenance-audit.js +8 -14
- package/dist/memory/provenance-live.d.ts +141 -0
- package/dist/memory/provenance-live.js +409 -0
- package/dist/memory/provenance-query.d.ts +5 -4
- package/dist/memory/provenance-query.js +21 -29
- package/dist/memory/provenance-resolver.d.ts +191 -0
- package/dist/memory/provenance-resolver.js +155 -0
- package/dist/memory/provenance.d.ts +1 -11
- package/dist/memory/provenance.js +2 -33
- package/dist/memory/recall-sanitize.d.ts +55 -0
- package/dist/memory/recall-sanitize.js +144 -0
- package/dist/memory/secret-filter.d.ts +32 -0
- package/dist/memory/secret-filter.js +126 -0
- package/dist/memory/truth-store.d.ts +2 -1
- package/dist/memory/truth-store.js +1 -4
- package/dist/memory/types.d.ts +74 -24
- package/dist/memory/types.js +75 -5
- package/dist/memory/write-adapters.d.ts +98 -0
- package/dist/memory/write-adapters.js +242 -0
- package/dist/operations/owner-action-effects.d.ts +53 -0
- package/dist/operations/owner-action-effects.js +96 -0
- package/dist/provenance/source-ref.d.ts +1 -1
- package/dist/provenance/source-ref.js +0 -2
- package/dist/registry/corrections.d.ts +31 -0
- package/dist/registry/corrections.js +332 -0
- package/dist/registry/record-identity.d.ts +49 -0
- package/dist/registry/record-identity.js +133 -0
- package/dist/registry/store.d.ts +101 -0
- package/dist/registry/store.js +395 -0
- package/dist/registry/types.d.ts +28 -0
- package/dist/registry/types.js +5 -0
- package/dist/relevance-scorer.js +2 -2
- package/dist/runtime/agent-event-bus.d.ts +50 -0
- package/dist/runtime/agent-event-bus.js +101 -0
- package/dist/runtime/drivers/claude-cli-wrapper.d.ts +152 -0
- package/dist/runtime/drivers/claude-cli-wrapper.js +375 -0
- package/dist/runtime/drivers/claude-effort.d.ts +4 -0
- package/dist/runtime/drivers/claude-effort.js +16 -0
- package/dist/runtime/drivers/cli-arg-redaction.d.ts +2 -0
- package/dist/runtime/drivers/cli-arg-redaction.js +18 -0
- package/dist/runtime/drivers/cli-secret-redaction.d.ts +17 -0
- package/dist/runtime/drivers/cli-secret-redaction.js +126 -0
- package/dist/runtime/drivers/codex-app-server-process.d.ts +277 -0
- package/dist/runtime/drivers/codex-app-server-process.js +2342 -0
- package/dist/runtime/drivers/codex-auxiliary-tools.d.ts +17 -0
- package/dist/runtime/drivers/codex-auxiliary-tools.js +267 -0
- package/dist/runtime/drivers/codex-home.d.ts +20 -0
- package/dist/runtime/drivers/codex-home.js +674 -0
- package/dist/runtime/drivers/codex-thread-registry.d.ts +63 -0
- package/dist/runtime/drivers/codex-thread-registry.js +433 -0
- package/dist/runtime/drivers/persistent-cli-adapter.d.ts +160 -0
- package/dist/runtime/drivers/persistent-cli-adapter.js +396 -0
- package/dist/runtime/drivers/persistent-cli-process.d.ts +425 -0
- package/dist/runtime/drivers/persistent-cli-process.js +1529 -0
- package/dist/runtime/drivers/types.d.ts +456 -0
- package/dist/runtime/drivers/types.js +110 -0
- package/dist/runtime/mailbox.d.ts +223 -0
- package/dist/runtime/mailbox.js +582 -0
- package/dist/runtime/model-run-store.d.ts +28 -0
- package/dist/{model-runs/store.js → runtime/model-run-store.js} +81 -41
- package/dist/{model-runs/types.d.ts → runtime/model-run-types.d.ts} +33 -4
- package/dist/{model-runs/types.js → runtime/model-run-types.js} +1 -1
- package/dist/runtime/native-effect-observer.d.ts +22 -0
- package/dist/runtime/native-effect-observer.js +103 -0
- package/dist/runtime/native-input-journal.d.ts +46 -0
- package/dist/runtime/native-input-journal.js +308 -0
- package/dist/runtime/native-prompt.d.ts +118 -0
- package/dist/runtime/native-prompt.js +496 -0
- package/dist/runtime/native-session.d.ts +10 -0
- package/dist/{agent-graph/index.js → runtime/native-session.js} +9 -6
- package/dist/runtime/native-tool-trace-observer.d.ts +5 -0
- package/dist/runtime/native-tool-trace-observer.js +65 -0
- package/dist/runtime/native-turn.d.ts +272 -0
- package/dist/runtime/native-turn.js +1041 -0
- package/dist/runtime/operations.d.ts +236 -0
- package/dist/runtime/operations.js +565 -0
- package/dist/runtime/post-tool-handler.d.ts +74 -0
- package/dist/runtime/post-tool-handler.js +137 -0
- package/dist/runtime/prompt-layers.d.ts +126 -0
- package/dist/runtime/prompt-layers.js +239 -0
- package/dist/runtime/runtime-process.d.ts +102 -0
- package/dist/runtime/runtime-process.js +206 -0
- package/dist/runtime/runtime.d.ts +278 -0
- package/dist/runtime/runtime.js +563 -0
- package/dist/runtime/session-pool.d.ts +176 -0
- package/dist/runtime/session-pool.js +369 -0
- package/dist/runtime/stimulus-payload.d.ts +5 -0
- package/dist/runtime/stimulus-payload.js +50 -0
- package/dist/runtime/subagent-bridge.d.ts +24 -0
- package/dist/{agent-graph/types.js → runtime/subagent-bridge.js} +1 -1
- package/dist/runtime/text-completion.d.ts +22 -0
- package/dist/{agent-situation/types.js → runtime/text-completion.js} +1 -1
- package/dist/runtime/token-estimator.d.ts +25 -0
- package/dist/runtime/token-estimator.js +75 -0
- package/dist/runtime/tool-trace-store.d.ts +22 -0
- package/dist/{model-runs → runtime}/tool-trace-store.js +46 -27
- package/dist/runtime/trace-summary.d.ts +5 -0
- package/dist/runtime/trace-summary.js +74 -0
- package/dist/runtime/turn-text.d.ts +23 -0
- package/dist/runtime/turn-text.js +41 -0
- package/dist/storage/database.d.ts +67 -0
- package/dist/storage/database.js +158 -0
- package/package.json +20 -34
- package/dist/agent-graph/alias-write.d.ts +0 -3
- package/dist/agent-graph/alias-write.js +0 -234
- package/dist/agent-graph/entity-resolve.d.ts +0 -3
- package/dist/agent-graph/entity-resolve.js +0 -271
- package/dist/agent-graph/errors.d.ts +0 -4
- package/dist/agent-graph/errors.js +0 -11
- package/dist/agent-graph/graph-query.d.ts +0 -5
- package/dist/agent-graph/graph-query.js +0 -360
- package/dist/agent-graph/index.d.ts +0 -6
- package/dist/agent-situation/builder.d.ts +0 -8
- package/dist/agent-situation/builder.js +0 -239
- package/dist/agent-situation/cache-key.d.ts +0 -4
- package/dist/agent-situation/cache-key.js +0 -64
- package/dist/agent-situation/index.d.ts +0 -7
- package/dist/agent-situation/index.js +0 -28
- package/dist/agent-situation/packet-store.d.ts +0 -27
- package/dist/agent-situation/packet-store.js +0 -228
- package/dist/agent-situation/ranking-policy.d.ts +0 -18
- package/dist/agent-situation/ranking-policy.js +0 -75
- package/dist/agent-situation/source-readers.d.ts +0 -52
- package/dist/agent-situation/source-readers.js +0 -300
- package/dist/agent-situation/types.d.ts +0 -195
- package/dist/cases/wiki-page-index.d.ts +0 -37
- package/dist/cases/wiki-page-index.js +0 -201
- package/dist/config-loader.d.ts +0 -68
- package/dist/config-loader.js +0 -239
- package/dist/connectors/event-index.d.ts +0 -24
- package/dist/connectors/event-index.js +0 -245
- package/dist/connectors/raw-query.d.ts +0 -40
- package/dist/connectors/raw-query.js +0 -412
- package/dist/connectors/types.d.ts +0 -117
- package/dist/connectors/types.js +0 -3
- package/dist/context-compile/boundary-defaults.d.ts +0 -12
- package/dist/context-compile/boundary-defaults.js +0 -105
- package/dist/context-compile/channel-grant.js +0 -66
- package/dist/context-compile/compiler-policy.d.ts +0 -39
- package/dist/context-compile/compiler-policy.js +0 -147
- package/dist/context-compile/compiler.d.ts +0 -18
- package/dist/context-compile/compiler.js +0 -404
- package/dist/context-compile/index.d.ts +0 -10
- package/dist/context-compile/index.js +0 -26
- package/dist/context-compile/packet-store.d.ts +0 -8
- package/dist/context-compile/packet-store.js +0 -239
- package/dist/context-compile/ref.d.ts +0 -7
- package/dist/context-compile/ref.js +0 -102
- package/dist/context-compile/source-readers.d.ts +0 -70
- package/dist/context-compile/source-readers.js +0 -840
- package/dist/context-compile/types.d.ts +0 -155
- package/dist/context-compile/types.js +0 -11
- package/dist/context-compile/visibility.d.ts +0 -21
- package/dist/context-compile/visibility.js +0 -224
- package/dist/decision-tracker.d.ts +0 -206
- package/dist/decision-tracker.js +0 -481
- package/dist/edges/ref-validation.d.ts +0 -7
- package/dist/edges/ref-validation.js +0 -321
- package/dist/edges/store.d.ts +0 -8
- package/dist/edges/store.js +0 -129
- package/dist/embedding-client.d.ts +0 -28
- package/dist/embedding-client.js +0 -53
- package/dist/embedding-server/index.d.ts +0 -67
- package/dist/embedding-server/index.js +0 -405
- package/dist/embedding-server/mobile/auth.d.ts +0 -44
- package/dist/embedding-server/mobile/auth.js +0 -150
- package/dist/embedding-server/mobile/daemon.d.ts +0 -129
- package/dist/embedding-server/mobile/daemon.js +0 -313
- package/dist/embedding-server/mobile/output-parser.d.ts +0 -115
- package/dist/embedding-server/mobile/output-parser.js +0 -241
- package/dist/embedding-server/mobile/session-api.d.ts +0 -57
- package/dist/embedding-server/mobile/session-api.js +0 -261
- package/dist/embedding-server/mobile/session-manager.d.ts +0 -138
- package/dist/embedding-server/mobile/session-manager.js +0 -378
- package/dist/embedding-server/mobile/websocket-handler.d.ts +0 -137
- package/dist/embedding-server/mobile/websocket-handler.js +0 -486
- package/dist/embeddings.d.ts +0 -68
- package/dist/embeddings.js +0 -244
- package/dist/entities/audit-metrics.d.ts +0 -68
- package/dist/entities/audit-metrics.js +0 -124
- package/dist/entities/candidate-generator.d.ts +0 -8
- package/dist/entities/candidate-generator.js +0 -24
- package/dist/entities/entity-linked-decision-counts.d.ts +0 -2
- package/dist/entities/entity-linked-decision-counts.js +0 -29
- package/dist/entities/entity-list.d.ts +0 -22
- package/dist/entities/entity-list.js +0 -3
- package/dist/entities/entity-orphan-list.d.ts +0 -17
- package/dist/entities/entity-orphan-list.js +0 -3
- package/dist/entities/entity-search.d.ts +0 -20
- package/dist/entities/entity-search.js +0 -3
- package/dist/entities/errors.d.ts +0 -55
- package/dist/entities/errors.js +0 -100
- package/dist/entities/lineage-store.d.ts +0 -28
- package/dist/entities/lineage-store.js +0 -204
- package/dist/entities/normalization.d.ts +0 -14
- package/dist/entities/normalization.js +0 -63
- package/dist/entities/policy-store.d.ts +0 -8
- package/dist/entities/policy-store.js +0 -75
- package/dist/entities/policy-types.d.ts +0 -72
- package/dist/entities/policy-types.js +0 -59
- package/dist/entities/projection.d.ts +0 -8
- package/dist/entities/projection.js +0 -82
- package/dist/entities/provenance-query.d.ts +0 -32
- package/dist/entities/provenance-query.js +0 -3
- package/dist/entities/read-identity.d.ts +0 -31
- package/dist/entities/read-identity.js +0 -264
- package/dist/entities/recall-bridge.d.ts +0 -5
- package/dist/entities/recall-bridge.js +0 -171
- package/dist/entities/resolution-engine.d.ts +0 -8
- package/dist/entities/resolution-engine.js +0 -3
- package/dist/entities/rollback-preview.d.ts +0 -35
- package/dist/entities/rollback-preview.js +0 -3
- package/dist/entities/source-locator.d.ts +0 -3
- package/dist/entities/source-locator.js +0 -32
- package/dist/entities/store.d.ts +0 -69
- package/dist/entities/store.js +0 -544
- package/dist/entities/types.d.ts +0 -166
- package/dist/entities/types.js +0 -30
- package/dist/memory/extraction-prompt.d.ts +0 -4
- package/dist/memory/extraction-prompt.js +0 -89
- package/dist/memory/scope-backfill.d.ts +0 -14
- package/dist/memory/scope-backfill.js +0 -3
- package/dist/memory/scope-store.d.ts +0 -4
- package/dist/memory/scope-store.js +0 -9
- package/dist/memory-store.d.ts +0 -87
- package/dist/memory-store.js +0 -78
- package/dist/model-runs/store.d.ts +0 -13
- package/dist/model-runs/tool-trace-store.d.ts +0 -8
- package/dist/notification-manager.d.ts +0 -7
- package/dist/notification-manager.js +0 -12
- package/dist/outcome-tracker.d.ts +0 -151
- package/dist/outcome-tracker.js +0 -271
- package/dist/query-intent.d.ts +0 -23
- package/dist/query-intent.js +0 -114
- package/dist/runtime/trusted-provenance.d.ts +0 -3
- package/dist/runtime/trusted-provenance.js +0 -6
- package/dist/search/question-type.d.ts +0 -5
- package/dist/search/question-type.js +0 -41
- package/dist/test-utils.d.ts +0 -80
- package/dist/test-utils.js +0 -218
- package/dist/tier-validator.d.ts +0 -43
- package/dist/tier-validator.js +0 -159
- package/dist/time-formatter.d.ts +0 -25
- package/dist/time-formatter.js +0 -93
- /package/dist/{search → knowledge}/feedback-store.d.ts +0 -0
- /package/dist/{search → knowledge}/feedback-store.js +0 -0
- /package/dist/{search → knowledge}/ranker-features.d.ts +0 -0
- /package/dist/{search → knowledge}/ranker-rescore.d.ts +0 -0
- /package/dist/{search → knowledge}/ranker-rescore.js +0 -0
- /package/dist/{search → knowledge}/ranker-trainer.d.ts +0 -0
- /package/dist/{search → knowledge}/ranker-trainer.js +0 -0
- /package/dist/{search → knowledge}/search-quality.js +0 -0
|
@@ -8,17 +8,185 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
8
8
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
9
9
|
};
|
|
10
10
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
-
exports.NodeSQLiteAdapter = void 0;
|
|
11
|
+
exports.NodeSQLiteAdapter = exports.CORE_MIGRATION_SOURCE = void 0;
|
|
12
12
|
const path_1 = __importDefault(require("path"));
|
|
13
|
-
const os_1 = __importDefault(require("os"));
|
|
14
13
|
const fs_1 = __importDefault(require("fs"));
|
|
15
|
-
const base_adapter_js_1 = require("./base-adapter.js");
|
|
16
14
|
const node_sqlite_statement_js_1 = require("./node-sqlite-statement.js");
|
|
17
15
|
const debug_logger_js_1 = require("../debug-logger.js");
|
|
18
|
-
const
|
|
19
|
-
const
|
|
20
|
-
const DEFAULT_DB_PATH = path_1.default.join(os_1.default.homedir(), '.claude', 'mama-memory.db');
|
|
16
|
+
const embedder_js_1 = require("../embedding/embedder.js");
|
|
17
|
+
const commitment_revision_migration_js_1 = require("../knowledge/commitment-revision-migration.js");
|
|
21
18
|
const SQLITE_IDENTIFIER_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
19
|
+
// Migration 071 origin invariant, kept in ONE place so the DDL the rebuild
|
|
20
|
+
// writes and the shape verification that admits a table cannot drift. A row is
|
|
21
|
+
// valid only with a model-only origin or a complete operation/actor pair.
|
|
22
|
+
const TOOL_TRACES_ORIGIN_CHECK = `CHECK (
|
|
23
|
+
(model_run_id IS NOT NULL AND operation_id IS NULL AND actor_principal_id IS NULL)
|
|
24
|
+
OR (
|
|
25
|
+
operation_id IS NOT NULL AND length(trim(operation_id)) > 0
|
|
26
|
+
AND actor_principal_id IS NOT NULL AND length(trim(actor_principal_id)) > 0
|
|
27
|
+
)
|
|
28
|
+
)`;
|
|
29
|
+
const TOOL_TRACES_MODEL_FK = 'FOREIGN KEY (model_run_id) REFERENCES model_runs(model_run_id)';
|
|
30
|
+
const WORKFLOW_KIND_MIGRATION_COLUMNS = [
|
|
31
|
+
'id',
|
|
32
|
+
'topic',
|
|
33
|
+
'decision',
|
|
34
|
+
'reasoning',
|
|
35
|
+
'outcome',
|
|
36
|
+
'failure_reason',
|
|
37
|
+
'limitation',
|
|
38
|
+
'user_involvement',
|
|
39
|
+
'session_id',
|
|
40
|
+
'supersedes',
|
|
41
|
+
'superseded_by',
|
|
42
|
+
'refined_from',
|
|
43
|
+
'confidence',
|
|
44
|
+
'created_at',
|
|
45
|
+
'updated_at',
|
|
46
|
+
'needs_validation',
|
|
47
|
+
'validation_attempts',
|
|
48
|
+
'last_validated_at',
|
|
49
|
+
'usage_count',
|
|
50
|
+
'trust_context',
|
|
51
|
+
'usage_success',
|
|
52
|
+
'usage_failure',
|
|
53
|
+
'time_saved',
|
|
54
|
+
'evidence',
|
|
55
|
+
'alternatives',
|
|
56
|
+
'risks',
|
|
57
|
+
'event_date',
|
|
58
|
+
'kind',
|
|
59
|
+
'status',
|
|
60
|
+
'summary',
|
|
61
|
+
'is_static',
|
|
62
|
+
'event_datetime',
|
|
63
|
+
'agent_id',
|
|
64
|
+
'model_run_id',
|
|
65
|
+
'envelope_hash',
|
|
66
|
+
'gateway_call_id',
|
|
67
|
+
'source_refs_json',
|
|
68
|
+
'provenance_json',
|
|
69
|
+
'item_id',
|
|
70
|
+
'record_kind',
|
|
71
|
+
'payload_json',
|
|
72
|
+
'applies_from',
|
|
73
|
+
'applies_until',
|
|
74
|
+
'duration_days',
|
|
75
|
+
];
|
|
76
|
+
function normalizeSqlText(sql) {
|
|
77
|
+
return sql.replace(/\s+/g, '').toLowerCase();
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Split a CREATE TABLE body (the text between the outermost parentheses) into
|
|
81
|
+
* its top-level column and table-constraint clauses. Respects nested
|
|
82
|
+
* parentheses (CHECK expressions), and single/double/backtick/bracket quoted
|
|
83
|
+
* identifiers and string literals. This is a targeted parser for reconstructing
|
|
84
|
+
* ONE known table, not a general SQL engine.
|
|
85
|
+
*/
|
|
86
|
+
function splitCreateTableClauses(createSql) {
|
|
87
|
+
const open = createSql.indexOf('(');
|
|
88
|
+
const close = createSql.lastIndexOf(')');
|
|
89
|
+
if (open < 0 || close <= open) {
|
|
90
|
+
return [];
|
|
91
|
+
}
|
|
92
|
+
const body = createSql.slice(open + 1, close);
|
|
93
|
+
const clauses = [];
|
|
94
|
+
let depth = 0;
|
|
95
|
+
let current = '';
|
|
96
|
+
let quote = null;
|
|
97
|
+
for (let i = 0; i < body.length; i++) {
|
|
98
|
+
const ch = body[i];
|
|
99
|
+
if (quote) {
|
|
100
|
+
current += ch;
|
|
101
|
+
if (ch === quote) {
|
|
102
|
+
// Doubled quote is an escaped quote, not a close (SQLite identifier/string rule).
|
|
103
|
+
if (quote !== ']' && body[i + 1] === quote) {
|
|
104
|
+
current += body[++i];
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
quote = null;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (ch === "'" || ch === '"' || ch === '`') {
|
|
113
|
+
quote = ch;
|
|
114
|
+
current += ch;
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
if (ch === '[') {
|
|
118
|
+
quote = ']';
|
|
119
|
+
current += ch;
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
if (ch === '(') {
|
|
123
|
+
depth++;
|
|
124
|
+
current += ch;
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
if (ch === ')') {
|
|
128
|
+
depth--;
|
|
129
|
+
current += ch;
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
if (ch === ',' && depth === 0) {
|
|
133
|
+
if (current.trim()) {
|
|
134
|
+
clauses.push(current.trim());
|
|
135
|
+
}
|
|
136
|
+
current = '';
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
current += ch;
|
|
140
|
+
}
|
|
141
|
+
if (current.trim()) {
|
|
142
|
+
clauses.push(current.trim());
|
|
143
|
+
}
|
|
144
|
+
return clauses;
|
|
145
|
+
}
|
|
146
|
+
const TABLE_CONSTRAINT_KEYWORDS = new Set(['constraint', 'primary', 'unique', 'check', 'foreign']);
|
|
147
|
+
function clauseIsTableConstraint(clause) {
|
|
148
|
+
const firstToken = clause.trimStart().split(/[\s(]/, 1)[0].toLowerCase();
|
|
149
|
+
return TABLE_CONSTRAINT_KEYWORDS.has(firstToken);
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Quote a decoded identifier for safe emission in SQL. Any column/table name we
|
|
153
|
+
* recovered from a CREATE TABLE clause or sqlite_master (possibly containing a
|
|
154
|
+
* space, an embedded double quote, or a reserved word) is re-quoted with SQLite
|
|
155
|
+
* double-quote rules: wrap in double quotes and double any embedded double quote.
|
|
156
|
+
* clauseColumnName strips quoting, so the INSERT/SELECT column list MUST re-quote
|
|
157
|
+
* or a name like `runtime note` becomes two bare tokens and breaks the migration.
|
|
158
|
+
*/
|
|
159
|
+
function quoteSqlIdentifier(name) {
|
|
160
|
+
return `"${name.replace(/"/g, '""')}"`;
|
|
161
|
+
}
|
|
162
|
+
/** Column name of a column-definition clause, stripping quotes/brackets. */
|
|
163
|
+
function clauseColumnName(clause) {
|
|
164
|
+
const trimmed = clause.trimStart();
|
|
165
|
+
const first = trimmed[0];
|
|
166
|
+
if (first === '"' || first === '`' || first === "'") {
|
|
167
|
+
let name = '';
|
|
168
|
+
for (let i = 1; i < trimmed.length; i++) {
|
|
169
|
+
if (trimmed[i] === first) {
|
|
170
|
+
if (trimmed[i + 1] === first) {
|
|
171
|
+
name += first;
|
|
172
|
+
i++;
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
break;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
name += trimmed[i];
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return name;
|
|
183
|
+
}
|
|
184
|
+
if (first === '[') {
|
|
185
|
+
const end = trimmed.indexOf(']');
|
|
186
|
+
return end > 0 ? trimmed.slice(1, end) : trimmed.slice(1);
|
|
187
|
+
}
|
|
188
|
+
return trimmed.split(/[\s(]/, 1)[0];
|
|
189
|
+
}
|
|
22
190
|
// Prefer better-sqlite3 (includes FTS5) over node:sqlite (lacks FTS5)
|
|
23
191
|
let BetterSQLite3 = null;
|
|
24
192
|
let DatabaseSync = null;
|
|
@@ -82,12 +250,15 @@ class NodeSQLiteConnection {
|
|
|
82
250
|
return this.connected;
|
|
83
251
|
}
|
|
84
252
|
}
|
|
85
|
-
|
|
253
|
+
/** The core is one source among others; it is only first because others build on it. */
|
|
254
|
+
exports.CORE_MIGRATION_SOURCE = 'core';
|
|
255
|
+
class NodeSQLiteAdapter {
|
|
256
|
+
transactionDepth = 0;
|
|
86
257
|
config;
|
|
87
258
|
db = null;
|
|
88
|
-
_vectorSearchEnabled = true;
|
|
89
259
|
vectorCache = new Map();
|
|
90
260
|
topicCache = new Map();
|
|
261
|
+
kindCache = new Map();
|
|
91
262
|
// Effective status (status, falling back to outcome) per decision rowid. Used as a
|
|
92
263
|
// search-time optimization only - recallMemory's post-filter stays the authority
|
|
93
264
|
// (this cache can lag a status UPDATE until the next reloadVectorCache).
|
|
@@ -95,23 +266,26 @@ class NodeSQLiteAdapter extends base_adapter_js_1.DatabaseAdapter {
|
|
|
95
266
|
decisionsHasStatusColumns = false;
|
|
96
267
|
decisionsColumnInfoChecked = false;
|
|
97
268
|
constructor(config = {}) {
|
|
98
|
-
super();
|
|
99
269
|
this.config = config;
|
|
100
270
|
}
|
|
101
|
-
get vectorSearchEnabled() {
|
|
102
|
-
return this._vectorSearchEnabled;
|
|
103
|
-
}
|
|
104
271
|
getDbPath() {
|
|
105
272
|
const envPath = process.env.MAMA_DB_PATH || process.env.MAMA_DATABASE_PATH;
|
|
106
273
|
const configPath = this.config.dbPath;
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
274
|
+
const targetPath = configPath || envPath;
|
|
275
|
+
// The adapter used to expand `~` and `${HOME}` here, which made a shared library
|
|
276
|
+
// the thing that decides what home means. Whoever set the variable knows; this
|
|
277
|
+
// says so rather than guessing on their behalf.
|
|
278
|
+
if (targetPath &&
|
|
279
|
+
(targetPath === '~' || targetPath.startsWith('~/') || targetPath.includes('${HOME}'))) {
|
|
280
|
+
throw new Error(`Database path is not resolved (${targetPath}). ` +
|
|
281
|
+
'Expand it where the home directory is known and pass the result.');
|
|
282
|
+
}
|
|
283
|
+
if (!targetPath) {
|
|
284
|
+
// There used to be a default here: ~/.claude/mama-memory.db. A shared core that
|
|
285
|
+
// knows where one product keeps its database is not shared, and a caller who
|
|
286
|
+
// forgot to say got a database it never named -- which is worse than an error,
|
|
287
|
+
// because it works.
|
|
288
|
+
throw new Error('No database path. Pass dbPath, or set MAMA_DB_PATH before opening the database.');
|
|
115
289
|
}
|
|
116
290
|
return targetPath;
|
|
117
291
|
}
|
|
@@ -155,19 +329,25 @@ class NodeSQLiteAdapter extends base_adapter_js_1.DatabaseAdapter {
|
|
|
155
329
|
if (!this.isConnected()) {
|
|
156
330
|
throw new Error('Database not connected');
|
|
157
331
|
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
}
|
|
332
|
+
const decisionCols = this.refreshDecisionColumnInfo();
|
|
333
|
+
const kindSelect = decisionCols.has('kind') ? 'kind' : 'NULL AS kind';
|
|
161
334
|
const cacheSelect = this.decisionsHasStatusColumns
|
|
162
|
-
?
|
|
163
|
-
:
|
|
335
|
+
? `SELECT topic, ${kindSelect}, status, outcome FROM decisions WHERE rowid = ?`
|
|
336
|
+
: `SELECT topic, ${kindSelect}, NULL AS status, NULL AS outcome FROM decisions WHERE rowid = ?`;
|
|
164
337
|
const row = this.prepare(cacheSelect).get(rowid);
|
|
165
338
|
if (!row) {
|
|
166
339
|
this.statusCache.delete(rowid);
|
|
167
340
|
this.topicCache.delete(rowid);
|
|
341
|
+
this.kindCache.delete(rowid);
|
|
168
342
|
return;
|
|
169
343
|
}
|
|
170
344
|
this.topicCache.set(rowid, row.topic);
|
|
345
|
+
if (row.kind) {
|
|
346
|
+
this.kindCache.set(rowid, row.kind);
|
|
347
|
+
}
|
|
348
|
+
else {
|
|
349
|
+
this.kindCache.delete(rowid);
|
|
350
|
+
}
|
|
171
351
|
const effectiveStatus = row.status || row.outcome;
|
|
172
352
|
if (effectiveStatus) {
|
|
173
353
|
this.statusCache.set(rowid, effectiveStatus);
|
|
@@ -195,6 +375,7 @@ class NodeSQLiteAdapter extends base_adapter_js_1.DatabaseAdapter {
|
|
|
195
375
|
if (tableCheck.length === 0) {
|
|
196
376
|
this.vectorCache.clear();
|
|
197
377
|
this.topicCache.clear();
|
|
378
|
+
this.kindCache.clear();
|
|
198
379
|
this.statusCache.clear();
|
|
199
380
|
return;
|
|
200
381
|
}
|
|
@@ -215,15 +396,19 @@ class NodeSQLiteAdapter extends base_adapter_js_1.DatabaseAdapter {
|
|
|
215
396
|
// A missing column just leaves statusCache empty; the api-layer post-filter
|
|
216
397
|
// remains the authority.
|
|
217
398
|
this.topicCache.clear();
|
|
399
|
+
this.kindCache.clear();
|
|
218
400
|
this.statusCache.clear();
|
|
219
401
|
const decisionCols = this.refreshDecisionColumnInfo();
|
|
402
|
+
const kindSelect = decisionCols.has('kind') ? 'kind' : 'NULL AS kind';
|
|
220
403
|
const statusSelect = decisionCols.has('status') ? 'status' : 'NULL AS status';
|
|
221
404
|
const outcomeSelect = decisionCols.has('outcome') ? 'outcome' : 'NULL AS outcome';
|
|
222
405
|
const topicRows = this.db
|
|
223
|
-
.prepare(`SELECT rowid, topic, ${statusSelect}, ${outcomeSelect} FROM decisions`)
|
|
406
|
+
.prepare(`SELECT rowid, topic, ${kindSelect}, ${statusSelect}, ${outcomeSelect} FROM decisions`)
|
|
224
407
|
.all();
|
|
225
408
|
for (const row of topicRows) {
|
|
226
409
|
this.topicCache.set(row.rowid, row.topic);
|
|
410
|
+
if (row.kind)
|
|
411
|
+
this.kindCache.set(row.rowid, row.kind);
|
|
227
412
|
const effectiveStatus = row.status || row.outcome;
|
|
228
413
|
if (effectiveStatus) {
|
|
229
414
|
this.statusCache.set(row.rowid, effectiveStatus);
|
|
@@ -233,7 +418,7 @@ class NodeSQLiteAdapter extends base_adapter_js_1.DatabaseAdapter {
|
|
|
233
418
|
const elapsed = Date.now() - start;
|
|
234
419
|
(0, debug_logger_js_1.info)(`[node-sqlite-adapter] Vector cache loaded: ${count} embeddings in ${elapsed}ms`);
|
|
235
420
|
if (count > CACHE_WARN_THRESHOLD) {
|
|
236
|
-
(0, debug_logger_js_1.warn)(`[node-sqlite-adapter] Vector cache holds ${count} embeddings
|
|
421
|
+
(0, debug_logger_js_1.warn)(`[node-sqlite-adapter] Vector cache holds ${count} embeddings -- consider LRU eviction or on-demand loading for large datasets`);
|
|
237
422
|
}
|
|
238
423
|
}
|
|
239
424
|
disconnect() {
|
|
@@ -259,30 +444,68 @@ class NodeSQLiteAdapter extends base_adapter_js_1.DatabaseAdapter {
|
|
|
259
444
|
this.db.exec(sql);
|
|
260
445
|
}
|
|
261
446
|
transaction(fn) {
|
|
447
|
+
return this.runTransaction(fn, 'deferred');
|
|
448
|
+
}
|
|
449
|
+
transactionImmediate(fn) {
|
|
450
|
+
return this.runTransaction(fn, 'immediate');
|
|
451
|
+
}
|
|
452
|
+
runTransaction(fn, mode) {
|
|
262
453
|
if (!this.isConnected()) {
|
|
263
454
|
throw new Error('Database not connected');
|
|
264
455
|
}
|
|
265
|
-
this.
|
|
456
|
+
const depth = this.transactionDepth;
|
|
457
|
+
const vectorSnapshot = new Map(this.vectorCache);
|
|
458
|
+
const topicSnapshot = new Map(this.topicCache);
|
|
459
|
+
const kindSnapshot = new Map(this.kindCache);
|
|
460
|
+
const statusSnapshot = new Map(this.statusCache);
|
|
461
|
+
const savepoint = `mama_nested_${depth}`;
|
|
462
|
+
this.exec(depth === 0
|
|
463
|
+
? mode === 'immediate'
|
|
464
|
+
? 'BEGIN IMMEDIATE'
|
|
465
|
+
: 'BEGIN TRANSACTION'
|
|
466
|
+
: `SAVEPOINT ${savepoint}`);
|
|
467
|
+
this.transactionDepth += 1;
|
|
266
468
|
try {
|
|
267
469
|
const result = fn();
|
|
268
470
|
if (((typeof result === 'object' && result !== null) || typeof result === 'function') &&
|
|
269
471
|
typeof result.then === 'function') {
|
|
270
472
|
throw new Error('DatabaseAdapter.transaction() callbacks must be synchronous');
|
|
271
473
|
}
|
|
272
|
-
this.exec('COMMIT');
|
|
474
|
+
this.exec(depth === 0 ? 'COMMIT' : `RELEASE SAVEPOINT ${savepoint}`);
|
|
475
|
+
this.transactionDepth = depth;
|
|
273
476
|
return result;
|
|
274
477
|
}
|
|
275
478
|
catch (error) {
|
|
479
|
+
this.vectorCache = vectorSnapshot;
|
|
480
|
+
this.topicCache = topicSnapshot;
|
|
481
|
+
this.kindCache = kindSnapshot;
|
|
482
|
+
this.statusCache = statusSnapshot;
|
|
483
|
+
this.transactionDepth = depth;
|
|
484
|
+
let cleanupError;
|
|
276
485
|
try {
|
|
277
|
-
this.exec('ROLLBACK');
|
|
486
|
+
this.exec(depth === 0 ? 'ROLLBACK' : `ROLLBACK TO SAVEPOINT ${savepoint}`);
|
|
487
|
+
if (depth > 0) {
|
|
488
|
+
this.exec(`RELEASE SAVEPOINT ${savepoint}`);
|
|
489
|
+
}
|
|
278
490
|
}
|
|
279
|
-
catch {
|
|
280
|
-
|
|
491
|
+
catch (rollbackError) {
|
|
492
|
+
cleanupError = rollbackError;
|
|
493
|
+
}
|
|
494
|
+
if (cleanupError !== undefined) {
|
|
495
|
+
const failures = [error, cleanupError];
|
|
496
|
+
try {
|
|
497
|
+
this.disconnect();
|
|
498
|
+
}
|
|
499
|
+
catch (disconnectError) {
|
|
500
|
+
failures.push(disconnectError);
|
|
501
|
+
this.db = null;
|
|
502
|
+
}
|
|
503
|
+
throw new AggregateError(failures, 'Transaction settlement and rollback cleanup both failed; adapter disconnected');
|
|
281
504
|
}
|
|
282
505
|
throw error;
|
|
283
506
|
}
|
|
284
507
|
}
|
|
285
|
-
vectorSearch(embedding, limit = 5, topicPrefix, excludeStatuses) {
|
|
508
|
+
vectorSearch(embedding, limit = 5, topicPrefix, excludeStatuses, kind) {
|
|
286
509
|
if (!this.isConnected()) {
|
|
287
510
|
throw new Error('Database not connected');
|
|
288
511
|
}
|
|
@@ -300,6 +523,13 @@ class NodeSQLiteAdapter extends base_adapter_js_1.DatabaseAdapter {
|
|
|
300
523
|
if (!topic || !topic.startsWith(topicPrefix))
|
|
301
524
|
continue;
|
|
302
525
|
}
|
|
526
|
+
// Pre-filter by memory kind so unrelated records cannot consume the top-K
|
|
527
|
+
// candidate slots before the recall layer applies its requested kind.
|
|
528
|
+
if (kind !== undefined &&
|
|
529
|
+
(Array.isArray(kind)
|
|
530
|
+
? !kind.some((value) => value === this.kindCache.get(rowid))
|
|
531
|
+
: this.kindCache.get(rowid) !== kind))
|
|
532
|
+
continue;
|
|
303
533
|
// Pre-filter by effective status so superseded history does not occupy
|
|
304
534
|
// top-K slots (the api-layer post-filter remains the authority)
|
|
305
535
|
if (excluded) {
|
|
@@ -307,7 +537,7 @@ class NodeSQLiteAdapter extends base_adapter_js_1.DatabaseAdapter {
|
|
|
307
537
|
if (status && excluded.has(status))
|
|
308
538
|
continue;
|
|
309
539
|
}
|
|
310
|
-
const similarity = (0,
|
|
540
|
+
const similarity = (0, embedder_js_1.cosineSimilarity)(candidate, queryVector);
|
|
311
541
|
if (bestMatches.length < effectiveLimit) {
|
|
312
542
|
bestMatches.push({ rowid, similarity, distance: 1 - similarity });
|
|
313
543
|
if (bestMatches.length === effectiveLimit) {
|
|
@@ -349,24 +579,93 @@ class NodeSQLiteAdapter extends base_adapter_js_1.DatabaseAdapter {
|
|
|
349
579
|
const result = this.prepare('SELECT last_insert_rowid() as rowid').get();
|
|
350
580
|
return result.rowid;
|
|
351
581
|
}
|
|
352
|
-
|
|
582
|
+
/**
|
|
583
|
+
* Run migrations from one source, or from several in order.
|
|
584
|
+
*
|
|
585
|
+
* A migration belongs to whoever wrote it, and its identity is (source,
|
|
586
|
+
* version). One author could key on the number alone; a second cannot, because
|
|
587
|
+
* the next number is already spoken for. Without this there is nowhere for a
|
|
588
|
+
* package's own table to live except inside the core.
|
|
589
|
+
*
|
|
590
|
+
* The core's source runs first and the rest follow, so a later source can
|
|
591
|
+
* reference what the core declared. A plain directory means the core's own.
|
|
592
|
+
*/
|
|
593
|
+
/**
|
|
594
|
+
* Record that one source's migration ran. Before (source, version) there was
|
|
595
|
+
* one author, so the column did not exist; a database that has not reached 085
|
|
596
|
+
* yet still has the old shape and is still the core's.
|
|
597
|
+
*/
|
|
598
|
+
stampMigration(sourceName, version) {
|
|
599
|
+
if (this.tableColumns('schema_version').has('source')) {
|
|
600
|
+
this.prepare('INSERT OR IGNORE INTO schema_version (source, version) VALUES (?, ?)').run(sourceName, version);
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
this.prepare('INSERT OR IGNORE INTO schema_version (version) VALUES (?)').run(version);
|
|
604
|
+
}
|
|
605
|
+
runMigrations(sources) {
|
|
606
|
+
const list = typeof sources === 'string' ? [{ name: exports.CORE_MIGRATION_SOURCE, dir: sources }] : sources;
|
|
607
|
+
for (const source of list) {
|
|
608
|
+
this.runMigrationSource(source.dir, source.name);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
runMigrationSource(migrationsDir, sourceName) {
|
|
353
612
|
if (!this.isConnected()) {
|
|
354
613
|
throw new Error('Database not connected');
|
|
355
614
|
}
|
|
615
|
+
const isCore = sourceName === exports.CORE_MIGRATION_SOURCE;
|
|
356
616
|
const tables = this.prepare(`
|
|
357
617
|
SELECT name FROM sqlite_master
|
|
358
618
|
WHERE type='table' AND name='schema_version'
|
|
359
619
|
`).all();
|
|
360
620
|
let currentVersion = 0;
|
|
361
621
|
if (tables.length > 0) {
|
|
362
|
-
|
|
622
|
+
// Progress is per source. A shared MAX() would let one author's numbering
|
|
623
|
+
// declare another author's migrations already applied.
|
|
624
|
+
const hasSource = this.tableColumns('schema_version').has('source');
|
|
625
|
+
const version = (hasSource
|
|
626
|
+
? this.prepare('SELECT MAX(version) as version FROM schema_version WHERE source = ?').get(sourceName)
|
|
627
|
+
: this.prepare('SELECT MAX(version) as version FROM schema_version').get());
|
|
363
628
|
currentVersion = version?.version || 0;
|
|
364
629
|
}
|
|
365
|
-
(0, debug_logger_js_1.info)(`[node-sqlite-adapter] Current schema version: ${currentVersion}`);
|
|
630
|
+
(0, debug_logger_js_1.info)(`[node-sqlite-adapter] Current schema version (${sourceName}): ${currentVersion}`);
|
|
366
631
|
const migrationFiles = fs_1.default
|
|
367
632
|
.readdirSync(migrationsDir)
|
|
368
633
|
.filter((file) => file.endsWith('.sql'))
|
|
369
634
|
.sort();
|
|
635
|
+
if (isCore &&
|
|
636
|
+
currentVersion >= 72 &&
|
|
637
|
+
fs_1.default.existsSync(path_1.default.join(migrationsDir, '072-observation-versions.sql')) &&
|
|
638
|
+
this.needsObservationVersionsRepair072()) {
|
|
639
|
+
this.recoverObservationVersionsMigration072();
|
|
640
|
+
}
|
|
641
|
+
if (isCore &&
|
|
642
|
+
currentVersion >= 73 &&
|
|
643
|
+
this.tableExists('registry_nodes') &&
|
|
644
|
+
fs_1.default.existsSync(path_1.default.join(migrationsDir, '073-registry-corrections.sql')) &&
|
|
645
|
+
this.needsRegistryCorrectionsRepair073()) {
|
|
646
|
+
this.recoverRegistryCorrectionsMigration073(migrationsDir);
|
|
647
|
+
}
|
|
648
|
+
if (isCore &&
|
|
649
|
+
currentVersion >= 74 &&
|
|
650
|
+
this.tableExists('twin_edges') &&
|
|
651
|
+
fs_1.default.existsSync(path_1.default.join(migrationsDir, '074-work-graph-ref-kinds.sql')) &&
|
|
652
|
+
this.needsWorkGraphRefsRepair074()) {
|
|
653
|
+
this.recoverWorkGraphRefsMigration074();
|
|
654
|
+
}
|
|
655
|
+
if (isCore &&
|
|
656
|
+
currentVersion >= 77 &&
|
|
657
|
+
this.tableExists('decisions') &&
|
|
658
|
+
fs_1.default.existsSync(path_1.default.join(migrationsDir, '077-legacy-record-kind.sql')) &&
|
|
659
|
+
!this.legacyRecordKindShape077()) {
|
|
660
|
+
this.recoverLegacyRecordKindMigration077();
|
|
661
|
+
}
|
|
662
|
+
if (isCore &&
|
|
663
|
+
currentVersion >= 79 &&
|
|
664
|
+
this.tableExists('twin_edges') &&
|
|
665
|
+
fs_1.default.existsSync(path_1.default.join(migrationsDir, '079-twin-edge-relations.sql')) &&
|
|
666
|
+
this.needsTwinEdgeRelationsRepair079()) {
|
|
667
|
+
this.recoverTwinEdgeRelationsMigration079();
|
|
668
|
+
}
|
|
370
669
|
for (const file of migrationFiles) {
|
|
371
670
|
const versionMatch = file.match(/^(\d+)-/);
|
|
372
671
|
if (!versionMatch) {
|
|
@@ -376,9 +675,37 @@ class NodeSQLiteAdapter extends base_adapter_js_1.DatabaseAdapter {
|
|
|
376
675
|
if (version <= currentVersion) {
|
|
377
676
|
continue;
|
|
378
677
|
}
|
|
678
|
+
if (isCore && version === 99) {
|
|
679
|
+
try {
|
|
680
|
+
this.exec('BEGIN TRANSACTION');
|
|
681
|
+
(0, commitment_revision_migration_js_1.backfillCommitmentRevisionGraph)(this);
|
|
682
|
+
this.exec(fs_1.default.readFileSync(path_1.default.join(migrationsDir, file), 'utf8'));
|
|
683
|
+
this.stampMigration(sourceName, version);
|
|
684
|
+
this.exec('COMMIT');
|
|
685
|
+
(0, debug_logger_js_1.info)(`[node-sqlite-adapter] Migration ${file} applied successfully`);
|
|
686
|
+
}
|
|
687
|
+
catch (err) {
|
|
688
|
+
this.exec('ROLLBACK');
|
|
689
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
690
|
+
throw new Error(`Migration ${file} failed: ${message}`);
|
|
691
|
+
}
|
|
692
|
+
continue;
|
|
693
|
+
}
|
|
694
|
+
if (isCore && version === 98) {
|
|
695
|
+
const decisionColumns = this.tableColumns('decisions');
|
|
696
|
+
if (!this.tableExists('decisions_fts') ||
|
|
697
|
+
!WORKFLOW_KIND_MIGRATION_COLUMNS.every((column) => decisionColumns.has(column))) {
|
|
698
|
+
(0, debug_logger_js_1.warn)(`[node-sqlite-adapter] Migration ${file} deferred: the full decisions projection is not present`);
|
|
699
|
+
continue;
|
|
700
|
+
}
|
|
701
|
+
this.rebuildWorkflowMemoryKind098(migrationsDir);
|
|
702
|
+
this.stampMigration(sourceName, version);
|
|
703
|
+
(0, debug_logger_js_1.info)(`[node-sqlite-adapter] Migration ${file} reconciled successfully`);
|
|
704
|
+
continue;
|
|
705
|
+
}
|
|
379
706
|
// Runtime MetricsStore may already own project_id/channel_id. Reconcile
|
|
380
707
|
// this additive migration atomically instead of accepting a duplicate skip.
|
|
381
|
-
if (version === 68) {
|
|
708
|
+
if (isCore && version === 68) {
|
|
382
709
|
if (!this.tableExists('tool_traces')) {
|
|
383
710
|
// Legacy version ledgers can have skipped 033; the structural repair
|
|
384
711
|
// below creates its table before reconciling 068. Do not stamp it yet.
|
|
@@ -388,6 +715,76 @@ class NodeSQLiteAdapter extends base_adapter_js_1.DatabaseAdapter {
|
|
|
388
715
|
(0, debug_logger_js_1.info)(`[node-sqlite-adapter] Migration ${file} reconciled successfully`);
|
|
389
716
|
continue;
|
|
390
717
|
}
|
|
718
|
+
// Relaxing tool_traces.model_run_id to nullable and adding the operation
|
|
719
|
+
// origin CHECK needs a table rebuild that also preserves any extra runtime
|
|
720
|
+
// columns/indexes a live database carries - more than the static SQL file
|
|
721
|
+
// can express. Reconcile with a dynamic rebuild instead of exec'ing the SQL.
|
|
722
|
+
if (isCore && version === 71) {
|
|
723
|
+
if (!this.tableExists('tool_traces')) {
|
|
724
|
+
// A legacy ledger may have skipped 033; the structural repair below
|
|
725
|
+
// creates the table first, then this rebuild reconciles it. Do not
|
|
726
|
+
// stamp 71 yet.
|
|
727
|
+
continue;
|
|
728
|
+
}
|
|
729
|
+
this.recoverServiceOperationOriginsMigration071();
|
|
730
|
+
(0, debug_logger_js_1.info)(`[node-sqlite-adapter] Migration ${file} reconciled successfully`);
|
|
731
|
+
continue;
|
|
732
|
+
}
|
|
733
|
+
if (isCore && version === 72) {
|
|
734
|
+
// `observation_versions` is the core's own table. This used to skip the
|
|
735
|
+
// migration entirely when `connector_event_index` was absent -- back when
|
|
736
|
+
// the connector index WAS the substrate -- so a consumer without the
|
|
737
|
+
// connector package silently got no observations at all, and every later
|
|
738
|
+
// migration touching them was swallowed as "no such table".
|
|
739
|
+
if (!this.tableExists('observation_versions')) {
|
|
740
|
+
this.exec(fs_1.default.readFileSync(path_1.default.join(migrationsDir, file), 'utf8'));
|
|
741
|
+
this.stampMigration(sourceName, version);
|
|
742
|
+
(0, debug_logger_js_1.info)(`[node-sqlite-adapter] Migration ${file} applied`);
|
|
743
|
+
continue;
|
|
744
|
+
}
|
|
745
|
+
this.recoverObservationVersionsMigration072();
|
|
746
|
+
(0, debug_logger_js_1.info)(`[node-sqlite-adapter] Migration ${file} reconciled successfully`);
|
|
747
|
+
continue;
|
|
748
|
+
}
|
|
749
|
+
if (isCore && version === 73) {
|
|
750
|
+
this.recoverRegistryCorrectionsMigration073(migrationsDir);
|
|
751
|
+
(0, debug_logger_js_1.info)(`[node-sqlite-adapter] Migration ${file} reconciled successfully`);
|
|
752
|
+
continue;
|
|
753
|
+
}
|
|
754
|
+
if (isCore && version === 74) {
|
|
755
|
+
if (!this.tableExists('twin_edges')) {
|
|
756
|
+
continue;
|
|
757
|
+
}
|
|
758
|
+
this.recoverWorkGraphRefsMigration074();
|
|
759
|
+
(0, debug_logger_js_1.info)(`[node-sqlite-adapter] Migration ${file} reconciled successfully`);
|
|
760
|
+
continue;
|
|
761
|
+
}
|
|
762
|
+
if (isCore && version === 77) {
|
|
763
|
+
this.recoverLegacyRecordKindMigration077();
|
|
764
|
+
(0, debug_logger_js_1.info)(`[node-sqlite-adapter] Migration ${file} reconciled successfully`);
|
|
765
|
+
continue;
|
|
766
|
+
}
|
|
767
|
+
// Extending the twin_edges relation CHECK needs the same dynamic rebuild
|
|
768
|
+
// as 074 so custom columns, indexes, triggers, and FK children survive;
|
|
769
|
+
// the static SQL file cannot express that. Reconcile instead of exec'ing.
|
|
770
|
+
if (isCore && version === 79) {
|
|
771
|
+
if (!this.tableExists('twin_edges')) {
|
|
772
|
+
continue;
|
|
773
|
+
}
|
|
774
|
+
this.recoverTwinEdgeRelationsMigration079();
|
|
775
|
+
(0, debug_logger_js_1.info)(`[node-sqlite-adapter] Migration ${file} reconciled successfully`);
|
|
776
|
+
continue;
|
|
777
|
+
}
|
|
778
|
+
if (isCore && version === 92) {
|
|
779
|
+
// Legacy high-version stores may carry only a skeletal decisions table.
|
|
780
|
+
// An external-content FTS rebuild cannot read columns that do not exist;
|
|
781
|
+
// leave this migration unstamped until the canonical table is present.
|
|
782
|
+
const columns = this.tableColumns('decisions');
|
|
783
|
+
if (!['topic', 'decision', 'reasoning'].every((column) => columns.has(column))) {
|
|
784
|
+
(0, debug_logger_js_1.warn)(`[node-sqlite-adapter] Migration ${file} deferred: decisions content is incomplete`);
|
|
785
|
+
continue;
|
|
786
|
+
}
|
|
787
|
+
}
|
|
391
788
|
const migrationPath = path_1.default.join(migrationsDir, file);
|
|
392
789
|
const migrationSQL = fs_1.default.readFileSync(migrationPath, 'utf8');
|
|
393
790
|
(0, debug_logger_js_1.info)(`[node-sqlite-adapter] Applying migration: ${file}`);
|
|
@@ -395,7 +792,7 @@ class NodeSQLiteAdapter extends base_adapter_js_1.DatabaseAdapter {
|
|
|
395
792
|
this.exec('BEGIN TRANSACTION');
|
|
396
793
|
this.exec(migrationSQL);
|
|
397
794
|
this.exec('COMMIT');
|
|
398
|
-
this.
|
|
795
|
+
this.stampMigration(sourceName, version);
|
|
399
796
|
(0, debug_logger_js_1.info)(`[node-sqlite-adapter] Migration ${file} applied successfully`);
|
|
400
797
|
}
|
|
401
798
|
catch (err) {
|
|
@@ -406,43 +803,9 @@ class NodeSQLiteAdapter extends base_adapter_js_1.DatabaseAdapter {
|
|
|
406
803
|
(0, debug_logger_js_1.info)(`[node-sqlite-adapter] Migration ${file} recovered successfully`);
|
|
407
804
|
continue;
|
|
408
805
|
}
|
|
409
|
-
if (message.includes('duplicate column') && version === 34) {
|
|
410
|
-
this.recoverConnectorEventScopeMigration034();
|
|
411
|
-
(0, debug_logger_js_1.info)(`[node-sqlite-adapter] Migration ${file} recovered successfully`);
|
|
412
|
-
continue;
|
|
413
|
-
}
|
|
414
|
-
if (message.includes('duplicate column') && version === 39) {
|
|
415
|
-
this.recoverConnectorEventOperatorSeqMigration039();
|
|
416
|
-
(0, debug_logger_js_1.info)(`[node-sqlite-adapter] Migration ${file} recovered successfully`);
|
|
417
|
-
continue;
|
|
418
|
-
}
|
|
419
|
-
if (message.includes('duplicate column') && version === 62) {
|
|
420
|
-
this.recoverConnectorEventSequencesMigration062();
|
|
421
|
-
(0, debug_logger_js_1.info)(`[node-sqlite-adapter] Migration ${file} recovered successfully`);
|
|
422
|
-
continue;
|
|
423
|
-
}
|
|
424
|
-
if (message.includes('duplicate column') && version === 67) {
|
|
425
|
-
this.recoverConnectorEventSourceEntityIdMigration067();
|
|
426
|
-
(0, debug_logger_js_1.info)(`[node-sqlite-adapter] Migration ${file} recovered successfully`);
|
|
427
|
-
continue;
|
|
428
|
-
}
|
|
429
|
-
if (message.includes('no such column') && version === 62) {
|
|
430
|
-
(0, debug_logger_js_1.warn)(`[node-sqlite-adapter] Migration ${file} deferred until connector structure recovery (${message})`);
|
|
431
|
-
continue;
|
|
432
|
-
}
|
|
433
|
-
if (version === 63 &&
|
|
434
|
-
(message.includes('no such column') || message.includes('no such table'))) {
|
|
435
|
-
(0, debug_logger_js_1.warn)(`[node-sqlite-adapter] Migration ${file} deferred until connector structure recovery (${message})`);
|
|
436
|
-
continue;
|
|
437
|
-
}
|
|
438
|
-
if (version === 67 &&
|
|
439
|
-
(message.includes('no such column') || message.includes('no such table'))) {
|
|
440
|
-
(0, debug_logger_js_1.warn)(`[node-sqlite-adapter] Migration ${file} deferred until connector structure recovery (${message})`);
|
|
441
|
-
continue;
|
|
442
|
-
}
|
|
443
806
|
if (message.includes('duplicate column')) {
|
|
444
807
|
(0, debug_logger_js_1.warn)(`[node-sqlite-adapter] Migration ${file} skipped (duplicate column - already applied)`);
|
|
445
|
-
this.
|
|
808
|
+
this.stampMigration(sourceName, version);
|
|
446
809
|
continue;
|
|
447
810
|
}
|
|
448
811
|
if (message.includes('no such table')) {
|
|
@@ -452,14 +815,16 @@ class NodeSQLiteAdapter extends base_adapter_js_1.DatabaseAdapter {
|
|
|
452
815
|
throw new Error(`Migration ${file} failed: ${message}`);
|
|
453
816
|
}
|
|
454
817
|
(0, debug_logger_js_1.warn)(`[node-sqlite-adapter] Migration ${file} skipped: ALTER TABLE on non-existent table (${message})`);
|
|
455
|
-
this.
|
|
818
|
+
this.stampMigration(sourceName, version);
|
|
456
819
|
continue;
|
|
457
820
|
}
|
|
458
821
|
(0, debug_logger_js_1.error)(`[node-sqlite-adapter] Migration ${file} failed:`, err);
|
|
459
822
|
throw new Error(`Migration ${file} failed: ${message}`);
|
|
460
823
|
}
|
|
461
824
|
}
|
|
462
|
-
|
|
825
|
+
if (isCore) {
|
|
826
|
+
this.repairSkippedFeatureMigrations(migrationsDir);
|
|
827
|
+
}
|
|
463
828
|
const embeddingsTables = this.prepare(`SELECT name FROM sqlite_master WHERE type='table' AND name='embeddings'`).all();
|
|
464
829
|
if (embeddingsTables.length === 0) {
|
|
465
830
|
throw new Error('Embeddings table is missing after migrations. Add the required embeddings migration.');
|
|
@@ -467,6 +832,7 @@ class NodeSQLiteAdapter extends base_adapter_js_1.DatabaseAdapter {
|
|
|
467
832
|
this.migrateFromVssMemories();
|
|
468
833
|
}
|
|
469
834
|
repairSkippedFeatureMigrations(migrationsDir) {
|
|
835
|
+
this.repairRegistryIdentityStructures(migrationsDir);
|
|
470
836
|
if (this.tableExists('decisions')) {
|
|
471
837
|
const decisionColumns = this.tableColumns('decisions');
|
|
472
838
|
const hasMissingMemoryProvenanceColumn = [
|
|
@@ -493,72 +859,59 @@ class NodeSQLiteAdapter extends base_adapter_js_1.DatabaseAdapter {
|
|
|
493
859
|
}
|
|
494
860
|
// Also repairs databases already stamped 68 by the former generic duplicate
|
|
495
861
|
// skip, which rolled back the new columns before advancing schema_version.
|
|
496
|
-
// Only enter the recovery transaction when something is actually missing
|
|
862
|
+
// Only enter the recovery transaction when something is actually missing --
|
|
497
863
|
// a complete database must not take the write path on every runMigrations().
|
|
498
864
|
if (fs_1.default.existsSync(path_1.default.join(migrationsDir, '068-tool-trace-diagnostics.sql')) &&
|
|
499
865
|
this.needsToolTraceDiagnosticsRepair068()) {
|
|
500
866
|
this.recoverToolTraceDiagnosticsMigration068();
|
|
501
867
|
}
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
(0, debug_logger_js_1.info)('[node-sqlite-adapter] Repaired skipped connector event operator sequence migration');
|
|
526
|
-
}
|
|
527
|
-
const connectorColumnsAfterOperatorSeqRepair = this.tableColumns('connector_event_index');
|
|
528
|
-
if (connectorColumnsAfterOperatorSeqRepair.has('source_timestamp_ms')) {
|
|
529
|
-
const hasMissingObservationSeqFeature = !connectorColumnsAfterOperatorSeqRepair.has('operator_observation_seq') ||
|
|
530
|
-
!this.tableExists('connector_event_index_observation_cursors') ||
|
|
531
|
-
!this.indexExists('idx_connector_event_index_observation_seq') ||
|
|
532
|
-
!this.triggerExists('trg_connector_event_index_operator_ingest_seq_au') ||
|
|
533
|
-
!this.triggerExists('trg_connector_event_index_observation_seq_ai') ||
|
|
534
|
-
!this.triggerExists('trg_connector_event_index_observation_seq_au') ||
|
|
535
|
-
!this.triggerExists('trg_connector_event_index_observation_seq_explicit_ai') ||
|
|
536
|
-
!this.schemaVersionExists(62);
|
|
537
|
-
if (hasMissingObservationSeqFeature) {
|
|
538
|
-
this.recoverConnectorEventSequencesMigration062();
|
|
539
|
-
(0, debug_logger_js_1.info)('[node-sqlite-adapter] Repaired skipped connector event observation sequence migration');
|
|
540
|
-
}
|
|
541
|
-
const hasMissingLegacyRefreshFeature = !this.triggerExists('trg_connector_event_index_legacy_content_refresh_au') ||
|
|
542
|
-
!this.schemaVersionExists(63);
|
|
543
|
-
if (hasMissingLegacyRefreshFeature) {
|
|
544
|
-
this.recoverConnectorEventLegacyRefreshMigration063();
|
|
545
|
-
(0, debug_logger_js_1.info)('[node-sqlite-adapter] Repaired skipped legacy connector event refresh migration');
|
|
546
|
-
}
|
|
547
|
-
}
|
|
548
|
-
const hasMissingSourceEntityIdFeature = !this.tableColumns('connector_event_index').has('source_entity_id') ||
|
|
549
|
-
!this.indexExists('idx_connector_event_source_entity') ||
|
|
550
|
-
!this.schemaVersionExists(67);
|
|
551
|
-
if (hasMissingSourceEntityIdFeature) {
|
|
552
|
-
this.recoverConnectorEventSourceEntityIdMigration067();
|
|
553
|
-
(0, debug_logger_js_1.info)('[node-sqlite-adapter] Repaired skipped connector event source_entity_id migration');
|
|
554
|
-
}
|
|
868
|
+
// Runs after the 068 repair so tool_traces already carries the diagnostic
|
|
869
|
+
// columns. Only enters the rebuild when the operation-origin shape is
|
|
870
|
+
// actually missing - a complete database must not rebuild on every open.
|
|
871
|
+
if (fs_1.default.existsSync(path_1.default.join(migrationsDir, '071-service-operation-origins.sql')) &&
|
|
872
|
+
this.tableExists('tool_traces') &&
|
|
873
|
+
this.needsServiceOperationOriginsRepair071()) {
|
|
874
|
+
this.recoverServiceOperationOriginsMigration071();
|
|
875
|
+
}
|
|
876
|
+
if (fs_1.default.existsSync(path_1.default.join(migrationsDir, '072-observation-versions.sql')) &&
|
|
877
|
+
this.needsObservationVersionsRepair072()) {
|
|
878
|
+
this.recoverObservationVersionsMigration072();
|
|
879
|
+
(0, debug_logger_js_1.info)('[node-sqlite-adapter] Repaired skipped observation versions migration');
|
|
880
|
+
}
|
|
881
|
+
if (fs_1.default.existsSync(path_1.default.join(migrationsDir, '073-registry-corrections.sql')) &&
|
|
882
|
+
this.needsRegistryCorrectionsRepair073()) {
|
|
883
|
+
this.recoverRegistryCorrectionsMigration073(migrationsDir);
|
|
884
|
+
(0, debug_logger_js_1.info)('[node-sqlite-adapter] Repaired skipped registry corrections migration');
|
|
885
|
+
}
|
|
886
|
+
if (fs_1.default.existsSync(path_1.default.join(migrationsDir, '074-work-graph-ref-kinds.sql')) &&
|
|
887
|
+
this.tableExists('twin_edges') &&
|
|
888
|
+
this.needsWorkGraphRefsRepair074()) {
|
|
889
|
+
this.recoverWorkGraphRefsMigration074();
|
|
890
|
+
(0, debug_logger_js_1.info)('[node-sqlite-adapter] Repaired skipped work graph ref migration');
|
|
555
891
|
}
|
|
556
892
|
if (!this.tableExists('twin_edges')) {
|
|
557
893
|
this.applyRepairMigration(migrationsDir, '035-create-twin-edges.sql', 'twin edge ledger');
|
|
558
894
|
}
|
|
559
|
-
if (
|
|
560
|
-
|
|
561
|
-
this.
|
|
895
|
+
if (fs_1.default.existsSync(path_1.default.join(migrationsDir, '072-observation-versions.sql')) &&
|
|
896
|
+
this.needsObservationVersionsRepair072()) {
|
|
897
|
+
this.recoverObservationVersionsMigration072();
|
|
898
|
+
}
|
|
899
|
+
if (fs_1.default.existsSync(path_1.default.join(migrationsDir, '073-registry-corrections.sql')) &&
|
|
900
|
+
this.needsRegistryCorrectionsRepair073()) {
|
|
901
|
+
this.recoverRegistryCorrectionsMigration073(migrationsDir);
|
|
902
|
+
}
|
|
903
|
+
if (fs_1.default.existsSync(path_1.default.join(migrationsDir, '074-work-graph-ref-kinds.sql')) &&
|
|
904
|
+
this.needsWorkGraphRefsRepair074()) {
|
|
905
|
+
this.recoverWorkGraphRefsMigration074();
|
|
906
|
+
}
|
|
907
|
+
// A database stamped >= 79 whose twin_edges was rebuilt from 035/074 above
|
|
908
|
+
// still carries the pre-079 edge_type CHECK; extend it here so the missing
|
|
909
|
+
// relation list is repaired in the same pass.
|
|
910
|
+
if (fs_1.default.existsSync(path_1.default.join(migrationsDir, '079-twin-edge-relations.sql')) &&
|
|
911
|
+
this.tableExists('twin_edges') &&
|
|
912
|
+
this.needsTwinEdgeRelationsRepair079()) {
|
|
913
|
+
this.recoverTwinEdgeRelationsMigration079();
|
|
914
|
+
(0, debug_logger_js_1.info)('[node-sqlite-adapter] Repaired skipped twin edge relations migration');
|
|
562
915
|
}
|
|
563
916
|
if (!this.tableExists('context_packets')) {
|
|
564
917
|
this.applyRepairMigration(migrationsDir, '037-create-context-packets.sql', 'context packet store');
|
|
@@ -591,6 +944,391 @@ class NodeSQLiteAdapter extends base_adapter_js_1.DatabaseAdapter {
|
|
|
591
944
|
}
|
|
592
945
|
this.assertMigration041Complete();
|
|
593
946
|
}
|
|
947
|
+
repairRegistryIdentityStructures(migrationsDir) {
|
|
948
|
+
const columnsMatch = (table, expected) => {
|
|
949
|
+
if (!this.tableExists(table)) {
|
|
950
|
+
return false;
|
|
951
|
+
}
|
|
952
|
+
const rows = this.prepare(`PRAGMA table_info("${table}")`).all();
|
|
953
|
+
return (rows.length === expected.length &&
|
|
954
|
+
expected.every(([name, type, notnull, dflt, pk]) => {
|
|
955
|
+
const row = rows.find((candidate) => candidate.name === name);
|
|
956
|
+
return Boolean(row &&
|
|
957
|
+
row.type.toUpperCase() === type &&
|
|
958
|
+
row.notnull === notnull &&
|
|
959
|
+
row.pk === pk &&
|
|
960
|
+
row.dflt_value === dflt);
|
|
961
|
+
}));
|
|
962
|
+
};
|
|
963
|
+
const fkMatch = (table, expected) => {
|
|
964
|
+
if (!this.tableExists(table)) {
|
|
965
|
+
return false;
|
|
966
|
+
}
|
|
967
|
+
const rows = this.prepare(`PRAGMA foreign_key_list("${table}")`).all();
|
|
968
|
+
return expected.every(([from, target, to, onDelete]) => rows.some((row) => row.from === from &&
|
|
969
|
+
row.table === target &&
|
|
970
|
+
row.to === to &&
|
|
971
|
+
row.on_delete.toUpperCase() === onDelete));
|
|
972
|
+
};
|
|
973
|
+
const indexMatch = (name, columns) => {
|
|
974
|
+
if (!this.indexExists(name)) {
|
|
975
|
+
return false;
|
|
976
|
+
}
|
|
977
|
+
const rows = this.prepare(`PRAGMA index_info("${name}")`).all();
|
|
978
|
+
return (rows.length === columns.length &&
|
|
979
|
+
rows.sort((a, b) => a.seqno - b.seqno).every((row, index) => row.name === columns[index]));
|
|
980
|
+
};
|
|
981
|
+
const registrySql = this.tableExists('registry_nodes')
|
|
982
|
+
? this.prepare("SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'registry_nodes'").get()
|
|
983
|
+
: undefined;
|
|
984
|
+
const aliasSql = this.tableExists('registry_aliases')
|
|
985
|
+
? String(this.prepare("SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'registry_aliases'").get()?.sql ?? '')
|
|
986
|
+
: '';
|
|
987
|
+
const scopeSql = this.tableExists('registry_scope_bindings')
|
|
988
|
+
? String(this.prepare("SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'registry_scope_bindings'").get()?.sql ?? '')
|
|
989
|
+
: '';
|
|
990
|
+
const registryMalformed = Boolean(registrySql) &&
|
|
991
|
+
(!columnsMatch('registry_nodes', [
|
|
992
|
+
['id', 'TEXT', 0, null, 1],
|
|
993
|
+
['kind', 'TEXT', 1, null, 0],
|
|
994
|
+
['name', 'TEXT', 1, null, 0],
|
|
995
|
+
['parent_id', 'TEXT', 0, null, 0],
|
|
996
|
+
['merged_into', 'TEXT', 0, null, 0],
|
|
997
|
+
['merge_reason', 'TEXT', 0, null, 0],
|
|
998
|
+
['note', 'TEXT', 0, null, 0],
|
|
999
|
+
['created_at', 'INTEGER', 1, null, 0],
|
|
1000
|
+
['updated_at', 'INTEGER', 1, null, 0],
|
|
1001
|
+
]) ||
|
|
1002
|
+
!columnsMatch('registry_aliases', [
|
|
1003
|
+
['node_id', 'TEXT', 1, null, 0],
|
|
1004
|
+
['kind', 'TEXT', 1, null, 1],
|
|
1005
|
+
['alias', 'TEXT', 1, null, 2],
|
|
1006
|
+
['alias_display', 'TEXT', 1, null, 0],
|
|
1007
|
+
['scope_kind', 'TEXT', 1, null, 3],
|
|
1008
|
+
['scope_id', 'TEXT', 1, null, 4],
|
|
1009
|
+
['created_at', 'INTEGER', 1, null, 0],
|
|
1010
|
+
]) ||
|
|
1011
|
+
!columnsMatch('registry_scope_bindings', [
|
|
1012
|
+
['node_id', 'TEXT', 1, null, 1],
|
|
1013
|
+
['scope_kind', 'TEXT', 1, null, 2],
|
|
1014
|
+
['scope_id', 'TEXT', 1, null, 3],
|
|
1015
|
+
]) ||
|
|
1016
|
+
!fkMatch('registry_nodes', [
|
|
1017
|
+
['parent_id', 'registry_nodes', 'id', 'SET NULL'],
|
|
1018
|
+
['merged_into', 'registry_nodes', 'id', 'SET NULL'],
|
|
1019
|
+
]) ||
|
|
1020
|
+
!fkMatch('registry_aliases', [['node_id', 'registry_nodes', 'id', 'CASCADE']]) ||
|
|
1021
|
+
!fkMatch('registry_scope_bindings', [['node_id', 'registry_nodes', 'id', 'CASCADE']]) ||
|
|
1022
|
+
!normalizeSqlText(String(registrySql.sql ?? '')).includes(normalizeSqlText("CHECK (kind IN ('item', 'person', 'client'))")) ||
|
|
1023
|
+
!normalizeSqlText(aliasSql).includes(normalizeSqlText("CHECK (kind IN ('item', 'person', 'client'))")) ||
|
|
1024
|
+
!normalizeSqlText(scopeSql).includes(normalizeSqlText("CHECK (scope_kind IN ('global', 'user', 'channel', 'project'))")) ||
|
|
1025
|
+
!indexMatch('idx_registry_nodes_kind', ['kind', 'merged_into']) ||
|
|
1026
|
+
!indexMatch('idx_registry_nodes_parent', ['parent_id']) ||
|
|
1027
|
+
!indexMatch('idx_registry_aliases_node', ['node_id']) ||
|
|
1028
|
+
!indexMatch('idx_registry_aliases_scope', ['kind', 'alias', 'scope_kind', 'scope_id']) ||
|
|
1029
|
+
!indexMatch('idx_registry_scope_lookup', ['scope_kind', 'scope_id', 'node_id']));
|
|
1030
|
+
if (registryMalformed) {
|
|
1031
|
+
this.rebuildRegistry069(migrationsDir);
|
|
1032
|
+
}
|
|
1033
|
+
const registryStructureMissing = !this.tableExists('registry_nodes') ||
|
|
1034
|
+
!this.tableExists('registry_aliases') ||
|
|
1035
|
+
!this.tableExists('registry_scope_bindings') ||
|
|
1036
|
+
!this.indexExists('idx_registry_nodes_kind') ||
|
|
1037
|
+
!this.indexExists('idx_registry_nodes_parent') ||
|
|
1038
|
+
!this.indexExists('idx_registry_aliases_node') ||
|
|
1039
|
+
!this.indexExists('idx_registry_scope_lookup');
|
|
1040
|
+
if (registryStructureMissing) {
|
|
1041
|
+
this.applyRepairMigration(migrationsDir, '069-create-registry-nodes.sql', 'registry nodes and aliases');
|
|
1042
|
+
}
|
|
1043
|
+
if (!this.tableExists('decisions')) {
|
|
1044
|
+
return;
|
|
1045
|
+
}
|
|
1046
|
+
if (this.tableColumns('decisions').has('item_id')) {
|
|
1047
|
+
const itemColumn = this.prepare('PRAGMA table_info("decisions")').all().find((column) => column.name === 'item_id');
|
|
1048
|
+
if (!itemColumn ||
|
|
1049
|
+
itemColumn.type.toUpperCase() !== 'TEXT' ||
|
|
1050
|
+
itemColumn.notnull !== 0 ||
|
|
1051
|
+
itemColumn.pk !== 0 ||
|
|
1052
|
+
itemColumn.dflt_value !== null) {
|
|
1053
|
+
throw new Error('Migration 070 cannot safely repair noncanonical decisions.item_id');
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
const actorSql = this.tableExists('record_actors')
|
|
1057
|
+
? this.prepare("SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'record_actors'").get()
|
|
1058
|
+
: undefined;
|
|
1059
|
+
const actorMalformed = Boolean(actorSql) &&
|
|
1060
|
+
(!columnsMatch('record_actors', [
|
|
1061
|
+
['record_id', 'TEXT', 1, null, 1],
|
|
1062
|
+
['person_id', 'TEXT', 1, null, 2],
|
|
1063
|
+
['role', 'TEXT', 1, null, 3],
|
|
1064
|
+
['position', 'INTEGER', 1, '0', 0],
|
|
1065
|
+
['created_at', 'INTEGER', 1, null, 0],
|
|
1066
|
+
]) ||
|
|
1067
|
+
!fkMatch('record_actors', [['record_id', 'decisions', 'id', 'CASCADE']]) ||
|
|
1068
|
+
!indexMatch('idx_record_actors_person', ['person_id', 'record_id']));
|
|
1069
|
+
if (actorMalformed) {
|
|
1070
|
+
this.rebuildRecordActors070();
|
|
1071
|
+
}
|
|
1072
|
+
const needsRecordIdentityRepair = !this.tableColumns('decisions').has('item_id') ||
|
|
1073
|
+
!this.tableExists('record_actors') ||
|
|
1074
|
+
!this.indexExists('idx_decisions_item') ||
|
|
1075
|
+
!this.indexExists('idx_record_actors_person');
|
|
1076
|
+
if (!needsRecordIdentityRepair) {
|
|
1077
|
+
return;
|
|
1078
|
+
}
|
|
1079
|
+
this.exec('BEGIN TRANSACTION');
|
|
1080
|
+
try {
|
|
1081
|
+
if (!this.tableColumns('decisions').has('item_id')) {
|
|
1082
|
+
this.exec('ALTER TABLE decisions ADD COLUMN item_id TEXT');
|
|
1083
|
+
}
|
|
1084
|
+
this.exec('CREATE INDEX IF NOT EXISTS idx_decisions_item ON decisions(item_id)');
|
|
1085
|
+
this.exec(`CREATE TABLE IF NOT EXISTS record_actors (
|
|
1086
|
+
record_id TEXT NOT NULL REFERENCES decisions(id) ON DELETE CASCADE,
|
|
1087
|
+
person_id TEXT NOT NULL,
|
|
1088
|
+
role TEXT NOT NULL,
|
|
1089
|
+
position INTEGER NOT NULL DEFAULT 0,
|
|
1090
|
+
created_at INTEGER NOT NULL,
|
|
1091
|
+
PRIMARY KEY (record_id, person_id, role)
|
|
1092
|
+
)`);
|
|
1093
|
+
this.exec('CREATE INDEX IF NOT EXISTS idx_record_actors_person ON record_actors(person_id, record_id)');
|
|
1094
|
+
this.prepare('INSERT OR IGNORE INTO schema_version (version, description) VALUES (?, ?)').run(70, 'Record identity bindings');
|
|
1095
|
+
this.exec('COMMIT');
|
|
1096
|
+
(0, debug_logger_js_1.info)('[node-sqlite-adapter] Repaired skipped record identity migration');
|
|
1097
|
+
}
|
|
1098
|
+
catch (repairError) {
|
|
1099
|
+
this.exec('ROLLBACK');
|
|
1100
|
+
throw repairError;
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
rebuildRegistry069(migrationsDir) {
|
|
1104
|
+
const originalTableSql = (table) => String(this.prepare("SELECT sql FROM sqlite_master WHERE type='table' AND name = ?").get(table)?.sql ?? '');
|
|
1105
|
+
const extraConstraints = (table, canonicalFragments) => {
|
|
1106
|
+
const clauses = splitCreateTableClauses(originalTableSql(table));
|
|
1107
|
+
if (clauses.length === 0) {
|
|
1108
|
+
throw new Error(`Migration 069 cannot parse ${table} definition`);
|
|
1109
|
+
}
|
|
1110
|
+
for (const clause of clauses.filter((candidate) => !clauseIsTableConstraint(candidate))) {
|
|
1111
|
+
const normalized = normalizeSqlText(clause);
|
|
1112
|
+
const hasInlineConstraint = ['check(', 'unique', 'collate', 'references'].some((token) => normalized.includes(token));
|
|
1113
|
+
const columnName = clauseColumnName(clause);
|
|
1114
|
+
const managedConstraint = (columnName === 'kind' && normalized.includes('check(')) ||
|
|
1115
|
+
(columnName === 'scope_kind' && normalized.includes('check(')) ||
|
|
1116
|
+
(['parent_id', 'merged_into', 'node_id'].includes(columnName) &&
|
|
1117
|
+
normalized.includes('references'));
|
|
1118
|
+
if (hasInlineConstraint &&
|
|
1119
|
+
!managedConstraint &&
|
|
1120
|
+
!canonicalFragments.some((fragment) => normalized.includes(normalizeSqlText(fragment)))) {
|
|
1121
|
+
throw new Error(`Migration 069 cannot safely preserve inline constraint in ${table}`);
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
return clauses.filter((clause) => {
|
|
1125
|
+
if (!clauseIsTableConstraint(clause)) {
|
|
1126
|
+
return false;
|
|
1127
|
+
}
|
|
1128
|
+
const normalized = normalizeSqlText(clause);
|
|
1129
|
+
return !canonicalFragments.some((fragment) => normalized.includes(normalizeSqlText(fragment)));
|
|
1130
|
+
});
|
|
1131
|
+
};
|
|
1132
|
+
const nodeExtras = extraConstraints('registry_nodes', [
|
|
1133
|
+
"CHECK (kind IN ('item', 'person', 'client'))",
|
|
1134
|
+
'FOREIGN KEY (parent_id) REFERENCES registry_nodes(id)',
|
|
1135
|
+
'FOREIGN KEY (merged_into) REFERENCES registry_nodes(id)',
|
|
1136
|
+
'REFERENCES registry_nodes(id) ON DELETE SET NULL',
|
|
1137
|
+
]);
|
|
1138
|
+
const aliasExtras = this.tableExists('registry_aliases')
|
|
1139
|
+
? extraConstraints('registry_aliases', [
|
|
1140
|
+
"CHECK (kind IN ('item', 'person', 'client'))",
|
|
1141
|
+
'PRIMARY KEY (kind, alias, scope_kind, scope_id)',
|
|
1142
|
+
// The pre-scope shape, which is exactly what this rebuild exists to
|
|
1143
|
+
// convert. Listing only the post-migration spelling made every database
|
|
1144
|
+
// written before alias scoping look like it carried a constraint this
|
|
1145
|
+
// code does not understand, so the rebuild preserved it and emitted a
|
|
1146
|
+
// CREATE TABLE with two PRIMARY KEY clauses. The daemon could not open
|
|
1147
|
+
// its own older database.
|
|
1148
|
+
'PRIMARY KEY (kind, alias)',
|
|
1149
|
+
'FOREIGN KEY (node_id) REFERENCES registry_nodes(id)',
|
|
1150
|
+
'REFERENCES registry_nodes(id) ON DELETE CASCADE',
|
|
1151
|
+
])
|
|
1152
|
+
: [];
|
|
1153
|
+
const scopeExtras = this.tableExists('registry_scope_bindings')
|
|
1154
|
+
? extraConstraints('registry_scope_bindings', [
|
|
1155
|
+
"CHECK (scope_kind IN ('global', 'user', 'channel', 'project'))",
|
|
1156
|
+
'PRIMARY KEY (node_id, scope_kind, scope_id)',
|
|
1157
|
+
'FOREIGN KEY (node_id) REFERENCES registry_nodes(id)',
|
|
1158
|
+
'REFERENCES registry_nodes(id) ON DELETE CASCADE',
|
|
1159
|
+
])
|
|
1160
|
+
: [];
|
|
1161
|
+
const nodes = this.prepare('SELECT * FROM registry_nodes').all();
|
|
1162
|
+
const aliases = this.tableExists('registry_aliases')
|
|
1163
|
+
? this.prepare('SELECT * FROM registry_aliases').all()
|
|
1164
|
+
: [];
|
|
1165
|
+
const scopes = this.tableExists('registry_scope_bindings')
|
|
1166
|
+
? this.prepare('SELECT * FROM registry_scope_bindings').all()
|
|
1167
|
+
: [];
|
|
1168
|
+
const nodeIds = new Set();
|
|
1169
|
+
for (const row of nodes) {
|
|
1170
|
+
if (typeof row.id !== 'string' ||
|
|
1171
|
+
!row.id ||
|
|
1172
|
+
!['item', 'person', 'client'].includes(String(row.kind)) ||
|
|
1173
|
+
typeof row.name !== 'string' ||
|
|
1174
|
+
!row.name.trim()) {
|
|
1175
|
+
throw new Error('Migration 069 source row violates registry node contract');
|
|
1176
|
+
}
|
|
1177
|
+
nodeIds.add(row.id);
|
|
1178
|
+
}
|
|
1179
|
+
const aliasKeys = new Set();
|
|
1180
|
+
for (const row of aliases) {
|
|
1181
|
+
const key = `${String(row.kind)}:${String(row.alias)}:${String(row.scope_kind ?? 'global')}:${String(row.scope_id ?? '*')}`;
|
|
1182
|
+
if (!nodeIds.has(String(row.node_id)) ||
|
|
1183
|
+
!['item', 'person', 'client'].includes(String(row.kind)) ||
|
|
1184
|
+
typeof row.alias !== 'string' ||
|
|
1185
|
+
!row.alias ||
|
|
1186
|
+
aliasKeys.has(key)) {
|
|
1187
|
+
throw new Error('Migration 069 source row violates registry alias contract');
|
|
1188
|
+
}
|
|
1189
|
+
aliasKeys.add(key);
|
|
1190
|
+
}
|
|
1191
|
+
const customObjects = this.prepare(`SELECT sql FROM sqlite_master
|
|
1192
|
+
WHERE type IN ('index', 'trigger')
|
|
1193
|
+
AND tbl_name IN ('registry_nodes', 'registry_aliases', 'registry_scope_bindings')
|
|
1194
|
+
AND sql IS NOT NULL
|
|
1195
|
+
AND name NOT IN (
|
|
1196
|
+
'idx_registry_nodes_kind', 'idx_registry_nodes_parent',
|
|
1197
|
+
'idx_registry_aliases_node', 'idx_registry_aliases_scope',
|
|
1198
|
+
'idx_registry_scope_lookup'
|
|
1199
|
+
)`).all();
|
|
1200
|
+
let migrationSQL = fs_1.default.readFileSync(path_1.default.join(migrationsDir, '069-create-registry-nodes.sql'), 'utf8');
|
|
1201
|
+
const inject = (table, extras) => {
|
|
1202
|
+
if (extras.length === 0) {
|
|
1203
|
+
return;
|
|
1204
|
+
}
|
|
1205
|
+
const pattern = new RegExp(`(CREATE TABLE IF NOT EXISTS ${table} \\([\\s\\S]*?)(\\n\\);)`);
|
|
1206
|
+
if (!pattern.test(migrationSQL)) {
|
|
1207
|
+
throw new Error(`Migration 069 cannot preserve ${table} constraints`);
|
|
1208
|
+
}
|
|
1209
|
+
migrationSQL = migrationSQL.replace(pattern, `$1,\n ${extras.join(',\n ')}$2`);
|
|
1210
|
+
};
|
|
1211
|
+
inject('registry_nodes', nodeExtras);
|
|
1212
|
+
inject('registry_aliases', aliasExtras);
|
|
1213
|
+
inject('registry_scope_bindings', scopeExtras);
|
|
1214
|
+
const previousForeignKeys = this.readForeignKeysEnabled();
|
|
1215
|
+
this.exec('PRAGMA foreign_keys = OFF');
|
|
1216
|
+
if (this.readForeignKeysEnabled()) {
|
|
1217
|
+
throw new Error('Migration 069 could not disable foreign_keys before rebuild');
|
|
1218
|
+
}
|
|
1219
|
+
try {
|
|
1220
|
+
this.transaction(() => {
|
|
1221
|
+
this.exec('DROP TABLE IF EXISTS registry_scope_bindings');
|
|
1222
|
+
this.exec('DROP TABLE IF EXISTS registry_aliases');
|
|
1223
|
+
this.exec('DROP TABLE registry_nodes');
|
|
1224
|
+
this.exec(migrationSQL);
|
|
1225
|
+
const insertNode = this.prepare(`INSERT INTO registry_nodes
|
|
1226
|
+
(id, kind, name, parent_id, merged_into, merge_reason, note, created_at, updated_at)
|
|
1227
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`);
|
|
1228
|
+
for (const row of nodes) {
|
|
1229
|
+
insertNode.run(row.id, row.kind, row.name, row.parent_id ?? null, row.merged_into ?? null, row.merge_reason ?? null, row.note ?? null, typeof row.created_at === 'number' ? row.created_at : 0, typeof row.updated_at === 'number'
|
|
1230
|
+
? row.updated_at
|
|
1231
|
+
: typeof row.created_at === 'number'
|
|
1232
|
+
? row.created_at
|
|
1233
|
+
: 0);
|
|
1234
|
+
}
|
|
1235
|
+
const insertAlias = this.prepare(`INSERT INTO registry_aliases
|
|
1236
|
+
(node_id, kind, alias, alias_display, scope_kind, scope_id, created_at)
|
|
1237
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`);
|
|
1238
|
+
for (const row of aliases) {
|
|
1239
|
+
insertAlias.run(row.node_id, row.kind, row.alias, typeof row.alias_display === 'string' ? row.alias_display : row.alias, typeof row.scope_kind === 'string' ? row.scope_kind : 'global', typeof row.scope_id === 'string' ? row.scope_id : '*', typeof row.created_at === 'number' ? row.created_at : 0);
|
|
1240
|
+
}
|
|
1241
|
+
const insertScope = this.prepare('INSERT INTO registry_scope_bindings (node_id, scope_kind, scope_id) VALUES (?, ?, ?)');
|
|
1242
|
+
for (const row of scopes) {
|
|
1243
|
+
insertScope.run(row.node_id, row.scope_kind, row.scope_id);
|
|
1244
|
+
}
|
|
1245
|
+
for (const object of customObjects) {
|
|
1246
|
+
this.exec(object.sql);
|
|
1247
|
+
}
|
|
1248
|
+
const violations = this.prepare('PRAGMA foreign_key_check').all();
|
|
1249
|
+
if (violations.length > 0) {
|
|
1250
|
+
throw new Error('Migration 069 repair left foreign key violations');
|
|
1251
|
+
}
|
|
1252
|
+
});
|
|
1253
|
+
}
|
|
1254
|
+
finally {
|
|
1255
|
+
this.exec(`PRAGMA foreign_keys = ${previousForeignKeys ? 'ON' : 'OFF'}`);
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
rebuildRecordActors070() {
|
|
1259
|
+
const originalSql = String(this.prepare("SELECT sql FROM sqlite_master WHERE type='table' AND name='record_actors'").get()?.sql ?? '');
|
|
1260
|
+
const actorExtras = splitCreateTableClauses(originalSql).filter((clause) => {
|
|
1261
|
+
if (!clauseIsTableConstraint(clause)) {
|
|
1262
|
+
return false;
|
|
1263
|
+
}
|
|
1264
|
+
const normalized = normalizeSqlText(clause);
|
|
1265
|
+
return ![
|
|
1266
|
+
'PRIMARY KEY (record_id, person_id, role)',
|
|
1267
|
+
'FOREIGN KEY (record_id) REFERENCES decisions(id)',
|
|
1268
|
+
].some((fragment) => normalized.includes(normalizeSqlText(fragment)));
|
|
1269
|
+
});
|
|
1270
|
+
for (const clause of splitCreateTableClauses(originalSql).filter((candidate) => !clauseIsTableConstraint(candidate))) {
|
|
1271
|
+
const normalized = normalizeSqlText(clause);
|
|
1272
|
+
const hasInlineConstraint = ['check(', 'unique', 'collate', 'references'].some((token) => normalized.includes(token));
|
|
1273
|
+
const canonicalReference = normalized.includes('record_idtextnotnullreferencesdecisions(id)ondeletecascade');
|
|
1274
|
+
if (hasInlineConstraint && !canonicalReference) {
|
|
1275
|
+
throw new Error('Migration 070 cannot safely preserve inline record_actors constraint');
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
const rows = this.prepare('SELECT * FROM record_actors').all();
|
|
1279
|
+
const keys = new Set();
|
|
1280
|
+
for (const row of rows) {
|
|
1281
|
+
const key = `${String(row.record_id)}:${String(row.person_id)}:${String(row.role)}`;
|
|
1282
|
+
const decision = this.prepare('SELECT 1 FROM decisions WHERE id = ?').get(row.record_id);
|
|
1283
|
+
if (!decision ||
|
|
1284
|
+
typeof row.person_id !== 'string' ||
|
|
1285
|
+
!row.person_id ||
|
|
1286
|
+
typeof row.role !== 'string' ||
|
|
1287
|
+
!row.role ||
|
|
1288
|
+
keys.has(key)) {
|
|
1289
|
+
throw new Error('Migration 070 source row violates record actor contract');
|
|
1290
|
+
}
|
|
1291
|
+
keys.add(key);
|
|
1292
|
+
}
|
|
1293
|
+
const customObjects = this.prepare(`SELECT sql FROM sqlite_master
|
|
1294
|
+
WHERE type IN ('index', 'trigger') AND tbl_name = 'record_actors'
|
|
1295
|
+
AND sql IS NOT NULL AND name != 'idx_record_actors_person'`).all();
|
|
1296
|
+
const previousForeignKeys = this.readForeignKeysEnabled();
|
|
1297
|
+
this.exec('PRAGMA foreign_keys = OFF');
|
|
1298
|
+
if (this.readForeignKeysEnabled()) {
|
|
1299
|
+
throw new Error('Migration 070 could not disable foreign_keys before rebuild');
|
|
1300
|
+
}
|
|
1301
|
+
try {
|
|
1302
|
+
this.transaction(() => {
|
|
1303
|
+
this.exec('DROP TABLE record_actors');
|
|
1304
|
+
this.exec(`CREATE TABLE record_actors (
|
|
1305
|
+
record_id TEXT NOT NULL REFERENCES decisions(id) ON DELETE CASCADE,
|
|
1306
|
+
person_id TEXT NOT NULL,
|
|
1307
|
+
role TEXT NOT NULL,
|
|
1308
|
+
position INTEGER NOT NULL DEFAULT 0,
|
|
1309
|
+
created_at INTEGER NOT NULL,
|
|
1310
|
+
PRIMARY KEY (record_id, person_id, role)
|
|
1311
|
+
${actorExtras.length > 0 ? `, ${actorExtras.join(', ')}` : ''}
|
|
1312
|
+
)`);
|
|
1313
|
+
this.exec('CREATE INDEX idx_record_actors_person ON record_actors(person_id, record_id)');
|
|
1314
|
+
const insert = this.prepare(`INSERT INTO record_actors
|
|
1315
|
+
(record_id, person_id, role, position, created_at) VALUES (?, ?, ?, ?, ?)`);
|
|
1316
|
+
for (const row of rows) {
|
|
1317
|
+
insert.run(row.record_id, row.person_id, row.role, typeof row.position === 'number' ? row.position : 0, typeof row.created_at === 'number' ? row.created_at : 0);
|
|
1318
|
+
}
|
|
1319
|
+
for (const object of customObjects) {
|
|
1320
|
+
this.exec(object.sql);
|
|
1321
|
+
}
|
|
1322
|
+
const violations = this.prepare('PRAGMA foreign_key_check').all();
|
|
1323
|
+
if (violations.length > 0) {
|
|
1324
|
+
throw new Error('Migration 070 repair left foreign key violations');
|
|
1325
|
+
}
|
|
1326
|
+
});
|
|
1327
|
+
}
|
|
1328
|
+
finally {
|
|
1329
|
+
this.exec(`PRAGMA foreign_keys = ${previousForeignKeys ? 'ON' : 'OFF'}`);
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
594
1332
|
applyRepairMigration(migrationsDir, fileName, label) {
|
|
595
1333
|
const migrationPath = path_1.default.join(migrationsDir, fileName);
|
|
596
1334
|
if (!fs_1.default.existsSync(migrationPath)) {
|
|
@@ -609,6 +1347,29 @@ class NodeSQLiteAdapter extends base_adapter_js_1.DatabaseAdapter {
|
|
|
609
1347
|
throw new Error(`Repair migration ${fileName} failed: ${message}`);
|
|
610
1348
|
}
|
|
611
1349
|
}
|
|
1350
|
+
rebuildWorkflowMemoryKind098(migrationsDir) {
|
|
1351
|
+
const migrationPath = path_1.default.join(migrationsDir, '098-workflow-memory-kind.sql');
|
|
1352
|
+
if (!fs_1.default.existsSync(migrationPath)) {
|
|
1353
|
+
throw new Error('Migration 098 workflow-kind rebuild SQL is missing');
|
|
1354
|
+
}
|
|
1355
|
+
const previousForeignKeys = this.readForeignKeysEnabled();
|
|
1356
|
+
this.exec('PRAGMA foreign_keys = OFF');
|
|
1357
|
+
if (this.readForeignKeysEnabled()) {
|
|
1358
|
+
throw new Error('Migration 098 could not disable foreign_keys before rebuilding decisions');
|
|
1359
|
+
}
|
|
1360
|
+
try {
|
|
1361
|
+
this.transaction(() => {
|
|
1362
|
+
this.exec(fs_1.default.readFileSync(migrationPath, 'utf8'));
|
|
1363
|
+
});
|
|
1364
|
+
const violations = this.prepare('PRAGMA foreign_key_check').all();
|
|
1365
|
+
if (violations.length > 0) {
|
|
1366
|
+
throw new Error('Migration 098 left foreign key violations');
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
finally {
|
|
1370
|
+
this.exec(`PRAGMA foreign_keys = ${previousForeignKeys ? 'ON' : 'OFF'}`);
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
612
1373
|
operatorMemoryCommitIntentTableSql() {
|
|
613
1374
|
const tableDefinition = this.prepare("SELECT sql FROM sqlite_master WHERE type='table' AND name = 'operator_memory_commit_intents'").get();
|
|
614
1375
|
return tableDefinition?.sql ?? '';
|
|
@@ -726,6 +1487,1090 @@ class NodeSQLiteAdapter extends base_adapter_js_1.DatabaseAdapter {
|
|
|
726
1487
|
this.prepare('INSERT OR IGNORE INTO schema_version (version) VALUES (?)').run(68);
|
|
727
1488
|
});
|
|
728
1489
|
}
|
|
1490
|
+
toolTracesTableSql() {
|
|
1491
|
+
const row = this.prepare("SELECT sql FROM sqlite_master WHERE type='table' AND name='tool_traces'").get();
|
|
1492
|
+
return row?.sql ?? '';
|
|
1493
|
+
}
|
|
1494
|
+
tableSql(name) {
|
|
1495
|
+
const row = this.prepare("SELECT sql FROM sqlite_master WHERE type='table' AND name = ?").get(name);
|
|
1496
|
+
if (!row?.sql) {
|
|
1497
|
+
throw new Error(`Missing CREATE TABLE definition for ${name}`);
|
|
1498
|
+
}
|
|
1499
|
+
return row.sql;
|
|
1500
|
+
}
|
|
1501
|
+
storedObjectsForTable(table, excludedIndexes = new Set()) {
|
|
1502
|
+
const indexes = this.prepare("SELECT name, sql FROM sqlite_master WHERE type='index' AND tbl_name = ? AND sql IS NOT NULL").all(table)
|
|
1503
|
+
.filter((row) => !excludedIndexes.has(row.name))
|
|
1504
|
+
.map((row) => row.sql);
|
|
1505
|
+
const triggers = this.prepare("SELECT sql FROM sqlite_master WHERE type='trigger' AND tbl_name = ? AND sql IS NOT NULL").all(table).map((row) => row.sql);
|
|
1506
|
+
return { indexes, triggers };
|
|
1507
|
+
}
|
|
1508
|
+
tablesInForeignKeyGraph(parent) {
|
|
1509
|
+
const graph = new Set([parent]);
|
|
1510
|
+
const tables = this.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'").all();
|
|
1511
|
+
for (const { name } of tables) {
|
|
1512
|
+
const refs = this.prepare('SELECT "table" AS ref FROM pragma_foreign_key_list(?)').all(name);
|
|
1513
|
+
if (refs.some((ref) => ref.ref === parent)) {
|
|
1514
|
+
graph.add(name);
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1517
|
+
return [...graph];
|
|
1518
|
+
}
|
|
1519
|
+
observationVersionsShape072() {
|
|
1520
|
+
if (!this.tableExists('observation_versions')) {
|
|
1521
|
+
return false;
|
|
1522
|
+
}
|
|
1523
|
+
const required = [
|
|
1524
|
+
'observation_id',
|
|
1525
|
+
'source',
|
|
1526
|
+
'source_id',
|
|
1527
|
+
'producer_version_id',
|
|
1528
|
+
'body',
|
|
1529
|
+
'body_location_json',
|
|
1530
|
+
'author',
|
|
1531
|
+
'source_at',
|
|
1532
|
+
'observed_at',
|
|
1533
|
+
'content_hash',
|
|
1534
|
+
'metadata_json',
|
|
1535
|
+
'scope_json',
|
|
1536
|
+
];
|
|
1537
|
+
const columns = this.tableColumns('observation_versions');
|
|
1538
|
+
const clauses = splitCreateTableClauses(this.tableSql('observation_versions'));
|
|
1539
|
+
const columnClauses = new Map(clauses
|
|
1540
|
+
.filter((clause) => !clauseIsTableConstraint(clause))
|
|
1541
|
+
.map((clause) => [clauseColumnName(clause), normalizeSqlText(clause)]));
|
|
1542
|
+
const bodyXor = normalizeSqlText('CHECK ((body IS NOT NULL AND body_location_json IS NULL) OR ' +
|
|
1543
|
+
'(body IS NULL AND body_location_json IS NOT NULL))');
|
|
1544
|
+
const columnInfo = new Map(this.prepare('PRAGMA table_info(observation_versions)').all().map((column) => [column.name, column]));
|
|
1545
|
+
// Whether any index points AT an observation is that index's own business.
|
|
1546
|
+
// This migration declares the evidence; it does not declare its readers.
|
|
1547
|
+
return (required.every((column) => columns.has(column)) &&
|
|
1548
|
+
required.every((column) => {
|
|
1549
|
+
const expected = ['source_at', 'observed_at'].includes(column) ? 'INTEGER' : 'TEXT';
|
|
1550
|
+
return columnInfo.get(column)?.type.toUpperCase() === expected;
|
|
1551
|
+
}) &&
|
|
1552
|
+
columnInfo.get('observation_id')?.pk === 1 &&
|
|
1553
|
+
['source', 'source_id', 'observed_at', 'content_hash', 'metadata_json', 'scope_json'].every((column) => columnInfo.get(column)?.notnull === 1) &&
|
|
1554
|
+
columnClauses.get('source') ===
|
|
1555
|
+
normalizeSqlText('source TEXT NOT NULL CHECK (length(trim(source)) > 0)') &&
|
|
1556
|
+
columnClauses.get('source_id') ===
|
|
1557
|
+
normalizeSqlText('source_id TEXT NOT NULL CHECK (length(trim(source_id)) > 0)') &&
|
|
1558
|
+
columnClauses.get('content_hash') ===
|
|
1559
|
+
normalizeSqlText('content_hash TEXT NOT NULL CHECK (length(trim(content_hash)) > 0)') &&
|
|
1560
|
+
clauses.some((clause) => normalizeSqlText(clause) === bodyXor) &&
|
|
1561
|
+
this.indexExists('observation_source_versions') &&
|
|
1562
|
+
normalizeSqlText(this.indexSql('observation_source_versions')).includes('onobservation_versions(source,source_id,observed_at,observation_id)'));
|
|
1563
|
+
}
|
|
1564
|
+
needsObservationVersionsRepair072() {
|
|
1565
|
+
return !this.observationVersionsShape072() || !this.schemaVersionExists(72);
|
|
1566
|
+
}
|
|
1567
|
+
recoverObservationVersionsMigration072() {
|
|
1568
|
+
if (this.observationVersionsShape072()) {
|
|
1569
|
+
this.transaction(() => {
|
|
1570
|
+
this.prepare('INSERT OR IGNORE INTO schema_version (version, description) VALUES (?, ?)').run(72, 'Immutable connector and owner observation versions');
|
|
1571
|
+
});
|
|
1572
|
+
return;
|
|
1573
|
+
}
|
|
1574
|
+
const canonical = new Map([
|
|
1575
|
+
['observation_id', 'observation_id TEXT PRIMARY KEY'],
|
|
1576
|
+
['source', 'source TEXT NOT NULL CHECK (length(trim(source)) > 0)'],
|
|
1577
|
+
['source_id', 'source_id TEXT NOT NULL CHECK (length(trim(source_id)) > 0)'],
|
|
1578
|
+
['producer_version_id', 'producer_version_id TEXT'],
|
|
1579
|
+
['body', 'body TEXT'],
|
|
1580
|
+
['body_location_json', 'body_location_json TEXT'],
|
|
1581
|
+
['author', 'author TEXT'],
|
|
1582
|
+
['source_at', 'source_at INTEGER'],
|
|
1583
|
+
['observed_at', 'observed_at INTEGER NOT NULL'],
|
|
1584
|
+
['content_hash', 'content_hash TEXT NOT NULL CHECK (length(trim(content_hash)) > 0)'],
|
|
1585
|
+
['metadata_json', 'metadata_json TEXT NOT NULL'],
|
|
1586
|
+
['scope_json', 'scope_json TEXT NOT NULL'],
|
|
1587
|
+
]);
|
|
1588
|
+
let existingNames = [];
|
|
1589
|
+
let columnDefs = [...canonical.values()];
|
|
1590
|
+
let constraints = [
|
|
1591
|
+
'CHECK ((body IS NOT NULL AND body_location_json IS NULL) OR ' +
|
|
1592
|
+
'(body IS NULL AND body_location_json IS NOT NULL))',
|
|
1593
|
+
];
|
|
1594
|
+
const bodyXor = normalizeSqlText(constraints[0]);
|
|
1595
|
+
let objects = { indexes: [], triggers: [] };
|
|
1596
|
+
if (this.tableExists('observation_versions')) {
|
|
1597
|
+
const clauses = splitCreateTableClauses(this.tableSql('observation_versions'));
|
|
1598
|
+
if (clauses.length === 0) {
|
|
1599
|
+
throw new Error('Migration 072 recovery failed: unreadable observation_versions');
|
|
1600
|
+
}
|
|
1601
|
+
const columns = clauses.filter((clause) => !clauseIsTableConstraint(clause));
|
|
1602
|
+
existingNames = columns.map(clauseColumnName);
|
|
1603
|
+
const missing = [...canonical.keys()].filter((name) => !existingNames.includes(name));
|
|
1604
|
+
const count = this.prepare('SELECT COUNT(*) AS count FROM observation_versions').get().count;
|
|
1605
|
+
if (missing.length > 0 && count > 0) {
|
|
1606
|
+
throw new Error(`Migration 072 cannot preserve populated rows missing columns: ${missing.join(', ')}`);
|
|
1607
|
+
}
|
|
1608
|
+
for (const clause of columns) {
|
|
1609
|
+
const name = clauseColumnName(clause);
|
|
1610
|
+
if (!canonical.has(name)) {
|
|
1611
|
+
continue;
|
|
1612
|
+
}
|
|
1613
|
+
const normalized = normalizeSqlText(clause);
|
|
1614
|
+
const expected = normalizeSqlText(canonical.get(name));
|
|
1615
|
+
const legacyExpected = normalizeSqlText(name === 'observation_id'
|
|
1616
|
+
? 'observation_id TEXT PRIMARY KEY'
|
|
1617
|
+
: [
|
|
1618
|
+
'source',
|
|
1619
|
+
'source_id',
|
|
1620
|
+
'observed_at',
|
|
1621
|
+
'content_hash',
|
|
1622
|
+
'metadata_json',
|
|
1623
|
+
'scope_json',
|
|
1624
|
+
].includes(name)
|
|
1625
|
+
? `${name} ${['observed_at', 'source_at'].includes(name) ? 'INTEGER' : 'TEXT'} NOT NULL`
|
|
1626
|
+
: `${name} ${name === 'source_at' ? 'INTEGER' : 'TEXT'}`);
|
|
1627
|
+
if (normalized !== expected &&
|
|
1628
|
+
normalized !== legacyExpected &&
|
|
1629
|
+
/collate|generated|references|\bunique\b|\bcheck\b/i.test(clause)) {
|
|
1630
|
+
throw new Error(`Migration 072 cannot safely preserve inline constraint on ${name}`);
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
const extras = columns.filter((clause) => !canonical.has(clauseColumnName(clause)));
|
|
1634
|
+
columnDefs = [...canonical.values(), ...extras];
|
|
1635
|
+
const customConstraints = clauses.filter((clause) => clauseIsTableConstraint(clause) &&
|
|
1636
|
+
!(normalizeSqlText(clause).includes('bodyisnotnullandbody_location_jsonisnull') &&
|
|
1637
|
+
normalizeSqlText(clause).includes('bodyisnullandbody_location_jsonisnotnull')));
|
|
1638
|
+
for (const constraint of clauses.filter(clauseIsTableConstraint)) {
|
|
1639
|
+
const normalizedConstraint = normalizeSqlText(constraint);
|
|
1640
|
+
if ((normalizedConstraint.includes('body') ||
|
|
1641
|
+
normalizedConstraint.includes('body_location_json')) &&
|
|
1642
|
+
normalizedConstraint !== bodyXor) {
|
|
1643
|
+
throw new Error('Migration 072 cannot safely preserve conflicting body CHECK');
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
constraints = [...constraints, ...customConstraints];
|
|
1647
|
+
objects = this.storedObjectsForTable('observation_versions', new Set(['observation_source_versions']));
|
|
1648
|
+
}
|
|
1649
|
+
const previousForeignKeys = this.readForeignKeysEnabled();
|
|
1650
|
+
this.exec('PRAGMA foreign_keys = OFF');
|
|
1651
|
+
if (this.readForeignKeysEnabled()) {
|
|
1652
|
+
throw new Error('Migration 072 recovery failed: could not disable foreign_keys');
|
|
1653
|
+
}
|
|
1654
|
+
try {
|
|
1655
|
+
this.transaction(() => {
|
|
1656
|
+
if (this.tableExists('observation_versions')) {
|
|
1657
|
+
this.exec(`CREATE TABLE observation_versions_072_new (\n ${[...columnDefs, ...constraints].join(',\n ')}\n)`);
|
|
1658
|
+
if (existingNames.length > 0) {
|
|
1659
|
+
const columns = existingNames.map(quoteSqlIdentifier).join(', ');
|
|
1660
|
+
this.exec(`INSERT INTO observation_versions_072_new (${columns}) SELECT ${columns} FROM observation_versions`);
|
|
1661
|
+
}
|
|
1662
|
+
this.exec('DROP TABLE observation_versions');
|
|
1663
|
+
this.exec('ALTER TABLE observation_versions_072_new RENAME TO observation_versions');
|
|
1664
|
+
}
|
|
1665
|
+
else {
|
|
1666
|
+
this.exec(`CREATE TABLE observation_versions (\n ${[...columnDefs, ...constraints].join(',\n ')}\n)`);
|
|
1667
|
+
}
|
|
1668
|
+
for (const sql of objects.indexes) {
|
|
1669
|
+
this.exec(sql);
|
|
1670
|
+
}
|
|
1671
|
+
this.exec('CREATE INDEX observation_source_versions ON observation_versions(source, source_id, observed_at, observation_id)');
|
|
1672
|
+
for (const sql of objects.triggers) {
|
|
1673
|
+
this.exec(sql);
|
|
1674
|
+
}
|
|
1675
|
+
if (!this.observationVersionsShape072()) {
|
|
1676
|
+
throw new Error('Migration 072 recovery failed: incomplete observation shape');
|
|
1677
|
+
}
|
|
1678
|
+
for (const table of this.tablesInForeignKeyGraph('observation_versions')) {
|
|
1679
|
+
if (this.prepare('SELECT 1 FROM pragma_foreign_key_check(?)').all(table).length > 0) {
|
|
1680
|
+
throw new Error(`Migration 072 recovery failed: foreign key violations after rebuild (${table})`);
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
this.prepare('INSERT OR IGNORE INTO schema_version (version, description) VALUES (?, ?)').run(72, 'Immutable connector and owner observation versions');
|
|
1684
|
+
});
|
|
1685
|
+
}
|
|
1686
|
+
finally {
|
|
1687
|
+
this.exec(`PRAGMA foreign_keys = ${previousForeignKeys ? 'ON' : 'OFF'}`);
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1690
|
+
registryCorrectionsShape073() {
|
|
1691
|
+
const expected = new Map([
|
|
1692
|
+
['registry_identity_state', ['singleton', 'revision']],
|
|
1693
|
+
[
|
|
1694
|
+
'registry_corrections',
|
|
1695
|
+
[
|
|
1696
|
+
'command_id',
|
|
1697
|
+
'operation',
|
|
1698
|
+
'expected_revision',
|
|
1699
|
+
'committed_revision',
|
|
1700
|
+
'principal_id',
|
|
1701
|
+
'agent_id',
|
|
1702
|
+
'origin',
|
|
1703
|
+
'payload_json',
|
|
1704
|
+
'reason',
|
|
1705
|
+
'evidence_json',
|
|
1706
|
+
'scope_json',
|
|
1707
|
+
'receipt_json',
|
|
1708
|
+
'created_at',
|
|
1709
|
+
],
|
|
1710
|
+
],
|
|
1711
|
+
[
|
|
1712
|
+
'registry_ref_assignments',
|
|
1713
|
+
[
|
|
1714
|
+
'command_id',
|
|
1715
|
+
'edge_id',
|
|
1716
|
+
'endpoint',
|
|
1717
|
+
'original_kind',
|
|
1718
|
+
'original_id',
|
|
1719
|
+
'resolved_node_id',
|
|
1720
|
+
'committed_revision',
|
|
1721
|
+
'created_at',
|
|
1722
|
+
],
|
|
1723
|
+
],
|
|
1724
|
+
]);
|
|
1725
|
+
if (!([...expected].every(([table, columns]) => this.tableExists(table) &&
|
|
1726
|
+
columns.every((column) => this.tableColumns(table).has(column))) &&
|
|
1727
|
+
this.indexExists('idx_registry_ref_assignments_current') &&
|
|
1728
|
+
normalizeSqlText(this.indexSql('idx_registry_ref_assignments_current')).includes('onregistry_ref_assignments(edge_id,endpoint,committed_revisiondesc)'))) {
|
|
1729
|
+
return false;
|
|
1730
|
+
}
|
|
1731
|
+
const stateSql = normalizeSqlText(this.tableSql('registry_identity_state'));
|
|
1732
|
+
const correctionsSql = normalizeSqlText(this.tableSql('registry_corrections'));
|
|
1733
|
+
const assignmentsSql = normalizeSqlText(this.tableSql('registry_ref_assignments'));
|
|
1734
|
+
const correctionClauses = splitCreateTableClauses(this.tableSql('registry_corrections'));
|
|
1735
|
+
const aggregateAuthorityCheck = normalizeSqlText(`CHECK (
|
|
1736
|
+
(origin = 'trusted' AND principal_id IS NOT NULL AND length(trim(principal_id)) > 0
|
|
1737
|
+
AND agent_id IS NOT NULL AND length(trim(agent_id)) > 0)
|
|
1738
|
+
OR (origin = 'legacy_unattributed' AND principal_id IS NULL AND agent_id IS NULL)
|
|
1739
|
+
)`);
|
|
1740
|
+
const stateRows = this.prepare('SELECT singleton, revision FROM registry_identity_state ORDER BY singleton').all();
|
|
1741
|
+
const correctionInfo = new Map(this.prepare('PRAGMA table_info(registry_corrections)').all().map((column) => [column.name, column]));
|
|
1742
|
+
const assignmentInfo = new Map(this.prepare('PRAGMA table_info(registry_ref_assignments)').all().map((column) => [column.name, column]));
|
|
1743
|
+
const assignmentFks = this.prepare('SELECT "from" AS from_col, "table" AS target, "to" AS to_col, on_delete FROM pragma_foreign_key_list(?)').all('registry_ref_assignments');
|
|
1744
|
+
return (stateSql.includes('singletonintegerprimarykeycheck(singleton=1)') &&
|
|
1745
|
+
stateSql.includes('revisionintegernotnullcheck(revision>=0)') &&
|
|
1746
|
+
stateRows.length === 1 &&
|
|
1747
|
+
stateRows[0]?.singleton === 1 &&
|
|
1748
|
+
Number.isSafeInteger(stateRows[0]?.revision) &&
|
|
1749
|
+
(stateRows[0]?.revision ?? -1) >= 0 &&
|
|
1750
|
+
correctionInfo.get('command_id')?.pk === 1 &&
|
|
1751
|
+
[
|
|
1752
|
+
'command_id',
|
|
1753
|
+
'operation',
|
|
1754
|
+
'expected_revision',
|
|
1755
|
+
'committed_revision',
|
|
1756
|
+
'principal_id',
|
|
1757
|
+
'agent_id',
|
|
1758
|
+
'origin',
|
|
1759
|
+
'payload_json',
|
|
1760
|
+
'reason',
|
|
1761
|
+
'evidence_json',
|
|
1762
|
+
'scope_json',
|
|
1763
|
+
'receipt_json',
|
|
1764
|
+
'created_at',
|
|
1765
|
+
].every((name) => {
|
|
1766
|
+
const column = correctionInfo.get(name);
|
|
1767
|
+
return ['expected_revision', 'committed_revision', 'created_at'].includes(name)
|
|
1768
|
+
? column?.type.toUpperCase() === 'INTEGER'
|
|
1769
|
+
: column?.type.toUpperCase() === 'TEXT';
|
|
1770
|
+
}) &&
|
|
1771
|
+
[
|
|
1772
|
+
'operation',
|
|
1773
|
+
'expected_revision',
|
|
1774
|
+
'committed_revision',
|
|
1775
|
+
'origin',
|
|
1776
|
+
'payload_json',
|
|
1777
|
+
'reason',
|
|
1778
|
+
'evidence_json',
|
|
1779
|
+
'scope_json',
|
|
1780
|
+
'receipt_json',
|
|
1781
|
+
'created_at',
|
|
1782
|
+
].every((column) => correctionInfo.get(column)?.notnull === 1) &&
|
|
1783
|
+
correctionsSql.includes("operationin('add_alias','merge','split','assign_refs')") &&
|
|
1784
|
+
correctionsSql.includes('expected_revision>=0') &&
|
|
1785
|
+
correctionsSql.includes('committed_revisionintegernotnullunique') &&
|
|
1786
|
+
correctionsSql.includes('committed_revision>expected_revision') &&
|
|
1787
|
+
correctionsSql.includes("originin('trusted','legacy_unattributed')") &&
|
|
1788
|
+
correctionsSql.includes('length(trim(reason))>0') &&
|
|
1789
|
+
correctionClauses.some((clause) => normalizeSqlText(clause) === aggregateAuthorityCheck) &&
|
|
1790
|
+
assignmentInfo.get('command_id')?.pk === 1 &&
|
|
1791
|
+
[
|
|
1792
|
+
'command_id',
|
|
1793
|
+
'edge_id',
|
|
1794
|
+
'endpoint',
|
|
1795
|
+
'original_kind',
|
|
1796
|
+
'original_id',
|
|
1797
|
+
'resolved_node_id',
|
|
1798
|
+
'committed_revision',
|
|
1799
|
+
'created_at',
|
|
1800
|
+
].every((name) => {
|
|
1801
|
+
const column = assignmentInfo.get(name);
|
|
1802
|
+
return ['committed_revision', 'created_at'].includes(name)
|
|
1803
|
+
? column?.type.toUpperCase() === 'INTEGER'
|
|
1804
|
+
: column?.type.toUpperCase() === 'TEXT';
|
|
1805
|
+
}) &&
|
|
1806
|
+
assignmentInfo.get('edge_id')?.pk === 2 &&
|
|
1807
|
+
assignmentInfo.get('endpoint')?.pk === 3 &&
|
|
1808
|
+
[
|
|
1809
|
+
'command_id',
|
|
1810
|
+
'edge_id',
|
|
1811
|
+
'endpoint',
|
|
1812
|
+
'original_kind',
|
|
1813
|
+
'original_id',
|
|
1814
|
+
'committed_revision',
|
|
1815
|
+
'created_at',
|
|
1816
|
+
].every((column) => assignmentInfo.get(column)?.notnull === 1) &&
|
|
1817
|
+
assignmentInfo.get('resolved_node_id')?.notnull === 0 &&
|
|
1818
|
+
assignmentsSql.includes("endpointin('from','to')") &&
|
|
1819
|
+
assignmentFks.some((fk) => fk.from_col === 'command_id' &&
|
|
1820
|
+
fk.target === 'registry_corrections' &&
|
|
1821
|
+
fk.to_col === 'command_id' &&
|
|
1822
|
+
fk.on_delete.toUpperCase() === 'CASCADE') &&
|
|
1823
|
+
assignmentFks.some((fk) => fk.from_col === 'resolved_node_id' && fk.target === 'registry_nodes' && fk.to_col === 'id'));
|
|
1824
|
+
}
|
|
1825
|
+
needsRegistryCorrectionsRepair073() {
|
|
1826
|
+
return !this.registryCorrectionsShape073() || !this.schemaVersionExists(73);
|
|
1827
|
+
}
|
|
1828
|
+
recoverRegistryCorrectionsMigration073(migrationsDir) {
|
|
1829
|
+
const tables = ['registry_identity_state', 'registry_corrections', 'registry_ref_assignments'];
|
|
1830
|
+
const malformed = tables.filter((table) => this.tableExists(table) &&
|
|
1831
|
+
!(table === 'registry_identity_state'
|
|
1832
|
+
? ['singleton', 'revision']
|
|
1833
|
+
: table === 'registry_corrections'
|
|
1834
|
+
? [
|
|
1835
|
+
'command_id',
|
|
1836
|
+
'operation',
|
|
1837
|
+
'expected_revision',
|
|
1838
|
+
'committed_revision',
|
|
1839
|
+
'principal_id',
|
|
1840
|
+
'agent_id',
|
|
1841
|
+
'origin',
|
|
1842
|
+
'payload_json',
|
|
1843
|
+
'reason',
|
|
1844
|
+
'evidence_json',
|
|
1845
|
+
'scope_json',
|
|
1846
|
+
'receipt_json',
|
|
1847
|
+
'created_at',
|
|
1848
|
+
]
|
|
1849
|
+
: [
|
|
1850
|
+
'command_id',
|
|
1851
|
+
'edge_id',
|
|
1852
|
+
'endpoint',
|
|
1853
|
+
'original_kind',
|
|
1854
|
+
'original_id',
|
|
1855
|
+
'resolved_node_id',
|
|
1856
|
+
'committed_revision',
|
|
1857
|
+
'created_at',
|
|
1858
|
+
]).every((column) => this.tableColumns(table).has(column)));
|
|
1859
|
+
const allTablesPresent = tables.every((table) => this.tableExists(table));
|
|
1860
|
+
for (const table of tables) {
|
|
1861
|
+
if (!this.tableExists(table)) {
|
|
1862
|
+
continue;
|
|
1863
|
+
}
|
|
1864
|
+
const requiredColumns = table === 'registry_identity_state'
|
|
1865
|
+
? ['singleton', 'revision']
|
|
1866
|
+
: table === 'registry_corrections'
|
|
1867
|
+
? [
|
|
1868
|
+
'command_id',
|
|
1869
|
+
'operation',
|
|
1870
|
+
'expected_revision',
|
|
1871
|
+
'committed_revision',
|
|
1872
|
+
'principal_id',
|
|
1873
|
+
'agent_id',
|
|
1874
|
+
'origin',
|
|
1875
|
+
'payload_json',
|
|
1876
|
+
'reason',
|
|
1877
|
+
'evidence_json',
|
|
1878
|
+
'scope_json',
|
|
1879
|
+
'receipt_json',
|
|
1880
|
+
'created_at',
|
|
1881
|
+
]
|
|
1882
|
+
: [
|
|
1883
|
+
'command_id',
|
|
1884
|
+
'edge_id',
|
|
1885
|
+
'endpoint',
|
|
1886
|
+
'original_kind',
|
|
1887
|
+
'original_id',
|
|
1888
|
+
'resolved_node_id',
|
|
1889
|
+
'committed_revision',
|
|
1890
|
+
'created_at',
|
|
1891
|
+
];
|
|
1892
|
+
if (allTablesPresent &&
|
|
1893
|
+
requiredColumns.every((column) => this.tableColumns(table).has(column)) &&
|
|
1894
|
+
!this.registryCorrectionsShape073()) {
|
|
1895
|
+
throw new Error(`Migration 073 cannot safely repair structurally incompatible table ${table}`);
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1898
|
+
for (const table of malformed) {
|
|
1899
|
+
const count = this.prepare(`SELECT COUNT(*) AS count FROM ${quoteSqlIdentifier(table)}`).get().count;
|
|
1900
|
+
if (count > 0) {
|
|
1901
|
+
throw new Error(`Migration 073 cannot safely repair populated incompatible table ${table}`);
|
|
1902
|
+
}
|
|
1903
|
+
const excluded = table === 'registry_ref_assignments'
|
|
1904
|
+
? new Set(['idx_registry_ref_assignments_current'])
|
|
1905
|
+
: new Set();
|
|
1906
|
+
const objects = this.storedObjectsForTable(table, excluded);
|
|
1907
|
+
if (objects.indexes.length > 0 ||
|
|
1908
|
+
objects.triggers.length > 0 ||
|
|
1909
|
+
this.tablesInForeignKeyGraph(table).length > 1 ||
|
|
1910
|
+
/\b(?:check|unique|references)\b/i.test(this.tableSql(table))) {
|
|
1911
|
+
throw new Error(`Migration 073 cannot safely replace incompatible table ${table}`);
|
|
1912
|
+
}
|
|
1913
|
+
}
|
|
1914
|
+
const sql = fs_1.default.readFileSync(path_1.default.join(migrationsDir, '073-registry-corrections.sql'), 'utf8');
|
|
1915
|
+
this.transaction(() => {
|
|
1916
|
+
if (malformed.includes('registry_ref_assignments')) {
|
|
1917
|
+
this.exec('DROP TABLE registry_ref_assignments');
|
|
1918
|
+
}
|
|
1919
|
+
if (malformed.includes('registry_corrections')) {
|
|
1920
|
+
this.exec('DROP TABLE registry_corrections');
|
|
1921
|
+
}
|
|
1922
|
+
if (malformed.includes('registry_identity_state')) {
|
|
1923
|
+
this.exec('DROP TABLE registry_identity_state');
|
|
1924
|
+
}
|
|
1925
|
+
this.exec(sql);
|
|
1926
|
+
if (!this.registryCorrectionsShape073()) {
|
|
1927
|
+
throw new Error('Migration 073 recovery failed: incomplete correction shape');
|
|
1928
|
+
}
|
|
1929
|
+
this.prepare('INSERT OR IGNORE INTO schema_version (version, description) VALUES (?, ?)').run(73, 'Atomic scoped registry correction history');
|
|
1930
|
+
});
|
|
1931
|
+
}
|
|
1932
|
+
workGraphRefsShape074() {
|
|
1933
|
+
if (!this.tableExists('twin_edges')) {
|
|
1934
|
+
return false;
|
|
1935
|
+
}
|
|
1936
|
+
const clauses = splitCreateTableClauses(this.tableSql('twin_edges'));
|
|
1937
|
+
const columnClauses = new Map(clauses
|
|
1938
|
+
.filter((clause) => !clauseIsTableConstraint(clause))
|
|
1939
|
+
.map((clause) => [clauseColumnName(clause), normalizeSqlText(clause)]));
|
|
1940
|
+
// 079 extends the relation list with 'refines','contradicts','amends'; a
|
|
1941
|
+
// table already at that shape satisfies the 074 endpoint contract too.
|
|
1942
|
+
const edgeTypeClause = clauses.find((clause) => !clauseIsTableConstraint(clause) && clauseColumnName(clause) === 'edge_type');
|
|
1943
|
+
const edgeTypeOk = edgeTypeClause !== undefined &&
|
|
1944
|
+
[
|
|
1945
|
+
"edge_type TEXT NOT NULL CHECK (edge_type IN ('supersedes','builds_on','debates','synthesizes','mentions','derived_from','case_member','alias_of','next_action_for','blocks'))",
|
|
1946
|
+
"edge_type TEXT NOT NULL CHECK (edge_type IN ('supersedes','refines','contradicts','builds_on','debates','synthesizes','mentions','derived_from','case_member','alias_of','next_action_for','blocks','amends'))",
|
|
1947
|
+
].some((definition) => normalizeSqlText(edgeTypeClause) === normalizeSqlText(definition));
|
|
1948
|
+
const exactColumns = new Map([
|
|
1949
|
+
[
|
|
1950
|
+
'confidence',
|
|
1951
|
+
'confidence REAL NOT NULL DEFAULT 1.0 CHECK (confidence >= 0.0 AND confidence <= 1.0)',
|
|
1952
|
+
],
|
|
1953
|
+
['source', "source TEXT NOT NULL CHECK (source IN ('agent','human','code'))"],
|
|
1954
|
+
['content_hash', 'content_hash BLOB NOT NULL CHECK(length(content_hash)=32)'],
|
|
1955
|
+
]);
|
|
1956
|
+
const required = [
|
|
1957
|
+
'edge_id',
|
|
1958
|
+
'edge_type',
|
|
1959
|
+
'subject_kind',
|
|
1960
|
+
'subject_id',
|
|
1961
|
+
'object_kind',
|
|
1962
|
+
'object_id',
|
|
1963
|
+
'relation_attrs_json',
|
|
1964
|
+
'confidence',
|
|
1965
|
+
'source',
|
|
1966
|
+
'agent_id',
|
|
1967
|
+
'model_run_id',
|
|
1968
|
+
'envelope_hash',
|
|
1969
|
+
'human_actor_id',
|
|
1970
|
+
'human_actor_role',
|
|
1971
|
+
'authority_scope_json',
|
|
1972
|
+
'reason_classification',
|
|
1973
|
+
'reason_text',
|
|
1974
|
+
'evidence_refs_json',
|
|
1975
|
+
'request_idempotency_key',
|
|
1976
|
+
'edge_idempotency_key',
|
|
1977
|
+
'content_hash',
|
|
1978
|
+
'created_at',
|
|
1979
|
+
];
|
|
1980
|
+
const info = new Map(this.prepare('PRAGMA table_info(twin_edges)').all().map((column) => [column.name, column]));
|
|
1981
|
+
return (required.every((column) => info.has(column)) &&
|
|
1982
|
+
required.every((column) => {
|
|
1983
|
+
const expected = column === 'confidence'
|
|
1984
|
+
? 'REAL'
|
|
1985
|
+
: column === 'created_at'
|
|
1986
|
+
? 'INTEGER'
|
|
1987
|
+
: column === 'content_hash'
|
|
1988
|
+
? 'BLOB'
|
|
1989
|
+
: 'TEXT';
|
|
1990
|
+
return info.get(column)?.type.toUpperCase() === expected;
|
|
1991
|
+
}) &&
|
|
1992
|
+
info.get('edge_id')?.pk === 1 &&
|
|
1993
|
+
[
|
|
1994
|
+
'edge_type',
|
|
1995
|
+
'subject_kind',
|
|
1996
|
+
'subject_id',
|
|
1997
|
+
'object_kind',
|
|
1998
|
+
'object_id',
|
|
1999
|
+
'confidence',
|
|
2000
|
+
'source',
|
|
2001
|
+
'content_hash',
|
|
2002
|
+
'created_at',
|
|
2003
|
+
].every((column) => info.get(column)?.notnull === 1) &&
|
|
2004
|
+
edgeTypeOk &&
|
|
2005
|
+
[...exactColumns].every(([name, definition]) => columnClauses.get(name) === normalizeSqlText(definition)) &&
|
|
2006
|
+
clauses.some((clause) => !clauseIsTableConstraint(clause) &&
|
|
2007
|
+
clauseColumnName(clause) === 'subject_kind' &&
|
|
2008
|
+
normalizeSqlText(clause) ===
|
|
2009
|
+
normalizeSqlText("subject_kind TEXT NOT NULL CHECK (subject_kind IN ('memory','case','entity','report','edge','registry','observation'))")) &&
|
|
2010
|
+
clauses.some((clause) => !clauseIsTableConstraint(clause) &&
|
|
2011
|
+
clauseColumnName(clause) === 'object_kind' &&
|
|
2012
|
+
normalizeSqlText(clause) ===
|
|
2013
|
+
normalizeSqlText("object_kind TEXT NOT NULL CHECK (object_kind IN ('memory','case','entity','report','edge','raw','registry','observation'))")) &&
|
|
2014
|
+
!clauses.some((clause) => clauseIsTableConstraint(clause) && /\b(?:subject_kind|object_kind)\b/i.test(clause)) &&
|
|
2015
|
+
this.indexExists('idx_twin_edges_subject') &&
|
|
2016
|
+
this.indexExists('idx_twin_edges_object') &&
|
|
2017
|
+
this.indexExists('idx_twin_edges_model_run_id') &&
|
|
2018
|
+
this.indexExists('idx_twin_edges_request_idempotency') &&
|
|
2019
|
+
this.indexExists('ux_twin_edges_model_run_edge_idempotency') &&
|
|
2020
|
+
normalizeSqlText(this.indexSql('idx_twin_edges_subject')).includes('ontwin_edges(subject_kind,subject_id,created_atdesc)') &&
|
|
2021
|
+
normalizeSqlText(this.indexSql('idx_twin_edges_object')).includes('ontwin_edges(object_kind,object_id,created_atdesc)') &&
|
|
2022
|
+
normalizeSqlText(this.indexSql('idx_twin_edges_model_run_id')).includes('ontwin_edges(model_run_id,created_atdesc)') &&
|
|
2023
|
+
normalizeSqlText(this.indexSql('idx_twin_edges_request_idempotency')).includes('ontwin_edges(model_run_id,request_idempotency_key,created_atdesc)wheremodel_run_idisnotnullandrequest_idempotency_keyisnotnull') &&
|
|
2024
|
+
normalizeSqlText(this.indexSql('ux_twin_edges_model_run_edge_idempotency')).includes('uniqueindexux_twin_edges_model_run_edge_idempotencyontwin_edges(model_run_id,edge_idempotency_key)wheremodel_run_idisnotnullandedge_idempotency_keyisnotnull'));
|
|
2025
|
+
}
|
|
2026
|
+
needsWorkGraphRefsRepair074() {
|
|
2027
|
+
return !this.workGraphRefsShape074() || !this.schemaVersionExists(74);
|
|
2028
|
+
}
|
|
2029
|
+
recoverWorkGraphRefsMigration074() {
|
|
2030
|
+
if (!this.tableExists('twin_edges')) {
|
|
2031
|
+
throw new Error('Migration 074 recovery failed: missing twin_edges');
|
|
2032
|
+
}
|
|
2033
|
+
if (this.workGraphRefsShape074()) {
|
|
2034
|
+
this.transaction(() => {
|
|
2035
|
+
this.prepare('INSERT OR IGNORE INTO schema_version (version, description) VALUES (?, ?)').run(74, 'Registry and observation work graph references');
|
|
2036
|
+
});
|
|
2037
|
+
return;
|
|
2038
|
+
}
|
|
2039
|
+
const clauses = splitCreateTableClauses(this.tableSql('twin_edges'));
|
|
2040
|
+
if (clauses.length === 0) {
|
|
2041
|
+
throw new Error('Migration 074 recovery failed: unreadable twin_edges');
|
|
2042
|
+
}
|
|
2043
|
+
const existingColumns = [];
|
|
2044
|
+
const columns = [];
|
|
2045
|
+
const constraints = [];
|
|
2046
|
+
const subjectDef = "subject_kind TEXT NOT NULL CHECK (subject_kind IN ('memory','case','entity','report','edge','registry','observation'))";
|
|
2047
|
+
const objectDef = "object_kind TEXT NOT NULL CHECK (object_kind IN ('memory','case','entity','report','edge','raw','registry','observation'))";
|
|
2048
|
+
const legacySubjectDef = normalizeSqlText("subject_kind TEXT NOT NULL CHECK (subject_kind IN ('memory','case','entity','report','edge'))");
|
|
2049
|
+
const legacyObjectDef = normalizeSqlText("object_kind TEXT NOT NULL CHECK (object_kind IN ('memory','case','entity','report','edge','raw'))");
|
|
2050
|
+
const exactProtectedColumns = new Map([
|
|
2051
|
+
[
|
|
2052
|
+
'confidence',
|
|
2053
|
+
'confidence REAL NOT NULL DEFAULT 1.0 CHECK (confidence >= 0.0 AND confidence <= 1.0)',
|
|
2054
|
+
],
|
|
2055
|
+
['source', "source TEXT NOT NULL CHECK (source IN ('agent','human','code'))"],
|
|
2056
|
+
['content_hash', 'content_hash BLOB NOT NULL CHECK(length(content_hash)=32)'],
|
|
2057
|
+
]);
|
|
2058
|
+
// The edge_type CHECK was extended by migration 079; a table carrying the
|
|
2059
|
+
// newer relation list is preserved as-is instead of being rebuilt down.
|
|
2060
|
+
const allowedEdgeTypeDefs = new Set([
|
|
2061
|
+
"edge_type TEXT NOT NULL CHECK (edge_type IN ('supersedes','builds_on','debates','synthesizes','mentions','derived_from','case_member','alias_of','next_action_for','blocks'))",
|
|
2062
|
+
"edge_type TEXT NOT NULL CHECK (edge_type IN ('supersedes','refines','contradicts','builds_on','debates','synthesizes','mentions','derived_from','case_member','alias_of','next_action_for','blocks','amends'))",
|
|
2063
|
+
].map(normalizeSqlText));
|
|
2064
|
+
for (const clause of clauses) {
|
|
2065
|
+
if (clauseIsTableConstraint(clause)) {
|
|
2066
|
+
if (/\b(?:subject_kind|object_kind)\b/i.test(clause)) {
|
|
2067
|
+
throw new Error('Migration 074 cannot safely preserve conflicting endpoint CHECK');
|
|
2068
|
+
}
|
|
2069
|
+
constraints.push(clause);
|
|
2070
|
+
continue;
|
|
2071
|
+
}
|
|
2072
|
+
const name = clauseColumnName(clause);
|
|
2073
|
+
existingColumns.push(name);
|
|
2074
|
+
if (name === 'subject_kind') {
|
|
2075
|
+
const normalizedClause = normalizeSqlText(clause);
|
|
2076
|
+
if (normalizedClause !== legacySubjectDef &&
|
|
2077
|
+
normalizedClause !== normalizeSqlText(subjectDef)) {
|
|
2078
|
+
throw new Error('Migration 074 cannot safely preserve inline constraint on subject_kind');
|
|
2079
|
+
}
|
|
2080
|
+
columns.push(subjectDef);
|
|
2081
|
+
}
|
|
2082
|
+
else if (name === 'object_kind') {
|
|
2083
|
+
const normalizedClause = normalizeSqlText(clause);
|
|
2084
|
+
if (normalizedClause !== legacyObjectDef &&
|
|
2085
|
+
normalizedClause !== normalizeSqlText(objectDef)) {
|
|
2086
|
+
throw new Error('Migration 074 cannot safely preserve inline constraint on object_kind');
|
|
2087
|
+
}
|
|
2088
|
+
columns.push(objectDef);
|
|
2089
|
+
}
|
|
2090
|
+
else if (name === 'edge_type') {
|
|
2091
|
+
if (!allowedEdgeTypeDefs.has(normalizeSqlText(clause))) {
|
|
2092
|
+
throw new Error('Migration 074 cannot safely preserve inline constraint on edge_type');
|
|
2093
|
+
}
|
|
2094
|
+
columns.push(clause);
|
|
2095
|
+
}
|
|
2096
|
+
else if (exactProtectedColumns.has(name)) {
|
|
2097
|
+
if (normalizeSqlText(clause) !== normalizeSqlText(exactProtectedColumns.get(name))) {
|
|
2098
|
+
throw new Error(`Migration 074 cannot safely preserve inline constraint on ${name}`);
|
|
2099
|
+
}
|
|
2100
|
+
columns.push(clause);
|
|
2101
|
+
}
|
|
2102
|
+
else {
|
|
2103
|
+
columns.push(clause);
|
|
2104
|
+
}
|
|
2105
|
+
}
|
|
2106
|
+
const objects = this.storedObjectsForTable('twin_edges', new Set([
|
|
2107
|
+
'idx_twin_edges_subject',
|
|
2108
|
+
'idx_twin_edges_object',
|
|
2109
|
+
'idx_twin_edges_model_run_id',
|
|
2110
|
+
'idx_twin_edges_request_idempotency',
|
|
2111
|
+
'ux_twin_edges_model_run_edge_idempotency',
|
|
2112
|
+
]));
|
|
2113
|
+
const previousForeignKeys = this.readForeignKeysEnabled();
|
|
2114
|
+
this.exec('PRAGMA foreign_keys = OFF');
|
|
2115
|
+
if (this.readForeignKeysEnabled()) {
|
|
2116
|
+
throw new Error('Migration 074 recovery failed: could not disable foreign_keys');
|
|
2117
|
+
}
|
|
2118
|
+
try {
|
|
2119
|
+
this.transaction(() => {
|
|
2120
|
+
this.exec(`CREATE TABLE twin_edges_074_new (\n ${[...columns, ...constraints].join(',\n ')}\n)`);
|
|
2121
|
+
const names = existingColumns.map(quoteSqlIdentifier).join(', ');
|
|
2122
|
+
this.exec(`INSERT INTO twin_edges_074_new (${names}) SELECT ${names} FROM twin_edges`);
|
|
2123
|
+
this.exec('DROP TABLE twin_edges');
|
|
2124
|
+
this.exec('ALTER TABLE twin_edges_074_new RENAME TO twin_edges');
|
|
2125
|
+
for (const sql of objects.indexes) {
|
|
2126
|
+
this.exec(sql);
|
|
2127
|
+
}
|
|
2128
|
+
this.exec(`
|
|
2129
|
+
CREATE INDEX idx_twin_edges_subject
|
|
2130
|
+
ON twin_edges(subject_kind, subject_id, created_at DESC);
|
|
2131
|
+
CREATE INDEX idx_twin_edges_object
|
|
2132
|
+
ON twin_edges(object_kind, object_id, created_at DESC);
|
|
2133
|
+
CREATE INDEX idx_twin_edges_model_run_id
|
|
2134
|
+
ON twin_edges(model_run_id, created_at DESC);
|
|
2135
|
+
CREATE INDEX idx_twin_edges_request_idempotency
|
|
2136
|
+
ON twin_edges(model_run_id, request_idempotency_key, created_at DESC)
|
|
2137
|
+
WHERE model_run_id IS NOT NULL AND request_idempotency_key IS NOT NULL;
|
|
2138
|
+
CREATE UNIQUE INDEX ux_twin_edges_model_run_edge_idempotency
|
|
2139
|
+
ON twin_edges(model_run_id, edge_idempotency_key)
|
|
2140
|
+
WHERE model_run_id IS NOT NULL AND edge_idempotency_key IS NOT NULL;
|
|
2141
|
+
`);
|
|
2142
|
+
for (const sql of objects.triggers) {
|
|
2143
|
+
this.exec(sql);
|
|
2144
|
+
}
|
|
2145
|
+
if (!this.workGraphRefsShape074()) {
|
|
2146
|
+
throw new Error('Migration 074 recovery failed: incomplete work graph shape');
|
|
2147
|
+
}
|
|
2148
|
+
for (const table of this.tablesInForeignKeyGraph('twin_edges')) {
|
|
2149
|
+
if (this.prepare('SELECT 1 FROM pragma_foreign_key_check(?)').all(table).length > 0) {
|
|
2150
|
+
throw new Error(`Migration 074 recovery failed: foreign key violations after rebuild (${table})`);
|
|
2151
|
+
}
|
|
2152
|
+
}
|
|
2153
|
+
this.prepare('INSERT OR IGNORE INTO schema_version (version, description) VALUES (?, ?)').run(74, 'Registry and observation work graph references');
|
|
2154
|
+
});
|
|
2155
|
+
}
|
|
2156
|
+
finally {
|
|
2157
|
+
this.exec(`PRAGMA foreign_keys = ${previousForeignKeys ? 'ON' : 'OFF'}`);
|
|
2158
|
+
}
|
|
2159
|
+
}
|
|
2160
|
+
/**
|
|
2161
|
+
* 079 endpoint shape: the 074 work-graph shape plus the extended edge_type
|
|
2162
|
+
* relation list ('refines','contradicts','amends') used by authored record
|
|
2163
|
+
* links. `workGraphRefsShape074` already accepts this edge_type variant, so
|
|
2164
|
+
* the extra check here is the only difference.
|
|
2165
|
+
*/
|
|
2166
|
+
twinEdgeRelationsShape079() {
|
|
2167
|
+
if (!this.tableExists('twin_edges')) {
|
|
2168
|
+
return false;
|
|
2169
|
+
}
|
|
2170
|
+
const clauses = splitCreateTableClauses(this.tableSql('twin_edges'));
|
|
2171
|
+
const edgeTypeClause = clauses.find((clause) => !clauseIsTableConstraint(clause) && clauseColumnName(clause) === 'edge_type');
|
|
2172
|
+
return (this.workGraphRefsShape074() &&
|
|
2173
|
+
edgeTypeClause !== undefined &&
|
|
2174
|
+
normalizeSqlText(edgeTypeClause) ===
|
|
2175
|
+
normalizeSqlText("edge_type TEXT NOT NULL CHECK (edge_type IN ('supersedes','refines','contradicts','builds_on','debates','synthesizes','mentions','derived_from','case_member','alias_of','next_action_for','blocks','amends'))") &&
|
|
2176
|
+
!clauses.some((clause) => clauseIsTableConstraint(clause) && /\bedge_type\b/i.test(clause)));
|
|
2177
|
+
}
|
|
2178
|
+
needsTwinEdgeRelationsRepair079() {
|
|
2179
|
+
return !this.twinEdgeRelationsShape079() || !this.schemaVersionExists(79);
|
|
2180
|
+
}
|
|
2181
|
+
/**
|
|
2182
|
+
* Rebuild twin_edges with the extended relation CHECK while preserving every
|
|
2183
|
+
* existing column (including runtime-added custom columns), index, trigger,
|
|
2184
|
+
* and foreign-key child - the same contract recoverWorkGraphRefsMigration074
|
|
2185
|
+
* honours. Only the edge_type clause is rewritten; a table already carrying
|
|
2186
|
+
* the extended list is just stamped.
|
|
2187
|
+
*/
|
|
2188
|
+
recoverTwinEdgeRelationsMigration079() {
|
|
2189
|
+
if (!this.tableExists('twin_edges')) {
|
|
2190
|
+
throw new Error('Migration 079 recovery failed: missing twin_edges');
|
|
2191
|
+
}
|
|
2192
|
+
if (this.twinEdgeRelationsShape079()) {
|
|
2193
|
+
this.transaction(() => {
|
|
2194
|
+
this.prepare('INSERT OR IGNORE INTO schema_version (version, description) VALUES (?, ?)').run(79, 'Twin edge relations for judgment links (refines/contradicts/amends)');
|
|
2195
|
+
});
|
|
2196
|
+
return;
|
|
2197
|
+
}
|
|
2198
|
+
const clauses = splitCreateTableClauses(this.tableSql('twin_edges'));
|
|
2199
|
+
if (clauses.length === 0) {
|
|
2200
|
+
throw new Error('Migration 079 recovery failed: unreadable twin_edges');
|
|
2201
|
+
}
|
|
2202
|
+
const edgeTypeDef = "edge_type TEXT NOT NULL CHECK (edge_type IN ('supersedes','refines','contradicts','builds_on','debates','synthesizes','mentions','derived_from','case_member','alias_of','next_action_for','blocks','amends'))";
|
|
2203
|
+
const allowedEdgeTypeDefs = new Set([
|
|
2204
|
+
"edge_type TEXT NOT NULL CHECK (edge_type IN ('supersedes','builds_on','debates','synthesizes','mentions','derived_from','case_member','alias_of','next_action_for','blocks'))",
|
|
2205
|
+
edgeTypeDef,
|
|
2206
|
+
].map(normalizeSqlText));
|
|
2207
|
+
const existingColumns = [];
|
|
2208
|
+
const columns = [];
|
|
2209
|
+
const constraints = [];
|
|
2210
|
+
for (const clause of clauses) {
|
|
2211
|
+
if (clauseIsTableConstraint(clause)) {
|
|
2212
|
+
if (/\bedge_type\b/i.test(clause)) {
|
|
2213
|
+
throw new Error('Migration 079 cannot safely preserve conflicting edge_type CHECK');
|
|
2214
|
+
}
|
|
2215
|
+
constraints.push(clause);
|
|
2216
|
+
continue;
|
|
2217
|
+
}
|
|
2218
|
+
const name = clauseColumnName(clause);
|
|
2219
|
+
existingColumns.push(name);
|
|
2220
|
+
if (name === 'edge_type') {
|
|
2221
|
+
if (!allowedEdgeTypeDefs.has(normalizeSqlText(clause))) {
|
|
2222
|
+
throw new Error('Migration 079 cannot safely preserve inline constraint on edge_type');
|
|
2223
|
+
}
|
|
2224
|
+
columns.push(edgeTypeDef);
|
|
2225
|
+
}
|
|
2226
|
+
else {
|
|
2227
|
+
columns.push(clause);
|
|
2228
|
+
}
|
|
2229
|
+
}
|
|
2230
|
+
const objects = this.storedObjectsForTable('twin_edges', new Set([
|
|
2231
|
+
'idx_twin_edges_subject',
|
|
2232
|
+
'idx_twin_edges_object',
|
|
2233
|
+
'idx_twin_edges_model_run_id',
|
|
2234
|
+
'idx_twin_edges_request_idempotency',
|
|
2235
|
+
'ux_twin_edges_model_run_edge_idempotency',
|
|
2236
|
+
]));
|
|
2237
|
+
const previousForeignKeys = this.readForeignKeysEnabled();
|
|
2238
|
+
this.exec('PRAGMA foreign_keys = OFF');
|
|
2239
|
+
if (this.readForeignKeysEnabled()) {
|
|
2240
|
+
throw new Error('Migration 079 recovery failed: could not disable foreign_keys');
|
|
2241
|
+
}
|
|
2242
|
+
try {
|
|
2243
|
+
this.transaction(() => {
|
|
2244
|
+
this.exec(`CREATE TABLE twin_edges_079_new (\n ${[...columns, ...constraints].join(',\n ')}\n)`);
|
|
2245
|
+
// Generated columns (table_xinfo hidden: 2 = VIRTUAL, 3 = STORED) refuse
|
|
2246
|
+
// direct writes; the rebuilt table recomputes them from its own clause.
|
|
2247
|
+
const generatedColumns = new Set(this.prepare('SELECT name FROM pragma_table_xinfo(?) WHERE hidden IN (2, 3)').all('twin_edges').map((row) => row.name));
|
|
2248
|
+
const names = existingColumns
|
|
2249
|
+
.filter((name) => !generatedColumns.has(name))
|
|
2250
|
+
.map(quoteSqlIdentifier)
|
|
2251
|
+
.join(', ');
|
|
2252
|
+
this.exec(`INSERT INTO twin_edges_079_new (${names}) SELECT ${names} FROM twin_edges`);
|
|
2253
|
+
this.exec('DROP TABLE twin_edges');
|
|
2254
|
+
this.exec('ALTER TABLE twin_edges_079_new RENAME TO twin_edges');
|
|
2255
|
+
for (const sql of objects.indexes) {
|
|
2256
|
+
this.exec(sql);
|
|
2257
|
+
}
|
|
2258
|
+
this.exec(`
|
|
2259
|
+
CREATE INDEX idx_twin_edges_subject
|
|
2260
|
+
ON twin_edges(subject_kind, subject_id, created_at DESC);
|
|
2261
|
+
CREATE INDEX idx_twin_edges_object
|
|
2262
|
+
ON twin_edges(object_kind, object_id, created_at DESC);
|
|
2263
|
+
CREATE INDEX idx_twin_edges_model_run_id
|
|
2264
|
+
ON twin_edges(model_run_id, created_at DESC);
|
|
2265
|
+
CREATE INDEX idx_twin_edges_request_idempotency
|
|
2266
|
+
ON twin_edges(model_run_id, request_idempotency_key, created_at DESC)
|
|
2267
|
+
WHERE model_run_id IS NOT NULL AND request_idempotency_key IS NOT NULL;
|
|
2268
|
+
CREATE UNIQUE INDEX ux_twin_edges_model_run_edge_idempotency
|
|
2269
|
+
ON twin_edges(model_run_id, edge_idempotency_key)
|
|
2270
|
+
WHERE model_run_id IS NOT NULL AND edge_idempotency_key IS NOT NULL;
|
|
2271
|
+
`);
|
|
2272
|
+
for (const sql of objects.triggers) {
|
|
2273
|
+
this.exec(sql);
|
|
2274
|
+
}
|
|
2275
|
+
if (!this.twinEdgeRelationsShape079()) {
|
|
2276
|
+
throw new Error('Migration 079 recovery failed: incomplete twin edge relations shape');
|
|
2277
|
+
}
|
|
2278
|
+
for (const table of this.tablesInForeignKeyGraph('twin_edges')) {
|
|
2279
|
+
if (this.prepare('SELECT 1 FROM pragma_foreign_key_check(?)').all(table).length > 0) {
|
|
2280
|
+
throw new Error(`Migration 079 recovery failed: foreign key violations after rebuild (${table})`);
|
|
2281
|
+
}
|
|
2282
|
+
}
|
|
2283
|
+
this.prepare('INSERT OR IGNORE INTO schema_version (version, description) VALUES (?, ?)').run(79, 'Twin edge relations for judgment links (refines/contradicts/amends)');
|
|
2284
|
+
});
|
|
2285
|
+
}
|
|
2286
|
+
finally {
|
|
2287
|
+
this.exec(`PRAGMA foreign_keys = ${previousForeignKeys ? 'ON' : 'OFF'}`);
|
|
2288
|
+
}
|
|
2289
|
+
}
|
|
2290
|
+
legacyRecordKindShape077() {
|
|
2291
|
+
if (!this.tableExists('decisions')) {
|
|
2292
|
+
return false;
|
|
2293
|
+
}
|
|
2294
|
+
const clauses = splitCreateTableClauses(this.tableSql('decisions'));
|
|
2295
|
+
const recordKindClause = clauses.find((clause) => !clauseIsTableConstraint(clause) && clauseColumnName(clause) === 'record_kind');
|
|
2296
|
+
return Boolean(recordKindClause &&
|
|
2297
|
+
normalizeSqlText(recordKindClause) ===
|
|
2298
|
+
normalizeSqlText("record_kind TEXT NOT NULL DEFAULT 'legacy' CHECK (record_kind IN ('legacy', 'judgment', 'commitment'))") &&
|
|
2299
|
+
!clauses.some((clause) => clauseIsTableConstraint(clause) && /\brecord_kind\b/i.test(clause)));
|
|
2300
|
+
}
|
|
2301
|
+
recoverLegacyRecordKindMigration077() {
|
|
2302
|
+
if (!this.tableExists('decisions')) {
|
|
2303
|
+
throw new Error('Migration 077 recovery failed: missing decisions');
|
|
2304
|
+
}
|
|
2305
|
+
if (this.legacyRecordKindShape077()) {
|
|
2306
|
+
this.transaction(() => {
|
|
2307
|
+
this.prepare('INSERT OR IGNORE INTO schema_version (version, description) VALUES (?, ?)').run(77, 'Distinguish legacy memory records from agent judgments');
|
|
2308
|
+
});
|
|
2309
|
+
return;
|
|
2310
|
+
}
|
|
2311
|
+
const clauses = splitCreateTableClauses(this.tableSql('decisions'));
|
|
2312
|
+
if (clauses.length === 0) {
|
|
2313
|
+
throw new Error('Migration 077 recovery failed: unreadable decisions');
|
|
2314
|
+
}
|
|
2315
|
+
const existingColumns = [];
|
|
2316
|
+
const columns = [];
|
|
2317
|
+
const constraints = [];
|
|
2318
|
+
let hasRecordKind = false;
|
|
2319
|
+
for (const clause of clauses) {
|
|
2320
|
+
if (clauseIsTableConstraint(clause)) {
|
|
2321
|
+
if (/\brecord_kind\b/i.test(clause)) {
|
|
2322
|
+
throw new Error('Migration 077 cannot safely preserve conflicting record_kind CHECK');
|
|
2323
|
+
}
|
|
2324
|
+
constraints.push(clause);
|
|
2325
|
+
continue;
|
|
2326
|
+
}
|
|
2327
|
+
const name = clauseColumnName(clause);
|
|
2328
|
+
existingColumns.push(name);
|
|
2329
|
+
if (name === 'record_kind') {
|
|
2330
|
+
hasRecordKind = true;
|
|
2331
|
+
columns.push("record_kind TEXT NOT NULL DEFAULT 'legacy' CHECK (record_kind IN ('legacy', 'judgment', 'commitment'))");
|
|
2332
|
+
}
|
|
2333
|
+
else {
|
|
2334
|
+
columns.push(clause);
|
|
2335
|
+
}
|
|
2336
|
+
}
|
|
2337
|
+
if (!hasRecordKind) {
|
|
2338
|
+
throw new Error('Migration 077 recovery failed: record_kind column is missing');
|
|
2339
|
+
}
|
|
2340
|
+
const objects = this.storedObjectsForTable('decisions');
|
|
2341
|
+
const previousForeignKeys = this.readForeignKeysEnabled();
|
|
2342
|
+
this.exec('PRAGMA foreign_keys = OFF');
|
|
2343
|
+
if (this.readForeignKeysEnabled()) {
|
|
2344
|
+
throw new Error('Migration 077 recovery failed: could not disable foreign_keys');
|
|
2345
|
+
}
|
|
2346
|
+
try {
|
|
2347
|
+
this.transaction(() => {
|
|
2348
|
+
this.exec(`CREATE TABLE decisions_077_new (\n ${[...columns, ...constraints].join(',\n ')}\n)`);
|
|
2349
|
+
const names = existingColumns.map(quoteSqlIdentifier).join(', ');
|
|
2350
|
+
const source = existingColumns
|
|
2351
|
+
.map((name) => name === 'record_kind'
|
|
2352
|
+
? `CASE WHEN ${this.tableExists('judgment_commands') ? 'EXISTS (SELECT 1 FROM judgment_commands jc WHERE jc.record_id = decisions.id)' : '0'} THEN record_kind ELSE 'legacy' END`
|
|
2353
|
+
: quoteSqlIdentifier(name))
|
|
2354
|
+
.join(', ');
|
|
2355
|
+
this.exec(`INSERT INTO decisions_077_new (${names}) SELECT ${source} FROM decisions`);
|
|
2356
|
+
this.exec('DROP TABLE decisions');
|
|
2357
|
+
this.exec('ALTER TABLE decisions_077_new RENAME TO decisions');
|
|
2358
|
+
for (const sql of objects.indexes) {
|
|
2359
|
+
this.exec(sql);
|
|
2360
|
+
}
|
|
2361
|
+
for (const sql of objects.triggers) {
|
|
2362
|
+
this.exec(sql);
|
|
2363
|
+
}
|
|
2364
|
+
if (!this.legacyRecordKindShape077()) {
|
|
2365
|
+
throw new Error('Migration 077 recovery failed: incomplete record_kind shape');
|
|
2366
|
+
}
|
|
2367
|
+
for (const table of this.tablesInForeignKeyGraph('decisions')) {
|
|
2368
|
+
if (this.prepare('SELECT 1 FROM pragma_foreign_key_check(?)').all(table).length > 0) {
|
|
2369
|
+
throw new Error(`Migration 077 recovery failed: foreign key violations after rebuild (${table})`);
|
|
2370
|
+
}
|
|
2371
|
+
}
|
|
2372
|
+
this.prepare('INSERT OR IGNORE INTO schema_version (version, description) VALUES (?, ?)').run(77, 'Distinguish legacy memory records from agent judgments');
|
|
2373
|
+
});
|
|
2374
|
+
}
|
|
2375
|
+
finally {
|
|
2376
|
+
this.exec(`PRAGMA foreign_keys = ${previousForeignKeys ? 'ON' : 'OFF'}`);
|
|
2377
|
+
}
|
|
2378
|
+
}
|
|
2379
|
+
toolTracesHasOperationOriginShape() {
|
|
2380
|
+
const columns = this.tableColumns('tool_traces');
|
|
2381
|
+
if (!columns.has('operation_id') || !columns.has('actor_principal_id')) {
|
|
2382
|
+
return false;
|
|
2383
|
+
}
|
|
2384
|
+
const modelRunColumn = this.prepare('PRAGMA table_info(tool_traces)').all().find((column) => column.name === 'model_run_id');
|
|
2385
|
+
if (!modelRunColumn || modelRunColumn.notnull !== 0) {
|
|
2386
|
+
return false;
|
|
2387
|
+
}
|
|
2388
|
+
const normalized = normalizeSqlText(this.toolTracesTableSql());
|
|
2389
|
+
// The FULL origin CHECK must be present, not merely a prefix: a weakened
|
|
2390
|
+
// `CHECK (model_run_id IS NOT NULL OR 1)` would let a both-null-origin row
|
|
2391
|
+
// exist, so a table carrying it is NOT migrated and must be repaired.
|
|
2392
|
+
if (!normalized.includes(normalizeSqlText(TOOL_TRACES_ORIGIN_CHECK))) {
|
|
2393
|
+
return false;
|
|
2394
|
+
}
|
|
2395
|
+
// The model_runs FK must survive so a legacy/causal model reference is real.
|
|
2396
|
+
if (!normalized.includes(normalizeSqlText(TOOL_TRACES_MODEL_FK))) {
|
|
2397
|
+
return false;
|
|
2398
|
+
}
|
|
2399
|
+
// The origin lookup indexes are part of the shape this migration guarantees.
|
|
2400
|
+
return (this.indexExists('idx_tool_traces_operation_id') &&
|
|
2401
|
+
this.indexExists('idx_tool_traces_actor_principal'));
|
|
2402
|
+
}
|
|
2403
|
+
needsServiceOperationOriginsRepair071() {
|
|
2404
|
+
if (!this.tableExists('tool_traces')) {
|
|
2405
|
+
// The table itself is missing: let the recovery helper fail loudly.
|
|
2406
|
+
return true;
|
|
2407
|
+
}
|
|
2408
|
+
return !this.toolTracesHasOperationOriginShape() || !this.schemaVersionExists(71);
|
|
2409
|
+
}
|
|
2410
|
+
readForeignKeysEnabled() {
|
|
2411
|
+
const row = this.prepare('PRAGMA foreign_keys').get();
|
|
2412
|
+
return Boolean(row?.foreign_keys);
|
|
2413
|
+
}
|
|
2414
|
+
/**
|
|
2415
|
+
* tool_traces plus every table that declares a FK referencing it. This is the
|
|
2416
|
+
* relevant FK graph for the 071 rebuild: tool_traces' own model_runs reference
|
|
2417
|
+
* and any child (e.g. an ON DELETE CASCADE table) whose rows the rebuild must
|
|
2418
|
+
* have preserved. A valid child whose table name needs quoting (a space, an
|
|
2419
|
+
* embedded quote, a reserved word) is inspected correctly too: the name is fed
|
|
2420
|
+
* to the table-valued pragma as a bound argument, not interpolated -- so no
|
|
2421
|
+
* referencing child is silently skipped by an identifier regex.
|
|
2422
|
+
*/
|
|
2423
|
+
tablesInToolTracesFkGraph() {
|
|
2424
|
+
const graph = new Set(['tool_traces']);
|
|
2425
|
+
const tables = this.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'").all();
|
|
2426
|
+
for (const { name } of tables) {
|
|
2427
|
+
const fkList = this.prepare('SELECT "table" AS ref FROM pragma_foreign_key_list(?)').all(name);
|
|
2428
|
+
if (fkList.some((fk) => fk.ref === 'tool_traces')) {
|
|
2429
|
+
graph.add(name);
|
|
2430
|
+
}
|
|
2431
|
+
}
|
|
2432
|
+
return [...graph];
|
|
2433
|
+
}
|
|
2434
|
+
recoverServiceOperationOriginsMigration071() {
|
|
2435
|
+
if (!this.tableExists('tool_traces')) {
|
|
2436
|
+
throw new Error('Migration 071 recovery failed: missing table tool_traces');
|
|
2437
|
+
}
|
|
2438
|
+
// Idempotent: a database already carrying the full operation-origin shape
|
|
2439
|
+
// only needs its schema_version stamped. Never infer health from version.
|
|
2440
|
+
if (this.toolTracesHasOperationOriginShape()) {
|
|
2441
|
+
this.transaction(() => {
|
|
2442
|
+
this.prepare('INSERT OR IGNORE INTO schema_version (version, description) VALUES (?, ?)').run(71, 'Service operation-origin tool traces');
|
|
2443
|
+
});
|
|
2444
|
+
return;
|
|
2445
|
+
}
|
|
2446
|
+
// The 071 rebuild recreates the 068 scope indexes, which reference
|
|
2447
|
+
// owner_scope/project_id/channel_id. A legacy ledger stamped 68 but missing
|
|
2448
|
+
// those diagnostic columns (the main loop skips version 68 when it is
|
|
2449
|
+
// already stamped, so the in-loop 071 branch reaches here before the
|
|
2450
|
+
// end-of-run repair) would abort the rebuild with "no such column". Repair
|
|
2451
|
+
// the real 068 prerequisite first, reusing the same structural dependency
|
|
2452
|
+
// the late repair path already orders (068 before 071). This runs before
|
|
2453
|
+
// the FK toggle/transaction below, so no nested transaction is introduced,
|
|
2454
|
+
// and it is a no-op on a healthy database.
|
|
2455
|
+
if (this.needsToolTraceDiagnosticsRepair068()) {
|
|
2456
|
+
this.recoverToolTraceDiagnosticsMigration068();
|
|
2457
|
+
}
|
|
2458
|
+
// FK enforcement MUST be toggled OUTSIDE a transaction (SQLite silently
|
|
2459
|
+
// ignores the pragma while one is open). Disabling it before BEGIN is what
|
|
2460
|
+
// stops the DROP TABLE below from CASCADE-deleting real child rows; the
|
|
2461
|
+
// whole-graph foreign_key_check inside the transaction is the honest
|
|
2462
|
+
// preservation proof, never a post-deletion recount. Restore in finally.
|
|
2463
|
+
const previousForeignKeys = this.readForeignKeysEnabled();
|
|
2464
|
+
this.exec('PRAGMA foreign_keys = OFF');
|
|
2465
|
+
if (this.readForeignKeysEnabled()) {
|
|
2466
|
+
// A pragma that did not take effect means a transaction is already open;
|
|
2467
|
+
// proceeding would let CASCADE run. Refuse rather than pretend it worked.
|
|
2468
|
+
throw new Error('Migration 071 recovery failed: could not disable foreign_keys (transaction already open?)');
|
|
2469
|
+
}
|
|
2470
|
+
try {
|
|
2471
|
+
this.transaction(() => {
|
|
2472
|
+
// Preserve the COMPLETE existing table definition. Every column keeps its
|
|
2473
|
+
// exact declared type, NOT NULL/DEFAULT/CHECK/UNIQUE/COLLATE and quoted
|
|
2474
|
+
// name; only model_run_id is relaxed to nullable and the two origin
|
|
2475
|
+
// columns + origin CHECK are added. Reconstructing from name/type alone
|
|
2476
|
+
// would silently drop constraints and defaults a live database carries.
|
|
2477
|
+
const originalSql = this.toolTracesTableSql();
|
|
2478
|
+
const clauses = splitCreateTableClauses(originalSql);
|
|
2479
|
+
if (clauses.length === 0) {
|
|
2480
|
+
throw new Error('Migration 071 recovery failed: could not read tool_traces definition');
|
|
2481
|
+
}
|
|
2482
|
+
const existingColumnNames = [];
|
|
2483
|
+
const columnDefs = [];
|
|
2484
|
+
const constraintDefs = [];
|
|
2485
|
+
for (const clause of clauses) {
|
|
2486
|
+
if (clauseIsTableConstraint(clause)) {
|
|
2487
|
+
constraintDefs.push(clause);
|
|
2488
|
+
continue;
|
|
2489
|
+
}
|
|
2490
|
+
const name = clauseColumnName(clause);
|
|
2491
|
+
existingColumnNames.push(name);
|
|
2492
|
+
if (name === 'model_run_id') {
|
|
2493
|
+
// Relax the single NOT NULL so an operation-origin row (model NULL)
|
|
2494
|
+
// is allowed; everything else about the column is preserved.
|
|
2495
|
+
columnDefs.push(clause.replace(/\s+not\s+null\b/i, ''));
|
|
2496
|
+
}
|
|
2497
|
+
else {
|
|
2498
|
+
columnDefs.push(clause);
|
|
2499
|
+
}
|
|
2500
|
+
}
|
|
2501
|
+
const present = new Set(existingColumnNames);
|
|
2502
|
+
if (!present.has('operation_id')) {
|
|
2503
|
+
columnDefs.push('operation_id TEXT');
|
|
2504
|
+
}
|
|
2505
|
+
if (!present.has('actor_principal_id')) {
|
|
2506
|
+
columnDefs.push('actor_principal_id TEXT');
|
|
2507
|
+
}
|
|
2508
|
+
const constraints = [...constraintDefs];
|
|
2509
|
+
const hasModelFk = constraints.some((clause) => normalizeSqlText(clause).includes('referencesmodel_runs(model_run_id)'));
|
|
2510
|
+
if (!hasModelFk) {
|
|
2511
|
+
constraints.push(TOOL_TRACES_MODEL_FK);
|
|
2512
|
+
}
|
|
2513
|
+
constraints.push(TOOL_TRACES_ORIGIN_CHECK);
|
|
2514
|
+
// Preserve every custom/existing trigger on the old table; DROP TABLE
|
|
2515
|
+
// drops them, and RENAME does not carry them, so recreate from DDL.
|
|
2516
|
+
const triggerSqls = this.prepare("SELECT sql FROM sqlite_master WHERE type='trigger' AND tbl_name='tool_traces' AND sql IS NOT NULL").all().map((row) => row.sql);
|
|
2517
|
+
// Preserve every non-auto index on the old table (033/061/068 plus any
|
|
2518
|
+
// runtime-created ones) by recreating them from their stored DDL.
|
|
2519
|
+
const indexSqls = this.prepare("SELECT sql FROM sqlite_master WHERE type='index' AND tbl_name='tool_traces' AND sql IS NOT NULL").all().map((row) => row.sql);
|
|
2520
|
+
this.exec(`CREATE TABLE tool_traces_071_new (\n ${[...columnDefs, ...constraints].join(',\n ')}\n)`);
|
|
2521
|
+
// Copy every column the old table actually carries (canonical, runtime
|
|
2522
|
+
// extras, and any already-present operation_id/actor_principal_id values
|
|
2523
|
+
// from a partially-repaired shape - never dropping real origin values).
|
|
2524
|
+
// Each name is re-quoted: clauseColumnName decoded it, so a quoted name
|
|
2525
|
+
// carrying a space/embedded-quote/reserved word must be emitted quoted or
|
|
2526
|
+
// it would parse as several bare tokens and abort the rebuild.
|
|
2527
|
+
const columnList = existingColumnNames.map(quoteSqlIdentifier).join(', ');
|
|
2528
|
+
this.exec(`INSERT INTO tool_traces_071_new (${columnList}) SELECT ${columnList} FROM tool_traces`);
|
|
2529
|
+
this.exec('DROP TABLE tool_traces');
|
|
2530
|
+
this.exec('ALTER TABLE tool_traces_071_new RENAME TO tool_traces');
|
|
2531
|
+
for (const sql of indexSqls) {
|
|
2532
|
+
this.exec(sql.replace(/^CREATE\s+INDEX/i, 'CREATE INDEX IF NOT EXISTS'));
|
|
2533
|
+
}
|
|
2534
|
+
this.exec(`
|
|
2535
|
+
CREATE INDEX IF NOT EXISTS idx_tool_traces_model_run_id
|
|
2536
|
+
ON tool_traces(model_run_id, created_at DESC);
|
|
2537
|
+
CREATE INDEX IF NOT EXISTS idx_tool_traces_gateway_call_id
|
|
2538
|
+
ON tool_traces(gateway_call_id);
|
|
2539
|
+
CREATE INDEX IF NOT EXISTS idx_tool_traces_scope_recency
|
|
2540
|
+
ON tool_traces(owner_scope, project_id, created_at DESC, trace_id DESC);
|
|
2541
|
+
CREATE INDEX IF NOT EXISTS idx_tool_traces_channel_recency
|
|
2542
|
+
ON tool_traces(owner_scope, project_id, channel_id, created_at DESC, trace_id DESC);
|
|
2543
|
+
CREATE INDEX IF NOT EXISTS idx_tool_traces_operation_id
|
|
2544
|
+
ON tool_traces(operation_id, created_at DESC);
|
|
2545
|
+
CREATE INDEX IF NOT EXISTS idx_tool_traces_actor_principal
|
|
2546
|
+
ON tool_traces(actor_principal_id, created_at DESC);
|
|
2547
|
+
`);
|
|
2548
|
+
for (const sql of triggerSqls) {
|
|
2549
|
+
this.exec(sql);
|
|
2550
|
+
}
|
|
2551
|
+
if (!this.toolTracesHasOperationOriginShape()) {
|
|
2552
|
+
throw new Error('Migration 071 recovery failed: operation-origin shape missing after rebuild');
|
|
2553
|
+
}
|
|
2554
|
+
// Validate the RELEVANT FK graph, not merely tool_traces' own FK: check
|
|
2555
|
+
// tool_traces (its model_runs reference - catches a dangling row) AND
|
|
2556
|
+
// every table that references tool_traces (a CASCADE child that the
|
|
2557
|
+
// rebuild must have preserved). Any violation aborts the whole
|
|
2558
|
+
// transaction, so nothing partial or unattributed is ever committed.
|
|
2559
|
+
// Unrelated tables are deliberately NOT checked - a pre-existing
|
|
2560
|
+
// violation elsewhere must not block this upgrade.
|
|
2561
|
+
for (const table of this.tablesInToolTracesFkGraph()) {
|
|
2562
|
+
const violations = this.prepare('SELECT 1 FROM pragma_foreign_key_check(?)').all(table);
|
|
2563
|
+
if (violations.length > 0) {
|
|
2564
|
+
throw new Error(`Migration 071 recovery failed: foreign key violations after rebuild (${table})`);
|
|
2565
|
+
}
|
|
2566
|
+
}
|
|
2567
|
+
this.prepare('INSERT OR IGNORE INTO schema_version (version, description) VALUES (?, ?)').run(71, 'Service operation-origin tool traces');
|
|
2568
|
+
});
|
|
2569
|
+
}
|
|
2570
|
+
finally {
|
|
2571
|
+
this.exec(`PRAGMA foreign_keys = ${previousForeignKeys ? 'ON' : 'OFF'}`);
|
|
2572
|
+
}
|
|
2573
|
+
}
|
|
729
2574
|
recoverMemoryProvenanceMigration032() {
|
|
730
2575
|
this.transaction(() => {
|
|
731
2576
|
const expectedDecisionColumns = [
|
|
@@ -792,443 +2637,6 @@ class NodeSQLiteAdapter extends base_adapter_js_1.DatabaseAdapter {
|
|
|
792
2637
|
}
|
|
793
2638
|
}
|
|
794
2639
|
}
|
|
795
|
-
recoverConnectorEventScopeMigration034() {
|
|
796
|
-
this.transaction(() => {
|
|
797
|
-
const expectedColumns = [
|
|
798
|
-
'source_cursor',
|
|
799
|
-
'tenant_id',
|
|
800
|
-
'project_id',
|
|
801
|
-
'memory_scope_kind',
|
|
802
|
-
'memory_scope_id',
|
|
803
|
-
];
|
|
804
|
-
const columns = this.tableColumns('connector_event_index');
|
|
805
|
-
for (const column of expectedColumns) {
|
|
806
|
-
if (!columns.has(column)) {
|
|
807
|
-
this.exec(`ALTER TABLE connector_event_index ADD COLUMN ${column} TEXT`);
|
|
808
|
-
columns.add(column);
|
|
809
|
-
}
|
|
810
|
-
}
|
|
811
|
-
this.exec(`
|
|
812
|
-
CREATE INDEX IF NOT EXISTS idx_connector_event_scope
|
|
813
|
-
ON connector_event_index(tenant_id, project_id, memory_scope_kind, memory_scope_id)
|
|
814
|
-
`);
|
|
815
|
-
this.exec(`
|
|
816
|
-
CREATE INDEX IF NOT EXISTS idx_connector_event_source_cursor
|
|
817
|
-
ON connector_event_index(source_connector, source_cursor)
|
|
818
|
-
`);
|
|
819
|
-
this.assertMigration034Complete();
|
|
820
|
-
this.prepare('INSERT OR IGNORE INTO schema_version (version, description) VALUES (?, ?)').run(34, 'Add connector event scope columns');
|
|
821
|
-
});
|
|
822
|
-
}
|
|
823
|
-
assertMigration034Complete() {
|
|
824
|
-
const columns = this.tableColumns('connector_event_index');
|
|
825
|
-
for (const column of [
|
|
826
|
-
'source_cursor',
|
|
827
|
-
'tenant_id',
|
|
828
|
-
'project_id',
|
|
829
|
-
'memory_scope_kind',
|
|
830
|
-
'memory_scope_id',
|
|
831
|
-
]) {
|
|
832
|
-
if (!columns.has(column)) {
|
|
833
|
-
throw new Error(`Migration 034 recovery failed: missing connector_event_index.${column}`);
|
|
834
|
-
}
|
|
835
|
-
}
|
|
836
|
-
for (const indexName of ['idx_connector_event_scope', 'idx_connector_event_source_cursor']) {
|
|
837
|
-
if (!this.indexExists(indexName)) {
|
|
838
|
-
throw new Error(`Migration 034 recovery failed: missing index ${indexName}`);
|
|
839
|
-
}
|
|
840
|
-
}
|
|
841
|
-
}
|
|
842
|
-
recoverConnectorEventSourceEntityIdMigration067() {
|
|
843
|
-
this.transaction(() => {
|
|
844
|
-
const columns = this.tableColumns('connector_event_index');
|
|
845
|
-
if (!columns.has('source_entity_id')) {
|
|
846
|
-
this.exec('ALTER TABLE connector_event_index ADD COLUMN source_entity_id TEXT');
|
|
847
|
-
}
|
|
848
|
-
// Backfill only when metadata_json exists; a legacy/minimal table rebuilt by the repair path
|
|
849
|
-
// may not have it yet. New rows carry source_entity_id at write time regardless.
|
|
850
|
-
if (columns.has('metadata_json')) {
|
|
851
|
-
this.exec(`
|
|
852
|
-
UPDATE connector_event_index
|
|
853
|
-
SET source_entity_id = json_extract(metadata_json, '$.sourceEntityId')
|
|
854
|
-
WHERE source_entity_id IS NULL
|
|
855
|
-
AND metadata_json IS NOT NULL
|
|
856
|
-
AND json_extract(metadata_json, '$.sourceEntityId') IS NOT NULL
|
|
857
|
-
`);
|
|
858
|
-
}
|
|
859
|
-
this.exec(`
|
|
860
|
-
CREATE INDEX IF NOT EXISTS idx_connector_event_source_entity
|
|
861
|
-
ON connector_event_index(source_connector, source_entity_id)
|
|
862
|
-
`);
|
|
863
|
-
this.assertMigration067Complete();
|
|
864
|
-
this.prepare('INSERT OR IGNORE INTO schema_version (version, description) VALUES (?, ?)').run(67, 'Add connector_event_index source_entity_id');
|
|
865
|
-
});
|
|
866
|
-
}
|
|
867
|
-
assertMigration067Complete() {
|
|
868
|
-
const columns = this.tableColumns('connector_event_index');
|
|
869
|
-
if (!columns.has('source_entity_id')) {
|
|
870
|
-
throw new Error('Migration 067 recovery failed: missing connector_event_index.source_entity_id');
|
|
871
|
-
}
|
|
872
|
-
if (!this.indexExists('idx_connector_event_source_entity')) {
|
|
873
|
-
throw new Error('Migration 067 recovery failed: missing index idx_connector_event_source_entity');
|
|
874
|
-
}
|
|
875
|
-
}
|
|
876
|
-
recoverConnectorEventOperatorSeqMigration039() {
|
|
877
|
-
this.transaction(() => {
|
|
878
|
-
const columns = this.tableColumns('connector_event_index');
|
|
879
|
-
if (!columns.has('operator_ingest_seq')) {
|
|
880
|
-
this.exec(`
|
|
881
|
-
ALTER TABLE connector_event_index
|
|
882
|
-
ADD COLUMN operator_ingest_seq INTEGER CHECK (
|
|
883
|
-
operator_ingest_seq IS NULL OR operator_ingest_seq >= 1
|
|
884
|
-
)
|
|
885
|
-
`);
|
|
886
|
-
}
|
|
887
|
-
this.exec(`
|
|
888
|
-
CREATE TABLE IF NOT EXISTS connector_event_index_operator_seq_cursors (
|
|
889
|
-
source_connector TEXT NOT NULL,
|
|
890
|
-
channel TEXT NOT NULL DEFAULT '',
|
|
891
|
-
next_seq INTEGER NOT NULL CHECK (next_seq >= 1),
|
|
892
|
-
PRIMARY KEY (source_connector, channel)
|
|
893
|
-
)
|
|
894
|
-
`);
|
|
895
|
-
this.exec(`
|
|
896
|
-
WITH ranked_events AS (
|
|
897
|
-
SELECT
|
|
898
|
-
event_index_id,
|
|
899
|
-
ROW_NUMBER() OVER (
|
|
900
|
-
PARTITION BY source_connector, COALESCE(channel, '')
|
|
901
|
-
ORDER BY rowid ASC
|
|
902
|
-
) AS operator_seq
|
|
903
|
-
FROM connector_event_index
|
|
904
|
-
)
|
|
905
|
-
UPDATE connector_event_index
|
|
906
|
-
SET operator_ingest_seq = (
|
|
907
|
-
SELECT operator_seq
|
|
908
|
-
FROM ranked_events
|
|
909
|
-
WHERE ranked_events.event_index_id = connector_event_index.event_index_id
|
|
910
|
-
)
|
|
911
|
-
WHERE operator_ingest_seq IS NULL
|
|
912
|
-
`);
|
|
913
|
-
this.exec(`
|
|
914
|
-
INSERT OR IGNORE INTO connector_event_index_operator_seq_cursors (
|
|
915
|
-
source_connector,
|
|
916
|
-
channel,
|
|
917
|
-
next_seq
|
|
918
|
-
)
|
|
919
|
-
SELECT
|
|
920
|
-
source_connector,
|
|
921
|
-
COALESCE(channel, ''),
|
|
922
|
-
COALESCE(MAX(operator_ingest_seq), 0) + 1
|
|
923
|
-
FROM connector_event_index
|
|
924
|
-
GROUP BY source_connector, COALESCE(channel, '')
|
|
925
|
-
`);
|
|
926
|
-
this.exec(`
|
|
927
|
-
CREATE UNIQUE INDEX IF NOT EXISTS idx_connector_event_index_operator_scope_seq
|
|
928
|
-
ON connector_event_index(source_connector, COALESCE(channel, ''), operator_ingest_seq)
|
|
929
|
-
WHERE operator_ingest_seq IS NOT NULL
|
|
930
|
-
`);
|
|
931
|
-
this.exec(`
|
|
932
|
-
CREATE INDEX IF NOT EXISTS idx_connector_event_index_operator_cursor_order
|
|
933
|
-
ON connector_event_index(source_connector, channel, operator_ingest_seq)
|
|
934
|
-
`);
|
|
935
|
-
this.exec(`
|
|
936
|
-
CREATE TRIGGER IF NOT EXISTS trg_connector_event_index_operator_ingest_seq_ai
|
|
937
|
-
AFTER INSERT ON connector_event_index
|
|
938
|
-
WHEN NEW.operator_ingest_seq IS NULL
|
|
939
|
-
BEGIN
|
|
940
|
-
INSERT OR IGNORE INTO connector_event_index_operator_seq_cursors (
|
|
941
|
-
source_connector,
|
|
942
|
-
channel,
|
|
943
|
-
next_seq
|
|
944
|
-
)
|
|
945
|
-
VALUES (NEW.source_connector, COALESCE(NEW.channel, ''), 1);
|
|
946
|
-
|
|
947
|
-
UPDATE connector_event_index
|
|
948
|
-
SET operator_ingest_seq = (
|
|
949
|
-
SELECT next_seq
|
|
950
|
-
FROM connector_event_index_operator_seq_cursors
|
|
951
|
-
WHERE source_connector = NEW.source_connector
|
|
952
|
-
AND channel = COALESCE(NEW.channel, '')
|
|
953
|
-
)
|
|
954
|
-
WHERE event_index_id = NEW.event_index_id;
|
|
955
|
-
|
|
956
|
-
UPDATE connector_event_index_operator_seq_cursors
|
|
957
|
-
SET next_seq = next_seq + 1
|
|
958
|
-
WHERE source_connector = NEW.source_connector
|
|
959
|
-
AND channel = COALESCE(NEW.channel, '');
|
|
960
|
-
END
|
|
961
|
-
`);
|
|
962
|
-
this.exec(`
|
|
963
|
-
CREATE TRIGGER IF NOT EXISTS trg_connector_event_index_operator_ingest_seq_explicit_ai
|
|
964
|
-
AFTER INSERT ON connector_event_index
|
|
965
|
-
WHEN NEW.operator_ingest_seq IS NOT NULL
|
|
966
|
-
BEGIN
|
|
967
|
-
INSERT OR IGNORE INTO connector_event_index_operator_seq_cursors (
|
|
968
|
-
source_connector,
|
|
969
|
-
channel,
|
|
970
|
-
next_seq
|
|
971
|
-
)
|
|
972
|
-
VALUES (NEW.source_connector, COALESCE(NEW.channel, ''), 1);
|
|
973
|
-
|
|
974
|
-
UPDATE connector_event_index_operator_seq_cursors
|
|
975
|
-
SET next_seq = CASE
|
|
976
|
-
WHEN next_seq <= NEW.operator_ingest_seq THEN NEW.operator_ingest_seq + 1
|
|
977
|
-
ELSE next_seq
|
|
978
|
-
END
|
|
979
|
-
WHERE source_connector = NEW.source_connector
|
|
980
|
-
AND channel = COALESCE(NEW.channel, '');
|
|
981
|
-
END
|
|
982
|
-
`);
|
|
983
|
-
this.assertMigration039Complete();
|
|
984
|
-
this.prepare('INSERT OR IGNORE INTO schema_version (version, description) VALUES (?, ?)').run(39, 'Add connector event operator ingest sequence');
|
|
985
|
-
});
|
|
986
|
-
}
|
|
987
|
-
assertMigration039Complete() {
|
|
988
|
-
const columns = this.tableColumns('connector_event_index');
|
|
989
|
-
if (!columns.has('operator_ingest_seq')) {
|
|
990
|
-
throw new Error('Migration 039 recovery failed: missing connector_event_index.operator_ingest_seq');
|
|
991
|
-
}
|
|
992
|
-
if (!this.tableExists('connector_event_index_operator_seq_cursors')) {
|
|
993
|
-
throw new Error('Migration 039 recovery failed: missing connector_event_index_operator_seq_cursors');
|
|
994
|
-
}
|
|
995
|
-
for (const indexName of [
|
|
996
|
-
'idx_connector_event_index_operator_scope_seq',
|
|
997
|
-
'idx_connector_event_index_operator_cursor_order',
|
|
998
|
-
]) {
|
|
999
|
-
if (!this.indexExists(indexName)) {
|
|
1000
|
-
throw new Error(`Migration 039 recovery failed: missing index ${indexName}`);
|
|
1001
|
-
}
|
|
1002
|
-
}
|
|
1003
|
-
for (const triggerName of [
|
|
1004
|
-
'trg_connector_event_index_operator_ingest_seq_ai',
|
|
1005
|
-
'trg_connector_event_index_operator_ingest_seq_explicit_ai',
|
|
1006
|
-
]) {
|
|
1007
|
-
if (!this.triggerExists(triggerName)) {
|
|
1008
|
-
throw new Error(`Migration 039 recovery failed: missing trigger ${triggerName}`);
|
|
1009
|
-
}
|
|
1010
|
-
}
|
|
1011
|
-
}
|
|
1012
|
-
recoverConnectorEventSequencesMigration062() {
|
|
1013
|
-
this.transaction(() => {
|
|
1014
|
-
const columns = this.tableColumns('connector_event_index');
|
|
1015
|
-
if (!columns.has('operator_observation_seq')) {
|
|
1016
|
-
this.exec(`
|
|
1017
|
-
ALTER TABLE connector_event_index
|
|
1018
|
-
ADD COLUMN operator_observation_seq INTEGER CHECK (
|
|
1019
|
-
operator_observation_seq IS NULL OR operator_observation_seq >= 1
|
|
1020
|
-
)
|
|
1021
|
-
`);
|
|
1022
|
-
}
|
|
1023
|
-
this.exec(`
|
|
1024
|
-
CREATE TABLE IF NOT EXISTS connector_event_index_observation_cursors (
|
|
1025
|
-
source_connector TEXT PRIMARY KEY,
|
|
1026
|
-
next_seq INTEGER NOT NULL CHECK (next_seq >= 1)
|
|
1027
|
-
)
|
|
1028
|
-
`);
|
|
1029
|
-
this.exec(`
|
|
1030
|
-
WITH existing_max AS (
|
|
1031
|
-
SELECT source_connector, MAX(operator_observation_seq) AS max_seq
|
|
1032
|
-
FROM connector_event_index
|
|
1033
|
-
WHERE operator_observation_seq IS NOT NULL
|
|
1034
|
-
GROUP BY source_connector
|
|
1035
|
-
), ranked_nulls AS (
|
|
1036
|
-
SELECT event_index_id,
|
|
1037
|
-
source_connector,
|
|
1038
|
-
ROW_NUMBER() OVER (
|
|
1039
|
-
PARTITION BY source_connector
|
|
1040
|
-
ORDER BY source_timestamp_ms, rowid
|
|
1041
|
-
) AS seq
|
|
1042
|
-
FROM connector_event_index
|
|
1043
|
-
WHERE operator_observation_seq IS NULL
|
|
1044
|
-
)
|
|
1045
|
-
UPDATE connector_event_index
|
|
1046
|
-
SET operator_observation_seq = (
|
|
1047
|
-
SELECT COALESCE(existing_max.max_seq, 0) + ranked_nulls.seq
|
|
1048
|
-
FROM ranked_nulls
|
|
1049
|
-
LEFT JOIN existing_max
|
|
1050
|
-
ON existing_max.source_connector = ranked_nulls.source_connector
|
|
1051
|
-
WHERE ranked_nulls.event_index_id = connector_event_index.event_index_id
|
|
1052
|
-
)
|
|
1053
|
-
WHERE operator_observation_seq IS NULL
|
|
1054
|
-
`);
|
|
1055
|
-
this.exec(`
|
|
1056
|
-
INSERT INTO connector_event_index_observation_cursors (source_connector, next_seq)
|
|
1057
|
-
SELECT source_connector, MAX(operator_observation_seq) + 1
|
|
1058
|
-
FROM connector_event_index
|
|
1059
|
-
GROUP BY source_connector
|
|
1060
|
-
ON CONFLICT(source_connector) DO UPDATE SET next_seq = CASE
|
|
1061
|
-
WHEN connector_event_index_observation_cursors.next_seq < excluded.next_seq
|
|
1062
|
-
THEN excluded.next_seq
|
|
1063
|
-
ELSE connector_event_index_observation_cursors.next_seq
|
|
1064
|
-
END
|
|
1065
|
-
`);
|
|
1066
|
-
this.exec(`
|
|
1067
|
-
CREATE UNIQUE INDEX IF NOT EXISTS idx_connector_event_index_observation_seq
|
|
1068
|
-
ON connector_event_index(source_connector, operator_observation_seq)
|
|
1069
|
-
WHERE operator_observation_seq IS NOT NULL
|
|
1070
|
-
`);
|
|
1071
|
-
this.exec(`
|
|
1072
|
-
CREATE TRIGGER IF NOT EXISTS trg_connector_event_index_operator_ingest_seq_au
|
|
1073
|
-
AFTER UPDATE OF operator_ingest_seq ON connector_event_index
|
|
1074
|
-
WHEN NEW.operator_ingest_seq IS NULL AND OLD.operator_ingest_seq IS NOT NULL
|
|
1075
|
-
BEGIN
|
|
1076
|
-
INSERT INTO connector_event_index_operator_seq_cursors
|
|
1077
|
-
(source_connector, channel, next_seq)
|
|
1078
|
-
VALUES (NEW.source_connector, COALESCE(NEW.channel, ''), 1)
|
|
1079
|
-
ON CONFLICT(source_connector, channel) DO NOTHING;
|
|
1080
|
-
UPDATE connector_event_index
|
|
1081
|
-
SET operator_ingest_seq = (
|
|
1082
|
-
SELECT next_seq FROM connector_event_index_operator_seq_cursors
|
|
1083
|
-
WHERE source_connector = NEW.source_connector
|
|
1084
|
-
AND channel = COALESCE(NEW.channel, '')
|
|
1085
|
-
)
|
|
1086
|
-
WHERE event_index_id = NEW.event_index_id;
|
|
1087
|
-
UPDATE connector_event_index_operator_seq_cursors
|
|
1088
|
-
SET next_seq = next_seq + 1
|
|
1089
|
-
WHERE source_connector = NEW.source_connector
|
|
1090
|
-
AND channel = COALESCE(NEW.channel, '');
|
|
1091
|
-
END
|
|
1092
|
-
`);
|
|
1093
|
-
this.exec(`
|
|
1094
|
-
CREATE TRIGGER IF NOT EXISTS trg_connector_event_index_observation_seq_ai
|
|
1095
|
-
AFTER INSERT ON connector_event_index
|
|
1096
|
-
WHEN NEW.operator_observation_seq IS NULL
|
|
1097
|
-
BEGIN
|
|
1098
|
-
INSERT INTO connector_event_index_observation_cursors
|
|
1099
|
-
(source_connector, next_seq)
|
|
1100
|
-
VALUES (NEW.source_connector, 1)
|
|
1101
|
-
ON CONFLICT(source_connector) DO NOTHING;
|
|
1102
|
-
UPDATE connector_event_index
|
|
1103
|
-
SET operator_observation_seq = (
|
|
1104
|
-
SELECT next_seq FROM connector_event_index_observation_cursors
|
|
1105
|
-
WHERE source_connector = NEW.source_connector
|
|
1106
|
-
)
|
|
1107
|
-
WHERE event_index_id = NEW.event_index_id;
|
|
1108
|
-
UPDATE connector_event_index_observation_cursors
|
|
1109
|
-
SET next_seq = next_seq + 1
|
|
1110
|
-
WHERE source_connector = NEW.source_connector;
|
|
1111
|
-
END
|
|
1112
|
-
`);
|
|
1113
|
-
this.exec(`
|
|
1114
|
-
CREATE TRIGGER IF NOT EXISTS trg_connector_event_index_observation_seq_au
|
|
1115
|
-
AFTER UPDATE OF operator_observation_seq ON connector_event_index
|
|
1116
|
-
WHEN NEW.operator_observation_seq IS NULL AND OLD.operator_observation_seq IS NOT NULL
|
|
1117
|
-
BEGIN
|
|
1118
|
-
INSERT INTO connector_event_index_observation_cursors
|
|
1119
|
-
(source_connector, next_seq)
|
|
1120
|
-
VALUES (NEW.source_connector, 1)
|
|
1121
|
-
ON CONFLICT(source_connector) DO NOTHING;
|
|
1122
|
-
UPDATE connector_event_index
|
|
1123
|
-
SET operator_observation_seq = (
|
|
1124
|
-
SELECT next_seq FROM connector_event_index_observation_cursors
|
|
1125
|
-
WHERE source_connector = NEW.source_connector
|
|
1126
|
-
)
|
|
1127
|
-
WHERE event_index_id = NEW.event_index_id;
|
|
1128
|
-
UPDATE connector_event_index_observation_cursors
|
|
1129
|
-
SET next_seq = next_seq + 1
|
|
1130
|
-
WHERE source_connector = NEW.source_connector;
|
|
1131
|
-
END
|
|
1132
|
-
`);
|
|
1133
|
-
this.exec(`
|
|
1134
|
-
CREATE TRIGGER IF NOT EXISTS trg_connector_event_index_observation_seq_explicit_ai
|
|
1135
|
-
AFTER INSERT ON connector_event_index
|
|
1136
|
-
WHEN NEW.operator_observation_seq IS NOT NULL
|
|
1137
|
-
BEGIN
|
|
1138
|
-
INSERT INTO connector_event_index_observation_cursors
|
|
1139
|
-
(source_connector, next_seq)
|
|
1140
|
-
VALUES (NEW.source_connector, 1)
|
|
1141
|
-
ON CONFLICT(source_connector) DO NOTHING;
|
|
1142
|
-
UPDATE connector_event_index_observation_cursors
|
|
1143
|
-
SET next_seq = CASE
|
|
1144
|
-
WHEN next_seq <= NEW.operator_observation_seq THEN NEW.operator_observation_seq + 1
|
|
1145
|
-
ELSE next_seq
|
|
1146
|
-
END
|
|
1147
|
-
WHERE source_connector = NEW.source_connector;
|
|
1148
|
-
END
|
|
1149
|
-
`);
|
|
1150
|
-
this.assertMigration062Complete();
|
|
1151
|
-
this.prepare('INSERT OR IGNORE INTO schema_version (version, description) VALUES (?, ?)').run(62, 'Refresh connector event delivery and observation sequences');
|
|
1152
|
-
});
|
|
1153
|
-
}
|
|
1154
|
-
assertMigration062Complete() {
|
|
1155
|
-
const columns = this.tableColumns('connector_event_index');
|
|
1156
|
-
if (!columns.has('operator_observation_seq')) {
|
|
1157
|
-
throw new Error('Migration 062 recovery failed: missing connector_event_index.operator_observation_seq');
|
|
1158
|
-
}
|
|
1159
|
-
if (!this.tableExists('connector_event_index_observation_cursors')) {
|
|
1160
|
-
throw new Error('Migration 062 recovery failed: missing connector_event_index_observation_cursors');
|
|
1161
|
-
}
|
|
1162
|
-
const cursorColumns = this.tableColumns('connector_event_index_observation_cursors');
|
|
1163
|
-
for (const columnName of ['source_connector', 'next_seq']) {
|
|
1164
|
-
if (!cursorColumns.has(columnName)) {
|
|
1165
|
-
throw new Error(`Migration 062 recovery failed: missing connector_event_index_observation_cursors.${columnName}`);
|
|
1166
|
-
}
|
|
1167
|
-
}
|
|
1168
|
-
if (!this.indexExists('idx_connector_event_index_observation_seq')) {
|
|
1169
|
-
throw new Error('Migration 062 recovery failed: missing index idx_connector_event_index_observation_seq');
|
|
1170
|
-
}
|
|
1171
|
-
for (const triggerName of [
|
|
1172
|
-
'trg_connector_event_index_operator_ingest_seq_au',
|
|
1173
|
-
'trg_connector_event_index_observation_seq_ai',
|
|
1174
|
-
'trg_connector_event_index_observation_seq_au',
|
|
1175
|
-
'trg_connector_event_index_observation_seq_explicit_ai',
|
|
1176
|
-
]) {
|
|
1177
|
-
if (!this.triggerExists(triggerName)) {
|
|
1178
|
-
throw new Error(`Migration 062 recovery failed: missing trigger ${triggerName}`);
|
|
1179
|
-
}
|
|
1180
|
-
}
|
|
1181
|
-
}
|
|
1182
|
-
recoverConnectorEventLegacyRefreshMigration063() {
|
|
1183
|
-
this.transaction(() => {
|
|
1184
|
-
this.assertMigration062Complete();
|
|
1185
|
-
const connectorColumns = this.tableColumns('connector_event_index');
|
|
1186
|
-
for (const columnName of [
|
|
1187
|
-
'content_hash',
|
|
1188
|
-
'metadata_json',
|
|
1189
|
-
'source_timestamp_ms',
|
|
1190
|
-
'source_type',
|
|
1191
|
-
'channel',
|
|
1192
|
-
'operator_ingest_seq',
|
|
1193
|
-
'operator_observation_seq',
|
|
1194
|
-
]) {
|
|
1195
|
-
if (!connectorColumns.has(columnName)) {
|
|
1196
|
-
throw new Error(`Migration 063 recovery failed: missing connector_event_index.${columnName}`);
|
|
1197
|
-
}
|
|
1198
|
-
}
|
|
1199
|
-
this.exec(`
|
|
1200
|
-
CREATE TRIGGER IF NOT EXISTS trg_connector_event_index_legacy_content_refresh_au
|
|
1201
|
-
AFTER UPDATE OF content_hash, metadata_json, source_timestamp_ms, source_type, channel
|
|
1202
|
-
ON connector_event_index
|
|
1203
|
-
WHEN (
|
|
1204
|
-
OLD.content_hash IS NOT NEW.content_hash
|
|
1205
|
-
OR OLD.metadata_json IS NOT NEW.metadata_json
|
|
1206
|
-
OR OLD.source_timestamp_ms IS NOT NEW.source_timestamp_ms
|
|
1207
|
-
OR OLD.source_type IS NOT NEW.source_type
|
|
1208
|
-
OR OLD.channel IS NOT NEW.channel
|
|
1209
|
-
) AND (
|
|
1210
|
-
NEW.operator_ingest_seq IS OLD.operator_ingest_seq
|
|
1211
|
-
OR NEW.operator_observation_seq IS OLD.operator_observation_seq
|
|
1212
|
-
)
|
|
1213
|
-
BEGIN
|
|
1214
|
-
UPDATE connector_event_index
|
|
1215
|
-
SET operator_ingest_seq = CASE
|
|
1216
|
-
WHEN NEW.operator_ingest_seq IS OLD.operator_ingest_seq THEN NULL
|
|
1217
|
-
ELSE NEW.operator_ingest_seq
|
|
1218
|
-
END,
|
|
1219
|
-
operator_observation_seq = CASE
|
|
1220
|
-
WHEN NEW.operator_observation_seq IS OLD.operator_observation_seq THEN NULL
|
|
1221
|
-
ELSE NEW.operator_observation_seq
|
|
1222
|
-
END
|
|
1223
|
-
WHERE event_index_id = NEW.event_index_id;
|
|
1224
|
-
END
|
|
1225
|
-
`);
|
|
1226
|
-
if (!this.triggerExists('trg_connector_event_index_legacy_content_refresh_au')) {
|
|
1227
|
-
throw new Error('Migration 063 recovery failed: missing trigger trg_connector_event_index_legacy_content_refresh_au');
|
|
1228
|
-
}
|
|
1229
|
-
this.prepare('INSERT OR IGNORE INTO schema_version (version, description) VALUES (?, ?)').run(63, 'Refresh sequences for legacy connector event updates');
|
|
1230
|
-
});
|
|
1231
|
-
}
|
|
1232
2640
|
tableColumns(tableName) {
|
|
1233
2641
|
if (!SQLITE_IDENTIFIER_PATTERN.test(tableName)) {
|
|
1234
2642
|
throw new Error('Invalid SQLite table identifier');
|
|
@@ -1247,6 +2655,13 @@ class NodeSQLiteAdapter extends base_adapter_js_1.DatabaseAdapter {
|
|
|
1247
2655
|
const row = this.prepare("SELECT name FROM sqlite_master WHERE type='index' AND name = ?").get(indexName);
|
|
1248
2656
|
return row?.name === indexName;
|
|
1249
2657
|
}
|
|
2658
|
+
indexSql(indexName) {
|
|
2659
|
+
if (!SQLITE_IDENTIFIER_PATTERN.test(indexName)) {
|
|
2660
|
+
throw new Error('Invalid SQLite index identifier');
|
|
2661
|
+
}
|
|
2662
|
+
const row = this.prepare("SELECT sql FROM sqlite_master WHERE type='index' AND name = ?").get(indexName);
|
|
2663
|
+
return row?.sql ?? '';
|
|
2664
|
+
}
|
|
1250
2665
|
triggerExists(triggerName) {
|
|
1251
2666
|
const row = this.prepare("SELECT name FROM sqlite_master WHERE type='trigger' AND name = ?").get(triggerName);
|
|
1252
2667
|
return row?.name === triggerName;
|