@askdkc/kiokuko 0.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/LICENSE +21 -0
- package/README.ja.md +112 -0
- package/README.ko.md +112 -0
- package/README.md +147 -0
- package/README.zh-CN.md +112 -0
- package/dist/agent-file/atomic-write.d.ts +6 -0
- package/dist/agent-file/atomic-write.d.ts.map +1 -0
- package/dist/agent-file/atomic-write.js +36 -0
- package/dist/agent-file/atomic-write.js.map +1 -0
- package/dist/agent-file/managed-block.d.ts +12 -0
- package/dist/agent-file/managed-block.d.ts.map +1 -0
- package/dist/agent-file/managed-block.js +56 -0
- package/dist/agent-file/managed-block.js.map +1 -0
- package/dist/agent-file/render.d.ts +14 -0
- package/dist/agent-file/render.d.ts.map +1 -0
- package/dist/agent-file/render.js +41 -0
- package/dist/agent-file/render.js.map +1 -0
- package/dist/akinator/domain.d.ts +25 -0
- package/dist/akinator/domain.d.ts.map +1 -0
- package/dist/akinator/domain.js +255 -0
- package/dist/akinator/domain.js.map +1 -0
- package/dist/akinator/orchestrator.d.ts +8 -0
- package/dist/akinator/orchestrator.d.ts.map +1 -0
- package/dist/akinator/orchestrator.js +11 -0
- package/dist/akinator/orchestrator.js.map +1 -0
- package/dist/akinator/service.d.ts +35 -0
- package/dist/akinator/service.d.ts.map +1 -0
- package/dist/akinator/service.js +296 -0
- package/dist/akinator/service.js.map +1 -0
- package/dist/akinator/store.d.ts +96 -0
- package/dist/akinator/store.d.ts.map +1 -0
- package/dist/akinator/store.js +583 -0
- package/dist/akinator/store.js.map +1 -0
- package/dist/akinator/types.d.ts +49 -0
- package/dist/akinator/types.d.ts.map +1 -0
- package/dist/akinator/types.js +2 -0
- package/dist/akinator/types.js.map +1 -0
- package/dist/bin/kiokuko.d.ts +3 -0
- package/dist/bin/kiokuko.d.ts.map +1 -0
- package/dist/bin/kiokuko.js +6 -0
- package/dist/bin/kiokuko.js.map +1 -0
- package/dist/cli.d.ts +10 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +362 -0
- package/dist/cli.js.map +1 -0
- package/dist/client/runtime-discovery.d.ts +15 -0
- package/dist/client/runtime-discovery.d.ts.map +1 -0
- package/dist/client/runtime-discovery.js +45 -0
- package/dist/client/runtime-discovery.js.map +1 -0
- package/dist/client/server-client.d.ts +20 -0
- package/dist/client/server-client.d.ts.map +1 -0
- package/dist/client/server-client.js +424 -0
- package/dist/client/server-client.js.map +1 -0
- package/dist/commands/agent.d.ts +10 -0
- package/dist/commands/agent.d.ts.map +1 -0
- package/dist/commands/agent.js +283 -0
- package/dist/commands/agent.js.map +1 -0
- package/dist/commands/backup.d.ts +5 -0
- package/dist/commands/backup.d.ts.map +1 -0
- package/dist/commands/backup.js +14 -0
- package/dist/commands/backup.js.map +1 -0
- package/dist/commands/call.d.ts +7 -0
- package/dist/commands/call.d.ts.map +1 -0
- package/dist/commands/call.js +10 -0
- package/dist/commands/call.js.map +1 -0
- package/dist/commands/doctor.d.ts +31 -0
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/doctor.js +142 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/export.d.ts +21 -0
- package/dist/commands/export.d.ts.map +1 -0
- package/dist/commands/export.js +68 -0
- package/dist/commands/export.js.map +1 -0
- package/dist/commands/import.d.ts +15 -0
- package/dist/commands/import.d.ts.map +1 -0
- package/dist/commands/import.js +265 -0
- package/dist/commands/import.js.map +1 -0
- package/dist/commands/init.d.ts +14 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +26 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/ledger.d.ts +7 -0
- package/dist/commands/ledger.d.ts.map +1 -0
- package/dist/commands/ledger.js +66 -0
- package/dist/commands/ledger.js.map +1 -0
- package/dist/commands/link.d.ts +6 -0
- package/dist/commands/link.d.ts.map +1 -0
- package/dist/commands/link.js +6 -0
- package/dist/commands/link.js.map +1 -0
- package/dist/commands/promote.d.ts +6 -0
- package/dist/commands/promote.d.ts.map +1 -0
- package/dist/commands/promote.js +6 -0
- package/dist/commands/promote.js.map +1 -0
- package/dist/commands/purge.d.ts +10 -0
- package/dist/commands/purge.d.ts.map +1 -0
- package/dist/commands/purge.js +26 -0
- package/dist/commands/purge.js.map +1 -0
- package/dist/commands/read.d.ts +6 -0
- package/dist/commands/read.d.ts.map +1 -0
- package/dist/commands/read.js +4 -0
- package/dist/commands/read.js.map +1 -0
- package/dist/commands/recall.d.ts +6 -0
- package/dist/commands/recall.d.ts.map +1 -0
- package/dist/commands/recall.js +4 -0
- package/dist/commands/recall.js.map +1 -0
- package/dist/commands/record.d.ts +6 -0
- package/dist/commands/record.d.ts.map +1 -0
- package/dist/commands/record.js +4 -0
- package/dist/commands/record.js.map +1 -0
- package/dist/commands/search.d.ts +6 -0
- package/dist/commands/search.d.ts.map +1 -0
- package/dist/commands/search.js +4 -0
- package/dist/commands/search.js.map +1 -0
- package/dist/commands/serve.d.ts +34 -0
- package/dist/commands/serve.d.ts.map +1 -0
- package/dist/commands/serve.js +120 -0
- package/dist/commands/serve.js.map +1 -0
- package/dist/commands/server-status.d.ts +20 -0
- package/dist/commands/server-status.d.ts.map +1 -0
- package/dist/commands/server-status.js +21 -0
- package/dist/commands/server-status.js.map +1 -0
- package/dist/commands/server.d.ts +13 -0
- package/dist/commands/server.d.ts.map +1 -0
- package/dist/commands/server.js +89 -0
- package/dist/commands/server.js.map +1 -0
- package/dist/commands/setup.d.ts +32 -0
- package/dist/commands/setup.d.ts.map +1 -0
- package/dist/commands/setup.js +121 -0
- package/dist/commands/setup.js.map +1 -0
- package/dist/commands/supersede.d.ts +6 -0
- package/dist/commands/supersede.d.ts.map +1 -0
- package/dist/commands/supersede.js +6 -0
- package/dist/commands/supersede.js.map +1 -0
- package/dist/commands/use.d.ts +28 -0
- package/dist/commands/use.d.ts.map +1 -0
- package/dist/commands/use.js +176 -0
- package/dist/commands/use.js.map +1 -0
- package/dist/config/paths.d.ts +18 -0
- package/dist/config/paths.d.ts.map +1 -0
- package/dist/config/paths.js +104 -0
- package/dist/config/paths.js.map +1 -0
- package/dist/config/project-config.d.ts +10 -0
- package/dist/config/project-config.d.ts.map +1 -0
- package/dist/config/project-config.js +59 -0
- package/dist/config/project-config.js.map +1 -0
- package/dist/context/broker.d.ts +80 -0
- package/dist/context/broker.d.ts.map +1 -0
- package/dist/context/broker.js +437 -0
- package/dist/context/broker.js.map +1 -0
- package/dist/context/delivery.d.ts +66 -0
- package/dist/context/delivery.d.ts.map +1 -0
- package/dist/context/delivery.js +758 -0
- package/dist/context/delivery.js.map +1 -0
- package/dist/context/feedback.d.ts +75 -0
- package/dist/context/feedback.d.ts.map +1 -0
- package/dist/context/feedback.js +997 -0
- package/dist/context/feedback.js.map +1 -0
- package/dist/context/ranking.d.ts +98 -0
- package/dist/context/ranking.d.ts.map +1 -0
- package/dist/context/ranking.js +513 -0
- package/dist/context/ranking.js.map +1 -0
- package/dist/context/recommendations.d.ts +85 -0
- package/dist/context/recommendations.d.ts.map +1 -0
- package/dist/context/recommendations.js +342 -0
- package/dist/context/recommendations.js.map +1 -0
- package/dist/db/adapter.d.ts +25 -0
- package/dist/db/adapter.d.ts.map +1 -0
- package/dist/db/adapter.js +37 -0
- package/dist/db/adapter.js.map +1 -0
- package/dist/db/capabilities.d.ts +13 -0
- package/dist/db/capabilities.d.ts.map +1 -0
- package/dist/db/capabilities.js +25 -0
- package/dist/db/capabilities.js.map +1 -0
- package/dist/db/connection.d.ts +6 -0
- package/dist/db/connection.d.ts.map +1 -0
- package/dist/db/connection.js +42 -0
- package/dist/db/connection.js.map +1 -0
- package/dist/db/migrate.d.ts +9 -0
- package/dist/db/migrate.d.ts.map +1 -0
- package/dist/db/migrate.js +102 -0
- package/dist/db/migrate.js.map +1 -0
- package/dist/db/transaction.d.ts +4 -0
- package/dist/db/transaction.d.ts.map +1 -0
- package/dist/db/transaction.js +39 -0
- package/dist/db/transaction.js.map +1 -0
- package/dist/errors.d.ts +9 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +32 -0
- package/dist/errors.js.map +1 -0
- package/dist/gateway/agent-service.d.ts +69 -0
- package/dist/gateway/agent-service.d.ts.map +1 -0
- package/dist/gateway/agent-service.js +581 -0
- package/dist/gateway/agent-service.js.map +1 -0
- package/dist/gateway/checkpoint-service.d.ts +42 -0
- package/dist/gateway/checkpoint-service.d.ts.map +1 -0
- package/dist/gateway/checkpoint-service.js +295 -0
- package/dist/gateway/checkpoint-service.js.map +1 -0
- package/dist/knowledge/sources.d.ts +72 -0
- package/dist/knowledge/sources.d.ts.map +1 -0
- package/dist/knowledge/sources.js +406 -0
- package/dist/knowledge/sources.js.map +1 -0
- package/dist/ledger/archive.d.ts +50 -0
- package/dist/ledger/archive.d.ts.map +1 -0
- package/dist/ledger/archive.js +1072 -0
- package/dist/ledger/archive.js.map +1 -0
- package/dist/ledger/hash.d.ts +22 -0
- package/dist/ledger/hash.d.ts.map +1 -0
- package/dist/ledger/hash.js +25 -0
- package/dist/ledger/hash.js.map +1 -0
- package/dist/ledger/maintenance.d.ts +78 -0
- package/dist/ledger/maintenance.d.ts.map +1 -0
- package/dist/ledger/maintenance.js +683 -0
- package/dist/ledger/maintenance.js.map +1 -0
- package/dist/ledger/projection.d.ts +37 -0
- package/dist/ledger/projection.d.ts.map +1 -0
- package/dist/ledger/projection.js +307 -0
- package/dist/ledger/projection.js.map +1 -0
- package/dist/ledger/promotion.d.ts +28 -0
- package/dist/ledger/promotion.d.ts.map +1 -0
- package/dist/ledger/promotion.js +308 -0
- package/dist/ledger/promotion.js.map +1 -0
- package/dist/ledger/query.d.ts +55 -0
- package/dist/ledger/query.d.ts.map +1 -0
- package/dist/ledger/query.js +321 -0
- package/dist/ledger/query.js.map +1 -0
- package/dist/ledger/redaction.d.ts +8 -0
- package/dist/ledger/redaction.d.ts.map +1 -0
- package/dist/ledger/redaction.js +40 -0
- package/dist/ledger/redaction.js.map +1 -0
- package/dist/ledger/store.d.ts +46 -0
- package/dist/ledger/store.d.ts.map +1 -0
- package/dist/ledger/store.js +324 -0
- package/dist/ledger/store.js.map +1 -0
- package/dist/ledger/types.d.ts +119 -0
- package/dist/ledger/types.d.ts.map +1 -0
- package/dist/ledger/types.js +22 -0
- package/dist/ledger/types.js.map +1 -0
- package/dist/ledger/validate.d.ts +10 -0
- package/dist/ledger/validate.d.ts.map +1 -0
- package/dist/ledger/validate.js +183 -0
- package/dist/ledger/validate.js.map +1 -0
- package/dist/mcp/server.d.ts +9 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +86 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/memory/audit.d.ts +14 -0
- package/dist/memory/audit.d.ts.map +1 -0
- package/dist/memory/audit.js +16 -0
- package/dist/memory/audit.js.map +1 -0
- package/dist/memory/entries.d.ts +65 -0
- package/dist/memory/entries.d.ts.map +1 -0
- package/dist/memory/entries.js +200 -0
- package/dist/memory/entries.js.map +1 -0
- package/dist/memory/lifecycle.d.ts +29 -0
- package/dist/memory/lifecycle.d.ts.map +1 -0
- package/dist/memory/lifecycle.js +61 -0
- package/dist/memory/lifecycle.js.map +1 -0
- package/dist/memory/retrieval.d.ts +44 -0
- package/dist/memory/retrieval.d.ts.map +1 -0
- package/dist/memory/retrieval.js +174 -0
- package/dist/memory/retrieval.js.map +1 -0
- package/dist/memory/scoped-memory.d.ts +41 -0
- package/dist/memory/scoped-memory.d.ts.map +1 -0
- package/dist/memory/scoped-memory.js +65 -0
- package/dist/memory/scoped-memory.js.map +1 -0
- package/dist/memory/secrets.d.ts +6 -0
- package/dist/memory/secrets.d.ts.map +1 -0
- package/dist/memory/secrets.js +17 -0
- package/dist/memory/secrets.js.map +1 -0
- package/dist/memory/workspaces.d.ts +17 -0
- package/dist/memory/workspaces.d.ts.map +1 -0
- package/dist/memory/workspaces.js +92 -0
- package/dist/memory/workspaces.js.map +1 -0
- package/dist/repository/binding.d.ts +19 -0
- package/dist/repository/binding.d.ts.map +1 -0
- package/dist/repository/binding.js +78 -0
- package/dist/repository/binding.js.map +1 -0
- package/dist/repository/detect-root.d.ts +12 -0
- package/dist/repository/detect-root.d.ts.map +1 -0
- package/dist/repository/detect-root.js +57 -0
- package/dist/repository/detect-root.js.map +1 -0
- package/dist/repository/git-origin.d.ts +2 -0
- package/dist/repository/git-origin.d.ts.map +1 -0
- package/dist/repository/git-origin.js +15 -0
- package/dist/repository/git-origin.js.map +1 -0
- package/dist/repository/identity.d.ts +19 -0
- package/dist/repository/identity.d.ts.map +1 -0
- package/dist/repository/identity.js +23 -0
- package/dist/repository/identity.js.map +1 -0
- package/dist/repository/remote-url.d.ts +4 -0
- package/dist/repository/remote-url.d.ts.map +1 -0
- package/dist/repository/remote-url.js +36 -0
- package/dist/repository/remote-url.js.map +1 -0
- package/dist/security/sanitize.d.ts +4 -0
- package/dist/security/sanitize.d.ts.map +1 -0
- package/dist/security/sanitize.js +174 -0
- package/dist/security/sanitize.js.map +1 -0
- package/dist/serialization/envelope.d.ts +20 -0
- package/dist/serialization/envelope.d.ts.map +1 -0
- package/dist/serialization/envelope.js +11 -0
- package/dist/serialization/envelope.js.map +1 -0
- package/dist/serialization/validate.d.ts +31 -0
- package/dist/serialization/validate.d.ts.map +1 -0
- package/dist/serialization/validate.js +168 -0
- package/dist/serialization/validate.js.map +1 -0
- package/dist/server/agent-application.d.ts +6 -0
- package/dist/server/agent-application.d.ts.map +1 -0
- package/dist/server/agent-application.js +60 -0
- package/dist/server/agent-application.js.map +1 -0
- package/dist/server/app.d.ts +5 -0
- package/dist/server/app.d.ts.map +1 -0
- package/dist/server/app.js +87 -0
- package/dist/server/app.js.map +1 -0
- package/dist/server/auth.d.ts +2 -0
- package/dist/server/auth.d.ts.map +1 -0
- package/dist/server/auth.js +22 -0
- package/dist/server/auth.js.map +1 -0
- package/dist/server/http.d.ts +69 -0
- package/dist/server/http.d.ts.map +1 -0
- package/dist/server/http.js +349 -0
- package/dist/server/http.js.map +1 -0
- package/dist/server/idempotency.d.ts +12 -0
- package/dist/server/idempotency.d.ts.map +1 -0
- package/dist/server/idempotency.js +210 -0
- package/dist/server/idempotency.js.map +1 -0
- package/dist/server/instance-lock.d.ts +17 -0
- package/dist/server/instance-lock.d.ts.map +1 -0
- package/dist/server/instance-lock.js +118 -0
- package/dist/server/instance-lock.js.map +1 -0
- package/dist/server/router.d.ts +29 -0
- package/dist/server/router.d.ts.map +1 -0
- package/dist/server/router.js +112 -0
- package/dist/server/router.js.map +1 -0
- package/dist/server/routes/agent-events.d.ts +5 -0
- package/dist/server/routes/agent-events.d.ts.map +1 -0
- package/dist/server/routes/agent-events.js +46 -0
- package/dist/server/routes/agent-events.js.map +1 -0
- package/dist/server/routes/agent-intake.d.ts +5 -0
- package/dist/server/routes/agent-intake.d.ts.map +1 -0
- package/dist/server/routes/agent-intake.js +42 -0
- package/dist/server/routes/agent-intake.js.map +1 -0
- package/dist/server/routes/agent-promotions.d.ts +5 -0
- package/dist/server/routes/agent-promotions.d.ts.map +1 -0
- package/dist/server/routes/agent-promotions.js +52 -0
- package/dist/server/routes/agent-promotions.js.map +1 -0
- package/dist/server/routes/agent-runs.d.ts +25 -0
- package/dist/server/routes/agent-runs.d.ts.map +1 -0
- package/dist/server/routes/agent-runs.js +151 -0
- package/dist/server/routes/agent-runs.js.map +1 -0
- package/dist/server/routes/task5-support.d.ts +11 -0
- package/dist/server/routes/task5-support.d.ts.map +1 -0
- package/dist/server/routes/task5-support.js +22 -0
- package/dist/server/routes/task5-support.js.map +1 -0
- package/dist/server/routes/task5.d.ts +5 -0
- package/dist/server/routes/task5.d.ts.map +1 -0
- package/dist/server/routes/task5.js +72 -0
- package/dist/server/routes/task5.js.map +1 -0
- package/dist/server/runtime-descriptor.d.ts +24 -0
- package/dist/server/runtime-descriptor.d.ts.map +1 -0
- package/dist/server/runtime-descriptor.js +156 -0
- package/dist/server/runtime-descriptor.js.map +1 -0
- package/dist/server/write-queue.d.ts +20 -0
- package/dist/server/write-queue.d.ts.map +1 -0
- package/dist/server/write-queue.js +83 -0
- package/dist/server/write-queue.js.map +1 -0
- package/dist/setup/managed-text.d.ts +6 -0
- package/dist/setup/managed-text.d.ts.map +1 -0
- package/dist/setup/managed-text.js +42 -0
- package/dist/setup/managed-text.js.map +1 -0
- package/dist/setup/opencode-config.d.ts +3 -0
- package/dist/setup/opencode-config.d.ts.map +1 -0
- package/dist/setup/opencode-config.js +26 -0
- package/dist/setup/opencode-config.js.map +1 -0
- package/dist/setup/render.d.ts +8 -0
- package/dist/setup/render.d.ts.map +1 -0
- package/dist/setup/render.js +45 -0
- package/dist/setup/render.js.map +1 -0
- package/dist/web/server.d.ts +16 -0
- package/dist/web/server.d.ts.map +1 -0
- package/dist/web/server.js +424 -0
- package/dist/web/server.js.map +1 -0
- package/dist/web/ui.d.ts +2 -0
- package/dist/web/ui.d.ts.map +1 -0
- package/dist/web/ui.js +271 -0
- package/dist/web/ui.js.map +1 -0
- package/migrations/001_initial.sql +89 -0
- package/migrations/002_fts.sql +66 -0
- package/migrations/003_akinator.sql +30 -0
- package/migrations/004_agent_gateway.sql +259 -0
- package/package.json +45 -0
- package/templates/AGENTS.md +29 -0
|
@@ -0,0 +1,1072 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { withImmediateTransaction } from '../db/transaction.js';
|
|
3
|
+
import { KiokukoError } from '../errors.js';
|
|
4
|
+
import { findSecret } from '../memory/secrets.js';
|
|
5
|
+
import { canonicalJson, requireWorkspace } from '../serialization/validate.js';
|
|
6
|
+
import { hashLedgerEvent, GENESIS_HASH } from './hash.js';
|
|
7
|
+
import { CAPTURE_PROFILES, COVERAGE_LEVELS, LEDGER_EVENT_TYPES, RUN_STATUSES, TERMINAL_RUN_STATUSES, } from './types.js';
|
|
8
|
+
export const LEDGER_ARCHIVE_FORMAT = 'kiokuko-ledger-jsonl';
|
|
9
|
+
export const LEDGER_ARCHIVE_API_VERSION = '1';
|
|
10
|
+
export const LEDGER_ARCHIVE_VERSION = 1;
|
|
11
|
+
export const MAX_ARCHIVE_LINE_COUNT = 10_000;
|
|
12
|
+
export const MAX_ARCHIVE_LINE_BYTES = 512 * 1024;
|
|
13
|
+
export const MAX_ARCHIVE_TOTAL_BYTES = 64 * 1024 * 1024;
|
|
14
|
+
export const MAX_LEDGER_ARCHIVE_LINES = MAX_ARCHIVE_LINE_COUNT;
|
|
15
|
+
export const MAX_LEDGER_ARCHIVE_LINE_BYTES = MAX_ARCHIVE_LINE_BYTES;
|
|
16
|
+
export const MAX_LEDGER_ARCHIVE_BYTES = MAX_ARCHIVE_TOTAL_BYTES;
|
|
17
|
+
const COMMENT_MAX_BYTES = 4 * 1024;
|
|
18
|
+
const IDENTIFIER_MAX_LENGTH = 256;
|
|
19
|
+
const TEXT_MAX_LENGTH = 16 * 1024;
|
|
20
|
+
const TIMESTAMP_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
|
|
21
|
+
const HASH_PATTERN = /^[0-9a-f]{64}$/;
|
|
22
|
+
const PROFILE_FIELDS = new Set(['taskType', 'target', 'expected', 'constraints']);
|
|
23
|
+
const PROFILE_SOURCES = new Set(['inferred', 'client_supplied', 'user_answer']);
|
|
24
|
+
const TASK_TYPES = new Set(['build', 'debug', 'research', 'review', 'devops', 'writing', 'analysis']);
|
|
25
|
+
const REDACTION_KINDS = new Set([
|
|
26
|
+
'sensitive_key', 'secret_pattern', 'url', 'home_path', 'preview_truncated', 'environment_value', 'hidden_reasoning',
|
|
27
|
+
]);
|
|
28
|
+
const CONTEXT_FEEDBACK_VERDICTS = new Set(['helpful', 'irrelevant', 'stale', 'conflicting']);
|
|
29
|
+
const RECOMMENDATION_VERDICTS = new Set(['accepted', 'dismissed', 'resolved']);
|
|
30
|
+
const EVIDENCE_KINDS = new Set(['command', 'test', 'file', 'diff', 'url', 'artifact']);
|
|
31
|
+
const PURGE_TARGET_TYPES = new Set(['run', 'event', 'evidence', 'delivery', 'feedback', 'memory_link']);
|
|
32
|
+
const EMPTY_COUNTS = {
|
|
33
|
+
runs: 0,
|
|
34
|
+
sessions: 0,
|
|
35
|
+
answers: 0,
|
|
36
|
+
runIntakes: 0,
|
|
37
|
+
intakeFeedback: 0,
|
|
38
|
+
events: 0,
|
|
39
|
+
evidence: 0,
|
|
40
|
+
deliveries: 0,
|
|
41
|
+
deliveryEntries: 0,
|
|
42
|
+
contextFeedback: 0,
|
|
43
|
+
runFeedback: 0,
|
|
44
|
+
memoryLinks: 0,
|
|
45
|
+
purgeAudit: 0,
|
|
46
|
+
};
|
|
47
|
+
const RECORD_FIELDS = {
|
|
48
|
+
checksum: ['type', 'sha256'],
|
|
49
|
+
manifest: ['type', 'apiVersion', 'archiveVersion', 'format', 'workspace', 'counts'],
|
|
50
|
+
runs: [
|
|
51
|
+
'type', 'run_id', 'workspace', 'client_kind', 'client_version', 'source_session_id', 'parent_run_id',
|
|
52
|
+
'protocol_version', 'capture_profile', 'coverage_json', 'status', 'title', 'task_hash', 'metadata_json',
|
|
53
|
+
'last_sequence', 'last_source_sequence', 'started_at', 'ended_at', 'created_at', 'updated_at',
|
|
54
|
+
],
|
|
55
|
+
sessions: ['type', 'id', 'workspace', 'task_text', 'profile_json', 'status', 'question_count', 'created_at', 'updated_at'],
|
|
56
|
+
answers: ['type', 'session_id', 'question_id', 'answer_json', 'created_at'],
|
|
57
|
+
runIntakes: [
|
|
58
|
+
'type', 'run_id', 'session_id', 'policy_version', 'profile_schema_version', 'profile_sources_json',
|
|
59
|
+
'initial_profile_hash', 'recommended_tags_json', 'linked_at', 'finalized_at',
|
|
60
|
+
],
|
|
61
|
+
intakeFeedback: [
|
|
62
|
+
'type', 'feedback_id', 'run_id', 'session_id', 'question_id', 'profile_field', 'verdict', 'comment',
|
|
63
|
+
'actor', 'idempotency_key', 'created_at',
|
|
64
|
+
],
|
|
65
|
+
events: [
|
|
66
|
+
'type', 'event_id', 'run_id', 'sequence', 'source_event_id', 'source_sequence', 'event_type', 'source_type',
|
|
67
|
+
'actor', 'outcome', 'occurred_at', 'ingested_at', 'payload_json', 'redaction_json', 'previous_hash', 'event_hash',
|
|
68
|
+
],
|
|
69
|
+
evidence: [
|
|
70
|
+
'type', 'evidence_id', 'run_id', 'event_id', 'kind', 'locator', 'digest_algorithm', 'digest', 'byte_size', 'summary', 'created_at',
|
|
71
|
+
],
|
|
72
|
+
deliveries: [
|
|
73
|
+
'type', 'delivery_id', 'run_id', 'through_sequence', 'intake_session_id', 'task_profile_hash', 'query_hash',
|
|
74
|
+
'policy_version', 'external_sync_summary_json', 'char_budget', 'char_count', 'truncated', 'created_at',
|
|
75
|
+
],
|
|
76
|
+
deliveryEntries: ['type', 'delivery_id', 'entry_id', 'entry_revision', 'rank', 'score_components_json', 'selection_reason_json'],
|
|
77
|
+
contextFeedback: ['type', 'feedback_id', 'delivery_id', 'entry_id', 'run_id', 'verdict', 'comment', 'actor', 'idempotency_key', 'created_at'],
|
|
78
|
+
runFeedback: [
|
|
79
|
+
'type', 'feedback_id', 'run_id', 'outcome', 'recommendation_code', 'recommendation_verdict', 'rating', 'comment',
|
|
80
|
+
'actor', 'idempotency_key', 'created_at',
|
|
81
|
+
],
|
|
82
|
+
memoryLinks: ['type', 'link_id', 'run_id', 'event_id', 'delivery_id', 'entry_id', 'created_at'],
|
|
83
|
+
purgeAudit: [
|
|
84
|
+
'type', 'purge_id', 'run_id', 'event_id', 'delivery_id', 'entry_id', 'target_type', 'target_id', 'actor', 'reason', 'created_at',
|
|
85
|
+
],
|
|
86
|
+
};
|
|
87
|
+
const TABLE_FIELDS = {
|
|
88
|
+
runs: RECORD_FIELDS.runs.slice(1),
|
|
89
|
+
sessions: RECORD_FIELDS.sessions.slice(1),
|
|
90
|
+
answers: RECORD_FIELDS.answers.slice(1),
|
|
91
|
+
runIntakes: RECORD_FIELDS.runIntakes.slice(1),
|
|
92
|
+
intakeFeedback: RECORD_FIELDS.intakeFeedback.slice(1),
|
|
93
|
+
events: RECORD_FIELDS.events.slice(1),
|
|
94
|
+
evidence: RECORD_FIELDS.evidence.slice(1),
|
|
95
|
+
deliveries: RECORD_FIELDS.deliveries.slice(1),
|
|
96
|
+
deliveryEntries: RECORD_FIELDS.deliveryEntries.slice(1),
|
|
97
|
+
contextFeedback: RECORD_FIELDS.contextFeedback.slice(1),
|
|
98
|
+
runFeedback: RECORD_FIELDS.runFeedback.slice(1),
|
|
99
|
+
memoryLinks: RECORD_FIELDS.memoryLinks.slice(1),
|
|
100
|
+
purgeAudit: RECORD_FIELDS.purgeAudit.slice(1),
|
|
101
|
+
};
|
|
102
|
+
const TABLE_NAMES = {
|
|
103
|
+
runs: 'ledger_runs',
|
|
104
|
+
sessions: 'akinator_sessions',
|
|
105
|
+
answers: 'akinator_answers',
|
|
106
|
+
runIntakes: 'run_intakes',
|
|
107
|
+
intakeFeedback: 'intake_feedback',
|
|
108
|
+
events: 'ledger_events',
|
|
109
|
+
evidence: 'ledger_evidence',
|
|
110
|
+
deliveries: 'context_deliveries',
|
|
111
|
+
deliveryEntries: 'context_delivery_entries',
|
|
112
|
+
contextFeedback: 'context_feedback',
|
|
113
|
+
runFeedback: 'run_feedback',
|
|
114
|
+
memoryLinks: 'ledger_memory_links',
|
|
115
|
+
purgeAudit: 'ledger_purge_audit',
|
|
116
|
+
};
|
|
117
|
+
const RECORD_NAMES = {
|
|
118
|
+
runs: 'run',
|
|
119
|
+
sessions: 'session',
|
|
120
|
+
answers: 'answer',
|
|
121
|
+
runIntakes: 'run_intake',
|
|
122
|
+
intakeFeedback: 'intake_feedback',
|
|
123
|
+
events: 'event',
|
|
124
|
+
evidence: 'evidence',
|
|
125
|
+
deliveries: 'delivery',
|
|
126
|
+
deliveryEntries: 'delivery_entry',
|
|
127
|
+
contextFeedback: 'context_feedback',
|
|
128
|
+
runFeedback: 'run_feedback',
|
|
129
|
+
memoryLinks: 'memory_link',
|
|
130
|
+
purgeAudit: 'purge_audit',
|
|
131
|
+
};
|
|
132
|
+
const RECORD_TYPES = new Map(Object.entries(RECORD_NAMES).map(([key, value]) => [value, key]));
|
|
133
|
+
function fail(code, message, details = {}) {
|
|
134
|
+
throw new KiokukoError(code, message, details);
|
|
135
|
+
}
|
|
136
|
+
function validation() {
|
|
137
|
+
return fail('VALIDATION_ERROR', 'Ledger archive validation failed');
|
|
138
|
+
}
|
|
139
|
+
function integrity() {
|
|
140
|
+
return fail('INTEGRITY_ERROR', 'Ledger archive integrity validation failed');
|
|
141
|
+
}
|
|
142
|
+
function conflict() {
|
|
143
|
+
return fail('CONFLICT', 'Ledger archive conflicts with existing data');
|
|
144
|
+
}
|
|
145
|
+
function notFound() {
|
|
146
|
+
return fail('NOT_FOUND', 'Ledger archive reference was not found');
|
|
147
|
+
}
|
|
148
|
+
function databaseFailure() {
|
|
149
|
+
return fail('DATABASE_ERROR', 'Ledger archive database operation failed');
|
|
150
|
+
}
|
|
151
|
+
function isObject(value) {
|
|
152
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
153
|
+
return false;
|
|
154
|
+
const prototype = Object.getPrototypeOf(value);
|
|
155
|
+
return prototype === Object.prototype || prototype === null;
|
|
156
|
+
}
|
|
157
|
+
function assertObject(value) {
|
|
158
|
+
if (!isObject(value))
|
|
159
|
+
validation();
|
|
160
|
+
}
|
|
161
|
+
function assertFields(value, fields) {
|
|
162
|
+
const allowed = new Set(fields);
|
|
163
|
+
if (Object.keys(value).length !== fields.length || Object.keys(value).some((field) => !allowed.has(field)))
|
|
164
|
+
validation();
|
|
165
|
+
}
|
|
166
|
+
function stringValue(value, nonEmpty = true, maximum = TEXT_MAX_LENGTH) {
|
|
167
|
+
if (typeof value !== 'string' || (nonEmpty && value.length === 0) || value.length > maximum || value.includes('\u0000'))
|
|
168
|
+
validation();
|
|
169
|
+
return value;
|
|
170
|
+
}
|
|
171
|
+
function nullableString(value, maximum = TEXT_MAX_LENGTH) {
|
|
172
|
+
if (value === null)
|
|
173
|
+
return null;
|
|
174
|
+
return stringValue(value, true, maximum);
|
|
175
|
+
}
|
|
176
|
+
function integerValue(value, minimum = 0) {
|
|
177
|
+
if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < minimum)
|
|
178
|
+
validation();
|
|
179
|
+
return value;
|
|
180
|
+
}
|
|
181
|
+
function nullableInteger(value, minimum = 0) {
|
|
182
|
+
if (value === null)
|
|
183
|
+
return null;
|
|
184
|
+
return integerValue(value, minimum);
|
|
185
|
+
}
|
|
186
|
+
function timestampValue(value, nullable = false) {
|
|
187
|
+
if (nullable && value === null)
|
|
188
|
+
return null;
|
|
189
|
+
if (typeof value !== 'string' || !TIMESTAMP_PATTERN.test(value))
|
|
190
|
+
validation();
|
|
191
|
+
const date = new Date(value);
|
|
192
|
+
if (Number.isNaN(date.getTime()) || date.toISOString() !== value)
|
|
193
|
+
validation();
|
|
194
|
+
return value;
|
|
195
|
+
}
|
|
196
|
+
function hashValue(value, nullable = false) {
|
|
197
|
+
if (nullable && value === null)
|
|
198
|
+
return null;
|
|
199
|
+
if (typeof value !== 'string' || !HASH_PATTERN.test(value))
|
|
200
|
+
validation();
|
|
201
|
+
return value;
|
|
202
|
+
}
|
|
203
|
+
function enumValue(value, allowed) {
|
|
204
|
+
if (typeof value !== 'string' || !allowed.has(value))
|
|
205
|
+
validation();
|
|
206
|
+
return value;
|
|
207
|
+
}
|
|
208
|
+
function parsedJson(value, validator) {
|
|
209
|
+
if (typeof value !== 'string')
|
|
210
|
+
integrity();
|
|
211
|
+
let parsed;
|
|
212
|
+
try {
|
|
213
|
+
parsed = JSON.parse(value);
|
|
214
|
+
canonicalJson(parsed);
|
|
215
|
+
}
|
|
216
|
+
catch {
|
|
217
|
+
integrity();
|
|
218
|
+
}
|
|
219
|
+
try {
|
|
220
|
+
validator(parsed);
|
|
221
|
+
}
|
|
222
|
+
catch (error) {
|
|
223
|
+
if (error instanceof KiokukoError)
|
|
224
|
+
throw error;
|
|
225
|
+
integrity();
|
|
226
|
+
}
|
|
227
|
+
try {
|
|
228
|
+
return canonicalJson(parsed);
|
|
229
|
+
}
|
|
230
|
+
catch {
|
|
231
|
+
integrity();
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
function anyJson(value) {
|
|
235
|
+
try {
|
|
236
|
+
canonicalJson(value);
|
|
237
|
+
}
|
|
238
|
+
catch {
|
|
239
|
+
integrity();
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
function objectJson(value) {
|
|
243
|
+
if (!isObject(value))
|
|
244
|
+
integrity();
|
|
245
|
+
anyJson(value);
|
|
246
|
+
}
|
|
247
|
+
function coverageJson(value) {
|
|
248
|
+
if (!isObject(value))
|
|
249
|
+
integrity();
|
|
250
|
+
const fields = ['run', 'tool', 'command', 'file', 'approval'];
|
|
251
|
+
if (Object.keys(value).length !== fields.length || fields.some((field) => typeof value[field] !== 'string' || !COVERAGE_LEVELS.includes(value[field])))
|
|
252
|
+
integrity();
|
|
253
|
+
}
|
|
254
|
+
function metadataJson(value) {
|
|
255
|
+
objectJson(value);
|
|
256
|
+
}
|
|
257
|
+
function profileJson(value) {
|
|
258
|
+
if (!isObject(value) || Object.keys(value).length !== 4 || [...PROFILE_FIELDS].some((field) => !Object.hasOwn(value, field)))
|
|
259
|
+
integrity();
|
|
260
|
+
if (value.taskType !== null && (typeof value.taskType !== 'string' || !TASK_TYPES.has(value.taskType)))
|
|
261
|
+
integrity();
|
|
262
|
+
for (const field of ['target', 'expected', 'constraints']) {
|
|
263
|
+
if (value[field] !== null && typeof value[field] !== 'string')
|
|
264
|
+
integrity();
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
function answerJson(value) {
|
|
268
|
+
anyJson(value);
|
|
269
|
+
}
|
|
270
|
+
function profileSourcesJson(value) {
|
|
271
|
+
if (!isObject(value))
|
|
272
|
+
integrity();
|
|
273
|
+
for (const [field, source] of Object.entries(value)) {
|
|
274
|
+
if (!PROFILE_FIELDS.has(field) || typeof source !== 'string' || !PROFILE_SOURCES.has(source))
|
|
275
|
+
integrity();
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
function tagsJson(value) {
|
|
279
|
+
if (!Array.isArray(value))
|
|
280
|
+
integrity();
|
|
281
|
+
const seen = new Set();
|
|
282
|
+
for (const tag of value) {
|
|
283
|
+
if (typeof tag !== 'string' || tag.trim().length === 0 || tag.length > 200 || seen.has(tag))
|
|
284
|
+
integrity();
|
|
285
|
+
seen.add(tag);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
function redactionJson(value) {
|
|
289
|
+
if (!Array.isArray(value))
|
|
290
|
+
integrity();
|
|
291
|
+
for (const item of value) {
|
|
292
|
+
if (!isObject(item) || Object.keys(item).length !== 2 || typeof item.path !== 'string' || item.path.length === 0 || typeof item.kind !== 'string' || !REDACTION_KINDS.has(item.kind))
|
|
293
|
+
integrity();
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
function scoreJson(value) {
|
|
297
|
+
objectJson(value);
|
|
298
|
+
}
|
|
299
|
+
function commentValue(value) {
|
|
300
|
+
const result = nullableString(value, TEXT_MAX_LENGTH);
|
|
301
|
+
if (result !== null && Buffer.byteLength(result, 'utf8') > COMMENT_MAX_BYTES)
|
|
302
|
+
integrity();
|
|
303
|
+
return result;
|
|
304
|
+
}
|
|
305
|
+
function secretScan(record) {
|
|
306
|
+
let finding;
|
|
307
|
+
try {
|
|
308
|
+
finding = findSecret(canonicalJson(record));
|
|
309
|
+
}
|
|
310
|
+
catch {
|
|
311
|
+
integrity();
|
|
312
|
+
}
|
|
313
|
+
if (finding)
|
|
314
|
+
fail('SECURITY_REJECTION', 'Ledger archive contains secret residue', { kind: finding.kind });
|
|
315
|
+
}
|
|
316
|
+
function normalizeRecord(type, source, workspace, importing) {
|
|
317
|
+
const raw = { type, ...source };
|
|
318
|
+
assertObject(raw);
|
|
319
|
+
assertFields(raw, RECORD_FIELDS[type]);
|
|
320
|
+
const normalized = { type };
|
|
321
|
+
switch (type) {
|
|
322
|
+
case 'runs': {
|
|
323
|
+
if (stringValue(raw.workspace) !== workspace)
|
|
324
|
+
validation();
|
|
325
|
+
normalized.run_id = stringValue(raw.run_id, true, IDENTIFIER_MAX_LENGTH);
|
|
326
|
+
normalized.workspace = workspace;
|
|
327
|
+
normalized.client_kind = stringValue(raw.client_kind, true, IDENTIFIER_MAX_LENGTH);
|
|
328
|
+
normalized.client_version = nullableString(raw.client_version, IDENTIFIER_MAX_LENGTH);
|
|
329
|
+
normalized.source_session_id = nullableString(raw.source_session_id, IDENTIFIER_MAX_LENGTH);
|
|
330
|
+
normalized.parent_run_id = nullableString(raw.parent_run_id, IDENTIFIER_MAX_LENGTH);
|
|
331
|
+
normalized.protocol_version = stringValue(raw.protocol_version, true, 32);
|
|
332
|
+
normalized.capture_profile = enumValue(raw.capture_profile, new Set(CAPTURE_PROFILES));
|
|
333
|
+
normalized.coverage_json = parsedJson(raw.coverage_json, coverageJson);
|
|
334
|
+
normalized.status = enumValue(raw.status, new Set(RUN_STATUSES));
|
|
335
|
+
normalized.title = nullableString(raw.title);
|
|
336
|
+
normalized.task_hash = hashValue(raw.task_hash, true);
|
|
337
|
+
normalized.metadata_json = parsedJson(raw.metadata_json, metadataJson);
|
|
338
|
+
normalized.last_sequence = integerValue(raw.last_sequence);
|
|
339
|
+
normalized.last_source_sequence = nullableInteger(raw.last_source_sequence);
|
|
340
|
+
normalized.started_at = timestampValue(raw.started_at);
|
|
341
|
+
normalized.ended_at = timestampValue(raw.ended_at, true);
|
|
342
|
+
normalized.created_at = timestampValue(raw.created_at);
|
|
343
|
+
normalized.updated_at = timestampValue(raw.updated_at);
|
|
344
|
+
if (normalized.protocol_version !== '1')
|
|
345
|
+
validation();
|
|
346
|
+
const terminal = TERMINAL_RUN_STATUSES.includes(normalized.status);
|
|
347
|
+
if (terminal !== (normalized.ended_at !== null))
|
|
348
|
+
integrity();
|
|
349
|
+
break;
|
|
350
|
+
}
|
|
351
|
+
case 'sessions':
|
|
352
|
+
if (stringValue(raw.workspace) !== workspace)
|
|
353
|
+
validation();
|
|
354
|
+
normalized.id = stringValue(raw.id, true, IDENTIFIER_MAX_LENGTH);
|
|
355
|
+
normalized.workspace = workspace;
|
|
356
|
+
normalized.task_text = stringValue(raw.task_text);
|
|
357
|
+
normalized.profile_json = parsedJson(raw.profile_json, profileJson);
|
|
358
|
+
normalized.status = enumValue(raw.status, new Set(['active', 'ready', 'exhausted']));
|
|
359
|
+
const questionCount = integerValue(raw.question_count);
|
|
360
|
+
normalized.question_count = questionCount;
|
|
361
|
+
if (questionCount > 3)
|
|
362
|
+
integrity();
|
|
363
|
+
normalized.created_at = timestampValue(raw.created_at);
|
|
364
|
+
normalized.updated_at = timestampValue(raw.updated_at);
|
|
365
|
+
break;
|
|
366
|
+
case 'answers':
|
|
367
|
+
normalized.session_id = stringValue(raw.session_id, true, IDENTIFIER_MAX_LENGTH);
|
|
368
|
+
normalized.question_id = enumValue(raw.question_id, PROFILE_FIELDS);
|
|
369
|
+
normalized.answer_json = parsedJson(raw.answer_json, answerJson);
|
|
370
|
+
normalized.created_at = timestampValue(raw.created_at);
|
|
371
|
+
break;
|
|
372
|
+
case 'runIntakes':
|
|
373
|
+
normalized.run_id = stringValue(raw.run_id, true, IDENTIFIER_MAX_LENGTH);
|
|
374
|
+
normalized.session_id = stringValue(raw.session_id, true, IDENTIFIER_MAX_LENGTH);
|
|
375
|
+
normalized.policy_version = stringValue(raw.policy_version, true, IDENTIFIER_MAX_LENGTH);
|
|
376
|
+
normalized.profile_schema_version = integerValue(raw.profile_schema_version, 1);
|
|
377
|
+
normalized.profile_sources_json = parsedJson(raw.profile_sources_json, profileSourcesJson);
|
|
378
|
+
normalized.initial_profile_hash = hashValue(raw.initial_profile_hash, true);
|
|
379
|
+
normalized.recommended_tags_json = parsedJson(raw.recommended_tags_json, tagsJson);
|
|
380
|
+
normalized.linked_at = timestampValue(raw.linked_at);
|
|
381
|
+
normalized.finalized_at = timestampValue(raw.finalized_at, true);
|
|
382
|
+
if (normalized.finalized_at !== null && normalized.initial_profile_hash === null)
|
|
383
|
+
integrity();
|
|
384
|
+
break;
|
|
385
|
+
case 'intakeFeedback':
|
|
386
|
+
normalized.feedback_id = stringValue(raw.feedback_id, true, IDENTIFIER_MAX_LENGTH);
|
|
387
|
+
normalized.run_id = stringValue(raw.run_id, true, IDENTIFIER_MAX_LENGTH);
|
|
388
|
+
normalized.session_id = stringValue(raw.session_id, true, IDENTIFIER_MAX_LENGTH);
|
|
389
|
+
normalized.question_id = nullableString(raw.question_id, IDENTIFIER_MAX_LENGTH);
|
|
390
|
+
normalized.profile_field = nullableString(raw.profile_field, IDENTIFIER_MAX_LENGTH);
|
|
391
|
+
if ((normalized.question_id === null) === (normalized.profile_field === null))
|
|
392
|
+
validation();
|
|
393
|
+
if (normalized.question_id !== null)
|
|
394
|
+
enumValue(normalized.question_id, PROFILE_FIELDS);
|
|
395
|
+
if (normalized.profile_field !== null)
|
|
396
|
+
enumValue(normalized.profile_field, PROFILE_FIELDS);
|
|
397
|
+
normalized.verdict = enumValue(raw.verdict, new Set(['helpful', 'unnecessary', 'corrected']));
|
|
398
|
+
normalized.comment = commentValue(raw.comment);
|
|
399
|
+
normalized.actor = stringValue(raw.actor, true, IDENTIFIER_MAX_LENGTH);
|
|
400
|
+
normalized.idempotency_key = hashValue(raw.idempotency_key);
|
|
401
|
+
normalized.created_at = timestampValue(raw.created_at);
|
|
402
|
+
break;
|
|
403
|
+
case 'events':
|
|
404
|
+
normalized.event_id = stringValue(raw.event_id, true, IDENTIFIER_MAX_LENGTH);
|
|
405
|
+
normalized.run_id = stringValue(raw.run_id, true, IDENTIFIER_MAX_LENGTH);
|
|
406
|
+
normalized.sequence = integerValue(raw.sequence, 1);
|
|
407
|
+
normalized.source_event_id = nullableString(raw.source_event_id, IDENTIFIER_MAX_LENGTH);
|
|
408
|
+
normalized.source_sequence = nullableInteger(raw.source_sequence);
|
|
409
|
+
normalized.event_type = enumValue(raw.event_type, new Set(LEDGER_EVENT_TYPES));
|
|
410
|
+
normalized.source_type = nullableString(raw.source_type, IDENTIFIER_MAX_LENGTH);
|
|
411
|
+
normalized.actor = stringValue(raw.actor, true, IDENTIFIER_MAX_LENGTH);
|
|
412
|
+
normalized.outcome = nullableString(raw.outcome);
|
|
413
|
+
normalized.occurred_at = timestampValue(raw.occurred_at, true);
|
|
414
|
+
normalized.ingested_at = timestampValue(raw.ingested_at);
|
|
415
|
+
normalized.payload_json = parsedJson(raw.payload_json, answerJson);
|
|
416
|
+
normalized.redaction_json = parsedJson(raw.redaction_json, redactionJson);
|
|
417
|
+
normalized.previous_hash = hashValue(raw.previous_hash);
|
|
418
|
+
normalized.event_hash = hashValue(raw.event_hash);
|
|
419
|
+
break;
|
|
420
|
+
case 'evidence':
|
|
421
|
+
normalized.evidence_id = stringValue(raw.evidence_id, true, IDENTIFIER_MAX_LENGTH);
|
|
422
|
+
normalized.run_id = stringValue(raw.run_id, true, IDENTIFIER_MAX_LENGTH);
|
|
423
|
+
normalized.event_id = nullableString(raw.event_id, IDENTIFIER_MAX_LENGTH);
|
|
424
|
+
normalized.kind = enumValue(raw.kind, EVIDENCE_KINDS);
|
|
425
|
+
normalized.locator = stringValue(raw.locator);
|
|
426
|
+
normalized.digest_algorithm = nullableString(raw.digest_algorithm, IDENTIFIER_MAX_LENGTH);
|
|
427
|
+
normalized.digest = nullableString(raw.digest, IDENTIFIER_MAX_LENGTH);
|
|
428
|
+
normalized.byte_size = nullableInteger(raw.byte_size);
|
|
429
|
+
normalized.summary = nullableString(raw.summary);
|
|
430
|
+
normalized.created_at = timestampValue(raw.created_at);
|
|
431
|
+
break;
|
|
432
|
+
case 'deliveries':
|
|
433
|
+
normalized.delivery_id = stringValue(raw.delivery_id, true, IDENTIFIER_MAX_LENGTH);
|
|
434
|
+
normalized.run_id = stringValue(raw.run_id, true, IDENTIFIER_MAX_LENGTH);
|
|
435
|
+
normalized.through_sequence = integerValue(raw.through_sequence);
|
|
436
|
+
normalized.intake_session_id = nullableString(raw.intake_session_id, IDENTIFIER_MAX_LENGTH);
|
|
437
|
+
normalized.task_profile_hash = hashValue(raw.task_profile_hash);
|
|
438
|
+
normalized.query_hash = hashValue(raw.query_hash);
|
|
439
|
+
normalized.policy_version = stringValue(raw.policy_version, true, IDENTIFIER_MAX_LENGTH);
|
|
440
|
+
normalized.external_sync_summary_json = parsedJson(raw.external_sync_summary_json, objectJson);
|
|
441
|
+
normalized.char_budget = integerValue(raw.char_budget);
|
|
442
|
+
normalized.char_count = integerValue(raw.char_count);
|
|
443
|
+
const truncated = integerValue(raw.truncated);
|
|
444
|
+
const charCount = Number(normalized.char_count);
|
|
445
|
+
const charBudget = Number(normalized.char_budget);
|
|
446
|
+
normalized.truncated = truncated;
|
|
447
|
+
if (truncated > 1 || charCount > charBudget)
|
|
448
|
+
integrity();
|
|
449
|
+
normalized.created_at = timestampValue(raw.created_at);
|
|
450
|
+
break;
|
|
451
|
+
case 'deliveryEntries':
|
|
452
|
+
normalized.delivery_id = stringValue(raw.delivery_id, true, IDENTIFIER_MAX_LENGTH);
|
|
453
|
+
normalized.entry_id = stringValue(raw.entry_id, true, IDENTIFIER_MAX_LENGTH);
|
|
454
|
+
normalized.entry_revision = integerValue(raw.entry_revision, 1);
|
|
455
|
+
normalized.rank = integerValue(raw.rank, 1);
|
|
456
|
+
normalized.score_components_json = parsedJson(raw.score_components_json, scoreJson);
|
|
457
|
+
normalized.selection_reason_json = parsedJson(raw.selection_reason_json, scoreJson);
|
|
458
|
+
break;
|
|
459
|
+
case 'contextFeedback':
|
|
460
|
+
normalized.feedback_id = stringValue(raw.feedback_id, true, IDENTIFIER_MAX_LENGTH);
|
|
461
|
+
normalized.delivery_id = stringValue(raw.delivery_id, true, IDENTIFIER_MAX_LENGTH);
|
|
462
|
+
normalized.entry_id = stringValue(raw.entry_id, true, IDENTIFIER_MAX_LENGTH);
|
|
463
|
+
normalized.run_id = stringValue(raw.run_id, true, IDENTIFIER_MAX_LENGTH);
|
|
464
|
+
normalized.verdict = enumValue(raw.verdict, CONTEXT_FEEDBACK_VERDICTS);
|
|
465
|
+
normalized.comment = commentValue(raw.comment);
|
|
466
|
+
normalized.actor = stringValue(raw.actor, true, IDENTIFIER_MAX_LENGTH);
|
|
467
|
+
normalized.idempotency_key = hashValue(raw.idempotency_key);
|
|
468
|
+
normalized.created_at = timestampValue(raw.created_at);
|
|
469
|
+
break;
|
|
470
|
+
case 'runFeedback':
|
|
471
|
+
normalized.feedback_id = stringValue(raw.feedback_id, true, IDENTIFIER_MAX_LENGTH);
|
|
472
|
+
normalized.run_id = stringValue(raw.run_id, true, IDENTIFIER_MAX_LENGTH);
|
|
473
|
+
normalized.outcome = nullableString(raw.outcome);
|
|
474
|
+
normalized.recommendation_code = nullableString(raw.recommendation_code, IDENTIFIER_MAX_LENGTH);
|
|
475
|
+
normalized.recommendation_verdict = raw.recommendation_verdict === null ? null : enumValue(raw.recommendation_verdict, RECOMMENDATION_VERDICTS);
|
|
476
|
+
const rating = nullableInteger(raw.rating, 1);
|
|
477
|
+
normalized.rating = rating;
|
|
478
|
+
if (rating !== null && rating > 5)
|
|
479
|
+
integrity();
|
|
480
|
+
normalized.comment = commentValue(raw.comment);
|
|
481
|
+
normalized.actor = stringValue(raw.actor, true, IDENTIFIER_MAX_LENGTH);
|
|
482
|
+
normalized.idempotency_key = hashValue(raw.idempotency_key);
|
|
483
|
+
normalized.created_at = timestampValue(raw.created_at);
|
|
484
|
+
if (normalized.recommendation_code === null !== (normalized.recommendation_verdict === null))
|
|
485
|
+
integrity();
|
|
486
|
+
if (normalized.outcome === null && normalized.recommendation_code === null && normalized.rating === null)
|
|
487
|
+
integrity();
|
|
488
|
+
break;
|
|
489
|
+
case 'memoryLinks':
|
|
490
|
+
normalized.link_id = stringValue(raw.link_id, true, IDENTIFIER_MAX_LENGTH);
|
|
491
|
+
normalized.run_id = stringValue(raw.run_id, true, IDENTIFIER_MAX_LENGTH);
|
|
492
|
+
normalized.event_id = nullableString(raw.event_id, IDENTIFIER_MAX_LENGTH);
|
|
493
|
+
normalized.delivery_id = nullableString(raw.delivery_id, IDENTIFIER_MAX_LENGTH);
|
|
494
|
+
normalized.entry_id = stringValue(raw.entry_id, true, IDENTIFIER_MAX_LENGTH);
|
|
495
|
+
normalized.created_at = timestampValue(raw.created_at);
|
|
496
|
+
if (normalized.event_id === null && normalized.delivery_id === null)
|
|
497
|
+
integrity();
|
|
498
|
+
break;
|
|
499
|
+
case 'purgeAudit':
|
|
500
|
+
normalized.purge_id = stringValue(raw.purge_id, true, IDENTIFIER_MAX_LENGTH);
|
|
501
|
+
normalized.run_id = nullableString(raw.run_id, IDENTIFIER_MAX_LENGTH);
|
|
502
|
+
normalized.event_id = nullableString(raw.event_id, IDENTIFIER_MAX_LENGTH);
|
|
503
|
+
normalized.delivery_id = nullableString(raw.delivery_id, IDENTIFIER_MAX_LENGTH);
|
|
504
|
+
normalized.entry_id = nullableString(raw.entry_id, IDENTIFIER_MAX_LENGTH);
|
|
505
|
+
normalized.target_type = enumValue(raw.target_type, PURGE_TARGET_TYPES);
|
|
506
|
+
normalized.target_id = stringValue(raw.target_id, true, IDENTIFIER_MAX_LENGTH);
|
|
507
|
+
normalized.actor = stringValue(raw.actor, true, IDENTIFIER_MAX_LENGTH);
|
|
508
|
+
normalized.reason = nullableString(raw.reason);
|
|
509
|
+
normalized.created_at = timestampValue(raw.created_at);
|
|
510
|
+
break;
|
|
511
|
+
}
|
|
512
|
+
const result = normalized;
|
|
513
|
+
if (!importing)
|
|
514
|
+
secretScan(result);
|
|
515
|
+
return result;
|
|
516
|
+
}
|
|
517
|
+
function rows(database, sql, ...parameters) {
|
|
518
|
+
try {
|
|
519
|
+
return database.prepare(sql).all(...parameters);
|
|
520
|
+
}
|
|
521
|
+
catch {
|
|
522
|
+
databaseFailure();
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
function queryRows(database, type, workspace) {
|
|
526
|
+
const fields = TABLE_FIELDS[type].join(', ');
|
|
527
|
+
const table = TABLE_NAMES[type];
|
|
528
|
+
const scoped = {
|
|
529
|
+
runs: { sql: `SELECT ${fields} FROM ledger_runs WHERE workspace = ? ORDER BY run_id ASC`, parameters: [workspace] },
|
|
530
|
+
sessions: { sql: `SELECT s.${TABLE_FIELDS.sessions.join(', s.')} FROM akinator_sessions AS s JOIN run_intakes AS ri ON ri.session_id = s.id JOIN ledger_runs AS lr ON lr.run_id = ri.run_id WHERE lr.workspace = ? ORDER BY s.id ASC`, parameters: [workspace] },
|
|
531
|
+
answers: { sql: `SELECT a.${TABLE_FIELDS.answers.join(', a.')} FROM akinator_answers AS a JOIN akinator_sessions AS s ON s.id = a.session_id JOIN run_intakes AS ri ON ri.session_id = s.id JOIN ledger_runs AS lr ON lr.run_id = ri.run_id WHERE lr.workspace = ? ORDER BY a.session_id ASC, a.question_id ASC`, parameters: [workspace] },
|
|
532
|
+
runIntakes: { sql: `SELECT ri.${TABLE_FIELDS.runIntakes.join(', ri.')} FROM run_intakes AS ri JOIN ledger_runs AS lr ON lr.run_id = ri.run_id WHERE lr.workspace = ? ORDER BY ri.run_id ASC`, parameters: [workspace] },
|
|
533
|
+
intakeFeedback: { sql: `SELECT f.${TABLE_FIELDS.intakeFeedback.join(', f.')} FROM intake_feedback AS f JOIN ledger_runs AS lr ON lr.run_id = f.run_id WHERE lr.workspace = ? ORDER BY f.feedback_id ASC`, parameters: [workspace] },
|
|
534
|
+
events: { sql: `SELECT e.${TABLE_FIELDS.events.join(', e.')} FROM ledger_events AS e JOIN ledger_runs AS lr ON lr.run_id = e.run_id WHERE lr.workspace = ? ORDER BY e.run_id ASC, e.sequence ASC`, parameters: [workspace] },
|
|
535
|
+
evidence: { sql: `SELECT e.${TABLE_FIELDS.evidence.join(', e.')} FROM ledger_evidence AS e JOIN ledger_runs AS lr ON lr.run_id = e.run_id WHERE lr.workspace = ? ORDER BY e.run_id ASC, e.evidence_id ASC`, parameters: [workspace] },
|
|
536
|
+
deliveries: { sql: `SELECT d.${TABLE_FIELDS.deliveries.join(', d.')} FROM context_deliveries AS d JOIN ledger_runs AS lr ON lr.run_id = d.run_id WHERE lr.workspace = ? ORDER BY d.run_id ASC, d.delivery_id ASC`, parameters: [workspace] },
|
|
537
|
+
deliveryEntries: { sql: `SELECT c.${TABLE_FIELDS.deliveryEntries.join(', c.')} FROM context_delivery_entries AS c JOIN context_deliveries AS d ON d.delivery_id = c.delivery_id JOIN ledger_runs AS lr ON lr.run_id = d.run_id JOIN entries AS e ON e.id = c.entry_id AND e.workspace = ? WHERE lr.workspace = ? ORDER BY c.delivery_id ASC, c.entry_id ASC`, parameters: [workspace, workspace] },
|
|
538
|
+
contextFeedback: { sql: `SELECT f.${TABLE_FIELDS.contextFeedback.join(', f.')} FROM context_feedback AS f JOIN ledger_runs AS lr ON lr.run_id = f.run_id JOIN context_deliveries AS d ON d.delivery_id = f.delivery_id AND d.run_id = f.run_id JOIN context_delivery_entries AS c ON c.delivery_id = f.delivery_id AND c.entry_id = f.entry_id JOIN entries AS e ON e.id = f.entry_id AND e.workspace = ? WHERE lr.workspace = ? ORDER BY f.feedback_id ASC`, parameters: [workspace, workspace] },
|
|
539
|
+
runFeedback: { sql: `SELECT f.${TABLE_FIELDS.runFeedback.join(', f.')} FROM run_feedback AS f JOIN ledger_runs AS lr ON lr.run_id = f.run_id WHERE lr.workspace = ? ORDER BY f.feedback_id ASC`, parameters: [workspace] },
|
|
540
|
+
memoryLinks: { sql: `SELECT l.${TABLE_FIELDS.memoryLinks.join(', l.')} FROM ledger_memory_links AS l JOIN ledger_runs AS lr ON lr.run_id = l.run_id JOIN entries AS e ON e.id = l.entry_id AND e.workspace = ? WHERE lr.workspace = ? ORDER BY l.link_id ASC`, parameters: [workspace, workspace] },
|
|
541
|
+
purgeAudit: { sql: `SELECT p.${TABLE_FIELDS.purgeAudit.join(', p.')} FROM ledger_purge_audit AS p WHERE EXISTS (SELECT 1 FROM ledger_runs AS r WHERE r.run_id = p.run_id AND r.workspace = ?) OR EXISTS (SELECT 1 FROM ledger_events AS e JOIN ledger_runs AS r ON r.run_id = e.run_id WHERE e.event_id = p.event_id AND r.workspace = ?) OR EXISTS (SELECT 1 FROM context_deliveries AS d JOIN ledger_runs AS r ON r.run_id = d.run_id WHERE d.delivery_id = p.delivery_id AND r.workspace = ?) OR EXISTS (SELECT 1 FROM entries AS e WHERE e.id = p.entry_id AND e.workspace = ?) ORDER BY p.purge_id ASC`, parameters: [workspace, workspace, workspace, workspace] },
|
|
542
|
+
};
|
|
543
|
+
const selected = scoped[type];
|
|
544
|
+
const result = rows(database, selected.sql, ...selected.parameters);
|
|
545
|
+
return result.map((row) => normalizeRecord(type, row, workspace, false));
|
|
546
|
+
}
|
|
547
|
+
function countRecords(records) {
|
|
548
|
+
const counts = { ...EMPTY_COUNTS };
|
|
549
|
+
for (const type of Object.keys(counts))
|
|
550
|
+
counts[type] = records.get(type)?.length ?? 0;
|
|
551
|
+
return counts;
|
|
552
|
+
}
|
|
553
|
+
function checkBounds(content) {
|
|
554
|
+
if (typeof content !== 'string' || Buffer.byteLength(content, 'utf8') > MAX_ARCHIVE_TOTAL_BYTES || !content.endsWith('\n'))
|
|
555
|
+
validation();
|
|
556
|
+
const lines = content.split('\n');
|
|
557
|
+
if (lines.at(-1) !== '')
|
|
558
|
+
validation();
|
|
559
|
+
const serialized = lines.slice(0, -1);
|
|
560
|
+
if (serialized.length > MAX_ARCHIVE_LINE_COUNT || serialized.some((line) => line.length === 0 || Buffer.byteLength(line, 'utf8') > MAX_ARCHIVE_LINE_BYTES))
|
|
561
|
+
validation();
|
|
562
|
+
return serialized;
|
|
563
|
+
}
|
|
564
|
+
function buildArchive(workspace, records) {
|
|
565
|
+
const counts = countRecords(records);
|
|
566
|
+
const manifest = {
|
|
567
|
+
type: 'manifest',
|
|
568
|
+
apiVersion: LEDGER_ARCHIVE_API_VERSION,
|
|
569
|
+
archiveVersion: LEDGER_ARCHIVE_VERSION,
|
|
570
|
+
format: LEDGER_ARCHIVE_FORMAT,
|
|
571
|
+
workspace,
|
|
572
|
+
counts,
|
|
573
|
+
};
|
|
574
|
+
secretScan(manifest);
|
|
575
|
+
const ordered = Object.keys(counts).flatMap((type) => (records.get(type) ?? []).map((record) => ({ ...record, type: RECORD_NAMES[type] })));
|
|
576
|
+
const payloadLines = [manifest, ...ordered].map((record) => canonicalJson(record));
|
|
577
|
+
const payload = `${payloadLines.join('\n')}\n`;
|
|
578
|
+
const checksum = createHash('sha256').update(payload, 'utf8').digest('hex');
|
|
579
|
+
const content = `${canonicalJson({ type: 'checksum', sha256: checksum })}\n${payload}`;
|
|
580
|
+
checkBounds(content);
|
|
581
|
+
return { workspace, counts, checksum, content };
|
|
582
|
+
}
|
|
583
|
+
export function exportLedgerArchive(database, options) {
|
|
584
|
+
const workspace = requireWorkspace(options.workspace);
|
|
585
|
+
const records = new Map();
|
|
586
|
+
for (const type of Object.keys(EMPTY_COUNTS))
|
|
587
|
+
records.set(type, queryRows(database, type, workspace));
|
|
588
|
+
validateGraph(records, workspace);
|
|
589
|
+
return buildArchive(workspace, records);
|
|
590
|
+
}
|
|
591
|
+
function parseLine(line, fields) {
|
|
592
|
+
let parsed;
|
|
593
|
+
try {
|
|
594
|
+
parsed = JSON.parse(line);
|
|
595
|
+
}
|
|
596
|
+
catch {
|
|
597
|
+
integrity();
|
|
598
|
+
}
|
|
599
|
+
assertObject(parsed);
|
|
600
|
+
assertFields(parsed, fields);
|
|
601
|
+
try {
|
|
602
|
+
if (canonicalJson(parsed) !== line)
|
|
603
|
+
integrity();
|
|
604
|
+
}
|
|
605
|
+
catch {
|
|
606
|
+
integrity();
|
|
607
|
+
}
|
|
608
|
+
return parsed;
|
|
609
|
+
}
|
|
610
|
+
function parseCounts(value) {
|
|
611
|
+
assertObject(value);
|
|
612
|
+
assertFields(value, Object.keys(EMPTY_COUNTS));
|
|
613
|
+
const counts = { ...EMPTY_COUNTS };
|
|
614
|
+
for (const type of Object.keys(counts))
|
|
615
|
+
counts[type] = integerValue(value[type]);
|
|
616
|
+
return counts;
|
|
617
|
+
}
|
|
618
|
+
function parseArchive(content) {
|
|
619
|
+
const lines = checkBounds(content);
|
|
620
|
+
if (lines.length < 2)
|
|
621
|
+
integrity();
|
|
622
|
+
const checksum = parseLine(lines[0], RECORD_FIELDS.checksum);
|
|
623
|
+
if (checksum.type !== 'checksum' || typeof checksum.sha256 !== 'string' || !HASH_PATTERN.test(checksum.sha256))
|
|
624
|
+
integrity();
|
|
625
|
+
const payload = `${lines.slice(1).join('\n')}\n`;
|
|
626
|
+
const actual = createHash('sha256').update(payload, 'utf8').digest('hex');
|
|
627
|
+
if (actual !== checksum.sha256)
|
|
628
|
+
integrity();
|
|
629
|
+
const manifest = parseLine(lines[1], RECORD_FIELDS.manifest);
|
|
630
|
+
if (manifest.type !== 'manifest' || manifest.apiVersion !== LEDGER_ARCHIVE_API_VERSION || manifest.archiveVersion !== LEDGER_ARCHIVE_VERSION || manifest.format !== LEDGER_ARCHIVE_FORMAT)
|
|
631
|
+
validation();
|
|
632
|
+
secretScan(manifest);
|
|
633
|
+
const workspace = requireWorkspace(manifest.workspace);
|
|
634
|
+
const counts = parseCounts(manifest.counts);
|
|
635
|
+
const records = new Map();
|
|
636
|
+
for (const type of Object.keys(EMPTY_COUNTS))
|
|
637
|
+
records.set(type, []);
|
|
638
|
+
const seenManifest = [manifest];
|
|
639
|
+
if (seenManifest.length !== 1)
|
|
640
|
+
integrity();
|
|
641
|
+
for (const line of lines.slice(2)) {
|
|
642
|
+
let raw;
|
|
643
|
+
try {
|
|
644
|
+
raw = JSON.parse(line);
|
|
645
|
+
}
|
|
646
|
+
catch {
|
|
647
|
+
integrity();
|
|
648
|
+
}
|
|
649
|
+
assertObject(raw);
|
|
650
|
+
if (raw.type === 'manifest' || raw.type === 'checksum')
|
|
651
|
+
integrity();
|
|
652
|
+
const type = raw.type;
|
|
653
|
+
if (typeof type !== 'string')
|
|
654
|
+
validation();
|
|
655
|
+
const archiveType = RECORD_TYPES.get(type);
|
|
656
|
+
if (!archiveType)
|
|
657
|
+
validation();
|
|
658
|
+
const parsed = parseLine(line, RECORD_FIELDS[archiveType]);
|
|
659
|
+
const normalized = normalizeRecord(archiveType, parsed, workspace, true);
|
|
660
|
+
secretScan(normalized);
|
|
661
|
+
const bucket = records.get(archiveType);
|
|
662
|
+
if (!bucket)
|
|
663
|
+
integrity();
|
|
664
|
+
bucket.push(normalized);
|
|
665
|
+
}
|
|
666
|
+
for (const type of Object.keys(counts)) {
|
|
667
|
+
if (records.get(type)?.length !== counts[type])
|
|
668
|
+
integrity();
|
|
669
|
+
}
|
|
670
|
+
return { workspace, counts, records };
|
|
671
|
+
}
|
|
672
|
+
function identity(type, record) {
|
|
673
|
+
switch (type) {
|
|
674
|
+
case 'runs': return String(record.run_id);
|
|
675
|
+
case 'sessions': return String(record.id);
|
|
676
|
+
case 'answers': return `${record.session_id}\u0000${record.question_id}`;
|
|
677
|
+
case 'runIntakes': return String(record.run_id);
|
|
678
|
+
case 'intakeFeedback': return String(record.feedback_id);
|
|
679
|
+
case 'events': return String(record.event_id);
|
|
680
|
+
case 'evidence': return String(record.evidence_id);
|
|
681
|
+
case 'deliveries': return String(record.delivery_id);
|
|
682
|
+
case 'deliveryEntries': return `${record.delivery_id}\u0000${record.entry_id}`;
|
|
683
|
+
case 'contextFeedback': return String(record.feedback_id);
|
|
684
|
+
case 'runFeedback': return String(record.feedback_id);
|
|
685
|
+
case 'memoryLinks': return String(record.link_id);
|
|
686
|
+
case 'purgeAudit': return String(record.purge_id);
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
function ensureUniqueRecords(records) {
|
|
690
|
+
const globalEvents = new Set();
|
|
691
|
+
for (const type of Object.keys(EMPTY_COUNTS)) {
|
|
692
|
+
const seen = new Set();
|
|
693
|
+
for (const record of records.get(type) ?? []) {
|
|
694
|
+
const key = identity(type, record);
|
|
695
|
+
if (seen.has(key))
|
|
696
|
+
integrity();
|
|
697
|
+
seen.add(key);
|
|
698
|
+
if (type === 'events') {
|
|
699
|
+
if (globalEvents.has(String(record.event_id)))
|
|
700
|
+
integrity();
|
|
701
|
+
globalEvents.add(String(record.event_id));
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
function byId(records, type, key) {
|
|
707
|
+
return (records.get(type) ?? []).find((record) => identity(type, record) === key);
|
|
708
|
+
}
|
|
709
|
+
function validateGraph(records, workspace) {
|
|
710
|
+
ensureUniqueRecords(records);
|
|
711
|
+
const runs = records.get('runs') ?? [];
|
|
712
|
+
const sessions = records.get('sessions') ?? [];
|
|
713
|
+
const intakes = records.get('runIntakes') ?? [];
|
|
714
|
+
const runIds = new Set(runs.map((record) => String(record.run_id)));
|
|
715
|
+
const sessionIds = new Set(sessions.map((record) => String(record.id)));
|
|
716
|
+
const intakeByRun = new Map();
|
|
717
|
+
const intakeBySession = new Map();
|
|
718
|
+
for (const intake of intakes) {
|
|
719
|
+
const runId = String(intake.run_id);
|
|
720
|
+
const sessionId = String(intake.session_id);
|
|
721
|
+
if (!runIds.has(runId) || !sessionIds.has(sessionId) || intakeByRun.has(runId) || intakeBySession.has(sessionId))
|
|
722
|
+
integrity();
|
|
723
|
+
intakeByRun.set(runId, intake);
|
|
724
|
+
intakeBySession.set(sessionId, intake);
|
|
725
|
+
}
|
|
726
|
+
for (const session of sessions) {
|
|
727
|
+
if (String(session.workspace) !== workspace)
|
|
728
|
+
integrity();
|
|
729
|
+
}
|
|
730
|
+
const eventsByRun = new Map();
|
|
731
|
+
for (const event of records.get('events') ?? []) {
|
|
732
|
+
const runId = String(event.run_id);
|
|
733
|
+
if (!runIds.has(runId))
|
|
734
|
+
integrity();
|
|
735
|
+
const list = eventsByRun.get(runId) ?? [];
|
|
736
|
+
list.push(event);
|
|
737
|
+
eventsByRun.set(runId, list);
|
|
738
|
+
}
|
|
739
|
+
for (const run of runs) {
|
|
740
|
+
const runId = String(run.run_id);
|
|
741
|
+
const parent = run.parent_run_id === null ? null : String(run.parent_run_id);
|
|
742
|
+
if (parent !== null && !runIds.has(parent))
|
|
743
|
+
integrity();
|
|
744
|
+
const eventRows = [...(eventsByRun.get(runId) ?? [])].sort((left, right) => Number(left.sequence) - Number(right.sequence));
|
|
745
|
+
if (eventRows.length !== Number(run.last_sequence))
|
|
746
|
+
integrity();
|
|
747
|
+
let previous = GENESIS_HASH;
|
|
748
|
+
let maxSource = null;
|
|
749
|
+
for (let index = 0; index < eventRows.length; index += 1) {
|
|
750
|
+
const event = eventRows[index];
|
|
751
|
+
if (Number(event.sequence) !== index + 1 || event.previous_hash !== previous)
|
|
752
|
+
integrity();
|
|
753
|
+
const calculated = hashLedgerEvent({
|
|
754
|
+
runId,
|
|
755
|
+
sequence: Number(event.sequence),
|
|
756
|
+
eventId: String(event.event_id),
|
|
757
|
+
previousHash: String(event.previous_hash),
|
|
758
|
+
eventType: String(event.event_type),
|
|
759
|
+
...(event.source_event_id === null ? {} : { sourceEventId: String(event.source_event_id) }),
|
|
760
|
+
...(event.source_sequence === null ? {} : { sourceSequence: Number(event.source_sequence) }),
|
|
761
|
+
...(event.source_type === null ? {} : { sourceType: String(event.source_type) }),
|
|
762
|
+
actor: String(event.actor),
|
|
763
|
+
...(event.outcome === null ? {} : { outcome: String(event.outcome) }),
|
|
764
|
+
...(event.occurred_at === null ? {} : { occurredAt: String(event.occurred_at) }),
|
|
765
|
+
ingestedAt: String(event.ingested_at),
|
|
766
|
+
payload: JSON.parse(String(event.payload_json)),
|
|
767
|
+
redaction: JSON.parse(String(event.redaction_json)),
|
|
768
|
+
});
|
|
769
|
+
if (calculated !== event.event_hash)
|
|
770
|
+
integrity();
|
|
771
|
+
previous = String(event.event_hash);
|
|
772
|
+
if (event.source_sequence !== null)
|
|
773
|
+
maxSource = maxSource === null ? Number(event.source_sequence) : Math.max(maxSource, Number(event.source_sequence));
|
|
774
|
+
}
|
|
775
|
+
if ((run.last_source_sequence === null ? null : Number(run.last_source_sequence)) !== maxSource)
|
|
776
|
+
integrity();
|
|
777
|
+
if (run.status === 'active') {
|
|
778
|
+
const intake = intakeByRun.get(runId);
|
|
779
|
+
const session = intake ? byId(records, 'sessions', String(intake.session_id)) : undefined;
|
|
780
|
+
if (!intake || !session || !['ready', 'exhausted'].includes(String(session.status)))
|
|
781
|
+
integrity();
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
for (const intake of intakes) {
|
|
785
|
+
if (intake.finalized_at !== null) {
|
|
786
|
+
const session = byId(records, 'sessions', String(intake.session_id));
|
|
787
|
+
if (!session || !['ready', 'exhausted'].includes(String(session.status)))
|
|
788
|
+
integrity();
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
for (const answer of records.get('answers') ?? []) {
|
|
792
|
+
if (!sessionIds.has(String(answer.session_id)))
|
|
793
|
+
integrity();
|
|
794
|
+
}
|
|
795
|
+
for (const feedback of records.get('intakeFeedback') ?? []) {
|
|
796
|
+
const intake = intakeByRun.get(String(feedback.run_id));
|
|
797
|
+
if (!intake || intake.session_id !== feedback.session_id)
|
|
798
|
+
integrity();
|
|
799
|
+
}
|
|
800
|
+
const eventIds = new Set((records.get('events') ?? []).map((event) => String(event.event_id)));
|
|
801
|
+
for (const evidence of records.get('evidence') ?? []) {
|
|
802
|
+
if (!runIds.has(String(evidence.run_id)) || (evidence.event_id !== null && !eventIds.has(String(evidence.event_id))))
|
|
803
|
+
integrity();
|
|
804
|
+
}
|
|
805
|
+
const deliveryById = new Map((records.get('deliveries') ?? []).map((delivery) => [String(delivery.delivery_id), delivery]));
|
|
806
|
+
for (const delivery of records.get('deliveries') ?? []) {
|
|
807
|
+
const run = byId(records, 'runs', String(delivery.run_id));
|
|
808
|
+
if (!run || Number(delivery.through_sequence) > Number(run.last_sequence))
|
|
809
|
+
integrity();
|
|
810
|
+
if (delivery.intake_session_id !== null) {
|
|
811
|
+
const intake = intakeByRun.get(String(delivery.run_id));
|
|
812
|
+
if (!intake || intake.session_id !== delivery.intake_session_id)
|
|
813
|
+
integrity();
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
const deliveryEntries = new Set();
|
|
817
|
+
for (const entry of records.get('deliveryEntries') ?? []) {
|
|
818
|
+
const key = `${entry.delivery_id}\u0000${entry.entry_id}`;
|
|
819
|
+
if (deliveryEntries.has(key) || !deliveryById.has(String(entry.delivery_id)))
|
|
820
|
+
integrity();
|
|
821
|
+
deliveryEntries.add(key);
|
|
822
|
+
}
|
|
823
|
+
for (const feedback of records.get('contextFeedback') ?? []) {
|
|
824
|
+
const delivery = deliveryById.get(String(feedback.delivery_id));
|
|
825
|
+
if (!delivery || delivery.run_id !== feedback.run_id || !deliveryEntries.has(`${feedback.delivery_id}\u0000${feedback.entry_id}`))
|
|
826
|
+
integrity();
|
|
827
|
+
}
|
|
828
|
+
for (const feedback of records.get('runFeedback') ?? []) {
|
|
829
|
+
if (!runIds.has(String(feedback.run_id)))
|
|
830
|
+
integrity();
|
|
831
|
+
}
|
|
832
|
+
for (const link of records.get('memoryLinks') ?? []) {
|
|
833
|
+
if (!runIds.has(String(link.run_id)))
|
|
834
|
+
integrity();
|
|
835
|
+
if (link.event_id === null && link.delivery_id === null)
|
|
836
|
+
integrity();
|
|
837
|
+
if (link.event_id !== null) {
|
|
838
|
+
const event = (records.get('events') ?? []).find((candidate) => candidate.event_id === link.event_id);
|
|
839
|
+
if (!event || event.run_id !== link.run_id)
|
|
840
|
+
integrity();
|
|
841
|
+
}
|
|
842
|
+
if (link.delivery_id !== null) {
|
|
843
|
+
const delivery = deliveryById.get(String(link.delivery_id));
|
|
844
|
+
if (!delivery || delivery.run_id !== link.run_id)
|
|
845
|
+
integrity();
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
for (const purge of records.get('purgeAudit') ?? []) {
|
|
849
|
+
if (purge.run_id !== null && !runIds.has(String(purge.run_id)))
|
|
850
|
+
integrity();
|
|
851
|
+
if (purge.event_id !== null && !eventIds.has(String(purge.event_id)))
|
|
852
|
+
integrity();
|
|
853
|
+
if (purge.delivery_id !== null && !deliveryById.has(String(purge.delivery_id)))
|
|
854
|
+
integrity();
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
function selectExisting(database, type, record) {
|
|
858
|
+
const fields = TABLE_FIELDS[type].join(', ');
|
|
859
|
+
const table = TABLE_NAMES[type];
|
|
860
|
+
let sql = `SELECT ${fields} FROM ${table} WHERE `;
|
|
861
|
+
const parameters = [];
|
|
862
|
+
switch (type) {
|
|
863
|
+
case 'runs':
|
|
864
|
+
sql += 'run_id = ?';
|
|
865
|
+
parameters.push(String(record.run_id));
|
|
866
|
+
break;
|
|
867
|
+
case 'sessions':
|
|
868
|
+
sql += 'id = ?';
|
|
869
|
+
parameters.push(String(record.id));
|
|
870
|
+
break;
|
|
871
|
+
case 'answers':
|
|
872
|
+
sql += 'session_id = ? AND question_id = ?';
|
|
873
|
+
parameters.push(String(record.session_id), String(record.question_id));
|
|
874
|
+
break;
|
|
875
|
+
case 'runIntakes':
|
|
876
|
+
sql += 'run_id = ?';
|
|
877
|
+
parameters.push(String(record.run_id));
|
|
878
|
+
break;
|
|
879
|
+
case 'intakeFeedback':
|
|
880
|
+
sql += 'feedback_id = ?';
|
|
881
|
+
parameters.push(String(record.feedback_id));
|
|
882
|
+
break;
|
|
883
|
+
case 'events':
|
|
884
|
+
sql += 'event_id = ?';
|
|
885
|
+
parameters.push(String(record.event_id));
|
|
886
|
+
break;
|
|
887
|
+
case 'evidence':
|
|
888
|
+
sql += 'evidence_id = ?';
|
|
889
|
+
parameters.push(String(record.evidence_id));
|
|
890
|
+
break;
|
|
891
|
+
case 'deliveries':
|
|
892
|
+
sql += 'delivery_id = ?';
|
|
893
|
+
parameters.push(String(record.delivery_id));
|
|
894
|
+
break;
|
|
895
|
+
case 'deliveryEntries':
|
|
896
|
+
sql += 'delivery_id = ? AND entry_id = ?';
|
|
897
|
+
parameters.push(String(record.delivery_id), String(record.entry_id));
|
|
898
|
+
break;
|
|
899
|
+
case 'contextFeedback':
|
|
900
|
+
sql += 'feedback_id = ?';
|
|
901
|
+
parameters.push(String(record.feedback_id));
|
|
902
|
+
break;
|
|
903
|
+
case 'runFeedback':
|
|
904
|
+
sql += 'feedback_id = ?';
|
|
905
|
+
parameters.push(String(record.feedback_id));
|
|
906
|
+
break;
|
|
907
|
+
case 'memoryLinks':
|
|
908
|
+
sql += 'link_id = ?';
|
|
909
|
+
parameters.push(String(record.link_id));
|
|
910
|
+
break;
|
|
911
|
+
case 'purgeAudit':
|
|
912
|
+
sql += 'purge_id = ?';
|
|
913
|
+
parameters.push(String(record.purge_id));
|
|
914
|
+
break;
|
|
915
|
+
}
|
|
916
|
+
return rows(database, sql, ...parameters)[0];
|
|
917
|
+
}
|
|
918
|
+
function hasUniqueConflict(database, type, record) {
|
|
919
|
+
let sql;
|
|
920
|
+
const parameters = [];
|
|
921
|
+
switch (type) {
|
|
922
|
+
case 'runIntakes':
|
|
923
|
+
sql = 'SELECT 1 AS present FROM run_intakes WHERE session_id = ?';
|
|
924
|
+
parameters.push(String(record.session_id));
|
|
925
|
+
break;
|
|
926
|
+
case 'events':
|
|
927
|
+
if (record.source_event_id !== null) {
|
|
928
|
+
sql = 'SELECT 1 AS present FROM ledger_events WHERE run_id = ? AND source_event_id = ?';
|
|
929
|
+
parameters.push(String(record.run_id), String(record.source_event_id));
|
|
930
|
+
}
|
|
931
|
+
if (!sql) {
|
|
932
|
+
sql = 'SELECT 1 AS present FROM ledger_events WHERE run_id = ? AND sequence = ?';
|
|
933
|
+
parameters.push(String(record.run_id), Number(record.sequence));
|
|
934
|
+
}
|
|
935
|
+
break;
|
|
936
|
+
case 'deliveryEntries':
|
|
937
|
+
sql = 'SELECT 1 AS present FROM context_delivery_entries WHERE delivery_id = ? AND entry_id = ?';
|
|
938
|
+
parameters.push(String(record.delivery_id), String(record.entry_id));
|
|
939
|
+
break;
|
|
940
|
+
case 'intakeFeedback':
|
|
941
|
+
sql = 'SELECT 1 AS present FROM intake_feedback WHERE run_id = ? AND actor = ? AND idempotency_key = ?';
|
|
942
|
+
parameters.push(String(record.run_id), String(record.actor), String(record.idempotency_key));
|
|
943
|
+
break;
|
|
944
|
+
case 'contextFeedback':
|
|
945
|
+
sql = 'SELECT 1 AS present FROM context_feedback WHERE run_id = ? AND actor = ? AND idempotency_key = ?';
|
|
946
|
+
parameters.push(String(record.run_id), String(record.actor), String(record.idempotency_key));
|
|
947
|
+
break;
|
|
948
|
+
case 'runFeedback':
|
|
949
|
+
sql = 'SELECT 1 AS present FROM run_feedback WHERE run_id = ? AND actor = ? AND idempotency_key = ?';
|
|
950
|
+
parameters.push(String(record.run_id), String(record.actor), String(record.idempotency_key));
|
|
951
|
+
break;
|
|
952
|
+
default: return false;
|
|
953
|
+
}
|
|
954
|
+
return rows(database, sql, ...parameters).length > 0;
|
|
955
|
+
}
|
|
956
|
+
function ensureMemoryReference(database, record, workspace) {
|
|
957
|
+
if (record.type !== 'deliveryEntries' && record.type !== 'contextFeedback' && record.type !== 'memoryLinks')
|
|
958
|
+
return;
|
|
959
|
+
const entryId = String(record.entry_id);
|
|
960
|
+
const entry = rows(database, 'SELECT id, workspace, revision FROM entries WHERE id = ?', entryId)[0];
|
|
961
|
+
if (!entry)
|
|
962
|
+
notFound();
|
|
963
|
+
if (entry.workspace !== workspace)
|
|
964
|
+
conflict();
|
|
965
|
+
if (record.type === 'deliveryEntries' && Number(entry.revision) !== Number(record.entry_revision))
|
|
966
|
+
conflict();
|
|
967
|
+
}
|
|
968
|
+
function compareExisting(database, type, record, workspace) {
|
|
969
|
+
const existing = selectExisting(database, type, record);
|
|
970
|
+
if (existing) {
|
|
971
|
+
const normalized = normalizeRecord(type, existing, String(record.workspace ?? ''), true);
|
|
972
|
+
if (canonicalJson(normalized) !== canonicalJson(record))
|
|
973
|
+
conflict();
|
|
974
|
+
return 'duplicate';
|
|
975
|
+
}
|
|
976
|
+
if (hasUniqueConflict(database, type, record))
|
|
977
|
+
conflict();
|
|
978
|
+
ensureMemoryReference(database, record, workspace);
|
|
979
|
+
return 'new';
|
|
980
|
+
}
|
|
981
|
+
function insertRecord(database, type, record) {
|
|
982
|
+
const fields = TABLE_FIELDS[type];
|
|
983
|
+
const values = fields.map((field) => record[field]);
|
|
984
|
+
const placeholders = fields.map(() => '?').join(', ');
|
|
985
|
+
database.prepare(`INSERT INTO ${TABLE_NAMES[type]} (${fields.join(', ')}) VALUES (${placeholders})`).run(...values);
|
|
986
|
+
}
|
|
987
|
+
function orderedForImport(records) {
|
|
988
|
+
const output = [];
|
|
989
|
+
const runRecords = records.get('runs') ?? [];
|
|
990
|
+
const emittedRuns = new Set();
|
|
991
|
+
const emitRun = (run) => {
|
|
992
|
+
const runId = String(run.run_id);
|
|
993
|
+
if (emittedRuns.has(runId))
|
|
994
|
+
return;
|
|
995
|
+
const parentId = run.parent_run_id === null ? null : String(run.parent_run_id);
|
|
996
|
+
if (parentId !== null) {
|
|
997
|
+
const parent = runRecords.find((candidate) => String(candidate.run_id) === parentId);
|
|
998
|
+
if (parent)
|
|
999
|
+
emitRun(parent);
|
|
1000
|
+
}
|
|
1001
|
+
emittedRuns.add(runId);
|
|
1002
|
+
output.push(run);
|
|
1003
|
+
};
|
|
1004
|
+
for (const run of runRecords)
|
|
1005
|
+
emitRun(run);
|
|
1006
|
+
const orderedTypes = [
|
|
1007
|
+
'sessions', 'answers', 'runIntakes', 'intakeFeedback', 'events', 'evidence', 'deliveries',
|
|
1008
|
+
'deliveryEntries', 'contextFeedback', 'runFeedback', 'memoryLinks', 'purgeAudit',
|
|
1009
|
+
];
|
|
1010
|
+
for (const type of orderedTypes)
|
|
1011
|
+
output.push(...(records.get(type) ?? []));
|
|
1012
|
+
return output;
|
|
1013
|
+
}
|
|
1014
|
+
function emptyCounts() {
|
|
1015
|
+
return { ...EMPTY_COUNTS };
|
|
1016
|
+
}
|
|
1017
|
+
export function importLedgerArchive(database, options) {
|
|
1018
|
+
const parsed = parseArchive(options.content);
|
|
1019
|
+
if (options.workspace !== undefined && requireWorkspace(options.workspace) !== parsed.workspace)
|
|
1020
|
+
validation();
|
|
1021
|
+
validateGraph(parsed.records, parsed.workspace);
|
|
1022
|
+
if (database === undefined && !options.dryRun)
|
|
1023
|
+
databaseFailure();
|
|
1024
|
+
const imported = emptyCounts();
|
|
1025
|
+
const duplicates = emptyCounts();
|
|
1026
|
+
let conflicts = 0;
|
|
1027
|
+
const ordered = orderedForImport(parsed.records);
|
|
1028
|
+
if (database === undefined) {
|
|
1029
|
+
for (const record of ordered)
|
|
1030
|
+
imported[record.type] += 1;
|
|
1031
|
+
return { workspace: parsed.workspace, dryRun: true, counts: parsed.counts, imported, duplicates, conflicts };
|
|
1032
|
+
}
|
|
1033
|
+
const db = database;
|
|
1034
|
+
try {
|
|
1035
|
+
for (const record of ordered) {
|
|
1036
|
+
const type = record.type;
|
|
1037
|
+
try {
|
|
1038
|
+
const result = compareExisting(db, type, record, parsed.workspace);
|
|
1039
|
+
if (result === 'duplicate')
|
|
1040
|
+
duplicates[type] += 1;
|
|
1041
|
+
else
|
|
1042
|
+
imported[type] += 1;
|
|
1043
|
+
}
|
|
1044
|
+
catch (error) {
|
|
1045
|
+
if (options.dryRun && error instanceof KiokukoError && error.code === 'CONFLICT') {
|
|
1046
|
+
conflicts += 1;
|
|
1047
|
+
continue;
|
|
1048
|
+
}
|
|
1049
|
+
throw error;
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
if (options.dryRun)
|
|
1053
|
+
return { workspace: parsed.workspace, dryRun: true, counts: parsed.counts, imported, duplicates, conflicts };
|
|
1054
|
+
withImmediateTransaction(db, () => {
|
|
1055
|
+
for (const record of ordered) {
|
|
1056
|
+
const type = record.type;
|
|
1057
|
+
if (duplicates[type] > 0 && selectExisting(db, type, record))
|
|
1058
|
+
continue;
|
|
1059
|
+
insertRecord(db, type, record);
|
|
1060
|
+
}
|
|
1061
|
+
});
|
|
1062
|
+
}
|
|
1063
|
+
catch (error) {
|
|
1064
|
+
if (error instanceof KiokukoError)
|
|
1065
|
+
throw error;
|
|
1066
|
+
if (error instanceof Error && /unique|constraint/i.test(error.message))
|
|
1067
|
+
conflict();
|
|
1068
|
+
databaseFailure();
|
|
1069
|
+
}
|
|
1070
|
+
return { workspace: parsed.workspace, dryRun: false, counts: parsed.counts, imported, duplicates, conflicts };
|
|
1071
|
+
}
|
|
1072
|
+
//# sourceMappingURL=archive.js.map
|