@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,683 @@
|
|
|
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 { sanitizeJson } from '../security/sanitize.js';
|
|
6
|
+
import { canonicalJson, GENESIS_HASH, hashLedgerEvent } from './hash.js';
|
|
7
|
+
import { CAPTURE_PROFILES, COVERAGE_LEVELS, LEDGER_EVENT_TYPES, RUN_STATUSES, } from './types.js';
|
|
8
|
+
export const LEDGER_CHECK_NAMES = [
|
|
9
|
+
'runs', 'eventIdentity', 'eventHashChain', 'runCursors', 'runIntakes',
|
|
10
|
+
'references', 'contextDeliveries', 'feedbackLinks', 'storedValues', 'secretResidue',
|
|
11
|
+
];
|
|
12
|
+
const MAX_FINDINGS = 100;
|
|
13
|
+
const MAX_FINDINGS_PER_CHECK = 25;
|
|
14
|
+
const HASH = /^[0-9a-f]{64}$/u;
|
|
15
|
+
const TIMESTAMP = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/u;
|
|
16
|
+
const REDACTION_KINDS = new Set(['sensitive_key', 'secret_pattern', 'url', 'home_path', 'preview_truncated', 'environment_value', 'hidden_reasoning']);
|
|
17
|
+
const EVIDENCE_KINDS = new Set(['command', 'test', 'file', 'diff', 'url', 'artifact']);
|
|
18
|
+
const CONTEXT_FEEDBACK_VERDICTS = new Set(['helpful', 'irrelevant', 'stale', 'conflicting']);
|
|
19
|
+
const INTAKE_FEEDBACK_VERDICTS = new Set(['helpful', 'unnecessary', 'corrected']);
|
|
20
|
+
const RECOMMENDATION_VERDICTS = new Set(['accepted', 'dismissed', 'resolved']);
|
|
21
|
+
const SECRET_KEY = /(?:password|passwd|secret|token|api[-_]?key|authorization|cookie|credential|private[-_]?key|client[-_]?secret|refresh[-_]?token|access[-_]?token)/iu;
|
|
22
|
+
function errorValidation(message) { throw new KiokukoError('VALIDATION_ERROR', message); }
|
|
23
|
+
function errorDatabase() { throw new KiokukoError('DATABASE_ERROR', 'Ledger database operation failed'); }
|
|
24
|
+
function str(value) { return typeof value === 'string' ? value : undefined; }
|
|
25
|
+
function nullableStr(value) { return value === null ? null : str(value); }
|
|
26
|
+
function integer(value) { return typeof value === 'number' && Number.isSafeInteger(value) ? value : undefined; }
|
|
27
|
+
function isObject(value) {
|
|
28
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
29
|
+
return false;
|
|
30
|
+
const prototype = Object.getPrototypeOf(value);
|
|
31
|
+
return prototype === Object.prototype || prototype === null;
|
|
32
|
+
}
|
|
33
|
+
function validTimestamp(value) {
|
|
34
|
+
if (typeof value !== 'string' || !TIMESTAMP.test(value))
|
|
35
|
+
return false;
|
|
36
|
+
const time = Date.parse(value);
|
|
37
|
+
return Number.isFinite(time) && new Date(time).toISOString() === value;
|
|
38
|
+
}
|
|
39
|
+
function idHash(category, value) {
|
|
40
|
+
const text = str(value);
|
|
41
|
+
return text === undefined || text.length === 0 ? undefined : createHash('sha256').update(`${category}:${text}`, 'utf8').digest('hex');
|
|
42
|
+
}
|
|
43
|
+
function rowCount(database, sql, ...parameters) {
|
|
44
|
+
const value = database.prepare(sql).get(...parameters)?.count;
|
|
45
|
+
const result = typeof value === 'number' ? value : Number(value ?? 0);
|
|
46
|
+
return Number.isFinite(result) ? result : 0;
|
|
47
|
+
}
|
|
48
|
+
function emptyChecks() {
|
|
49
|
+
return Object.fromEntries(LEDGER_CHECK_NAMES.map((name) => [name, { ok: true, count: 0, findingCount: 0, findings: [], truncated: false }]));
|
|
50
|
+
}
|
|
51
|
+
class FindingCollector {
|
|
52
|
+
checks;
|
|
53
|
+
findings = [];
|
|
54
|
+
findingCount = 0;
|
|
55
|
+
findingsTruncated = false;
|
|
56
|
+
constructor(checks) {
|
|
57
|
+
this.checks = checks;
|
|
58
|
+
}
|
|
59
|
+
add(checkName, kind, category, value) {
|
|
60
|
+
const check = this.checks[checkName];
|
|
61
|
+
check.ok = false;
|
|
62
|
+
check.findingCount += 1;
|
|
63
|
+
this.findingCount += 1;
|
|
64
|
+
const hash = idHash(category, value);
|
|
65
|
+
const finding = { check: checkName, kind, category, ...(hash === undefined ? {} : { idHash: hash }) };
|
|
66
|
+
if (check.findings.length < MAX_FINDINGS_PER_CHECK && this.findings.length < MAX_FINDINGS) {
|
|
67
|
+
check.findings.push(finding);
|
|
68
|
+
this.findings.push(finding);
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
check.truncated = true;
|
|
72
|
+
this.findingsTruncated = true;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function scanValue(value) {
|
|
77
|
+
if (typeof value === 'string')
|
|
78
|
+
return findSecret(value) !== undefined;
|
|
79
|
+
if (Array.isArray(value))
|
|
80
|
+
return value.some(scanValue);
|
|
81
|
+
if (isObject(value))
|
|
82
|
+
return Object.entries(value).some(([key, child]) => SECRET_KEY.test(key) || scanValue(child));
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
function scanRow(row, category, value, findings) {
|
|
86
|
+
if (Object.entries(row).some(([key, item]) => SECRET_KEY.test(key) || scanValue(item)))
|
|
87
|
+
findings.add('secretResidue', 'secret_residue', category, value);
|
|
88
|
+
}
|
|
89
|
+
function parseJson(raw, checks, category, value, findings, shape) {
|
|
90
|
+
if (typeof raw !== 'string') {
|
|
91
|
+
for (const check of checks)
|
|
92
|
+
findings.add(check, 'malformed_json', category, value);
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
let parsed;
|
|
96
|
+
try {
|
|
97
|
+
parsed = JSON.parse(raw);
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
for (const check of checks)
|
|
101
|
+
findings.add(check, 'malformed_json', category, value);
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
104
|
+
try {
|
|
105
|
+
if (canonicalJson(parsed) !== raw)
|
|
106
|
+
for (const check of checks)
|
|
107
|
+
findings.add(check, 'noncanonical_json', category, value);
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
for (const check of checks)
|
|
111
|
+
findings.add(check, 'invalid_json_value', category, value);
|
|
112
|
+
return undefined;
|
|
113
|
+
}
|
|
114
|
+
if ((shape === 'object' && !isObject(parsed)) || (shape === 'array' && !Array.isArray(parsed))) {
|
|
115
|
+
for (const check of checks)
|
|
116
|
+
findings.add(check, 'invalid_json_shape', category, value);
|
|
117
|
+
return undefined;
|
|
118
|
+
}
|
|
119
|
+
return parsed;
|
|
120
|
+
}
|
|
121
|
+
function validCoverage(value) {
|
|
122
|
+
if (!isObject(value))
|
|
123
|
+
return false;
|
|
124
|
+
const keys = ['run', 'tool', 'command', 'file', 'approval'];
|
|
125
|
+
return Object.keys(value).length === keys.length && keys.every((key) => typeof value[key] === 'string' && COVERAGE_LEVELS.includes(value[key]));
|
|
126
|
+
}
|
|
127
|
+
function validRedactions(value) {
|
|
128
|
+
return Array.isArray(value) && value.every((item) => isObject(item) && typeof item.path === 'string' && typeof item.kind === 'string' && REDACTION_KINDS.has(item.kind));
|
|
129
|
+
}
|
|
130
|
+
function selectedRows(database, table, workspace) {
|
|
131
|
+
if (workspace === undefined)
|
|
132
|
+
return database.prepare(`SELECT * FROM ${table} ORDER BY run_id ASC`).all();
|
|
133
|
+
return database.prepare(`SELECT ${table}.* FROM ${table} JOIN ledger_runs AS r ON r.run_id = ${table}.run_id WHERE r.workspace = ? ORDER BY ${table}.run_id ASC`).all(workspace);
|
|
134
|
+
}
|
|
135
|
+
function selectedEvents(database, workspace) {
|
|
136
|
+
if (workspace === undefined)
|
|
137
|
+
return database.prepare('SELECT * FROM ledger_events ORDER BY run_id ASC, sequence ASC, event_id ASC').all();
|
|
138
|
+
return database.prepare(`SELECT e.* FROM ledger_events AS e JOIN ledger_runs AS r ON r.run_id = e.run_id WHERE r.workspace = ? ORDER BY e.run_id ASC, e.sequence ASC, e.event_id ASC`).all(workspace);
|
|
139
|
+
}
|
|
140
|
+
function counts(database, workspace) {
|
|
141
|
+
const child = (table) => workspace === undefined ? rowCount(database, `SELECT COUNT(*) AS count FROM ${table}`) : rowCount(database, `SELECT COUNT(*) AS count FROM ${table} AS c JOIN ledger_runs AS r ON r.run_id = c.run_id WHERE r.workspace = ?`, workspace);
|
|
142
|
+
const entries = workspace === undefined ? rowCount(database, 'SELECT COUNT(*) AS count FROM context_delivery_entries') : rowCount(database, 'SELECT COUNT(*) AS count FROM context_delivery_entries AS e JOIN context_deliveries AS d ON d.delivery_id = e.delivery_id JOIN ledger_runs AS r ON r.run_id = d.run_id WHERE r.workspace = ?', workspace);
|
|
143
|
+
const tombstones = workspace === undefined ? rowCount(database, 'SELECT COUNT(*) AS count FROM ledger_purge_audit') : rowCount(database, 'SELECT COUNT(*) AS count FROM ledger_purge_audit AS p JOIN ledger_runs AS r ON r.run_id = p.run_id WHERE r.workspace = ?', workspace);
|
|
144
|
+
return {
|
|
145
|
+
runs: workspace === undefined ? rowCount(database, 'SELECT COUNT(*) AS count FROM ledger_runs') : rowCount(database, 'SELECT COUNT(*) AS count FROM ledger_runs WHERE workspace = ?', workspace),
|
|
146
|
+
events: child('ledger_events'), evidence: child('ledger_evidence'), deliveries: child('context_deliveries'), deliveryEntries: entries,
|
|
147
|
+
intakeFeedback: child('intake_feedback'), contextFeedback: child('context_feedback'), runFeedback: child('run_feedback'), memoryLinks: child('ledger_memory_links'), tombstones,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
function inspectRuns(rows, findings) {
|
|
151
|
+
const runs = new Map();
|
|
152
|
+
for (const row of rows) {
|
|
153
|
+
const runId = str(row.run_id) ?? '';
|
|
154
|
+
runs.set(runId, row);
|
|
155
|
+
const status = str(row.status);
|
|
156
|
+
if (!status || !RUN_STATUSES.includes(status)) {
|
|
157
|
+
findings.add('runs', 'invalid_status', 'ledger_runs', row.run_id);
|
|
158
|
+
findings.add('storedValues', 'invalid_enum', 'ledger_runs', row.run_id);
|
|
159
|
+
}
|
|
160
|
+
if (row.capture_profile === undefined || !CAPTURE_PROFILES.includes(row.capture_profile))
|
|
161
|
+
findings.add('runs', 'invalid_enum', 'ledger_runs', row.run_id);
|
|
162
|
+
if (row.protocol_version !== '1')
|
|
163
|
+
findings.add('runs', 'invalid_enum', 'ledger_runs', row.run_id);
|
|
164
|
+
for (const field of ['started_at', 'created_at', 'updated_at'])
|
|
165
|
+
if (!validTimestamp(row[field])) {
|
|
166
|
+
findings.add('runs', 'invalid_timestamp', 'ledger_runs', row.run_id);
|
|
167
|
+
findings.add('storedValues', 'invalid_timestamp', 'ledger_runs', row.run_id);
|
|
168
|
+
}
|
|
169
|
+
if (row.ended_at !== null && !validTimestamp(row.ended_at))
|
|
170
|
+
findings.add('runs', 'invalid_timestamp', 'ledger_runs', row.run_id);
|
|
171
|
+
const terminal = status === 'completed' || status === 'failed' || status === 'cancelled' || status === 'interrupted';
|
|
172
|
+
if (terminal !== (row.ended_at !== null))
|
|
173
|
+
findings.add('runs', 'terminal_ended_at_mismatch', 'ledger_runs', row.run_id);
|
|
174
|
+
if (row.last_sequence === undefined || integer(row.last_sequence) === undefined || row.last_sequence < 0)
|
|
175
|
+
findings.add('runCursors', 'invalid_last_sequence', 'ledger_runs', row.run_id);
|
|
176
|
+
if (row.last_source_sequence !== null && (integer(row.last_source_sequence) === undefined || row.last_source_sequence < 0))
|
|
177
|
+
findings.add('runCursors', 'invalid_last_source_sequence', 'ledger_runs', row.run_id);
|
|
178
|
+
if (row.task_hash !== null && (typeof row.task_hash !== 'string' || !HASH.test(row.task_hash)))
|
|
179
|
+
findings.add('storedValues', 'invalid_hash_shape', 'ledger_runs', row.run_id);
|
|
180
|
+
const metadata = parseJson(row.metadata_json, ['runs', 'storedValues'], 'ledger_runs.metadata_json', row.run_id, findings, 'object');
|
|
181
|
+
const coverage = parseJson(row.coverage_json, ['runs', 'storedValues'], 'ledger_runs.coverage_json', row.run_id, findings, 'object');
|
|
182
|
+
if (coverage !== undefined && !validCoverage(coverage))
|
|
183
|
+
findings.add('runs', 'invalid_coverage_shape', 'ledger_runs.coverage_json', row.run_id);
|
|
184
|
+
if (metadata !== undefined && scanValue(metadata))
|
|
185
|
+
findings.add('secretResidue', 'secret_residue', 'ledger_runs.metadata_json', row.run_id);
|
|
186
|
+
scanRow(row, 'ledger_runs', row.run_id, findings);
|
|
187
|
+
}
|
|
188
|
+
return runs;
|
|
189
|
+
}
|
|
190
|
+
function inspectEvents(rows, runs, findings) {
|
|
191
|
+
const grouped = new Map();
|
|
192
|
+
const eventIds = new Set();
|
|
193
|
+
const sourceIds = new Set();
|
|
194
|
+
for (const row of rows) {
|
|
195
|
+
const runId = str(row.run_id) ?? '';
|
|
196
|
+
const list = grouped.get(runId) ?? [];
|
|
197
|
+
list.push(row);
|
|
198
|
+
grouped.set(runId, list);
|
|
199
|
+
const eventId = str(row.event_id);
|
|
200
|
+
if (!eventId)
|
|
201
|
+
findings.add('eventIdentity', 'invalid_event_id', 'ledger_events', row.event_id);
|
|
202
|
+
else if (eventIds.has(eventId))
|
|
203
|
+
findings.add('eventIdentity', 'duplicate_event_id', 'ledger_events', eventId);
|
|
204
|
+
else
|
|
205
|
+
eventIds.add(eventId);
|
|
206
|
+
const sourceEventId = nullableStr(row.source_event_id);
|
|
207
|
+
if (sourceEventId !== null && sourceEventId !== undefined) {
|
|
208
|
+
const sourceKey = `${runId}:${sourceEventId}`;
|
|
209
|
+
if (sourceIds.has(sourceKey))
|
|
210
|
+
findings.add('eventIdentity', 'duplicate_source_event_id', 'ledger_events', sourceEventId);
|
|
211
|
+
else
|
|
212
|
+
sourceIds.add(sourceKey);
|
|
213
|
+
}
|
|
214
|
+
if (integer(row.sequence) === undefined || row.sequence < 1) {
|
|
215
|
+
findings.add('eventIdentity', 'invalid_sequence', 'ledger_events', row.event_id);
|
|
216
|
+
findings.add('storedValues', 'invalid_sequence', 'ledger_events', row.event_id);
|
|
217
|
+
}
|
|
218
|
+
if (typeof row.event_type !== 'string' || !LEDGER_EVENT_TYPES.includes(row.event_type))
|
|
219
|
+
findings.add('storedValues', 'invalid_enum', 'ledger_events', row.event_id);
|
|
220
|
+
if (typeof row.actor !== 'string' || row.actor.length === 0 || !validTimestamp(row.ingested_at))
|
|
221
|
+
findings.add('storedValues', 'invalid_text_or_timestamp', 'ledger_events', row.event_id);
|
|
222
|
+
if (row.occurred_at !== null && !validTimestamp(row.occurred_at))
|
|
223
|
+
findings.add('storedValues', 'invalid_timestamp', 'ledger_events', row.event_id);
|
|
224
|
+
if (row.source_sequence !== null && (integer(row.source_sequence) === undefined || row.source_sequence < 0))
|
|
225
|
+
findings.add('storedValues', 'invalid_source_sequence', 'ledger_events', row.event_id);
|
|
226
|
+
const payload = parseJson(row.payload_json, ['storedValues', 'eventHashChain'], 'ledger_events.payload_json', row.event_id, findings, 'any');
|
|
227
|
+
const redactions = parseJson(row.redaction_json, ['storedValues', 'eventHashChain'], 'ledger_events.redaction_json', row.event_id, findings, 'array');
|
|
228
|
+
if (redactions !== undefined && !validRedactions(redactions))
|
|
229
|
+
findings.add('storedValues', 'invalid_redaction_shape', 'ledger_events.redaction_json', row.event_id);
|
|
230
|
+
if (typeof row.previous_hash !== 'string' || !HASH.test(row.previous_hash) || typeof row.event_hash !== 'string' || !HASH.test(row.event_hash)) {
|
|
231
|
+
findings.add('storedValues', 'invalid_hash_shape', 'ledger_events', row.event_id);
|
|
232
|
+
findings.add('eventHashChain', 'invalid_hash_shape', 'ledger_events', row.event_id);
|
|
233
|
+
}
|
|
234
|
+
if ((payload !== undefined && scanValue(payload)) || (redactions !== undefined && scanValue(redactions)))
|
|
235
|
+
findings.add('secretResidue', 'secret_residue', 'ledger_events', row.event_id);
|
|
236
|
+
scanRow(row, 'ledger_events', row.event_id, findings);
|
|
237
|
+
}
|
|
238
|
+
for (const [runId, list] of grouped) {
|
|
239
|
+
let expected = 1;
|
|
240
|
+
let previous = GENESIS_HASH;
|
|
241
|
+
let sourceMax = null;
|
|
242
|
+
for (const row of list) {
|
|
243
|
+
if (integer(row.sequence) !== expected)
|
|
244
|
+
findings.add('eventIdentity', 'non_contiguous_sequence', 'ledger_events', row.event_id);
|
|
245
|
+
expected = (integer(row.sequence) ?? expected) + 1;
|
|
246
|
+
if (integer(row.source_sequence) !== undefined)
|
|
247
|
+
sourceMax = sourceMax === null ? row.source_sequence : Math.max(sourceMax, row.source_sequence);
|
|
248
|
+
if (row.previous_hash !== previous)
|
|
249
|
+
findings.add('eventHashChain', 'previous_hash_mismatch', 'ledger_events', row.event_id);
|
|
250
|
+
const payload = parseJson(row.payload_json, [], 'ledger_events.payload_json', row.event_id, findings, 'any');
|
|
251
|
+
const redactions = parseJson(row.redaction_json, [], 'ledger_events.redaction_json', row.event_id, findings, 'array');
|
|
252
|
+
if (integer(row.sequence) !== undefined && typeof row.event_type === 'string' && payload !== undefined && validRedactions(redactions)) {
|
|
253
|
+
const calculated = hashLedgerEvent({
|
|
254
|
+
runId, sequence: row.sequence, eventId: str(row.event_id) ?? '', previousHash: str(row.previous_hash) ?? '', eventType: row.event_type,
|
|
255
|
+
...(typeof row.source_event_id === 'string' ? { sourceEventId: row.source_event_id } : {}),
|
|
256
|
+
...(integer(row.source_sequence) === undefined ? {} : { sourceSequence: row.source_sequence }),
|
|
257
|
+
...(typeof row.source_type === 'string' ? { sourceType: row.source_type } : {}), actor: str(row.actor) ?? '',
|
|
258
|
+
...(typeof row.outcome === 'string' ? { outcome: row.outcome } : {}), ...(typeof row.occurred_at === 'string' ? { occurredAt: row.occurred_at } : {}),
|
|
259
|
+
ingestedAt: str(row.ingested_at) ?? '', payload: payload, redaction: redactions,
|
|
260
|
+
});
|
|
261
|
+
if (calculated !== row.event_hash)
|
|
262
|
+
findings.add('eventHashChain', 'hash_mismatch', 'ledger_events', row.event_id);
|
|
263
|
+
}
|
|
264
|
+
previous = str(row.event_hash) ?? '';
|
|
265
|
+
}
|
|
266
|
+
const run = runs.get(runId);
|
|
267
|
+
if (!run)
|
|
268
|
+
findings.add('references', 'orphan_event_run', 'ledger_events', runId);
|
|
269
|
+
else {
|
|
270
|
+
const maxSequence = list.reduce((max, row) => Math.max(max, integer(row.sequence) ?? 0), 0);
|
|
271
|
+
if (integer(run.last_sequence) !== maxSequence)
|
|
272
|
+
findings.add('runCursors', 'last_sequence_mismatch', 'ledger_runs', runId);
|
|
273
|
+
const storedSource = run.last_source_sequence === null ? null : integer(run.last_source_sequence);
|
|
274
|
+
if (storedSource !== sourceMax)
|
|
275
|
+
findings.add('runCursors', 'last_source_sequence_mismatch', 'ledger_runs', runId);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
for (const [runId, run] of runs)
|
|
279
|
+
if (!grouped.has(runId) && (integer(run.last_sequence) !== 0 || run.last_source_sequence !== null))
|
|
280
|
+
findings.add('runCursors', 'empty_run_cursor_mismatch', 'ledger_runs', runId);
|
|
281
|
+
return grouped;
|
|
282
|
+
}
|
|
283
|
+
function inspectIntakes(database, workspace, runs, findings) {
|
|
284
|
+
const sql = `SELECT ri.*, r.workspace AS run_workspace, r.status AS run_status, s.workspace AS session_workspace, s.status AS session_status FROM run_intakes AS ri LEFT JOIN ledger_runs AS r ON r.run_id = ri.run_id LEFT JOIN akinator_sessions AS s ON s.id = ri.session_id${workspace === undefined ? '' : ' WHERE r.workspace = ?'} ORDER BY ri.run_id ASC, ri.session_id ASC`;
|
|
285
|
+
const rows = (workspace === undefined ? database.prepare(sql).all() : database.prepare(sql).all(workspace));
|
|
286
|
+
const runIds = new Set();
|
|
287
|
+
const sessions = new Set();
|
|
288
|
+
for (const row of rows) {
|
|
289
|
+
const runId = str(row.run_id) ?? '';
|
|
290
|
+
const sessionId = str(row.session_id) ?? '';
|
|
291
|
+
if (runIds.has(runId))
|
|
292
|
+
findings.add('runIntakes', 'duplicate_run_intake', 'run_intakes', runId);
|
|
293
|
+
if (sessions.has(sessionId))
|
|
294
|
+
findings.add('runIntakes', 'duplicate_session_intake', 'run_intakes', sessionId);
|
|
295
|
+
runIds.add(runId);
|
|
296
|
+
sessions.add(sessionId);
|
|
297
|
+
if (!runs.has(runId) || row.session_workspace === null || row.session_workspace === undefined)
|
|
298
|
+
findings.add('references', 'orphan_intake_reference', 'run_intakes', runId);
|
|
299
|
+
if (row.run_workspace !== row.session_workspace)
|
|
300
|
+
findings.add('runIntakes', 'workspace_mismatch', 'run_intakes', runId);
|
|
301
|
+
if (!['active', 'ready', 'exhausted'].includes(str(row.session_status) ?? ''))
|
|
302
|
+
findings.add('runIntakes', 'invalid_session_status', 'run_intakes', sessionId);
|
|
303
|
+
if (row.finalized_at !== null && !validTimestamp(row.finalized_at))
|
|
304
|
+
findings.add('runIntakes', 'invalid_finalized_at', 'run_intakes', runId);
|
|
305
|
+
if (row.session_status === 'active' && row.finalized_at !== null)
|
|
306
|
+
findings.add('runIntakes', 'active_session_finalized', 'run_intakes', runId);
|
|
307
|
+
if (['ready', 'exhausted'].includes(str(row.session_status) ?? '') && row.finalized_at === null)
|
|
308
|
+
findings.add('runIntakes', 'missing_finalization', 'run_intakes', runId);
|
|
309
|
+
if (row.run_status === 'active' && !['ready', 'exhausted'].includes(str(row.session_status) ?? ''))
|
|
310
|
+
findings.add('runIntakes', 'active_before_intake_finalization', 'run_intakes', runId);
|
|
311
|
+
if (row.run_status === 'intake' && ['ready', 'exhausted'].includes(str(row.session_status) ?? ''))
|
|
312
|
+
findings.add('runIntakes', 'run_not_activated_after_finalization', 'run_intakes', runId);
|
|
313
|
+
if (typeof row.policy_version !== 'string' || row.policy_version.length === 0)
|
|
314
|
+
findings.add('storedValues', 'invalid_text', 'run_intakes', runId);
|
|
315
|
+
if (integer(row.profile_schema_version) === undefined || row.profile_schema_version < 1)
|
|
316
|
+
findings.add('storedValues', 'invalid_profile_schema_version', 'run_intakes', runId);
|
|
317
|
+
if (!validTimestamp(row.linked_at))
|
|
318
|
+
findings.add('storedValues', 'invalid_timestamp', 'run_intakes', runId);
|
|
319
|
+
parseJson(row.profile_sources_json, ['runIntakes', 'storedValues'], 'run_intakes.profile_sources_json', runId, findings, 'object');
|
|
320
|
+
parseJson(row.recommended_tags_json, ['runIntakes', 'storedValues'], 'run_intakes.recommended_tags_json', runId, findings, 'array');
|
|
321
|
+
if (row.initial_profile_hash !== null && (typeof row.initial_profile_hash !== 'string' || !HASH.test(row.initial_profile_hash)))
|
|
322
|
+
findings.add('runIntakes', 'invalid_hash_shape', 'run_intakes', runId);
|
|
323
|
+
scanRow(row, 'run_intakes', runId, findings);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
function inspectReferences(database, workspace, findings) {
|
|
327
|
+
const scope = workspace === undefined ? '' : ' WHERE r.workspace = ?';
|
|
328
|
+
const param = workspace === undefined ? [] : [workspace];
|
|
329
|
+
const evidence = database.prepare(`SELECT le.*, r.workspace AS run_workspace, e.run_id AS event_run_id FROM ledger_evidence AS le LEFT JOIN ledger_runs AS r ON r.run_id = le.run_id LEFT JOIN ledger_events AS e ON e.event_id = le.event_id${scope} ORDER BY le.evidence_id ASC`).all(...param);
|
|
330
|
+
for (const row of evidence) {
|
|
331
|
+
if (row.run_workspace === null || row.run_workspace === undefined || (row.event_id !== null && row.event_run_id !== row.run_id))
|
|
332
|
+
findings.add('references', 'orphan_evidence_reference', 'ledger_evidence', row.evidence_id);
|
|
333
|
+
if (!EVIDENCE_KINDS.has(str(row.kind) ?? ''))
|
|
334
|
+
findings.add('storedValues', 'invalid_enum', 'ledger_evidence', row.evidence_id);
|
|
335
|
+
if (!validTimestamp(row.created_at))
|
|
336
|
+
findings.add('storedValues', 'invalid_timestamp', 'ledger_evidence', row.evidence_id);
|
|
337
|
+
if (row.byte_size !== null && (integer(row.byte_size) === undefined || row.byte_size < 0))
|
|
338
|
+
findings.add('storedValues', 'invalid_byte_size', 'ledger_evidence', row.evidence_id);
|
|
339
|
+
scanRow(row, 'ledger_evidence', row.evidence_id, findings);
|
|
340
|
+
}
|
|
341
|
+
const deliveries = database.prepare(`SELECT cd.*, r.workspace AS run_workspace, s.workspace AS intake_workspace FROM context_deliveries AS cd LEFT JOIN ledger_runs AS r ON r.run_id = cd.run_id LEFT JOIN akinator_sessions AS s ON s.id = cd.intake_session_id${scope} ORDER BY cd.delivery_id ASC`).all(...param);
|
|
342
|
+
for (const row of deliveries) {
|
|
343
|
+
if (row.run_workspace === null || row.run_workspace === undefined)
|
|
344
|
+
findings.add('references', 'orphan_delivery_run', 'context_deliveries', row.delivery_id);
|
|
345
|
+
if (row.intake_session_id !== null && row.intake_workspace !== row.run_workspace)
|
|
346
|
+
findings.add('references', 'delivery_intake_workspace_mismatch', 'context_deliveries', row.delivery_id);
|
|
347
|
+
scanRow(row, 'context_deliveries', row.delivery_id, findings);
|
|
348
|
+
}
|
|
349
|
+
const entries = database.prepare(`SELECT cde.*, cd.run_id, r.workspace AS run_workspace, e.revision AS current_revision, e.workspace AS entry_workspace FROM context_delivery_entries AS cde LEFT JOIN context_deliveries AS cd ON cd.delivery_id = cde.delivery_id LEFT JOIN ledger_runs AS r ON r.run_id = cd.run_id LEFT JOIN entries AS e ON e.id = cde.entry_id${scope} ORDER BY cde.delivery_id ASC, cde.entry_id ASC`).all(...param);
|
|
350
|
+
for (const row of entries) {
|
|
351
|
+
if (row.run_id === null || row.run_id === undefined || row.current_revision === null || row.current_revision === undefined || row.entry_workspace !== row.run_workspace)
|
|
352
|
+
findings.add('references', 'orphan_delivery_entry_reference', 'context_delivery_entries', row.entry_id);
|
|
353
|
+
if (integer(row.entry_revision) === undefined || row.entry_revision < 1 || (integer(row.current_revision) !== undefined && row.entry_revision > row.current_revision))
|
|
354
|
+
findings.add('contextDeliveries', 'invalid_entry_revision', 'context_delivery_entries', row.entry_id);
|
|
355
|
+
if (integer(row.rank) === undefined || row.rank < 1)
|
|
356
|
+
findings.add('contextDeliveries', 'invalid_rank', 'context_delivery_entries', row.entry_id);
|
|
357
|
+
parseJson(row.score_components_json, ['contextDeliveries', 'storedValues'], 'context_delivery_entries.score_components_json', row.entry_id, findings, 'object');
|
|
358
|
+
parseJson(row.selection_reason_json, ['contextDeliveries', 'storedValues'], 'context_delivery_entries.selection_reason_json', row.entry_id, findings, 'object');
|
|
359
|
+
scanRow(row, 'context_delivery_entries', row.entry_id, findings);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
function inspectContext(database, workspace, runs, findings) {
|
|
363
|
+
const scope = workspace === undefined ? '' : ' WHERE r.workspace = ?';
|
|
364
|
+
const param = workspace === undefined ? [] : [workspace];
|
|
365
|
+
const deliveries = database.prepare(`SELECT cd.*, r.last_sequence AS run_last_sequence FROM context_deliveries AS cd LEFT JOIN ledger_runs AS r ON r.run_id = cd.run_id${scope} ORDER BY cd.delivery_id ASC`).all(...param);
|
|
366
|
+
for (const row of deliveries) {
|
|
367
|
+
const through = integer(row.through_sequence);
|
|
368
|
+
const last = integer(row.run_last_sequence);
|
|
369
|
+
if (through === undefined || through < 0 || last === undefined || through > last)
|
|
370
|
+
findings.add('contextDeliveries', 'through_sequence_out_of_range', 'context_deliveries', row.delivery_id);
|
|
371
|
+
const budget = integer(row.char_budget);
|
|
372
|
+
const chars = integer(row.char_count);
|
|
373
|
+
if (budget === undefined || budget < 0 || chars === undefined || chars < 0 || chars > budget)
|
|
374
|
+
findings.add('contextDeliveries', 'char_budget_mismatch', 'context_deliveries', row.delivery_id);
|
|
375
|
+
if (row.truncated !== 0 && row.truncated !== 1)
|
|
376
|
+
findings.add('contextDeliveries', 'invalid_truncated_flag', 'context_deliveries', row.delivery_id);
|
|
377
|
+
if (typeof row.task_profile_hash !== 'string' || !HASH.test(row.task_profile_hash))
|
|
378
|
+
findings.add('storedValues', 'invalid_hash_shape', 'context_deliveries', row.delivery_id);
|
|
379
|
+
if (typeof row.query_hash !== 'string' || !HASH.test(row.query_hash))
|
|
380
|
+
findings.add('storedValues', 'invalid_hash_shape', 'context_deliveries', row.delivery_id);
|
|
381
|
+
if (typeof row.policy_version !== 'string' || row.policy_version.length === 0)
|
|
382
|
+
findings.add('storedValues', 'invalid_text', 'context_deliveries', row.delivery_id);
|
|
383
|
+
if (!validTimestamp(row.created_at))
|
|
384
|
+
findings.add('contextDeliveries', 'invalid_timestamp', 'context_deliveries', row.delivery_id);
|
|
385
|
+
parseJson(row.external_sync_summary_json, ['contextDeliveries', 'storedValues'], 'context_deliveries.external_sync_summary_json', row.delivery_id, findings, 'object');
|
|
386
|
+
if (!runs.has(str(row.run_id) ?? ''))
|
|
387
|
+
findings.add('references', 'orphan_delivery_run', 'context_deliveries', row.delivery_id);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
function inspectFeedback(database, workspace, findings) {
|
|
391
|
+
const scope = workspace === undefined ? '' : ' WHERE r.workspace = ?';
|
|
392
|
+
const param = workspace === undefined ? [] : [workspace];
|
|
393
|
+
const context = database.prepare(`SELECT cf.*, r.workspace AS run_workspace, cd.run_id AS delivery_run_id, cde.entry_id AS linked_entry_id, e.workspace AS entry_workspace FROM context_feedback AS cf LEFT JOIN ledger_runs AS r ON r.run_id = cf.run_id LEFT JOIN context_deliveries AS cd ON cd.delivery_id = cf.delivery_id LEFT JOIN context_delivery_entries AS cde ON cde.delivery_id = cf.delivery_id AND cde.entry_id = cf.entry_id LEFT JOIN entries AS e ON e.id = cf.entry_id${scope} ORDER BY cf.feedback_id ASC`).all(...param);
|
|
394
|
+
for (const row of context) {
|
|
395
|
+
if (row.run_workspace === null || row.run_workspace === undefined || row.delivery_run_id !== row.run_id || row.linked_entry_id !== row.entry_id || row.entry_workspace !== row.run_workspace)
|
|
396
|
+
findings.add('feedbackLinks', 'feedback_tuple_mismatch', 'context_feedback', row.feedback_id);
|
|
397
|
+
if (!CONTEXT_FEEDBACK_VERDICTS.has(str(row.verdict) ?? ''))
|
|
398
|
+
findings.add('feedbackLinks', 'invalid_feedback_verdict', 'context_feedback', row.feedback_id);
|
|
399
|
+
if (typeof row.actor !== 'string' || row.actor.length === 0)
|
|
400
|
+
findings.add('storedValues', 'invalid_text', 'context_feedback', row.feedback_id);
|
|
401
|
+
if (typeof row.idempotency_key !== 'string' || !HASH.test(row.idempotency_key))
|
|
402
|
+
findings.add('storedValues', 'invalid_hash_shape', 'context_feedback', row.feedback_id);
|
|
403
|
+
if (!validTimestamp(row.created_at))
|
|
404
|
+
findings.add('storedValues', 'invalid_timestamp', 'context_feedback', row.feedback_id);
|
|
405
|
+
scanRow(row, 'context_feedback', row.feedback_id, findings);
|
|
406
|
+
}
|
|
407
|
+
const run = database.prepare(`SELECT rf.*, r.workspace AS run_workspace FROM run_feedback AS rf LEFT JOIN ledger_runs AS r ON r.run_id = rf.run_id${scope} ORDER BY rf.feedback_id ASC`).all(...param);
|
|
408
|
+
for (const row of run) {
|
|
409
|
+
if (row.run_workspace === null || row.run_workspace === undefined)
|
|
410
|
+
findings.add('feedbackLinks', 'orphan_run_feedback', 'run_feedback', row.feedback_id);
|
|
411
|
+
if (row.recommendation_verdict !== null && !RECOMMENDATION_VERDICTS.has(str(row.recommendation_verdict) ?? ''))
|
|
412
|
+
findings.add('feedbackLinks', 'invalid_recommendation_verdict', 'run_feedback', row.feedback_id);
|
|
413
|
+
if ((row.recommendation_code === null) !== (row.recommendation_verdict === null))
|
|
414
|
+
findings.add('feedbackLinks', 'recommendation_tuple_mismatch', 'run_feedback', row.feedback_id);
|
|
415
|
+
if (row.rating !== null && (integer(row.rating) === undefined || row.rating < 1 || row.rating > 5))
|
|
416
|
+
findings.add('feedbackLinks', 'invalid_rating', 'run_feedback', row.feedback_id);
|
|
417
|
+
if (row.outcome === null && row.recommendation_code === null && row.rating === null)
|
|
418
|
+
findings.add('feedbackLinks', 'empty_feedback', 'run_feedback', row.feedback_id);
|
|
419
|
+
if (typeof row.actor !== 'string' || row.actor.length === 0)
|
|
420
|
+
findings.add('storedValues', 'invalid_text', 'run_feedback', row.feedback_id);
|
|
421
|
+
if (typeof row.idempotency_key !== 'string' || !HASH.test(row.idempotency_key))
|
|
422
|
+
findings.add('storedValues', 'invalid_hash_shape', 'run_feedback', row.feedback_id);
|
|
423
|
+
if (!validTimestamp(row.created_at))
|
|
424
|
+
findings.add('storedValues', 'invalid_timestamp', 'run_feedback', row.feedback_id);
|
|
425
|
+
scanRow(row, 'run_feedback', row.feedback_id, findings);
|
|
426
|
+
}
|
|
427
|
+
const intake = database.prepare(`SELECT ifb.*, r.workspace AS run_workspace, s.workspace AS session_workspace, ri.session_id AS linked_session_id FROM intake_feedback AS ifb LEFT JOIN ledger_runs AS r ON r.run_id = ifb.run_id LEFT JOIN akinator_sessions AS s ON s.id = ifb.session_id LEFT JOIN run_intakes AS ri ON ri.run_id = ifb.run_id AND ri.session_id = ifb.session_id${scope} ORDER BY ifb.feedback_id ASC`).all(...param);
|
|
428
|
+
for (const row of intake) {
|
|
429
|
+
if (row.run_workspace === null || row.run_workspace === undefined || row.session_workspace !== row.run_workspace || row.linked_session_id !== row.session_id)
|
|
430
|
+
findings.add('feedbackLinks', 'intake_feedback_tuple_mismatch', 'intake_feedback', row.feedback_id);
|
|
431
|
+
if (!INTAKE_FEEDBACK_VERDICTS.has(str(row.verdict) ?? ''))
|
|
432
|
+
findings.add('feedbackLinks', 'invalid_feedback_verdict', 'intake_feedback', row.feedback_id);
|
|
433
|
+
if ((row.question_id === null) === (row.profile_field === null))
|
|
434
|
+
findings.add('feedbackLinks', 'invalid_feedback_target', 'intake_feedback', row.feedback_id);
|
|
435
|
+
if (typeof row.actor !== 'string' || row.actor.length === 0)
|
|
436
|
+
findings.add('storedValues', 'invalid_text', 'intake_feedback', row.feedback_id);
|
|
437
|
+
if (typeof row.idempotency_key !== 'string' || !HASH.test(row.idempotency_key))
|
|
438
|
+
findings.add('storedValues', 'invalid_hash_shape', 'intake_feedback', row.feedback_id);
|
|
439
|
+
if (!validTimestamp(row.created_at))
|
|
440
|
+
findings.add('storedValues', 'invalid_timestamp', 'intake_feedback', row.feedback_id);
|
|
441
|
+
scanRow(row, 'intake_feedback', row.feedback_id, findings);
|
|
442
|
+
}
|
|
443
|
+
const links = database.prepare(`SELECT l.*, r.workspace AS run_workspace, ev.run_id AS event_run_id, d.run_id AS delivery_run_id, e.workspace AS entry_workspace FROM ledger_memory_links AS l LEFT JOIN ledger_runs AS r ON r.run_id = l.run_id LEFT JOIN ledger_events AS ev ON ev.event_id = l.event_id LEFT JOIN context_deliveries AS d ON d.delivery_id = l.delivery_id LEFT JOIN entries AS e ON e.id = l.entry_id${scope} ORDER BY l.link_id ASC`).all(...param);
|
|
444
|
+
for (const row of links) {
|
|
445
|
+
if (row.run_workspace === null || row.run_workspace === undefined || row.entry_workspace !== row.run_workspace)
|
|
446
|
+
findings.add('references', 'orphan_memory_link', 'ledger_memory_links', row.link_id);
|
|
447
|
+
if (row.event_id === null && row.delivery_id === null)
|
|
448
|
+
findings.add('feedbackLinks', 'memory_link_without_source', 'ledger_memory_links', row.link_id);
|
|
449
|
+
if (row.event_id !== null && row.event_run_id !== row.run_id)
|
|
450
|
+
findings.add('feedbackLinks', 'memory_link_event_mismatch', 'ledger_memory_links', row.link_id);
|
|
451
|
+
if (row.delivery_id !== null && row.delivery_run_id !== row.run_id)
|
|
452
|
+
findings.add('feedbackLinks', 'memory_link_delivery_mismatch', 'ledger_memory_links', row.link_id);
|
|
453
|
+
if (!validTimestamp(row.created_at))
|
|
454
|
+
findings.add('storedValues', 'invalid_timestamp', 'ledger_memory_links', row.link_id);
|
|
455
|
+
scanRow(row, 'ledger_memory_links', row.link_id, findings);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
function inspectTombstones(database, workspace, findings) {
|
|
459
|
+
const rows = workspace === undefined ? database.prepare('SELECT * FROM ledger_purge_audit ORDER BY purge_id ASC').all() : database.prepare('SELECT p.* FROM ledger_purge_audit AS p JOIN ledger_runs AS r ON r.run_id = p.run_id WHERE r.workspace = ? ORDER BY p.purge_id ASC').all(workspace);
|
|
460
|
+
for (const row of rows) {
|
|
461
|
+
if (typeof row.purge_id !== 'string' || row.purge_id.length === 0)
|
|
462
|
+
findings.add('storedValues', 'invalid_tombstone_id', 'ledger_purge_audit', row.purge_id);
|
|
463
|
+
if (!PURGE_TARGET_TYPES.includes(row.target_type))
|
|
464
|
+
findings.add('storedValues', 'invalid_tombstone_target_type', 'ledger_purge_audit', row.purge_id);
|
|
465
|
+
if (typeof row.target_id !== 'string' || row.target_id.length === 0 || typeof row.actor !== 'string' || row.actor.length === 0)
|
|
466
|
+
findings.add('storedValues', 'invalid_tombstone_text', 'ledger_purge_audit', row.purge_id);
|
|
467
|
+
if (!validTimestamp(row.created_at))
|
|
468
|
+
findings.add('storedValues', 'invalid_timestamp', 'ledger_purge_audit', row.purge_id);
|
|
469
|
+
scanRow(row, 'ledger_purge_audit', row.purge_id, findings);
|
|
470
|
+
}
|
|
471
|
+
return rows.length;
|
|
472
|
+
}
|
|
473
|
+
export function inspectLedger(database, options = {}) {
|
|
474
|
+
if (options.workspace !== undefined && (typeof options.workspace !== 'string' || options.workspace.length === 0))
|
|
475
|
+
errorValidation('workspace must be a non-empty string');
|
|
476
|
+
try {
|
|
477
|
+
const workspace = options.workspace;
|
|
478
|
+
const reportCounts = counts(database, workspace);
|
|
479
|
+
const checks = emptyChecks();
|
|
480
|
+
const findings = new FindingCollector(checks);
|
|
481
|
+
const runRows = workspace === undefined ? database.prepare('SELECT * FROM ledger_runs ORDER BY run_id ASC').all() : database.prepare('SELECT * FROM ledger_runs WHERE workspace = ? ORDER BY run_id ASC').all(workspace);
|
|
482
|
+
const eventRows = selectedEvents(database, workspace);
|
|
483
|
+
const runs = inspectRuns(runRows, findings);
|
|
484
|
+
inspectEvents(eventRows, runs, findings);
|
|
485
|
+
inspectIntakes(database, workspace, runs, findings);
|
|
486
|
+
inspectReferences(database, workspace, findings);
|
|
487
|
+
inspectContext(database, workspace, runs, findings);
|
|
488
|
+
inspectFeedback(database, workspace, findings);
|
|
489
|
+
const tombstoneCount = inspectTombstones(database, workspace, findings);
|
|
490
|
+
checks.runs.count = reportCounts.runs;
|
|
491
|
+
checks.eventIdentity.count = reportCounts.events;
|
|
492
|
+
checks.eventHashChain.count = reportCounts.events;
|
|
493
|
+
checks.runCursors.count = reportCounts.runs;
|
|
494
|
+
checks.runIntakes.count = workspace === undefined ? rowCount(database, 'SELECT COUNT(*) AS count FROM run_intakes') : rowCount(database, 'SELECT COUNT(*) AS count FROM run_intakes AS i JOIN ledger_runs AS r ON r.run_id = i.run_id WHERE r.workspace = ?', workspace);
|
|
495
|
+
checks.references.count = reportCounts.evidence + reportCounts.deliveries + reportCounts.deliveryEntries + reportCounts.memoryLinks;
|
|
496
|
+
checks.contextDeliveries.count = reportCounts.deliveries + reportCounts.deliveryEntries;
|
|
497
|
+
checks.feedbackLinks.count = reportCounts.intakeFeedback + reportCounts.contextFeedback + reportCounts.runFeedback + reportCounts.memoryLinks;
|
|
498
|
+
checks.storedValues.count = runRows.length + eventRows.length + reportCounts.evidence + reportCounts.deliveries + reportCounts.deliveryEntries + reportCounts.intakeFeedback + reportCounts.contextFeedback + reportCounts.runFeedback + reportCounts.memoryLinks + tombstoneCount;
|
|
499
|
+
checks.secretResidue.count = checks.storedValues.count;
|
|
500
|
+
return { ok: LEDGER_CHECK_NAMES.every((name) => checks[name].ok), workspace: workspace ?? null, counts: reportCounts, checks, findings: findings.findings, findingCount: findings.findingCount, findingsTruncated: findings.findingsTruncated, tombstoneCount };
|
|
501
|
+
}
|
|
502
|
+
catch (error) {
|
|
503
|
+
if (error instanceof KiokukoError)
|
|
504
|
+
throw error;
|
|
505
|
+
errorDatabase();
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
export const PURGE_TARGET_TYPES = ['run', 'event', 'evidence', 'delivery', 'feedback', 'memory_link'];
|
|
509
|
+
export const PURGE_BACKUP_WARNING = 'Backups may retain purged content and must be managed separately.';
|
|
510
|
+
const PURGE_FIELDS = new Set(['workspace', 'targetType', 'targetId', 'actor', 'reason', 'createdAt', 'purgeId', 'confirmed']);
|
|
511
|
+
const PURGE_TYPES = new Set(PURGE_TARGET_TYPES);
|
|
512
|
+
const PURGE_NOT_FOUND = 'Ledger purge target was not found';
|
|
513
|
+
const PURGE_CONFLICT = 'Ledger purge conflicts with an existing purge';
|
|
514
|
+
const EVENT_PURGE_CONFLICT = 'Single-event purge is not permitted because it would corrupt the hash chain';
|
|
515
|
+
function boundedPurgeText(value, label) {
|
|
516
|
+
if (typeof value !== 'string' || value.length === 0 || value.length > 256 || value.includes('\u0000'))
|
|
517
|
+
errorValidation(`${label} must be a non-empty bounded string`);
|
|
518
|
+
if (findSecret(value))
|
|
519
|
+
throw new KiokukoError('SECURITY_REJECTION', 'Purge input was rejected by privacy security policy');
|
|
520
|
+
return value;
|
|
521
|
+
}
|
|
522
|
+
function normalizePurge(value) {
|
|
523
|
+
if (!isObject(value) || Object.keys(value).some((key) => !PURGE_FIELDS.has(key)))
|
|
524
|
+
errorValidation('Invalid ledger purge input');
|
|
525
|
+
if (value.confirmed !== true)
|
|
526
|
+
throw new KiokukoError('VALIDATION_ERROR', 'Explicit purge confirmation is required');
|
|
527
|
+
const workspace = boundedPurgeText(value.workspace, 'workspace');
|
|
528
|
+
if (typeof value.targetType !== 'string' || !PURGE_TYPES.has(value.targetType))
|
|
529
|
+
errorValidation('Invalid ledger purge target type');
|
|
530
|
+
const targetType = value.targetType;
|
|
531
|
+
const targetId = boundedPurgeText(value.targetId, 'targetId');
|
|
532
|
+
const actor = boundedPurgeText(value.actor, 'actor');
|
|
533
|
+
if (!validTimestamp(value.createdAt))
|
|
534
|
+
errorValidation('createdAt must be an ISO-8601 UTC timestamp');
|
|
535
|
+
const purgeId = boundedPurgeText(value.purgeId, 'purgeId');
|
|
536
|
+
let reason = null;
|
|
537
|
+
if (value.reason !== undefined && value.reason !== null) {
|
|
538
|
+
if (typeof value.reason !== 'string')
|
|
539
|
+
errorValidation('reason must be a bounded string');
|
|
540
|
+
let sanitized;
|
|
541
|
+
try {
|
|
542
|
+
sanitized = sanitizeJson(value.reason).value;
|
|
543
|
+
}
|
|
544
|
+
catch {
|
|
545
|
+
errorValidation('reason must be a bounded string');
|
|
546
|
+
}
|
|
547
|
+
if (typeof sanitized !== 'string' || Buffer.byteLength(sanitized, 'utf8') > 2048)
|
|
548
|
+
errorValidation('reason must be a bounded string');
|
|
549
|
+
if (findSecret(sanitized))
|
|
550
|
+
throw new KiokukoError('SECURITY_REJECTION', 'Purge input was rejected by privacy security policy');
|
|
551
|
+
reason = sanitized;
|
|
552
|
+
}
|
|
553
|
+
return { workspace, targetType, targetId, actor, reason, createdAt: value.createdAt, purgeId };
|
|
554
|
+
}
|
|
555
|
+
function tombstone(row) { return { purgeId: str(row.purge_id) ?? '', runId: nullableStr(row.run_id) ?? null, eventId: nullableStr(row.event_id) ?? null, deliveryId: nullableStr(row.delivery_id) ?? null, entryId: nullableStr(row.entry_id) ?? null, targetType: row.target_type, targetId: str(row.target_id) ?? '', actor: str(row.actor) ?? '', reason: nullableStr(row.reason) ?? null, createdAt: str(row.created_at) ?? '' }; }
|
|
556
|
+
function purgeResult(value, deletedCount, replayed) { return { purgeId: value.purgeId, targetType: value.targetType, targetId: value.targetId, deletedCount, replayed, tombstone: value, backupWarning: PURGE_BACKUP_WARNING }; }
|
|
557
|
+
function samePurge(database, input, value) {
|
|
558
|
+
const storedWorkspace = value.runId === null
|
|
559
|
+
? undefined
|
|
560
|
+
: str(database.prepare('SELECT workspace FROM ledger_runs WHERE run_id = ?').get(value.runId)?.workspace);
|
|
561
|
+
return input.purgeId === value.purgeId
|
|
562
|
+
&& (storedWorkspace === undefined || storedWorkspace === input.workspace)
|
|
563
|
+
&& input.targetType === value.targetType
|
|
564
|
+
&& input.targetId === value.targetId
|
|
565
|
+
&& input.actor === value.actor
|
|
566
|
+
&& input.reason === value.reason
|
|
567
|
+
&& input.createdAt === value.createdAt;
|
|
568
|
+
}
|
|
569
|
+
function target(database, input) {
|
|
570
|
+
const id = input.targetId;
|
|
571
|
+
const workspace = input.workspace;
|
|
572
|
+
switch (input.targetType) {
|
|
573
|
+
case 'run': return database.prepare('SELECT run_id, workspace FROM ledger_runs WHERE run_id = ? AND workspace = ?').get(id, workspace);
|
|
574
|
+
case 'event': return database.prepare('SELECT e.event_id, e.run_id FROM ledger_events AS e JOIN ledger_runs AS r ON r.run_id = e.run_id WHERE e.event_id = ? AND r.workspace = ?').get(id, workspace);
|
|
575
|
+
case 'evidence': return database.prepare('SELECT le.evidence_id, le.run_id, le.event_id FROM ledger_evidence AS le JOIN ledger_runs AS r ON r.run_id = le.run_id WHERE le.evidence_id = ? AND r.workspace = ?').get(id, workspace);
|
|
576
|
+
case 'delivery': return database.prepare('SELECT cd.delivery_id, cd.run_id FROM context_deliveries AS cd JOIN ledger_runs AS r ON r.run_id = cd.run_id WHERE cd.delivery_id = ? AND r.workspace = ?').get(id, workspace);
|
|
577
|
+
case 'memory_link': return database.prepare('SELECT l.link_id, l.run_id, l.event_id, l.delivery_id, l.entry_id FROM ledger_memory_links AS l JOIN ledger_runs AS r ON r.run_id = l.run_id WHERE l.link_id = ? AND r.workspace = ?').get(id, workspace);
|
|
578
|
+
case 'feedback': {
|
|
579
|
+
const rows = [
|
|
580
|
+
database.prepare("SELECT feedback_id, run_id, 'context' AS feedback_table FROM context_feedback AS f JOIN ledger_runs AS r ON r.run_id = f.run_id WHERE f.feedback_id = ? AND r.workspace = ?").get(id, workspace),
|
|
581
|
+
database.prepare("SELECT feedback_id, run_id, 'run' AS feedback_table FROM run_feedback AS f JOIN ledger_runs AS r ON r.run_id = f.run_id WHERE f.feedback_id = ? AND r.workspace = ?").get(id, workspace),
|
|
582
|
+
database.prepare("SELECT feedback_id, run_id, 'intake' AS feedback_table FROM intake_feedback AS f JOIN ledger_runs AS r ON r.run_id = f.run_id WHERE f.feedback_id = ? AND r.workspace = ?").get(id, workspace),
|
|
583
|
+
].filter((row) => row !== undefined);
|
|
584
|
+
if (rows.length > 1)
|
|
585
|
+
throw new KiokukoError('CONFLICT', PURGE_CONFLICT);
|
|
586
|
+
return rows[0];
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
function insertPurgeTombstone(database, input, row) {
|
|
591
|
+
const runId = input.targetType === 'run' ? input.targetId : nullableStr(row.run_id) ?? null;
|
|
592
|
+
const eventId = input.targetType === 'evidence' || input.targetType === 'memory_link' ? nullableStr(row.event_id) ?? null : null;
|
|
593
|
+
const deliveryId = input.targetType === 'delivery' ? input.targetId : input.targetType === 'memory_link' ? nullableStr(row.delivery_id) ?? null : null;
|
|
594
|
+
const entryId = input.targetType === 'memory_link' ? nullableStr(row.entry_id) ?? null : null;
|
|
595
|
+
database.prepare('INSERT INTO ledger_purge_audit (purge_id, run_id, event_id, delivery_id, entry_id, target_type, target_id, actor, reason, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)').run(input.purgeId, runId, eventId, deliveryId, entryId, input.targetType, input.targetId, input.actor, input.reason, input.createdAt);
|
|
596
|
+
const saved = database.prepare('SELECT * FROM ledger_purge_audit WHERE purge_id = ?').get(input.purgeId);
|
|
597
|
+
if (!saved)
|
|
598
|
+
throw new KiokukoError('INTEGRITY_ERROR', 'Ledger purge tombstone could not be read back');
|
|
599
|
+
return tombstone(saved);
|
|
600
|
+
}
|
|
601
|
+
function runGraphCount(database, runId) {
|
|
602
|
+
let total = 1;
|
|
603
|
+
for (const table of ['run_intakes', 'intake_feedback', 'ledger_events', 'ledger_evidence', 'context_deliveries', 'context_feedback', 'run_feedback', 'ledger_memory_links'])
|
|
604
|
+
total += rowCount(database, `SELECT COUNT(*) AS count FROM ${table} WHERE run_id = ?`, runId);
|
|
605
|
+
total += rowCount(database, 'SELECT COUNT(*) AS count FROM context_delivery_entries AS e JOIN context_deliveries AS d ON d.delivery_id = e.delivery_id WHERE d.run_id = ?', runId);
|
|
606
|
+
const session = database.prepare('SELECT session_id FROM run_intakes WHERE run_id = ?').get(runId);
|
|
607
|
+
if (session) {
|
|
608
|
+
total += rowCount(database, 'SELECT COUNT(*) AS count FROM akinator_answers WHERE session_id = ?', session.session_id);
|
|
609
|
+
total += rowCount(database, 'SELECT COUNT(*) AS count FROM akinator_sessions WHERE id = ?', session.session_id);
|
|
610
|
+
}
|
|
611
|
+
return total;
|
|
612
|
+
}
|
|
613
|
+
function executePurge(database, input, row) {
|
|
614
|
+
if (input.targetType === 'event')
|
|
615
|
+
throw new KiokukoError('CONFLICT', EVENT_PURGE_CONFLICT);
|
|
616
|
+
const value = insertPurgeTombstone(database, input, row);
|
|
617
|
+
let deleted = 1;
|
|
618
|
+
switch (input.targetType) {
|
|
619
|
+
case 'run': {
|
|
620
|
+
deleted = runGraphCount(database, input.targetId);
|
|
621
|
+
const session = database.prepare('SELECT session_id FROM run_intakes WHERE run_id = ?').get(input.targetId);
|
|
622
|
+
database.prepare('DELETE FROM ledger_runs WHERE run_id = ?').run(input.targetId);
|
|
623
|
+
if (session) {
|
|
624
|
+
database.prepare('DELETE FROM akinator_answers WHERE session_id = ?').run(session.session_id);
|
|
625
|
+
if (!database.prepare('SELECT 1 AS present FROM run_intakes WHERE session_id = ?').get(session.session_id))
|
|
626
|
+
database.prepare('DELETE FROM akinator_sessions WHERE id = ?').run(session.session_id);
|
|
627
|
+
}
|
|
628
|
+
break;
|
|
629
|
+
}
|
|
630
|
+
case 'evidence':
|
|
631
|
+
database.prepare('DELETE FROM ledger_evidence WHERE evidence_id = ?').run(input.targetId);
|
|
632
|
+
break;
|
|
633
|
+
case 'delivery':
|
|
634
|
+
deleted += rowCount(database, 'SELECT COUNT(*) AS count FROM context_feedback WHERE delivery_id = ?', input.targetId);
|
|
635
|
+
deleted += rowCount(database, 'SELECT COUNT(*) AS count FROM context_delivery_entries WHERE delivery_id = ?', input.targetId);
|
|
636
|
+
deleted += rowCount(database, 'SELECT COUNT(*) AS count FROM ledger_memory_links WHERE delivery_id = ?', input.targetId);
|
|
637
|
+
database.prepare('DELETE FROM ledger_memory_links WHERE delivery_id = ?').run(input.targetId);
|
|
638
|
+
database.prepare('DELETE FROM context_deliveries WHERE delivery_id = ?').run(input.targetId);
|
|
639
|
+
break;
|
|
640
|
+
case 'feedback': {
|
|
641
|
+
const table = row.feedback_table === 'context' ? 'context_feedback' : row.feedback_table === 'run' ? 'run_feedback' : 'intake_feedback';
|
|
642
|
+
database.prepare(`DELETE FROM ${table} WHERE feedback_id = ?`).run(input.targetId);
|
|
643
|
+
break;
|
|
644
|
+
}
|
|
645
|
+
case 'memory_link':
|
|
646
|
+
database.prepare('DELETE FROM ledger_memory_links WHERE link_id = ?').run(input.targetId);
|
|
647
|
+
break;
|
|
648
|
+
default: break;
|
|
649
|
+
}
|
|
650
|
+
const saved = database.prepare('SELECT * FROM ledger_purge_audit WHERE purge_id = ?').get(input.purgeId);
|
|
651
|
+
if (!saved)
|
|
652
|
+
throw new KiokukoError('INTEGRITY_ERROR', 'Ledger purge tombstone disappeared');
|
|
653
|
+
return { value: tombstone(saved), count: deleted };
|
|
654
|
+
}
|
|
655
|
+
function purgeInTransaction(database, input) {
|
|
656
|
+
const existing = database.prepare('SELECT * FROM ledger_purge_audit WHERE purge_id = ?').get(input.purgeId);
|
|
657
|
+
if (existing) {
|
|
658
|
+
const value = tombstone(existing);
|
|
659
|
+
if (!samePurge(database, input, value))
|
|
660
|
+
throw new KiokukoError('CONFLICT', PURGE_CONFLICT);
|
|
661
|
+
return purgeResult(value, 0, true);
|
|
662
|
+
}
|
|
663
|
+
const row = target(database, input);
|
|
664
|
+
if (!row)
|
|
665
|
+
throw new KiokukoError('NOT_FOUND', PURGE_NOT_FOUND);
|
|
666
|
+
const result = executePurge(database, input, row);
|
|
667
|
+
return purgeResult(result.value, result.count, false);
|
|
668
|
+
}
|
|
669
|
+
function normalizePurgeError(error) { if (error instanceof KiokukoError)
|
|
670
|
+
throw error; throw new KiokukoError('DATABASE_ERROR', 'Ledger purge database operation failed'); }
|
|
671
|
+
export function purgeLedgerTargetInTransaction(database, input) { const value = normalizePurge(input); try {
|
|
672
|
+
return purgeInTransaction(database, value);
|
|
673
|
+
}
|
|
674
|
+
catch (error) {
|
|
675
|
+
normalizePurgeError(error);
|
|
676
|
+
} }
|
|
677
|
+
export function purgeLedgerTarget(database, input) { const value = normalizePurge(input); try {
|
|
678
|
+
return withImmediateTransaction(database, () => purgeInTransaction(database, value));
|
|
679
|
+
}
|
|
680
|
+
catch (error) {
|
|
681
|
+
normalizePurgeError(error);
|
|
682
|
+
} }
|
|
683
|
+
//# sourceMappingURL=maintenance.js.map
|