@jungjaehoon/mama-core 2.4.1 → 4.0.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/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 +1968 -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/commitments.d.ts +188 -0
- package/dist/knowledge/commitments.js +370 -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/judgments.d.ts +118 -0
- package/dist/knowledge/judgments.js +808 -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 +155 -0
- package/dist/runtime/drivers/claude-cli-wrapper.js +392 -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 +424 -0
- package/dist/runtime/drivers/persistent-cli-process.js +1543 -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
|
@@ -0,0 +1,1585 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Knowledge graph queries: how a topic got to where it is, and what else it
|
|
4
|
+
* argues with. Two surfaces live here: the decision-chain graph (supersedes /
|
|
5
|
+
* decision_edges) and the twin-edge graph (neighborhood, paths, timeline over
|
|
6
|
+
* `twin_edges`).
|
|
7
|
+
*
|
|
8
|
+
* Every function takes the adapter it reads through, so a caller cannot walk a
|
|
9
|
+
* graph in a database it did not open.
|
|
10
|
+
*
|
|
11
|
+
* @module knowledge/graph-query
|
|
12
|
+
*/
|
|
13
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
+
exports.AgentGraphValidationError = void 0;
|
|
15
|
+
exports.queryDecisionGraph = queryDecisionGraph;
|
|
16
|
+
exports.querySemanticEdges = querySemanticEdges;
|
|
17
|
+
exports.getGraphNeighborhood = getGraphNeighborhood;
|
|
18
|
+
exports.getGraphPaths = getGraphPaths;
|
|
19
|
+
exports.getGraphTimeline = getGraphTimeline;
|
|
20
|
+
exports.queryGraph = queryGraph;
|
|
21
|
+
const access_js_1 = require("../knowledge/access.js");
|
|
22
|
+
const judgments_js_1 = require("./judgments.js");
|
|
23
|
+
const observations_js_1 = require("./observations.js");
|
|
24
|
+
const case_store_js_1 = require("./case-store.js");
|
|
25
|
+
const store_js_1 = require("../registry/store.js");
|
|
26
|
+
const twin_edge_types_js_1 = require("../knowledge/twin-edge-types.js");
|
|
27
|
+
/**
|
|
28
|
+
* `refined_from` is stored as a JSON array of decision ids.
|
|
29
|
+
*
|
|
30
|
+
* Both branches of queryDecisionGraph used to answer a parse failure with an
|
|
31
|
+
* empty array, which reads downstream as "this decision refines nothing" - the
|
|
32
|
+
* same sentence a decision with no ancestry produces. Corruption is not
|
|
33
|
+
* ancestry, so it is reported instead. The stored value is left alone.
|
|
34
|
+
*/
|
|
35
|
+
function parseRefinedFrom(decision) {
|
|
36
|
+
if (!decision.refined_from || typeof decision.refined_from !== 'string') {
|
|
37
|
+
return decision.refined_from;
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
return JSON.parse(decision.refined_from);
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
44
|
+
throw new Error(`decision ${decision.id} has unreadable refined_from: ${message}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Walk a topic's supersedes chain with a recursive CTE.
|
|
49
|
+
*
|
|
50
|
+
* @param adapter - Database to read through
|
|
51
|
+
* @param topic - Decision topic to query
|
|
52
|
+
* @param anchorId - Start from this decision instead of the topic's current head
|
|
53
|
+
* @returns Decisions ordered by recency, each carrying its approved edges
|
|
54
|
+
*/
|
|
55
|
+
async function queryDecisionGraph(adapter, topic, anchorId) {
|
|
56
|
+
try {
|
|
57
|
+
if (!anchorId) {
|
|
58
|
+
const decisions = adapter
|
|
59
|
+
.prepare(`
|
|
60
|
+
WITH RECURSIVE decision_chain AS (
|
|
61
|
+
SELECT * FROM decisions WHERE topic = ? AND superseded_by IS NULL
|
|
62
|
+
UNION
|
|
63
|
+
SELECT d.* FROM decisions d
|
|
64
|
+
JOIN decision_chain dc ON d.id = dc.supersedes
|
|
65
|
+
)
|
|
66
|
+
SELECT * FROM decision_chain ORDER BY created_at DESC, id DESC
|
|
67
|
+
`)
|
|
68
|
+
.all(topic);
|
|
69
|
+
const edgesStmt = adapter.prepare(`
|
|
70
|
+
SELECT * FROM decision_edges
|
|
71
|
+
WHERE from_id = ?
|
|
72
|
+
AND (approved_by_user = 1 OR approved_by_user IS NULL)
|
|
73
|
+
`);
|
|
74
|
+
for (const decision of decisions) {
|
|
75
|
+
decision.edges = edgesStmt.all(decision.id);
|
|
76
|
+
decision.refined_from = parseRefinedFrom(decision);
|
|
77
|
+
}
|
|
78
|
+
return decisions;
|
|
79
|
+
}
|
|
80
|
+
const stmt = adapter.prepare(`
|
|
81
|
+
WITH RECURSIVE decision_chain AS (
|
|
82
|
+
SELECT * FROM (
|
|
83
|
+
SELECT * FROM decisions
|
|
84
|
+
WHERE id = ?
|
|
85
|
+
ORDER BY created_at DESC, id DESC
|
|
86
|
+
LIMIT 1
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
UNION ALL
|
|
90
|
+
|
|
91
|
+
-- Recursive case: Get previous decisions
|
|
92
|
+
SELECT d.* FROM decisions d
|
|
93
|
+
JOIN decision_chain dc ON d.id = dc.supersedes
|
|
94
|
+
)
|
|
95
|
+
SELECT * FROM decision_chain
|
|
96
|
+
ORDER BY created_at DESC
|
|
97
|
+
`);
|
|
98
|
+
const decisions = stmt.all(anchorId);
|
|
99
|
+
// Join with decision_edges to include relationships
|
|
100
|
+
// Prepare statement once outside loop for performance
|
|
101
|
+
const edgesStmt = adapter.prepare(`
|
|
102
|
+
SELECT * FROM decision_edges
|
|
103
|
+
WHERE from_id = ?
|
|
104
|
+
AND (approved_by_user = 1 OR approved_by_user IS NULL)
|
|
105
|
+
`);
|
|
106
|
+
for (const decision of decisions) {
|
|
107
|
+
decision.edges = edgesStmt.all(decision.id);
|
|
108
|
+
decision.refined_from = parseRefinedFrom(decision);
|
|
109
|
+
}
|
|
110
|
+
return decisions;
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
114
|
+
throw new Error(`Decision graph query failed: ${message}`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Outgoing and incoming semantic edges for a set of decisions.
|
|
119
|
+
*
|
|
120
|
+
* @param adapter - Database to read through
|
|
121
|
+
* @param decisionIds - Decision IDs to query edges for
|
|
122
|
+
* @returns Edges categorized by relationship and direction
|
|
123
|
+
*/
|
|
124
|
+
async function querySemanticEdges(adapter, decisionIds) {
|
|
125
|
+
if (!decisionIds || decisionIds.length === 0) {
|
|
126
|
+
return {
|
|
127
|
+
refines: [],
|
|
128
|
+
refined_by: [],
|
|
129
|
+
contradicts: [],
|
|
130
|
+
contradicted_by: [],
|
|
131
|
+
// Story 2.1: Extended edge types
|
|
132
|
+
builds_on: [],
|
|
133
|
+
built_on_by: [],
|
|
134
|
+
debates: [],
|
|
135
|
+
debated_by: [],
|
|
136
|
+
synthesizes: [],
|
|
137
|
+
synthesized_by: [],
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
try {
|
|
141
|
+
// Build placeholders for IN clause
|
|
142
|
+
const placeholders = decisionIds.map(() => '?').join(',');
|
|
143
|
+
// Story 2.1: Include new edge types in query
|
|
144
|
+
const edgeTypes = ['refines', 'contradicts', 'builds_on', 'debates', 'synthesizes'];
|
|
145
|
+
const edgeTypePlaceholders = edgeTypes.map(() => '?').join(',');
|
|
146
|
+
// Query outgoing edges (from_id = decision)
|
|
147
|
+
const outgoingStmt = adapter.prepare(`
|
|
148
|
+
SELECT e.*, d.topic, d.decision, d.confidence, d.created_at
|
|
149
|
+
FROM decision_edges e
|
|
150
|
+
JOIN decisions d ON e.to_id = d.id
|
|
151
|
+
WHERE e.from_id IN (${placeholders})
|
|
152
|
+
AND e.relationship IN (${edgeTypePlaceholders})
|
|
153
|
+
AND (e.approved_by_user = 1 OR e.approved_by_user IS NULL)
|
|
154
|
+
ORDER BY e.created_at DESC
|
|
155
|
+
`);
|
|
156
|
+
const outgoingEdges = outgoingStmt.all(...decisionIds, ...edgeTypes);
|
|
157
|
+
// Query incoming edges (to_id = decision)
|
|
158
|
+
const incomingStmt = adapter.prepare(`
|
|
159
|
+
SELECT e.*, d.topic, d.decision, d.confidence, d.created_at
|
|
160
|
+
FROM decision_edges e
|
|
161
|
+
JOIN decisions d ON e.from_id = d.id
|
|
162
|
+
WHERE e.to_id IN (${placeholders})
|
|
163
|
+
AND e.relationship IN (${edgeTypePlaceholders})
|
|
164
|
+
AND (e.approved_by_user = 1 OR e.approved_by_user IS NULL)
|
|
165
|
+
ORDER BY e.created_at DESC
|
|
166
|
+
`);
|
|
167
|
+
const incomingEdges = incomingStmt.all(...decisionIds, ...edgeTypes);
|
|
168
|
+
// Categorize edges (original + v1.3 extended)
|
|
169
|
+
const refines = outgoingEdges.filter((e) => e.relationship === 'refines');
|
|
170
|
+
const refined_by = incomingEdges.filter((e) => e.relationship === 'refines');
|
|
171
|
+
const contradicts = outgoingEdges.filter((e) => e.relationship === 'contradicts');
|
|
172
|
+
const contradicted_by = incomingEdges.filter((e) => e.relationship === 'contradicts');
|
|
173
|
+
// Story 2.1: New edge type categories
|
|
174
|
+
const builds_on = outgoingEdges.filter((e) => e.relationship === 'builds_on');
|
|
175
|
+
const built_on_by = incomingEdges.filter((e) => e.relationship === 'builds_on');
|
|
176
|
+
const debates = outgoingEdges.filter((e) => e.relationship === 'debates');
|
|
177
|
+
const debated_by = incomingEdges.filter((e) => e.relationship === 'debates');
|
|
178
|
+
const synthesizes = outgoingEdges.filter((e) => e.relationship === 'synthesizes');
|
|
179
|
+
const synthesized_by = incomingEdges.filter((e) => e.relationship === 'synthesizes');
|
|
180
|
+
return {
|
|
181
|
+
refines,
|
|
182
|
+
refined_by,
|
|
183
|
+
contradicts,
|
|
184
|
+
contradicted_by,
|
|
185
|
+
builds_on,
|
|
186
|
+
built_on_by,
|
|
187
|
+
debates,
|
|
188
|
+
debated_by,
|
|
189
|
+
synthesizes,
|
|
190
|
+
synthesized_by,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
catch (error) {
|
|
194
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
195
|
+
throw new Error(`Semantic edges query failed: ${message}`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
// ── Twin-edge graph ────────────────────────────────────────────────────────────
|
|
199
|
+
class AgentGraphValidationError extends Error {
|
|
200
|
+
constructor(message) {
|
|
201
|
+
super(message);
|
|
202
|
+
this.name = 'AgentGraphValidationError';
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
exports.AgentGraphValidationError = AgentGraphValidationError;
|
|
206
|
+
const DEFAULT_GRAPH_LIMIT = 100;
|
|
207
|
+
const DEFAULT_PATH_LIMIT = 10;
|
|
208
|
+
const MAX_DEPTH = 5;
|
|
209
|
+
// A path count cannot bound a dense graph with an unreachable target.
|
|
210
|
+
const MAX_PATH_FRONTIER = 1000;
|
|
211
|
+
const MAX_PATH_EDGE_WORK = 10000;
|
|
212
|
+
function refKey(ref) {
|
|
213
|
+
return `${ref.kind}:${ref.id}`;
|
|
214
|
+
}
|
|
215
|
+
function edgeKey(edge) {
|
|
216
|
+
return edge.edge_id;
|
|
217
|
+
}
|
|
218
|
+
function eventKey(event) {
|
|
219
|
+
if (event.kind === 'edge') {
|
|
220
|
+
return event.edge.edge_id;
|
|
221
|
+
}
|
|
222
|
+
return `${event.ref.kind}:${event.ref.id}`;
|
|
223
|
+
}
|
|
224
|
+
function eventKindRank(kind) {
|
|
225
|
+
if (kind === 'memory') {
|
|
226
|
+
return 1;
|
|
227
|
+
}
|
|
228
|
+
if (kind === 'raw') {
|
|
229
|
+
return 2;
|
|
230
|
+
}
|
|
231
|
+
if (kind === 'case') {
|
|
232
|
+
return 3;
|
|
233
|
+
}
|
|
234
|
+
return 4;
|
|
235
|
+
}
|
|
236
|
+
function normalizeDepth(value, fallback) {
|
|
237
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
238
|
+
return fallback;
|
|
239
|
+
}
|
|
240
|
+
return Math.max(0, Math.min(MAX_DEPTH, Math.floor(value)));
|
|
241
|
+
}
|
|
242
|
+
function normalizeLimit(value, fallback) {
|
|
243
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
244
|
+
return fallback;
|
|
245
|
+
}
|
|
246
|
+
return Math.max(1, Math.floor(value));
|
|
247
|
+
}
|
|
248
|
+
function addNode(nodes, seen, ref) {
|
|
249
|
+
const key = refKey(ref);
|
|
250
|
+
if (!seen.has(key)) {
|
|
251
|
+
seen.add(key);
|
|
252
|
+
nodes.push(ref);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
function addRef(refs, seen, ref) {
|
|
256
|
+
if (ref.kind === 'edge' || ref.kind === 'report') {
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
const key = refKey(ref);
|
|
260
|
+
if (!seen.has(key)) {
|
|
261
|
+
seen.add(key);
|
|
262
|
+
refs.push(ref);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
function oppositeRef(edge, current) {
|
|
266
|
+
const currentKey = refKey(current);
|
|
267
|
+
const refs = [];
|
|
268
|
+
if (refKey(edge.subject_ref) !== currentKey) {
|
|
269
|
+
refs.push(edge.subject_ref);
|
|
270
|
+
}
|
|
271
|
+
if (refKey(edge.object_ref) !== currentKey) {
|
|
272
|
+
refs.push(edge.object_ref);
|
|
273
|
+
}
|
|
274
|
+
if (refs.length === 0) {
|
|
275
|
+
refs.push(edge.object_ref);
|
|
276
|
+
}
|
|
277
|
+
return refs;
|
|
278
|
+
}
|
|
279
|
+
function listFilteredEdges(adapter, refs, input) {
|
|
280
|
+
// Spread, not a field-by-field re-listing. Re-listing is how the channel grant was
|
|
281
|
+
// silently dropped here while every other path enforced it: a new field on the
|
|
282
|
+
// visibility type simply never arrived, and nothing failed to say so.
|
|
283
|
+
return (0, access_js_1.listVisibleTwinEdgesForRefs)(adapter, refs, {
|
|
284
|
+
...input.visibility,
|
|
285
|
+
edgeTypes: input.edge_filters?.edge_types,
|
|
286
|
+
asOfMs: input.as_of_ms,
|
|
287
|
+
limit: input.limit,
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
function assertRefsVisible(adapter, refs, visibility, asOfMs) {
|
|
291
|
+
try {
|
|
292
|
+
(0, access_js_1.assertTwinRefsVisible)(adapter, refs, { ...visibility, asOfMs });
|
|
293
|
+
}
|
|
294
|
+
catch (error) {
|
|
295
|
+
// A visibility refusal is a denial, not a malformed query — keep its type
|
|
296
|
+
// so callers can answer denied instead of invalid.
|
|
297
|
+
if (error instanceof access_js_1.TwinRefNotVisibleError) {
|
|
298
|
+
throw error;
|
|
299
|
+
}
|
|
300
|
+
throw new AgentGraphValidationError(error instanceof Error ? error.message : String(error));
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
function graphVisibility(input) {
|
|
304
|
+
return {
|
|
305
|
+
scopes: input.scopes,
|
|
306
|
+
connectors: input.connectors,
|
|
307
|
+
connectorWideRead: input.connector_wide_read,
|
|
308
|
+
projectRefs: input.project_refs,
|
|
309
|
+
tenantId: input.tenant_id,
|
|
310
|
+
principalId: input.principal_id,
|
|
311
|
+
agentId: input.agent_id,
|
|
312
|
+
maxSourceMs: input.max_source_ms,
|
|
313
|
+
includeReplaced: input.history === 'all',
|
|
314
|
+
...(input.channels ? { channels: input.channels } : {}),
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
function projectCurrentEdges(adapter, edges, visibility) {
|
|
318
|
+
if (edges.length === 0) {
|
|
319
|
+
return [];
|
|
320
|
+
}
|
|
321
|
+
const edgeIds = [...new Set(edges.map((edge) => edge.edge_id))];
|
|
322
|
+
const placeholders = edgeIds.map(() => '?').join(', ');
|
|
323
|
+
const assignmentRows = adapter
|
|
324
|
+
.prepare(`SELECT edge_id, endpoint, resolved_node_id
|
|
325
|
+
FROM registry_ref_assignments assignment
|
|
326
|
+
WHERE edge_id IN (${placeholders})
|
|
327
|
+
AND committed_revision = (
|
|
328
|
+
SELECT MAX(current_assignment.committed_revision)
|
|
329
|
+
FROM registry_ref_assignments current_assignment
|
|
330
|
+
WHERE current_assignment.edge_id = assignment.edge_id
|
|
331
|
+
AND current_assignment.endpoint = assignment.endpoint
|
|
332
|
+
)`)
|
|
333
|
+
.all(...edgeIds);
|
|
334
|
+
const assignments = new Map(assignmentRows.map((row) => [
|
|
335
|
+
`${row.edge_id}\0${row.endpoint}`,
|
|
336
|
+
row.resolved_node_id === null
|
|
337
|
+
? null
|
|
338
|
+
: { kind: 'registry', id: row.resolved_node_id },
|
|
339
|
+
]));
|
|
340
|
+
const projections = edges.flatMap((edge) => ['from', 'to'].map((endpoint) => {
|
|
341
|
+
const original = endpoint === 'from' ? edge.subject_ref : edge.object_ref;
|
|
342
|
+
const key = `${edge.edge_id}\0${endpoint}`;
|
|
343
|
+
return {
|
|
344
|
+
edge_id: edge.edge_id,
|
|
345
|
+
endpoint,
|
|
346
|
+
original_ref: original,
|
|
347
|
+
current_ref: assignments.has(key) ? (assignments.get(key) ?? null) : original,
|
|
348
|
+
};
|
|
349
|
+
}));
|
|
350
|
+
const registryIds = [
|
|
351
|
+
...new Set(projections
|
|
352
|
+
.map((projection) => projection.current_ref)
|
|
353
|
+
.filter((ref) => ref?.kind === 'registry')
|
|
354
|
+
.map((ref) => ref.id)),
|
|
355
|
+
];
|
|
356
|
+
const resolvedRegistry = new Map();
|
|
357
|
+
if (registryIds.length > 0) {
|
|
358
|
+
const registryPlaceholders = registryIds.map(() => '?').join(', ');
|
|
359
|
+
const rows = adapter
|
|
360
|
+
.prepare(`WITH RECURSIVE registry_chain(origin_id, id, merged_into, depth) AS (
|
|
361
|
+
SELECT id, id, merged_into, 0 FROM registry_nodes WHERE id IN (${registryPlaceholders})
|
|
362
|
+
UNION ALL
|
|
363
|
+
SELECT chain.origin_id, node.id, node.merged_into, chain.depth + 1
|
|
364
|
+
FROM registry_chain chain
|
|
365
|
+
JOIN registry_nodes node ON node.id = chain.merged_into
|
|
366
|
+
WHERE chain.depth < 100
|
|
367
|
+
)
|
|
368
|
+
SELECT origin_id, id, merged_into, depth FROM registry_chain ORDER BY origin_id, depth`)
|
|
369
|
+
.all(...registryIds);
|
|
370
|
+
// The rows are ordered by origin and depth. Group their terminal row once:
|
|
371
|
+
// filtering the whole result for every origin made a wide graph O(N²).
|
|
372
|
+
const lastByOrigin = new Map();
|
|
373
|
+
for (const row of rows) {
|
|
374
|
+
lastByOrigin.set(row.origin_id, row);
|
|
375
|
+
}
|
|
376
|
+
for (const id of registryIds) {
|
|
377
|
+
const last = lastByOrigin.get(id);
|
|
378
|
+
resolvedRegistry.set(id, last && last.merged_into === null ? last.id : null);
|
|
379
|
+
}
|
|
380
|
+
const currentIds = [
|
|
381
|
+
...new Set([...resolvedRegistry.values()].filter((id) => !!id)),
|
|
382
|
+
];
|
|
383
|
+
const visibleIds = new Set();
|
|
384
|
+
if (!visibility.scopes || visibility.scopes.length === 0) {
|
|
385
|
+
currentIds.forEach((id) => visibleIds.add(id));
|
|
386
|
+
}
|
|
387
|
+
else if (currentIds.length > 0) {
|
|
388
|
+
const currentPlaceholders = currentIds.map(() => '?').join(', ');
|
|
389
|
+
const scopeClauses = visibility.scopes
|
|
390
|
+
.map(() => '(scope_kind = ? AND scope_id = ?)')
|
|
391
|
+
.join(' OR ');
|
|
392
|
+
const bindings = adapter
|
|
393
|
+
.prepare(`SELECT DISTINCT node_id FROM registry_scope_bindings
|
|
394
|
+
WHERE node_id IN (${currentPlaceholders}) AND (${scopeClauses})`)
|
|
395
|
+
.all(...currentIds, ...visibility.scopes.flatMap((scope) => [scope.kind, scope.id]));
|
|
396
|
+
bindings.forEach((row) => visibleIds.add(row.node_id));
|
|
397
|
+
}
|
|
398
|
+
for (const projection of projections) {
|
|
399
|
+
if (projection.current_ref?.kind === 'registry') {
|
|
400
|
+
const resolved = resolvedRegistry.get(projection.current_ref.id) ?? null;
|
|
401
|
+
projection.current_ref =
|
|
402
|
+
resolved && visibleIds.has(resolved) ? { kind: 'registry', id: resolved } : null;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
const nonRegistryRefs = projections
|
|
407
|
+
.map((projection) => projection.current_ref)
|
|
408
|
+
.filter((ref) => ref !== null && ref.kind !== 'registry');
|
|
409
|
+
const visibleNonRegistry = (0, access_js_1.visibleTwinRefKeysRecursive)(adapter, nonRegistryRefs, visibility);
|
|
410
|
+
for (const projection of projections) {
|
|
411
|
+
const current = projection.current_ref;
|
|
412
|
+
if (!current || current.kind === 'registry') {
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
if (!visibleNonRegistry.has(`${current.kind}\0${current.id}`)) {
|
|
416
|
+
projection.current_ref = null;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
return projections;
|
|
420
|
+
}
|
|
421
|
+
function numberMs(value, field, refId) {
|
|
422
|
+
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
423
|
+
return Math.floor(value);
|
|
424
|
+
}
|
|
425
|
+
throw new Error(`Invalid ${field} timestamp for graph timeline ref ${refId}.`);
|
|
426
|
+
}
|
|
427
|
+
function nullableNumberMs(value, field, refId) {
|
|
428
|
+
if (value === null || value === undefined) {
|
|
429
|
+
return null;
|
|
430
|
+
}
|
|
431
|
+
return numberMs(value, field, refId);
|
|
432
|
+
}
|
|
433
|
+
function parseTimestampMs(value, field, refId) {
|
|
434
|
+
if (typeof value === 'number') {
|
|
435
|
+
return Math.floor(value);
|
|
436
|
+
}
|
|
437
|
+
if (typeof value === 'string') {
|
|
438
|
+
const parsed = Date.parse(value);
|
|
439
|
+
if (Number.isFinite(parsed)) {
|
|
440
|
+
return parsed;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
throw new Error(`Invalid ${field} timestamp for graph timeline ref ${refId}.`);
|
|
444
|
+
}
|
|
445
|
+
function timelineWindow(input) {
|
|
446
|
+
const upperBounds = [input.to_ms, input.as_of_ms].filter((value) => typeof value === 'number');
|
|
447
|
+
return {
|
|
448
|
+
fromMs: input.from_ms,
|
|
449
|
+
toMs: upperBounds.length > 0 ? Math.min(...upperBounds) : undefined,
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
function isInTimelineWindow(atMs, window) {
|
|
453
|
+
return ((window.fromMs === undefined || atMs >= window.fromMs) &&
|
|
454
|
+
(window.toMs === undefined || atMs <= window.toMs));
|
|
455
|
+
}
|
|
456
|
+
function loadTimelineRecordEvent(adapter, ref) {
|
|
457
|
+
if (ref.kind === 'memory') {
|
|
458
|
+
const row = adapter
|
|
459
|
+
.prepare(`
|
|
460
|
+
SELECT id, topic, decision, created_at, event_datetime
|
|
461
|
+
FROM decisions
|
|
462
|
+
WHERE id = ?
|
|
463
|
+
LIMIT 1
|
|
464
|
+
`)
|
|
465
|
+
.get(ref.id);
|
|
466
|
+
if (!row) {
|
|
467
|
+
return null;
|
|
468
|
+
}
|
|
469
|
+
const eventDatetime = nullableNumberMs(row.event_datetime, 'decisions.event_datetime', ref.id);
|
|
470
|
+
const createdAt = numberMs(row.created_at, 'decisions.created_at', ref.id);
|
|
471
|
+
return {
|
|
472
|
+
kind: 'memory',
|
|
473
|
+
at_ms: eventDatetime ?? createdAt,
|
|
474
|
+
ref,
|
|
475
|
+
memory: {
|
|
476
|
+
id: row.id,
|
|
477
|
+
topic: row.topic,
|
|
478
|
+
decision: row.decision,
|
|
479
|
+
created_at: createdAt,
|
|
480
|
+
event_datetime: eventDatetime,
|
|
481
|
+
},
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
if (ref.kind === 'raw') {
|
|
485
|
+
const row = adapter
|
|
486
|
+
.prepare(
|
|
487
|
+
// A raw ref resolves against observations: one evidence space, and the
|
|
488
|
+
// connector index belongs to the package that has connectors. The aliases
|
|
489
|
+
// keep the wire shape callers already read.
|
|
490
|
+
`
|
|
491
|
+
SELECT
|
|
492
|
+
observation_id AS event_index_id, source AS source_connector, source_type,
|
|
493
|
+
source_id, source_locator, title, source_at AS event_datetime,
|
|
494
|
+
observed_at AS source_timestamp_ms, observation_id AS current_observation_id
|
|
495
|
+
FROM observation_versions
|
|
496
|
+
WHERE observation_id = ?
|
|
497
|
+
LIMIT 1
|
|
498
|
+
`)
|
|
499
|
+
.get(ref.id);
|
|
500
|
+
if (!row) {
|
|
501
|
+
return null;
|
|
502
|
+
}
|
|
503
|
+
const eventDatetime = nullableNumberMs(row.event_datetime, 'observation_versions.source_at', ref.id);
|
|
504
|
+
const sourceTimestampMs = numberMs(row.source_timestamp_ms, 'observation_versions.observed_at', ref.id);
|
|
505
|
+
return {
|
|
506
|
+
kind: 'raw',
|
|
507
|
+
at_ms: eventDatetime ?? sourceTimestampMs,
|
|
508
|
+
ref,
|
|
509
|
+
raw: {
|
|
510
|
+
event_index_id: row.event_index_id,
|
|
511
|
+
source_connector: row.source_connector,
|
|
512
|
+
source_type: row.source_type,
|
|
513
|
+
source_id: row.source_id,
|
|
514
|
+
source_locator: row.source_locator,
|
|
515
|
+
title: row.title,
|
|
516
|
+
event_datetime: eventDatetime,
|
|
517
|
+
source_timestamp_ms: sourceTimestampMs,
|
|
518
|
+
observation_ref: row.current_observation_id,
|
|
519
|
+
},
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
if (ref.kind === 'case') {
|
|
523
|
+
const row = adapter
|
|
524
|
+
.prepare(`
|
|
525
|
+
SELECT case_id, title, status, last_activity_at, created_at, updated_at
|
|
526
|
+
FROM case_truth
|
|
527
|
+
WHERE case_id = ?
|
|
528
|
+
LIMIT 1
|
|
529
|
+
`)
|
|
530
|
+
.get(ref.id);
|
|
531
|
+
if (!row) {
|
|
532
|
+
return null;
|
|
533
|
+
}
|
|
534
|
+
return {
|
|
535
|
+
kind: 'case',
|
|
536
|
+
at_ms: parseTimestampMs(row.last_activity_at ?? row.updated_at, 'case_truth.updated_at', ref.id),
|
|
537
|
+
ref,
|
|
538
|
+
case: {
|
|
539
|
+
case_id: row.case_id,
|
|
540
|
+
title: row.title,
|
|
541
|
+
status: row.status,
|
|
542
|
+
created_at: row.created_at,
|
|
543
|
+
updated_at: row.updated_at,
|
|
544
|
+
last_activity_at: row.last_activity_at,
|
|
545
|
+
},
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
return null;
|
|
549
|
+
}
|
|
550
|
+
function getGraphNeighborhood(adapter, input) {
|
|
551
|
+
const depth = normalizeDepth(input.depth, 1);
|
|
552
|
+
const limit = normalizeLimit(input.limit, DEFAULT_GRAPH_LIMIT);
|
|
553
|
+
const visibility = graphVisibility(input);
|
|
554
|
+
assertRefsVisible(adapter, [input.ref], visibility, input.as_of_ms);
|
|
555
|
+
const nodes = [];
|
|
556
|
+
const edges = [];
|
|
557
|
+
const seenNodes = new Set();
|
|
558
|
+
const seenEdges = new Set();
|
|
559
|
+
addNode(nodes, seenNodes, input.ref);
|
|
560
|
+
let frontier = [input.ref];
|
|
561
|
+
for (let currentDepth = 0; currentDepth < depth && frontier.length > 0; currentDepth++) {
|
|
562
|
+
const found = listFilteredEdges(adapter, frontier, {
|
|
563
|
+
visibility,
|
|
564
|
+
edge_filters: input.edge_filters,
|
|
565
|
+
as_of_ms: input.as_of_ms,
|
|
566
|
+
limit,
|
|
567
|
+
});
|
|
568
|
+
const direction = input.direction ?? 'both';
|
|
569
|
+
const frontierKeys = new Set(frontier.map(refKey));
|
|
570
|
+
const nextFrontier = [];
|
|
571
|
+
const nextKeys = new Set();
|
|
572
|
+
for (const edge of found) {
|
|
573
|
+
if (seenEdges.size >= limit) {
|
|
574
|
+
break;
|
|
575
|
+
}
|
|
576
|
+
const fromHere = frontierKeys.has(refKey(edge.subject_ref));
|
|
577
|
+
const toHere = frontierKeys.has(refKey(edge.object_ref));
|
|
578
|
+
if (direction === 'out' ? !fromHere : direction === 'in' ? !toHere : !fromHere && !toHere) {
|
|
579
|
+
continue;
|
|
580
|
+
}
|
|
581
|
+
if (!seenEdges.has(edgeKey(edge))) {
|
|
582
|
+
seenEdges.add(edgeKey(edge));
|
|
583
|
+
edges.push(edge);
|
|
584
|
+
}
|
|
585
|
+
const expand = direction === 'out'
|
|
586
|
+
? [edge.object_ref]
|
|
587
|
+
: direction === 'in'
|
|
588
|
+
? [edge.subject_ref]
|
|
589
|
+
: [edge.subject_ref, edge.object_ref];
|
|
590
|
+
for (const endpoint of expand) {
|
|
591
|
+
const before = seenNodes.size;
|
|
592
|
+
addNode(nodes, seenNodes, endpoint);
|
|
593
|
+
if (seenNodes.size > before && !nextKeys.has(refKey(endpoint))) {
|
|
594
|
+
nextKeys.add(refKey(endpoint));
|
|
595
|
+
nextFrontier.push(endpoint);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
frontier = nextFrontier;
|
|
600
|
+
}
|
|
601
|
+
return { nodes, edges, current_projection: projectCurrentEdges(adapter, edges, visibility) };
|
|
602
|
+
}
|
|
603
|
+
function getGraphPaths(adapter, input) {
|
|
604
|
+
const maxDepth = normalizeDepth(input.max_depth, 3);
|
|
605
|
+
const limit = normalizeLimit(input.limit, DEFAULT_PATH_LIMIT);
|
|
606
|
+
const visibility = { ...graphVisibility(input), asOfMs: input.as_of_ms };
|
|
607
|
+
assertRefsVisible(adapter, [input.from_ref, input.to_ref], visibility, input.as_of_ms);
|
|
608
|
+
const targetKey = refKey(input.to_ref);
|
|
609
|
+
const queue = [{ refs: [input.from_ref], edges: [] }];
|
|
610
|
+
const paths = [];
|
|
611
|
+
let edgeWork = 0;
|
|
612
|
+
let limitReached = false;
|
|
613
|
+
while (queue.length > 0 && paths.length < limit) {
|
|
614
|
+
const path = queue.shift();
|
|
615
|
+
if (!path) {
|
|
616
|
+
break;
|
|
617
|
+
}
|
|
618
|
+
const current = path.refs[path.refs.length - 1];
|
|
619
|
+
if (!current || path.edges.length >= maxDepth) {
|
|
620
|
+
continue;
|
|
621
|
+
}
|
|
622
|
+
const remaining = MAX_PATH_EDGE_WORK - edgeWork;
|
|
623
|
+
if (remaining === 0) {
|
|
624
|
+
limitReached = true;
|
|
625
|
+
break;
|
|
626
|
+
}
|
|
627
|
+
// Bound raw candidates before visibility filtering: hidden edges also cost work.
|
|
628
|
+
const candidates = (0, judgments_js_1.listTwinEdgesForRefs)(adapter, [current], {
|
|
629
|
+
newest: true,
|
|
630
|
+
limit: remaining + 1,
|
|
631
|
+
edgeTypes: input.edge_filters?.edge_types,
|
|
632
|
+
asOfMs: input.as_of_ms,
|
|
633
|
+
});
|
|
634
|
+
if (candidates.length > remaining)
|
|
635
|
+
limitReached = true;
|
|
636
|
+
const bounded = candidates.slice(0, remaining);
|
|
637
|
+
edgeWork += bounded.length;
|
|
638
|
+
const visible = (0, access_js_1.visibleTwinRefKeysRecursive)(adapter, bounded.flatMap((edge) => [edge.subject_ref, edge.object_ref]), visibility);
|
|
639
|
+
const edges = bounded.filter((edge) => visible.has(`${edge.subject_ref.kind}\0${edge.subject_ref.id}`) &&
|
|
640
|
+
visible.has(`${edge.object_ref.kind}\0${edge.object_ref.id}`));
|
|
641
|
+
for (const edge of edges) {
|
|
642
|
+
for (const next of oppositeRef(edge, current)) {
|
|
643
|
+
if (path.refs.some((ref) => refKey(ref) === refKey(next))) {
|
|
644
|
+
continue;
|
|
645
|
+
}
|
|
646
|
+
const nextPath = { refs: [...path.refs, next], edges: [...path.edges, edge] };
|
|
647
|
+
if (refKey(next) === targetKey) {
|
|
648
|
+
paths.push(nextPath);
|
|
649
|
+
if (paths.length >= limit) {
|
|
650
|
+
break;
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
else if (nextPath.edges.length < maxDepth) {
|
|
654
|
+
if (queue.length < MAX_PATH_FRONTIER)
|
|
655
|
+
queue.push(nextPath);
|
|
656
|
+
else
|
|
657
|
+
limitReached = true;
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
if (paths.length >= limit) {
|
|
661
|
+
break;
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
const pathEdges = [
|
|
666
|
+
...new Map(paths.flatMap((path) => path.edges).map((edge) => [edge.edge_id, edge])).values(),
|
|
667
|
+
];
|
|
668
|
+
return {
|
|
669
|
+
paths,
|
|
670
|
+
limit_reached: limitReached || paths.length >= limit,
|
|
671
|
+
current_projection: projectCurrentEdges(adapter, pathEdges, visibility),
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
|
+
/**
|
|
675
|
+
* A case seed's timeline is its canonical merge cluster's timeline: cases
|
|
676
|
+
* that merged into the terminal still own their edges, and the members
|
|
677
|
+
* themselves appear as case events. Members the caller cannot see do not
|
|
678
|
+
* expand — a merge is never a scope bypass.
|
|
679
|
+
*/
|
|
680
|
+
function timelineSeeds(adapter, ref, visibility) {
|
|
681
|
+
if (ref.kind !== 'case') {
|
|
682
|
+
return [ref];
|
|
683
|
+
}
|
|
684
|
+
const resolution = (0, case_store_js_1.resolveCanonicalCaseChain)(adapter, ref.id);
|
|
685
|
+
const cluster = (0, case_store_js_1.expandCaseChainForAssembly)(adapter, resolution.terminal_case_id, resolution.chain);
|
|
686
|
+
const members = cluster
|
|
687
|
+
.filter((id) => id !== ref.id)
|
|
688
|
+
.map((id) => ({ kind: 'case', id }));
|
|
689
|
+
if (members.length === 0) {
|
|
690
|
+
return [ref];
|
|
691
|
+
}
|
|
692
|
+
const visible = (0, access_js_1.visibleTwinRefKeysRecursive)(adapter, members, visibility);
|
|
693
|
+
return [ref, ...members.filter((member) => visible.has(`${member.kind}\0${member.id}`))];
|
|
694
|
+
}
|
|
695
|
+
function getGraphTimeline(adapter, input) {
|
|
696
|
+
const visibility = graphVisibility(input);
|
|
697
|
+
assertRefsVisible(adapter, [input.ref], visibility, input.as_of_ms);
|
|
698
|
+
const limit = normalizeLimit(input.limit, DEFAULT_GRAPH_LIMIT);
|
|
699
|
+
const window = timelineWindow(input);
|
|
700
|
+
const seeds = timelineSeeds(adapter, input.ref, visibility);
|
|
701
|
+
const edges = listFilteredEdges(adapter, seeds, {
|
|
702
|
+
visibility,
|
|
703
|
+
edge_filters: input.edge_filters,
|
|
704
|
+
as_of_ms: input.as_of_ms,
|
|
705
|
+
}).filter((edge) => isInTimelineWindow(edge.created_at, window));
|
|
706
|
+
const refs = [];
|
|
707
|
+
const seenRefs = new Set();
|
|
708
|
+
for (const seed of seeds) {
|
|
709
|
+
addRef(refs, seenRefs, seed);
|
|
710
|
+
}
|
|
711
|
+
for (const edge of edges) {
|
|
712
|
+
addRef(refs, seenRefs, edge.subject_ref);
|
|
713
|
+
addRef(refs, seenRefs, edge.object_ref);
|
|
714
|
+
}
|
|
715
|
+
const events = [];
|
|
716
|
+
for (const ref of refs) {
|
|
717
|
+
const event = loadTimelineRecordEvent(adapter, ref);
|
|
718
|
+
if (event && isInTimelineWindow(event.at_ms, window)) {
|
|
719
|
+
events.push(event);
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
events.push(...edges.map((edge) => ({ kind: 'edge', at_ms: edge.created_at, edge })));
|
|
723
|
+
const ordered = events
|
|
724
|
+
.filter((event) => {
|
|
725
|
+
const recorded = eventRecordedAt(event);
|
|
726
|
+
return ((input.recorded_from_ms === undefined || recorded >= input.recorded_from_ms) &&
|
|
727
|
+
(input.recorded_to_ms === undefined || recorded <= input.recorded_to_ms));
|
|
728
|
+
})
|
|
729
|
+
.sort((left, right) => left.at_ms - right.at_ms ||
|
|
730
|
+
eventKindRank(left.kind) - eventKindRank(right.kind) ||
|
|
731
|
+
eventKey(left).localeCompare(eventKey(right)));
|
|
732
|
+
return {
|
|
733
|
+
ref: input.ref,
|
|
734
|
+
events: ordered.slice(0, limit),
|
|
735
|
+
has_more: ordered.length > limit,
|
|
736
|
+
current_projection: projectCurrentEdges(adapter, edges, visibility),
|
|
737
|
+
};
|
|
738
|
+
}
|
|
739
|
+
/* --------------------------------------------------------------------------
|
|
740
|
+
* queryGraph — the §4.1 knowledge read surface over the twin-edge graph.
|
|
741
|
+
*
|
|
742
|
+
* All five views share one boundary: seeds resolve through explicit refs or the
|
|
743
|
+
* three search paths the design names (registry alias, decisions FTS,
|
|
744
|
+
* observation metadata); every ref is checked against the caller's visibility
|
|
745
|
+
* before it is read; and `history` decides whether the page shows refs as
|
|
746
|
+
* written ('all') or folded through merges and supersession to what is current
|
|
747
|
+
* ('current'). Node resolution follows identity chains (registry merges,
|
|
748
|
+
* decision supersession); edge endpoints resolve through T4 slot assignments —
|
|
749
|
+
* the same original ref can point at different current nodes on different
|
|
750
|
+
* edges, which is what a split means.
|
|
751
|
+
* --------------------------------------------------------------------------
|
|
752
|
+
*/
|
|
753
|
+
const SEARCH_SEED_LIMIT = 25;
|
|
754
|
+
const MEMORY_HISTORY_FOLLOW_LIMIT = 100;
|
|
755
|
+
function graphAccessInput(access) {
|
|
756
|
+
return {
|
|
757
|
+
scopes: access.scopes.map((scope) => ({ kind: scope.kind, id: scope.id })),
|
|
758
|
+
connectors: access.connectors ? [...access.connectors] : undefined,
|
|
759
|
+
connector_wide_read: access.connectorWideRead ? [...access.connectorWideRead] : undefined,
|
|
760
|
+
project_refs: access.projectRefs ? [...access.projectRefs] : undefined,
|
|
761
|
+
tenant_id: access.tenantId ?? null,
|
|
762
|
+
channels: access.channels ? { ...access.channels } : undefined,
|
|
763
|
+
principal_id: access.principalId,
|
|
764
|
+
agent_id: access.agentId,
|
|
765
|
+
max_source_ms: access.maxSourceMs,
|
|
766
|
+
};
|
|
767
|
+
}
|
|
768
|
+
/**
|
|
769
|
+
* Name-free entry: registry alias, decisions FTS, observation metadata. Search
|
|
770
|
+
* seeds are candidates, not citations — refs the caller cannot see are dropped
|
|
771
|
+
* here rather than asserted, because a search must not fail on a hit the
|
|
772
|
+
* caller was never allowed to read.
|
|
773
|
+
*/
|
|
774
|
+
function resolveSearchSeeds(adapter, search, access, visibility) {
|
|
775
|
+
const text = search.text.trim();
|
|
776
|
+
if (!text) {
|
|
777
|
+
return [];
|
|
778
|
+
}
|
|
779
|
+
const wants = (kind) => !search.kinds || search.kinds.includes(kind);
|
|
780
|
+
const seeds = [];
|
|
781
|
+
if (wants('registry')) {
|
|
782
|
+
for (const node of (0, store_js_1.resolveAliasCandidates)(adapter, text, { scopes: access.scopes })) {
|
|
783
|
+
seeds.push({ kind: 'registry', id: node.id });
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
if (wants('memory')) {
|
|
787
|
+
// An absent FTS table is a real empty answer, matching knowledge/search.ts.
|
|
788
|
+
const ftsTable = adapter
|
|
789
|
+
.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'decisions_fts'")
|
|
790
|
+
.get();
|
|
791
|
+
const terms = text.match(/[\p{L}\p{N}_]+/gu) ?? [];
|
|
792
|
+
if (ftsTable && terms.length > 0) {
|
|
793
|
+
// Preserve FTS's AND-of-terms recall without accepting user text as
|
|
794
|
+
// operators. A hyphenated name otherwise becomes subtraction and raises
|
|
795
|
+
// "no such column"; quoting the whole input would lose nonadjacent hits.
|
|
796
|
+
const literalTerms = terms.map((term) => `"${term.replaceAll('"', '""')}"`).join(' AND ');
|
|
797
|
+
const rows = adapter
|
|
798
|
+
.prepare(`SELECT d.id AS id
|
|
799
|
+
FROM decisions_fts JOIN decisions d ON decisions_fts.rowid = d.rowid
|
|
800
|
+
WHERE decisions_fts MATCH ?
|
|
801
|
+
ORDER BY rank
|
|
802
|
+
LIMIT ?`)
|
|
803
|
+
.all(literalTerms, SEARCH_SEED_LIMIT);
|
|
804
|
+
for (const row of rows) {
|
|
805
|
+
seeds.push({ kind: 'memory', id: row.id });
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
if (wants('observation')) {
|
|
810
|
+
const like = `%${text.replace(/[%_\\]/g, '\\$&')}%`;
|
|
811
|
+
const rows = adapter
|
|
812
|
+
.prepare(`SELECT observation_id
|
|
813
|
+
FROM observation_versions
|
|
814
|
+
WHERE source_id LIKE ? ESCAPE '\\' OR author LIKE ? ESCAPE '\\' OR metadata_json LIKE ? ESCAPE '\\'
|
|
815
|
+
LIMIT ?`)
|
|
816
|
+
.all(like, like, like, SEARCH_SEED_LIMIT);
|
|
817
|
+
for (const row of rows) {
|
|
818
|
+
seeds.push({ kind: 'observation', id: row.observation_id });
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
const visible = (0, access_js_1.visibleTwinRefKeysRecursive)(adapter, seeds, visibility);
|
|
822
|
+
return seeds.filter((ref) => visible.has(`${ref.kind}\0${ref.id}`));
|
|
823
|
+
}
|
|
824
|
+
function resolveSeeds(adapter, query, access, visibility) {
|
|
825
|
+
const explicit = query.seeds ?? [];
|
|
826
|
+
const resolved = query.search
|
|
827
|
+
? resolveSearchSeeds(adapter, query.search, access, visibility)
|
|
828
|
+
: [];
|
|
829
|
+
return { explicit, resolved };
|
|
830
|
+
}
|
|
831
|
+
/** Follow `superseded_by` to the record that currently speaks for this one. */
|
|
832
|
+
function currentMemoryRef(adapter, ref) {
|
|
833
|
+
let current = ref;
|
|
834
|
+
for (let hops = 0; hops < MEMORY_HISTORY_FOLLOW_LIMIT; hops += 1) {
|
|
835
|
+
const row = adapter
|
|
836
|
+
.prepare('SELECT superseded_by FROM decisions WHERE id = ?')
|
|
837
|
+
.get(current.id);
|
|
838
|
+
if (!row?.superseded_by) {
|
|
839
|
+
return current;
|
|
840
|
+
}
|
|
841
|
+
current = { kind: 'memory', id: row.superseded_by };
|
|
842
|
+
}
|
|
843
|
+
throw new AgentGraphValidationError(`Supersession chain from ${ref.id} does not settle within ${MEMORY_HISTORY_FOLLOW_LIMIT} hops.`);
|
|
844
|
+
}
|
|
845
|
+
/**
|
|
846
|
+
* The node a ref resolves to under identity corrections — registry merges for
|
|
847
|
+
* nodes, supersession heads for records. Returns null when the identity has no
|
|
848
|
+
* current node (a merge chain that settled on nothing).
|
|
849
|
+
*/
|
|
850
|
+
function resolveNodeIdentity(adapter, ref) {
|
|
851
|
+
if (ref.kind === 'registry') {
|
|
852
|
+
const node = (0, store_js_1.resolveNodeById)(adapter, ref.id);
|
|
853
|
+
return node ? { kind: 'registry', id: node.id } : null;
|
|
854
|
+
}
|
|
855
|
+
if (ref.kind === 'memory') {
|
|
856
|
+
return currentMemoryRef(adapter, ref);
|
|
857
|
+
}
|
|
858
|
+
if (ref.kind === 'case') {
|
|
859
|
+
// A merged case folds to its canonical terminal — the same identity
|
|
860
|
+
// contract registry merges and memory supersession already honor. A
|
|
861
|
+
// missing row resolves away like any other absent record.
|
|
862
|
+
const exists = adapter
|
|
863
|
+
.prepare('SELECT 1 FROM case_truth WHERE case_id = ? LIMIT 1')
|
|
864
|
+
.get(ref.id);
|
|
865
|
+
if (!exists) {
|
|
866
|
+
return null;
|
|
867
|
+
}
|
|
868
|
+
const resolution = (0, case_store_js_1.resolveCanonicalCaseChain)(adapter, ref.id);
|
|
869
|
+
return { kind: 'case', id: resolution.terminal_case_id };
|
|
870
|
+
}
|
|
871
|
+
return ref;
|
|
872
|
+
}
|
|
873
|
+
function readRegistryRow(adapter, id) {
|
|
874
|
+
const row = adapter.prepare('SELECT * FROM registry_nodes WHERE id = ?').get(id);
|
|
875
|
+
if (!row) {
|
|
876
|
+
return null;
|
|
877
|
+
}
|
|
878
|
+
return {
|
|
879
|
+
id: row.id,
|
|
880
|
+
kind: row.kind,
|
|
881
|
+
name: row.name,
|
|
882
|
+
parentId: row.parent_id,
|
|
883
|
+
mergedInto: row.merged_into,
|
|
884
|
+
note: row.note,
|
|
885
|
+
createdAt: row.created_at,
|
|
886
|
+
updatedAt: row.updated_at,
|
|
887
|
+
};
|
|
888
|
+
}
|
|
889
|
+
function windowSectionText(ctx, ref, sections) {
|
|
890
|
+
const section = ctx.query.section ?? 'summary';
|
|
891
|
+
const full = sections[section];
|
|
892
|
+
const offset = ctx.query.textOffset ?? 0;
|
|
893
|
+
const limit = ctx.query.textLimit;
|
|
894
|
+
const text = limit === undefined ? full.slice(offset) : full.slice(offset, offset + limit);
|
|
895
|
+
const complete = offset + text.length >= full.length;
|
|
896
|
+
return {
|
|
897
|
+
text,
|
|
898
|
+
content: {
|
|
899
|
+
complete,
|
|
900
|
+
nextRead: complete
|
|
901
|
+
? null
|
|
902
|
+
: {
|
|
903
|
+
seeds: [ref],
|
|
904
|
+
view: 'detail',
|
|
905
|
+
section,
|
|
906
|
+
textOffset: offset + text.length,
|
|
907
|
+
textLimit: limit,
|
|
908
|
+
},
|
|
909
|
+
},
|
|
910
|
+
};
|
|
911
|
+
}
|
|
912
|
+
function hydrateMemoryNode(adapter, ref, ctx) {
|
|
913
|
+
const row = adapter
|
|
914
|
+
.prepare(`SELECT id, topic, decision, reasoning, outcome, failure_reason, limitation, confidence,
|
|
915
|
+
event_date, event_datetime, created_at, updated_at, status, superseded_by,
|
|
916
|
+
refined_from, record_kind, kind
|
|
917
|
+
FROM decisions
|
|
918
|
+
WHERE id = ?
|
|
919
|
+
LIMIT 1`)
|
|
920
|
+
.get(ref.id);
|
|
921
|
+
if (!row) {
|
|
922
|
+
return null;
|
|
923
|
+
}
|
|
924
|
+
const recordedAt = numberMs(row.created_at, 'decisions.created_at', ref.id);
|
|
925
|
+
const appliesFrom = nullableNumberMs(row.event_datetime, 'decisions.event_datetime', ref.id);
|
|
926
|
+
let appliesUntil = null;
|
|
927
|
+
if (row.superseded_by) {
|
|
928
|
+
const superseder = adapter
|
|
929
|
+
.prepare('SELECT created_at FROM decisions WHERE id = ?')
|
|
930
|
+
.get(row.superseded_by);
|
|
931
|
+
if (superseder) {
|
|
932
|
+
appliesUntil = numberMs(superseder.created_at, 'decisions.created_at', row.superseded_by);
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
const workLink = adapter
|
|
936
|
+
.prepare(`SELECT a.commitment_id, a.revision, c.row_id, c.head_record_id, c.withdrawn
|
|
937
|
+
FROM commitment_assignments a
|
|
938
|
+
JOIN commitments c ON c.commitment_id = a.commitment_id
|
|
939
|
+
WHERE a.record_id = ?
|
|
940
|
+
ORDER BY a.revision DESC
|
|
941
|
+
LIMIT 1`)
|
|
942
|
+
.get(ref.id);
|
|
943
|
+
const work = workLink
|
|
944
|
+
? {
|
|
945
|
+
commitmentId: workLink.commitment_id,
|
|
946
|
+
rowId: workLink.row_id,
|
|
947
|
+
revision: workLink.revision,
|
|
948
|
+
latestJudgmentRef: { kind: 'memory', id: workLink.head_record_id },
|
|
949
|
+
}
|
|
950
|
+
: null;
|
|
951
|
+
const stateAtSnapshot = workLink?.withdrawn === 1
|
|
952
|
+
? 'withdrawn'
|
|
953
|
+
: row.status === 'superseded' || row.superseded_by
|
|
954
|
+
? 'replaced'
|
|
955
|
+
: appliesFrom !== null && appliesFrom > ctx.asOfMs
|
|
956
|
+
? 'not_yet_effective'
|
|
957
|
+
: 'current';
|
|
958
|
+
const refined = parseRefinedFrom({ id: ref.id, refined_from: row.refined_from });
|
|
959
|
+
const replaces = (Array.isArray(refined) ? refined : refined ? [refined] : []).map(String);
|
|
960
|
+
const payload = {};
|
|
961
|
+
if (row.reasoning !== null)
|
|
962
|
+
payload.reasoning = row.reasoning;
|
|
963
|
+
if (row.outcome !== null)
|
|
964
|
+
payload.outcome = row.outcome;
|
|
965
|
+
if (row.failure_reason !== null)
|
|
966
|
+
payload.failure_reason = row.failure_reason;
|
|
967
|
+
if (row.limitation !== null)
|
|
968
|
+
payload.limitation = row.limitation;
|
|
969
|
+
if (row.confidence !== null)
|
|
970
|
+
payload.confidence = row.confidence;
|
|
971
|
+
if (row.event_date !== null)
|
|
972
|
+
payload.event_date = row.event_date;
|
|
973
|
+
if (row.updated_at !== null)
|
|
974
|
+
payload.updated_at = row.updated_at;
|
|
975
|
+
const { text, content } = windowSectionText(ctx, ref, {
|
|
976
|
+
summary: row.decision ?? '',
|
|
977
|
+
reasoning: row.reasoning ?? '',
|
|
978
|
+
payload: JSON.stringify(payload),
|
|
979
|
+
});
|
|
980
|
+
const recordKind = row.record_kind === 'judgment' || row.record_kind === 'commitment'
|
|
981
|
+
? row.record_kind
|
|
982
|
+
: 'legacy';
|
|
983
|
+
return {
|
|
984
|
+
label: row.topic ?? ((row.decision ?? '').slice(0, 80) || ref.id),
|
|
985
|
+
data: {
|
|
986
|
+
kind: 'memory',
|
|
987
|
+
recordKind,
|
|
988
|
+
memoryKind: row.kind,
|
|
989
|
+
topic: row.topic ?? '',
|
|
990
|
+
summary: text,
|
|
991
|
+
recordedAt,
|
|
992
|
+
appliesFrom,
|
|
993
|
+
appliesUntil,
|
|
994
|
+
stateAtSnapshot,
|
|
995
|
+
replaces,
|
|
996
|
+
payload,
|
|
997
|
+
work,
|
|
998
|
+
content,
|
|
999
|
+
},
|
|
1000
|
+
};
|
|
1001
|
+
}
|
|
1002
|
+
function hydrateRegistryNode(adapter, ref, ctx, access) {
|
|
1003
|
+
const resolved = (0, store_js_1.resolveNodeById)(adapter, ref.id);
|
|
1004
|
+
const node = ctx.history === 'all' ? (readRegistryRow(adapter, ref.id) ?? resolved) : resolved;
|
|
1005
|
+
if (!node) {
|
|
1006
|
+
return null;
|
|
1007
|
+
}
|
|
1008
|
+
const admitted = access.scopes.length > 0 ? access.scopes : [{ kind: 'global', id: '*' }];
|
|
1009
|
+
const aliasRows = adapter
|
|
1010
|
+
.prepare(`SELECT DISTINCT alias_display FROM registry_aliases
|
|
1011
|
+
WHERE node_id = ?
|
|
1012
|
+
AND ((scope_kind = 'global' AND scope_id = '*')
|
|
1013
|
+
OR (scope_kind || ':' || scope_id) IN (${admitted.map(() => '?').join(', ')}))
|
|
1014
|
+
ORDER BY created_at, rowid`)
|
|
1015
|
+
.all(node.id, ...admitted.map((scope) => `${scope.kind}:${scope.id}`));
|
|
1016
|
+
const children = (0, store_js_1.listNodes)(adapter, { parentId: node.id, scopes: access.scopes });
|
|
1017
|
+
const touching = listFilteredEdges(adapter, [ref], { visibility: ctx.visibility });
|
|
1018
|
+
const unresolved = [];
|
|
1019
|
+
for (const projection of projectCurrentEdges(adapter, touching, ctx.visibility)) {
|
|
1020
|
+
if (projection.current_ref === null && refKey(projection.original_ref) === refKey(ref)) {
|
|
1021
|
+
unresolved.push({ edgeId: projection.edge_id, endpoint: projection.endpoint });
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
return {
|
|
1025
|
+
label: node.name,
|
|
1026
|
+
data: {
|
|
1027
|
+
kind: 'registry',
|
|
1028
|
+
nodeKind: node.kind,
|
|
1029
|
+
name: node.name,
|
|
1030
|
+
parentId: node.parentId,
|
|
1031
|
+
visibleAliases: aliasRows.map((row) => row.alias_display),
|
|
1032
|
+
identityRevision: (0, store_js_1.currentIdentityRevision)(adapter),
|
|
1033
|
+
visibleChildren: children.map((child) => ({
|
|
1034
|
+
kind: 'registry',
|
|
1035
|
+
id: child.id,
|
|
1036
|
+
name: child.name,
|
|
1037
|
+
})),
|
|
1038
|
+
unresolvedSlots: unresolved,
|
|
1039
|
+
},
|
|
1040
|
+
};
|
|
1041
|
+
}
|
|
1042
|
+
function hydrateObservationNode(adapter, ref) {
|
|
1043
|
+
const record = (0, observations_js_1.getObservationVersion)(adapter, ref.id);
|
|
1044
|
+
if (!record) {
|
|
1045
|
+
return null;
|
|
1046
|
+
}
|
|
1047
|
+
return {
|
|
1048
|
+
label: `${record.source}:${record.sourceId}`,
|
|
1049
|
+
data: {
|
|
1050
|
+
kind: 'observation',
|
|
1051
|
+
connector: record.source,
|
|
1052
|
+
sourceId: record.sourceId,
|
|
1053
|
+
sourceAt: record.sourceAt,
|
|
1054
|
+
observedAt: record.observedAt,
|
|
1055
|
+
contentHash: record.contentHash,
|
|
1056
|
+
},
|
|
1057
|
+
};
|
|
1058
|
+
}
|
|
1059
|
+
function hydrateRecordNode(adapter, ref) {
|
|
1060
|
+
if (ref.kind === 'case') {
|
|
1061
|
+
const row = adapter
|
|
1062
|
+
.prepare(`SELECT case_id, title, status, last_activity_at, created_at, updated_at
|
|
1063
|
+
FROM case_truth WHERE case_id = ? LIMIT 1`)
|
|
1064
|
+
.get(ref.id);
|
|
1065
|
+
if (!row) {
|
|
1066
|
+
return null;
|
|
1067
|
+
}
|
|
1068
|
+
return {
|
|
1069
|
+
label: String(row.title ?? ref.id),
|
|
1070
|
+
data: { kind: 'case', data: row },
|
|
1071
|
+
};
|
|
1072
|
+
}
|
|
1073
|
+
if (ref.kind === 'raw') {
|
|
1074
|
+
const row = adapter
|
|
1075
|
+
.prepare(`SELECT observation_id AS event_index_id, source AS source_connector, source_type,
|
|
1076
|
+
source_id, source_locator, title, source_at AS event_datetime,
|
|
1077
|
+
observed_at AS source_timestamp_ms, observation_id AS current_observation_id
|
|
1078
|
+
FROM observation_versions WHERE observation_id = ? LIMIT 1`)
|
|
1079
|
+
.get(ref.id);
|
|
1080
|
+
if (!row) {
|
|
1081
|
+
return null;
|
|
1082
|
+
}
|
|
1083
|
+
return {
|
|
1084
|
+
label: String(row.title ?? `${row.source_connector}:${row.source_id}`),
|
|
1085
|
+
data: { kind: 'raw', data: row },
|
|
1086
|
+
};
|
|
1087
|
+
}
|
|
1088
|
+
if (ref.kind === 'edge') {
|
|
1089
|
+
const edge = (0, judgments_js_1.getTwinEdge)(adapter, ref.id);
|
|
1090
|
+
if (!edge) {
|
|
1091
|
+
return null;
|
|
1092
|
+
}
|
|
1093
|
+
return {
|
|
1094
|
+
label: `${edge.edge_type}:${edge.edge_id}`,
|
|
1095
|
+
data: {
|
|
1096
|
+
kind: 'edge',
|
|
1097
|
+
data: {
|
|
1098
|
+
edge_id: edge.edge_id,
|
|
1099
|
+
edge_type: edge.edge_type,
|
|
1100
|
+
subject_ref: edge.subject_ref,
|
|
1101
|
+
object_ref: edge.object_ref,
|
|
1102
|
+
relation_attrs: (edge.relation_attrs ?? null),
|
|
1103
|
+
confidence: edge.confidence,
|
|
1104
|
+
source: edge.source,
|
|
1105
|
+
agent_id: edge.agent_id,
|
|
1106
|
+
model_run_id: edge.model_run_id,
|
|
1107
|
+
reason_classification: edge.reason_classification,
|
|
1108
|
+
reason_text: edge.reason_text,
|
|
1109
|
+
created_at: edge.created_at,
|
|
1110
|
+
},
|
|
1111
|
+
},
|
|
1112
|
+
};
|
|
1113
|
+
}
|
|
1114
|
+
// 'report' has no backing store; the ref id is the whole honest answer.
|
|
1115
|
+
return { label: ref.id, data: { kind: 'report', data: { id: ref.id } } };
|
|
1116
|
+
}
|
|
1117
|
+
function hydrateNode(adapter, ref, ctx, access) {
|
|
1118
|
+
if (ref.kind === 'memory') {
|
|
1119
|
+
return hydrateMemoryNode(adapter, ref, ctx);
|
|
1120
|
+
}
|
|
1121
|
+
if (ref.kind === 'registry') {
|
|
1122
|
+
const hydrated = hydrateRegistryNode(adapter, ref, ctx, access);
|
|
1123
|
+
return hydrated ? { label: hydrated.label, data: hydrated.data } : null;
|
|
1124
|
+
}
|
|
1125
|
+
if (ref.kind === 'observation') {
|
|
1126
|
+
return hydrateObservationNode(adapter, ref);
|
|
1127
|
+
}
|
|
1128
|
+
return hydrateRecordNode(adapter, ref);
|
|
1129
|
+
}
|
|
1130
|
+
function pageEdgeAttrs(edge) {
|
|
1131
|
+
return {
|
|
1132
|
+
edge_type: edge.edge_type,
|
|
1133
|
+
relation_attrs: (edge.relation_attrs ?? null),
|
|
1134
|
+
confidence: edge.confidence,
|
|
1135
|
+
source: edge.source,
|
|
1136
|
+
agent_id: edge.agent_id,
|
|
1137
|
+
model_run_id: edge.model_run_id,
|
|
1138
|
+
reason_classification: edge.reason_classification,
|
|
1139
|
+
reason_text: edge.reason_text,
|
|
1140
|
+
created_at: edge.created_at,
|
|
1141
|
+
content_hash: edge.content_hash.toString('hex'),
|
|
1142
|
+
};
|
|
1143
|
+
}
|
|
1144
|
+
function projectionKey(edgeId, endpoint) {
|
|
1145
|
+
return `${edgeId}${endpoint}`;
|
|
1146
|
+
}
|
|
1147
|
+
function mergeProjections(target, projections) {
|
|
1148
|
+
for (const projection of projections) {
|
|
1149
|
+
target.set(projectionKey(projection.edge_id, projection.endpoint), projection);
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
function eventRecordedAt(event) {
|
|
1153
|
+
if (event.kind === 'edge') {
|
|
1154
|
+
return event.edge.created_at;
|
|
1155
|
+
}
|
|
1156
|
+
if (event.kind === 'memory') {
|
|
1157
|
+
return event.memory.created_at;
|
|
1158
|
+
}
|
|
1159
|
+
if (event.kind === 'case') {
|
|
1160
|
+
return parseTimestampMs(event.case.created_at, 'case_truth.created_at', event.ref.id);
|
|
1161
|
+
}
|
|
1162
|
+
return event.raw.source_timestamp_ms;
|
|
1163
|
+
}
|
|
1164
|
+
function validateRelations(relations) {
|
|
1165
|
+
if (!relations) {
|
|
1166
|
+
return undefined;
|
|
1167
|
+
}
|
|
1168
|
+
const known = new Set(twin_edge_types_js_1.TWIN_EDGE_TYPES);
|
|
1169
|
+
const unknown = relations.filter((relation) => !known.has(relation));
|
|
1170
|
+
if (unknown.length > 0) {
|
|
1171
|
+
throw new judgments_js_1.JudgmentError('INVALID_QUERY', `Unsupported graph relation(s): ${unknown.join(', ')}. Known: ${twin_edge_types_js_1.TWIN_EDGE_TYPES.join(', ')}`);
|
|
1172
|
+
}
|
|
1173
|
+
return [...new Set(relations)];
|
|
1174
|
+
}
|
|
1175
|
+
function requireSeeds(view, seeds, searchProvided) {
|
|
1176
|
+
// A search that resolves to nothing is a legitimate answer — the lookup's
|
|
1177
|
+
// found:false — not a malformed query. Only a query with no anchor at all
|
|
1178
|
+
// (no explicit seeds, no search) is an error.
|
|
1179
|
+
if (seeds.length === 0 && !searchProvided) {
|
|
1180
|
+
throw new judgments_js_1.JudgmentError('INVALID_QUERY', `graph ${view} requires at least one seed (seeds or search).`);
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
function graphSnapshot(adapter, asOf) {
|
|
1184
|
+
const commandsTable = adapter
|
|
1185
|
+
.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'judgment_commands'")
|
|
1186
|
+
.get();
|
|
1187
|
+
const judgmentWatermark = commandsTable
|
|
1188
|
+
? (adapter
|
|
1189
|
+
.prepare('SELECT COALESCE(MAX(committed_watermark), 0) AS w FROM judgment_commands')
|
|
1190
|
+
.get().w ?? 0)
|
|
1191
|
+
: 0;
|
|
1192
|
+
return { judgmentWatermark, identityRevision: (0, store_js_1.currentIdentityRevision)(adapter), asOf };
|
|
1193
|
+
}
|
|
1194
|
+
function decodeBrowseCursor(value, history, relations) {
|
|
1195
|
+
let parsed;
|
|
1196
|
+
try {
|
|
1197
|
+
parsed = JSON.parse(Buffer.from(value, 'base64url').toString('utf8'));
|
|
1198
|
+
}
|
|
1199
|
+
catch {
|
|
1200
|
+
throw new judgments_js_1.JudgmentError('INVALID_QUERY', 'graph browse cursor is malformed');
|
|
1201
|
+
}
|
|
1202
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
1203
|
+
throw new judgments_js_1.JudgmentError('INVALID_QUERY', 'graph browse cursor is malformed');
|
|
1204
|
+
}
|
|
1205
|
+
const cursor = parsed;
|
|
1206
|
+
if (cursor.v !== 1 ||
|
|
1207
|
+
typeof cursor.asOf !== 'number' ||
|
|
1208
|
+
!Number.isFinite(cursor.asOf) ||
|
|
1209
|
+
typeof cursor.afterAt !== 'number' ||
|
|
1210
|
+
!Number.isFinite(cursor.afterAt) ||
|
|
1211
|
+
typeof cursor.afterId !== 'string' ||
|
|
1212
|
+
cursor.afterId.length === 0 ||
|
|
1213
|
+
cursor.history !== history ||
|
|
1214
|
+
cursor.relations !== relations) {
|
|
1215
|
+
throw new judgments_js_1.JudgmentError('INVALID_QUERY', 'graph browse cursor does not match this read');
|
|
1216
|
+
}
|
|
1217
|
+
return cursor;
|
|
1218
|
+
}
|
|
1219
|
+
function browseVisibleEdges(adapter, options) {
|
|
1220
|
+
const batchSize = 250;
|
|
1221
|
+
const maxScan = Math.max(batchSize, options.limit * 4);
|
|
1222
|
+
const admitted = [];
|
|
1223
|
+
let afterAt = options.cursor?.afterAt;
|
|
1224
|
+
let afterId = options.cursor?.afterId;
|
|
1225
|
+
let scanned = 0;
|
|
1226
|
+
let lastProcessed = null;
|
|
1227
|
+
let exhausted = false;
|
|
1228
|
+
const relations = options.relations ? new Set(options.relations) : null;
|
|
1229
|
+
while (scanned < maxScan && admitted.length < options.limit) {
|
|
1230
|
+
const rowLimit = Math.min(batchSize, maxScan - scanned);
|
|
1231
|
+
const rows = (afterAt === undefined
|
|
1232
|
+
? adapter
|
|
1233
|
+
.prepare('SELECT edge_id, created_at FROM twin_edges WHERE created_at <= ? ORDER BY created_at DESC, edge_id ASC LIMIT ?')
|
|
1234
|
+
.all(options.asOf, rowLimit)
|
|
1235
|
+
: adapter
|
|
1236
|
+
.prepare('SELECT edge_id, created_at FROM twin_edges WHERE created_at <= ? AND (created_at < ? OR (created_at = ? AND edge_id > ?)) ORDER BY created_at DESC, edge_id ASC LIMIT ?')
|
|
1237
|
+
.all(options.asOf, afterAt, afterAt, afterId, rowLimit));
|
|
1238
|
+
if (rows.length === 0) {
|
|
1239
|
+
exhausted = true;
|
|
1240
|
+
break;
|
|
1241
|
+
}
|
|
1242
|
+
const candidates = rows.map((row) => (0, judgments_js_1.getTwinEdge)(adapter, row.edge_id));
|
|
1243
|
+
const visible = (0, access_js_1.visibleTwinRefKeysRecursive)(adapter, candidates.flatMap((edge) => (edge ? [edge.subject_ref, edge.object_ref] : [])), { ...options.visibility, asOfMs: options.asOf });
|
|
1244
|
+
for (let i = 0; i < rows.length; i += 1) {
|
|
1245
|
+
const row = rows[i];
|
|
1246
|
+
const edge = candidates[i];
|
|
1247
|
+
lastProcessed = row;
|
|
1248
|
+
scanned += 1;
|
|
1249
|
+
if (edge &&
|
|
1250
|
+
(!relations || relations.has(edge.edge_type)) &&
|
|
1251
|
+
visible.has(`${edge.subject_ref.kind}\0${edge.subject_ref.id}`) &&
|
|
1252
|
+
visible.has(`${edge.object_ref.kind}\0${edge.object_ref.id}`)) {
|
|
1253
|
+
admitted.push(edge);
|
|
1254
|
+
}
|
|
1255
|
+
if (admitted.length >= options.limit)
|
|
1256
|
+
break;
|
|
1257
|
+
}
|
|
1258
|
+
if (lastProcessed) {
|
|
1259
|
+
afterAt = lastProcessed.created_at;
|
|
1260
|
+
afterId = lastProcessed.edge_id;
|
|
1261
|
+
}
|
|
1262
|
+
if (rows.length < rowLimit && admitted.length < options.limit) {
|
|
1263
|
+
exhausted = true;
|
|
1264
|
+
break;
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
if (!lastProcessed || exhausted)
|
|
1268
|
+
return { edges: admitted, nextCursor: null };
|
|
1269
|
+
const more = adapter
|
|
1270
|
+
.prepare('SELECT 1 FROM twin_edges WHERE created_at <= ? AND (created_at < ? OR (created_at = ? AND edge_id > ?)) LIMIT 1')
|
|
1271
|
+
.get(options.asOf, lastProcessed.created_at, lastProcessed.created_at, lastProcessed.edge_id);
|
|
1272
|
+
const nextCursor = more
|
|
1273
|
+
? Buffer.from(JSON.stringify({
|
|
1274
|
+
v: 1,
|
|
1275
|
+
asOf: options.asOf,
|
|
1276
|
+
afterAt: lastProcessed.created_at,
|
|
1277
|
+
afterId: lastProcessed.edge_id,
|
|
1278
|
+
history: options.history,
|
|
1279
|
+
relations: [...(options.relations ?? [])].sort().join(','),
|
|
1280
|
+
})).toString('base64url')
|
|
1281
|
+
: null;
|
|
1282
|
+
return { edges: admitted, nextCursor };
|
|
1283
|
+
}
|
|
1284
|
+
function queryGraph(adapter, query, access) {
|
|
1285
|
+
if (query.cursor !== undefined && query.view !== 'browse') {
|
|
1286
|
+
throw new judgments_js_1.JudgmentError('CURSOR_UNSUPPORTED', 'graph cursors are not issued yet; restart the query without cursor.');
|
|
1287
|
+
}
|
|
1288
|
+
const history = query.history ?? 'current';
|
|
1289
|
+
if (history !== 'current' && history !== 'all') {
|
|
1290
|
+
throw new judgments_js_1.JudgmentError('INVALID_QUERY', "history must be 'current' or 'all'");
|
|
1291
|
+
}
|
|
1292
|
+
const direction = query.direction ?? 'both';
|
|
1293
|
+
if (direction !== 'in' && direction !== 'out' && direction !== 'both') {
|
|
1294
|
+
throw new judgments_js_1.JudgmentError('INVALID_QUERY', "direction must be 'in', 'out', or 'both'");
|
|
1295
|
+
}
|
|
1296
|
+
const relationTypes = validateRelations(query.relations);
|
|
1297
|
+
const relationKey = [...(relationTypes ?? [])].sort().join(',');
|
|
1298
|
+
const browseCursor = query.view === 'browse' && query.cursor
|
|
1299
|
+
? decodeBrowseCursor(query.cursor, history, relationKey)
|
|
1300
|
+
: null;
|
|
1301
|
+
if (browseCursor && query.asOf !== undefined && query.asOf !== browseCursor.asOf) {
|
|
1302
|
+
throw new judgments_js_1.JudgmentError('INVALID_QUERY', 'graph browse cursor snapshot changed');
|
|
1303
|
+
}
|
|
1304
|
+
const accessInput = graphAccessInput(access);
|
|
1305
|
+
const visibility = graphVisibility({ ...accessInput, history });
|
|
1306
|
+
const asOf = browseCursor?.asOf ?? query.asOf ?? Date.now();
|
|
1307
|
+
const limit = normalizeLimit(query.limit, DEFAULT_GRAPH_LIMIT);
|
|
1308
|
+
const edgeFilters = relationTypes ? { edge_types: relationTypes } : undefined;
|
|
1309
|
+
const ctx = { visibility, history, query, asOfMs: asOf };
|
|
1310
|
+
const { explicit, resolved } = resolveSeeds(adapter, query, access, visibility);
|
|
1311
|
+
const seedRefs = [];
|
|
1312
|
+
const seenSeeds = new Set();
|
|
1313
|
+
for (const ref of [...explicit, ...resolved]) {
|
|
1314
|
+
if (!seenSeeds.has(refKey(ref))) {
|
|
1315
|
+
seenSeeds.add(refKey(ref));
|
|
1316
|
+
seedRefs.push(ref);
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
const nodeRefs = [];
|
|
1320
|
+
const seenNodes = new Set();
|
|
1321
|
+
const collectedEdges = [];
|
|
1322
|
+
const seenEdges = new Set();
|
|
1323
|
+
const projections = new Map();
|
|
1324
|
+
const reasons = new Set();
|
|
1325
|
+
const nodeOrder = new Map();
|
|
1326
|
+
let nextCursor = null;
|
|
1327
|
+
let sequence = 0;
|
|
1328
|
+
const addRefNode = (ref, order) => {
|
|
1329
|
+
if (!seenNodes.has(refKey(ref))) {
|
|
1330
|
+
seenNodes.add(refKey(ref));
|
|
1331
|
+
nodeRefs.push(ref);
|
|
1332
|
+
}
|
|
1333
|
+
if (order !== undefined && !nodeOrder.has(refKey(ref))) {
|
|
1334
|
+
nodeOrder.set(refKey(ref), order);
|
|
1335
|
+
}
|
|
1336
|
+
};
|
|
1337
|
+
const addEdge = (edge) => {
|
|
1338
|
+
if (!seenEdges.has(edgeKey(edge))) {
|
|
1339
|
+
seenEdges.add(edgeKey(edge));
|
|
1340
|
+
collectedEdges.push(edge);
|
|
1341
|
+
}
|
|
1342
|
+
};
|
|
1343
|
+
switch (query.view) {
|
|
1344
|
+
case 'browse': {
|
|
1345
|
+
if (query.seeds ||
|
|
1346
|
+
query.search ||
|
|
1347
|
+
query.from ||
|
|
1348
|
+
query.to ||
|
|
1349
|
+
query.maxDepth !== undefined ||
|
|
1350
|
+
query.direction !== undefined ||
|
|
1351
|
+
query.section !== undefined ||
|
|
1352
|
+
query.textOffset !== undefined ||
|
|
1353
|
+
query.textLimit !== undefined ||
|
|
1354
|
+
query.eventRange !== undefined ||
|
|
1355
|
+
query.recordedRange !== undefined) {
|
|
1356
|
+
throw new judgments_js_1.JudgmentError('INVALID_QUERY', 'graph browse accepts only history, relations, asOf, limit and cursor');
|
|
1357
|
+
}
|
|
1358
|
+
const page = browseVisibleEdges(adapter, {
|
|
1359
|
+
asOf,
|
|
1360
|
+
limit: Math.min(limit, 2000),
|
|
1361
|
+
cursor: browseCursor,
|
|
1362
|
+
visibility,
|
|
1363
|
+
relations: relationTypes,
|
|
1364
|
+
history,
|
|
1365
|
+
});
|
|
1366
|
+
for (const edge of page.edges) {
|
|
1367
|
+
addRefNode(edge.subject_ref, sequence++);
|
|
1368
|
+
addRefNode(edge.object_ref, sequence++);
|
|
1369
|
+
addEdge(edge);
|
|
1370
|
+
}
|
|
1371
|
+
mergeProjections(projections, projectCurrentEdges(adapter, page.edges, visibility));
|
|
1372
|
+
nextCursor = page.nextCursor;
|
|
1373
|
+
if (nextCursor)
|
|
1374
|
+
reasons.add('limit_reached');
|
|
1375
|
+
break;
|
|
1376
|
+
}
|
|
1377
|
+
case 'neighbors': {
|
|
1378
|
+
requireSeeds(query.view, seedRefs, query.search !== undefined);
|
|
1379
|
+
if (seedRefs.length === 0) {
|
|
1380
|
+
reasons.add('search_no_match');
|
|
1381
|
+
break;
|
|
1382
|
+
}
|
|
1383
|
+
for (const seed of seedRefs) {
|
|
1384
|
+
const result = getGraphNeighborhood(adapter, {
|
|
1385
|
+
ref: seed,
|
|
1386
|
+
depth: query.maxDepth,
|
|
1387
|
+
direction,
|
|
1388
|
+
...accessInput,
|
|
1389
|
+
history,
|
|
1390
|
+
edge_filters: edgeFilters,
|
|
1391
|
+
as_of_ms: query.asOf ?? null,
|
|
1392
|
+
limit,
|
|
1393
|
+
});
|
|
1394
|
+
for (const ref of result.nodes)
|
|
1395
|
+
addRefNode(ref, sequence++);
|
|
1396
|
+
for (const edge of result.edges)
|
|
1397
|
+
addEdge(edge);
|
|
1398
|
+
mergeProjections(projections, result.current_projection);
|
|
1399
|
+
if (result.edges.length >= limit)
|
|
1400
|
+
reasons.add('limit_reached');
|
|
1401
|
+
}
|
|
1402
|
+
break;
|
|
1403
|
+
}
|
|
1404
|
+
case 'paths': {
|
|
1405
|
+
if (!query.from || !query.to) {
|
|
1406
|
+
throw new judgments_js_1.JudgmentError('INVALID_QUERY', 'graph paths requires from and to refs.');
|
|
1407
|
+
}
|
|
1408
|
+
const result = getGraphPaths(adapter, {
|
|
1409
|
+
from_ref: query.from,
|
|
1410
|
+
to_ref: query.to,
|
|
1411
|
+
max_depth: query.maxDepth,
|
|
1412
|
+
...accessInput,
|
|
1413
|
+
history,
|
|
1414
|
+
edge_filters: edgeFilters,
|
|
1415
|
+
as_of_ms: query.asOf ?? null,
|
|
1416
|
+
limit,
|
|
1417
|
+
});
|
|
1418
|
+
if (result.limit_reached)
|
|
1419
|
+
reasons.add('limit_reached');
|
|
1420
|
+
for (const path of result.paths) {
|
|
1421
|
+
for (const ref of path.refs)
|
|
1422
|
+
addRefNode(ref, sequence++);
|
|
1423
|
+
for (const edge of path.edges)
|
|
1424
|
+
addEdge(edge);
|
|
1425
|
+
}
|
|
1426
|
+
mergeProjections(projections, result.current_projection);
|
|
1427
|
+
break;
|
|
1428
|
+
}
|
|
1429
|
+
case 'timeline': {
|
|
1430
|
+
requireSeeds(query.view, seedRefs, query.search !== undefined);
|
|
1431
|
+
if (seedRefs.length === 0) {
|
|
1432
|
+
reasons.add('search_no_match');
|
|
1433
|
+
break;
|
|
1434
|
+
}
|
|
1435
|
+
const merged = new Map();
|
|
1436
|
+
for (const seed of seedRefs) {
|
|
1437
|
+
const result = getGraphTimeline(adapter, {
|
|
1438
|
+
ref: seed,
|
|
1439
|
+
...accessInput,
|
|
1440
|
+
history,
|
|
1441
|
+
edge_filters: edgeFilters,
|
|
1442
|
+
from_ms: query.eventRange?.start,
|
|
1443
|
+
to_ms: query.eventRange?.end,
|
|
1444
|
+
recorded_from_ms: query.recordedRange?.start,
|
|
1445
|
+
recorded_to_ms: query.recordedRange?.end,
|
|
1446
|
+
as_of_ms: query.asOf ?? null,
|
|
1447
|
+
limit,
|
|
1448
|
+
});
|
|
1449
|
+
if (result.has_more)
|
|
1450
|
+
reasons.add('limit_reached');
|
|
1451
|
+
for (const event of result.events) {
|
|
1452
|
+
merged.set(`${event.kind}${eventKey(event)}`, event);
|
|
1453
|
+
}
|
|
1454
|
+
mergeProjections(projections, result.current_projection);
|
|
1455
|
+
}
|
|
1456
|
+
let events = [...merged.values()];
|
|
1457
|
+
events.sort((left, right) => left.at_ms - right.at_ms ||
|
|
1458
|
+
eventKindRank(left.kind) - eventKindRank(right.kind) ||
|
|
1459
|
+
eventKey(left).localeCompare(eventKey(right)));
|
|
1460
|
+
if (events.length > limit)
|
|
1461
|
+
reasons.add('limit_reached');
|
|
1462
|
+
events = events.slice(0, limit);
|
|
1463
|
+
for (const seed of seedRefs)
|
|
1464
|
+
addRefNode(seed, -1);
|
|
1465
|
+
for (const event of events) {
|
|
1466
|
+
if (event.kind === 'edge') {
|
|
1467
|
+
addEdge(event.edge);
|
|
1468
|
+
}
|
|
1469
|
+
else {
|
|
1470
|
+
addRefNode(event.ref, event.at_ms);
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
break;
|
|
1474
|
+
}
|
|
1475
|
+
case 'detail': {
|
|
1476
|
+
requireSeeds(query.view, seedRefs, query.search !== undefined);
|
|
1477
|
+
if (seedRefs.length > 0) {
|
|
1478
|
+
assertRefsVisible(adapter, seedRefs, visibility, query.asOf ?? null);
|
|
1479
|
+
for (const seed of seedRefs)
|
|
1480
|
+
addRefNode(seed, sequence++);
|
|
1481
|
+
}
|
|
1482
|
+
else {
|
|
1483
|
+
reasons.add('search_no_match');
|
|
1484
|
+
}
|
|
1485
|
+
break;
|
|
1486
|
+
}
|
|
1487
|
+
case 'overview': {
|
|
1488
|
+
// The name-free entry point: the registry's visible roots are the named
|
|
1489
|
+
// anchors a caller picks before narrowing into neighbors/detail.
|
|
1490
|
+
const roots = (0, store_js_1.listNodes)(adapter, { parentId: null, scopes: access.scopes });
|
|
1491
|
+
if (roots.length > limit)
|
|
1492
|
+
reasons.add('limit_reached');
|
|
1493
|
+
for (const node of roots.slice(0, limit)) {
|
|
1494
|
+
addRefNode({ kind: 'registry', id: node.id }, sequence++);
|
|
1495
|
+
}
|
|
1496
|
+
break;
|
|
1497
|
+
}
|
|
1498
|
+
default: {
|
|
1499
|
+
throw new judgments_js_1.JudgmentError('INVALID_QUERY', `graph view must be one of overview, browse, neighbors, timeline, paths, detail; got ${String(query.view)}.`);
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
const nodes = [];
|
|
1503
|
+
const emitted = new Set();
|
|
1504
|
+
for (const original of nodeRefs) {
|
|
1505
|
+
const resolved = resolveNodeIdentity(adapter, original);
|
|
1506
|
+
if (history === 'current') {
|
|
1507
|
+
if (resolved === null) {
|
|
1508
|
+
reasons.add('resolved_away');
|
|
1509
|
+
continue;
|
|
1510
|
+
}
|
|
1511
|
+
const visible = (0, access_js_1.visibleTwinRefKeysRecursive)(adapter, [resolved], {
|
|
1512
|
+
...visibility,
|
|
1513
|
+
asOfMs: query.asOf ?? null,
|
|
1514
|
+
});
|
|
1515
|
+
if (!visible.has(`${resolved.kind}\0${resolved.id}`)) {
|
|
1516
|
+
reasons.add('resolved_not_visible');
|
|
1517
|
+
continue;
|
|
1518
|
+
}
|
|
1519
|
+
if (emitted.has(refKey(resolved))) {
|
|
1520
|
+
continue;
|
|
1521
|
+
}
|
|
1522
|
+
emitted.add(refKey(resolved));
|
|
1523
|
+
const hydrated = hydrateNode(adapter, resolved, ctx, access);
|
|
1524
|
+
if (!hydrated) {
|
|
1525
|
+
reasons.add('records_missing');
|
|
1526
|
+
continue;
|
|
1527
|
+
}
|
|
1528
|
+
if (hydrated.data.kind === 'memory' &&
|
|
1529
|
+
(hydrated.data.stateAtSnapshot === 'replaced' ||
|
|
1530
|
+
hydrated.data.stateAtSnapshot === 'withdrawn')) {
|
|
1531
|
+
reasons.add('non_current_omitted');
|
|
1532
|
+
continue;
|
|
1533
|
+
}
|
|
1534
|
+
nodes.push({
|
|
1535
|
+
ref: resolved,
|
|
1536
|
+
resolvedRef: resolved,
|
|
1537
|
+
label: hydrated.label,
|
|
1538
|
+
data: hydrated.data,
|
|
1539
|
+
});
|
|
1540
|
+
}
|
|
1541
|
+
else {
|
|
1542
|
+
const hydrated = hydrateNode(adapter, original, ctx, access);
|
|
1543
|
+
if (!hydrated) {
|
|
1544
|
+
reasons.add('records_missing');
|
|
1545
|
+
continue;
|
|
1546
|
+
}
|
|
1547
|
+
nodes.push({
|
|
1548
|
+
ref: original,
|
|
1549
|
+
resolvedRef: resolved ?? original,
|
|
1550
|
+
label: hydrated.label,
|
|
1551
|
+
data: hydrated.data,
|
|
1552
|
+
});
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
if (query.view === 'timeline' || nodeOrder.size > 0) {
|
|
1556
|
+
nodes.sort((left, right) => (nodeOrder.get(`${left.ref.kind}:${left.ref.id}`) ?? Number.MAX_SAFE_INTEGER) -
|
|
1557
|
+
(nodeOrder.get(`${right.ref.kind}:${right.ref.id}`) ?? Number.MAX_SAFE_INTEGER));
|
|
1558
|
+
}
|
|
1559
|
+
const pageEdges = collectedEdges.map((edge) => {
|
|
1560
|
+
const fromProjection = projections.get(projectionKey(edge.edge_id, 'from'));
|
|
1561
|
+
const toProjection = projections.get(projectionKey(edge.edge_id, 'to'));
|
|
1562
|
+
return {
|
|
1563
|
+
id: edge.edge_id,
|
|
1564
|
+
relation: edge.edge_type,
|
|
1565
|
+
from: edge.subject_ref,
|
|
1566
|
+
to: edge.object_ref,
|
|
1567
|
+
resolvedFrom: fromProjection?.current_ref ?? edge.subject_ref,
|
|
1568
|
+
resolvedTo: toProjection?.current_ref ?? edge.object_ref,
|
|
1569
|
+
attrs: pageEdgeAttrs(edge),
|
|
1570
|
+
};
|
|
1571
|
+
});
|
|
1572
|
+
return {
|
|
1573
|
+
nodes,
|
|
1574
|
+
edges: pageEdges,
|
|
1575
|
+
coverage: {
|
|
1576
|
+
returned: nodes.length,
|
|
1577
|
+
total: null,
|
|
1578
|
+
complete: reasons.size === 0,
|
|
1579
|
+
reasons: [...reasons],
|
|
1580
|
+
},
|
|
1581
|
+
snapshot: graphSnapshot(adapter, asOf),
|
|
1582
|
+
nextCursor,
|
|
1583
|
+
};
|
|
1584
|
+
}
|
|
1585
|
+
//# sourceMappingURL=graph-query.js.map
|