@bitkyc08/opencodex 2.11.1 → 2.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/bin/ocx.mjs +4 -0
- package/gui/dist/assets/index-BNVYzdn0.css +1 -0
- package/gui/dist/assets/index-Cw1_mxO8.js +70 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/command-code.ts +94 -5
- package/src/adapters/cursor/live-transport.ts +14 -1
- package/src/adapters/google-tool-schema.ts +80 -15
- package/src/adapters/google.ts +24 -7
- package/src/adapters/openai-chat.ts +233 -305
- package/src/adapters/openai-responses.ts +62 -0
- package/src/adapters/tool-catalog-nudge.ts +2 -11
- package/src/claude/outbound.ts +42 -14
- package/src/cli/claude.ts +61 -9
- package/src/cli/codex-shim-autorestore.ts +2 -0
- package/src/cli/codex-shim-readiness.ts +8 -1
- package/src/cli/config-command.ts +4 -1
- package/src/cli/doctor.ts +10 -0
- package/src/cli/help.ts +15 -0
- package/src/cli/index.ts +27 -11
- package/src/cli/lab.ts +367 -0
- package/src/cli/provider.ts +9 -1
- package/src/clients/config-export.ts +100 -3
- package/src/codex/app-server-processes.ts +11 -8
- package/src/codex/auth-api.ts +92 -28
- package/src/codex/auth-collision.ts +5 -3
- package/src/codex/catalog/metadata.ts +7 -3
- package/src/codex/catalog/provider-fetch.ts +191 -47
- package/src/codex/catalog.ts +2 -2
- package/src/codex/history-job.ts +83 -18
- package/src/codex/history-migration-guardian.ts +9 -16
- package/src/codex/history-provider.ts +178 -1
- package/src/codex/history-transition.ts +5 -5
- package/src/codex/history-worker.ts +22 -2
- package/src/codex/native-profile-store.ts +1 -1
- package/src/codex/plan.ts +15 -0
- package/src/codex/quota.ts +6 -6
- package/src/codex/routing.ts +3 -3
- package/src/codex/subagent-model-fallback.ts +246 -22
- package/src/codex/user-identity.ts +63 -12
- package/src/config.ts +265 -32
- package/src/images/loop.ts +3 -1
- package/src/images/plan.ts +23 -13
- package/src/integrations/omp-yaml-source.ts +225 -0
- package/src/integrations/ownership.ts +5 -5
- package/src/integrations/state.ts +45 -6
- package/src/integrations/writer.ts +38 -3
- package/src/lab/artifacts/sanitize.ts +586 -0
- package/src/lab/artifacts/secure-fs.ts +475 -0
- package/src/lab/artifacts/store.ts +310 -0
- package/src/lab/conformance/assertion.ts +325 -0
- package/src/lab/conformance/digest.ts +22 -0
- package/src/lab/conformance/executor.ts +741 -0
- package/src/lab/conformance/fixture-provider.ts +27 -0
- package/src/lab/conformance/fixtures/live-v1-cases.json +175 -0
- package/src/lab/conformance/fixtures/protocol-v1-cases.json +461 -0
- package/src/lab/conformance/harness-budget.ts +47 -0
- package/src/lab/conformance/index.ts +5 -0
- package/src/lab/conformance/jcs.ts +24 -0
- package/src/lab/conformance/json-pointer.ts +39 -0
- package/src/lab/conformance/manifest.ts +180 -0
- package/src/lab/conformance/mcp-stub.ts +179 -0
- package/src/lab/conformance/negative-controls.ts +160 -0
- package/src/lab/conformance/observation.ts +355 -0
- package/src/lab/conformance/runner.ts +57 -0
- package/src/lab/conformance/sse-normalize.ts +59 -0
- package/src/lab/conformance/suite-manifest.ts +78 -0
- package/src/lab/conformance/types.ts +191 -0
- package/src/lab/constants.ts +126 -0
- package/src/lab/digest.ts +64 -0
- package/src/lab/events/errors.ts +9 -0
- package/src/lab/events/limits.ts +117 -0
- package/src/lab/events/types.ts +228 -0
- package/src/lab/events/validate.ts +780 -0
- package/src/lab/index.ts +36 -0
- package/src/lab/ledger/artifact-refs.ts +127 -0
- package/src/lab/ledger/invalidation.ts +136 -0
- package/src/lab/ledger/purge.ts +241 -0
- package/src/lab/ledger/store.ts +263 -0
- package/src/lab/live/credential-lease.ts +53 -0
- package/src/lab/live/destination.ts +155 -0
- package/src/lab/live/executor.ts +311 -0
- package/src/lab/live/inert-tools.ts +56 -0
- package/src/lab/live/manifest.ts +85 -0
- package/src/lab/live/mcp-loopback.ts +57 -0
- package/src/lab/live/runner.ts +19 -0
- package/src/lab/live/sandbox.ts +61 -0
- package/src/lab/live/suite-manifest.ts +41 -0
- package/src/lab/live/transport.ts +116 -0
- package/src/lab/live/types.ts +197 -0
- package/src/lab/observe/from-conformance.ts +344 -0
- package/src/lab/observe/from-live.ts +103 -0
- package/src/lab/paths.ts +65 -0
- package/src/lab/projection/rebuild.ts +477 -0
- package/src/lab/projection/schema.ts +122 -0
- package/src/lab/projection/verdicts.ts +438 -0
- package/src/lab/projection/verification.ts +332 -0
- package/src/lab/query/catalog.ts +72 -0
- package/src/lab/query/connection.ts +107 -0
- package/src/lab/query/constants.ts +4 -0
- package/src/lab/query/cursor.ts +132 -0
- package/src/lab/query/dto-map.ts +277 -0
- package/src/lab/query/errors.ts +22 -0
- package/src/lab/query/index.ts +34 -0
- package/src/lab/query/queries.ts +444 -0
- package/src/lab/query/types.ts +266 -0
- package/src/lab/subject/behavior-fingerprint.ts +77 -0
- package/src/lab/subject/installation-salt.ts +81 -0
- package/src/lab/subject/route-subject.ts +73 -0
- package/src/lib/bounded-body.ts +104 -0
- package/src/lib/lab-live-execution-authority.ts +13 -0
- package/src/lib/lab-live-host.ts +30 -0
- package/src/lib/lab-live-pinned-sender.ts +45 -0
- package/src/lib/pinned-http.ts +36 -7
- package/src/oauth/index.ts +20 -1
- package/src/oauth/local-token-detect.ts +11 -2
- package/src/oauth/login-cli.ts +38 -4
- package/src/providers/alibaba-region-migration.ts +1 -1
- package/src/providers/codex-capacity.ts +7 -4
- package/src/providers/command-code-efforts.ts +5 -2
- package/src/providers/derive.ts +105 -0
- package/src/providers/label.ts +1 -1
- package/src/providers/openai-tiers.ts +59 -3
- package/src/providers/opencode-zen-rate-limit.ts +102 -0
- package/src/providers/quota.ts +67 -21
- package/src/providers/registry.ts +118 -5
- package/src/router.ts +15 -1
- package/src/routing/analytics.ts +8 -7
- package/src/routing/evaluator.ts +51 -5
- package/src/routing/profile.ts +10 -1
- package/src/routing/trace.ts +42 -6
- package/src/server/auth-cors.ts +15 -0
- package/src/server/background-lifecycle.ts +169 -0
- package/src/server/chat-completions.ts +18 -3
- package/src/server/claude-messages.ts +1 -0
- package/src/server/images.ts +78 -11
- package/src/server/index.ts +72 -18
- package/src/server/lifecycle.ts +6 -0
- package/src/server/live.ts +5 -1
- package/src/server/management/agent-settings-routes.ts +41 -24
- package/src/server/management/body.ts +6 -0
- package/src/server/management/config-routes.ts +51 -8
- package/src/server/management/lab-routes.ts +388 -0
- package/src/server/management/logs-usage-routes.ts +19 -1
- package/src/server/management/model-rows.ts +25 -11
- package/src/server/management/native-integration-routes.ts +3 -3
- package/src/server/management/provider-routes.ts +5 -0
- package/src/server/management/shared.ts +8 -1
- package/src/server/management/usage-summary-cache.ts +2 -0
- package/src/server/management/vision-sidecar-options.ts +125 -0
- package/src/server/management-api.ts +2 -0
- package/src/server/request-decompress.ts +106 -5
- package/src/server/responses/collaboration.ts +4 -1
- package/src/server/responses/core.ts +117 -31
- package/src/server/responses-json-events.ts +50 -12
- package/src/server/search.ts +23 -7
- package/src/server/ws-bridge.ts +3 -1
- package/src/service.ts +40 -20
- package/src/types.ts +74 -0
- package/src/update/job.ts +10 -2
- package/src/usage/cost.ts +0 -0
- package/src/usage/expected-prices.ts +9 -0
- package/src/usage/log.ts +9 -9
- package/src/usage/user-cost-overlay-reconciler.ts +313 -0
- package/src/usage/user-cost-overlays.ts +314 -0
- package/src/vision/describe.ts +2 -2
- package/src/vision/eligibility.ts +229 -0
- package/src/vision/index.ts +20 -2
- package/src/web-search/executor.ts +2 -2
- package/src/web-search/index.ts +4 -2
- package/src/web-search/loop.ts +3 -1
- package/src/web-search/parse.ts +72 -3
- package/gui/dist/assets/index-BA1dgo4q.css +0 -1
- package/gui/dist/assets/index-DDZpgzKk.js +0 -70
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// CL-03 live result -> CL-02 immutable observation persistence.
|
|
2
|
+
import { createArtifactStore, type ArtifactStore } from "../artifacts/store";
|
|
3
|
+
import { sanitizeDiagnostic, truncateUtf8 } from "../artifacts/sanitize";
|
|
4
|
+
import { LAB_EVENT_SCHEMA_VERSION, LAB_PRODUCER, LAB_PRODUCER_VERSION, OBSERVATION_LIMIT_NAMES, type ObservationOutcome } from "../constants";
|
|
5
|
+
import { fixtureDigest, scenarioManifestDigest, subjectIdForSubject, suiteManifestDigest } from "../digest";
|
|
6
|
+
import type { FailureRecordV1, ObservationEvent } from "../events/types";
|
|
7
|
+
import { assignEventId } from "../events/validate";
|
|
8
|
+
import { appendLabEvent } from "../ledger/store";
|
|
9
|
+
import { ensureLabDirs } from "../paths";
|
|
10
|
+
import type { CaseAuthority, CaseRecord } from "../conformance/types";
|
|
11
|
+
import { trustedLiveResultRetryable } from "../live/executor";
|
|
12
|
+
import { expandLiveScenario } from "../live/manifest";
|
|
13
|
+
import { liveSuiteManifestObjectForCase } from "../live/suite-manifest";
|
|
14
|
+
import type { LiveScenarioRunResult } from "../live/types";
|
|
15
|
+
|
|
16
|
+
export interface PersistLiveOptions { configDir?: string; recordedAt?: number; startedAt?: number; completedAt?: number; producerVersion?: string; artifactStore?: ArtifactStore }
|
|
17
|
+
export interface PersistedLiveObservation { event: ObservationEvent; ledgerPath: string }
|
|
18
|
+
|
|
19
|
+
function outcomeFromLiveResult(result: LiveScenarioRunResult): ObservationOutcome {
|
|
20
|
+
if (result.passed) return "pass";
|
|
21
|
+
switch (result.classification) {
|
|
22
|
+
case "timeout":
|
|
23
|
+
case "budget_exhausted":
|
|
24
|
+
case "authentication_blocked":
|
|
25
|
+
case "quota_blocked":
|
|
26
|
+
case "region_blocked":
|
|
27
|
+
case "network_failure":
|
|
28
|
+
case "provider_transient":
|
|
29
|
+
return "blocked";
|
|
30
|
+
case "protocol_failure":
|
|
31
|
+
case "capability_failure":
|
|
32
|
+
case "behavioral_failure":
|
|
33
|
+
return "fail";
|
|
34
|
+
case "inconclusive":
|
|
35
|
+
case "harness_failure":
|
|
36
|
+
return "inconclusive";
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function failureFromLiveResult(result: LiveScenarioRunResult, retryable: boolean): FailureRecordV1 | undefined {
|
|
41
|
+
if (result.passed || result.classification === "inconclusive") return undefined;
|
|
42
|
+
const attribution: FailureRecordV1["attribution"] = result.classification === "harness_failure"
|
|
43
|
+
? "harness"
|
|
44
|
+
: ["authentication_blocked", "quota_blocked", "region_blocked", "network_failure", "provider_transient", "timeout", "budget_exhausted"].includes(result.classification)
|
|
45
|
+
? "environment"
|
|
46
|
+
: "route";
|
|
47
|
+
return {
|
|
48
|
+
class: result.classification,
|
|
49
|
+
code: result.secondaryCode ?? result.classification,
|
|
50
|
+
retryable,
|
|
51
|
+
attribution,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function requireExecutionTimes(result: LiveScenarioRunResult, opts: PersistLiveOptions): { startedAt: number; completedAt: number } {
|
|
56
|
+
const startedAt = opts.startedAt ?? result.startedAt; const completedAt = opts.completedAt ?? result.completedAt;
|
|
57
|
+
if (!Number.isInteger(startedAt) || !Number.isInteger(completedAt) || startedAt < 0 || completedAt < startedAt) throw new Error("invalid persisted live execution timestamps");
|
|
58
|
+
return { startedAt, completedAt };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function observationFromLiveResult(result: LiveScenarioRunResult, caseRecord: CaseRecord, authority: CaseAuthority, opts: PersistLiveOptions = {}): { event: ObservationEvent; artifacts: ReturnType<ArtifactStore["put"]>[] } {
|
|
62
|
+
const retryable = trustedLiveResultRetryable(result, caseRecord, authority);
|
|
63
|
+
if (!result.routeSubject) throw new Error("live evidence without an exact RouteSubjectV1 is not persistable");
|
|
64
|
+
const { startedAt, completedAt } = requireExecutionTimes(result, opts);
|
|
65
|
+
const paths = ensureLabDirs(opts.configDir); const ownsStore = !opts.artifactStore; const store = opts.artifactStore ?? createArtifactStore(paths.artifactsDir);
|
|
66
|
+
try {
|
|
67
|
+
const recordedAt = opts.recordedAt ?? completedAt;
|
|
68
|
+
const expandedScenario = expandLiveScenario(caseRecord, authority); const scenarioDigest = scenarioManifestDigest(expandedScenario);
|
|
69
|
+
const suiteExpanded = liveSuiteManifestObjectForCase(caseRecord, authority); const suiteDigest = suiteManifestDigest(suiteExpanded);
|
|
70
|
+
const fixtureDigests: string[] = []; const artifacts: ReturnType<ArtifactStore["put"]>[] = [];
|
|
71
|
+
const putFixture = (fixture: CaseRecord["fixture"]) => {
|
|
72
|
+
const bytes = new TextEncoder().encode(fixture.bytesUtf8); const digest = fixtureDigest(bytes); fixtureDigests.push(digest);
|
|
73
|
+
artifacts.push(store.put({ artifactClass: "fixture", payload: bytes, expectedDigest: digest, mediaType: fixture.mediaType }));
|
|
74
|
+
};
|
|
75
|
+
putFixture(caseRecord.fixture); if (caseRecord.initiatingRequest) putFixture(caseRecord.initiatingRequest);
|
|
76
|
+
artifacts.push(store.put({ artifactClass: "scenario_manifest", payload: expandedScenario, expectedDigest: scenarioDigest }));
|
|
77
|
+
artifacts.push(store.put({ artifactClass: "suite_manifest", payload: suiteExpanded, expectedDigest: suiteDigest }));
|
|
78
|
+
artifacts.push(store.put({ artifactClass: "assertion_report", payload: { scenarioId: result.scenarioId, passed: result.passed, classification: result.classification,
|
|
79
|
+
assertions: result.assertionResults.map((row) => ({ id: row.id, operator: row.operator, required: row.required, passed: row.passed, observedSummary: row.observedSummary, reason: row.reason })) } }));
|
|
80
|
+
const subject = result.routeSubject; const subjectId = subjectIdForSubject(subject); const authorityLimits = authority.manifestDefaults.executionLimits;
|
|
81
|
+
const limits: Record<string, number | null> = {}; for (const key of OBSERVATION_LIMIT_NAMES) if (key in authorityLimits) limits[key] = authorityLimits[key] ?? null;
|
|
82
|
+
const failure = failureFromLiveResult(result, retryable);
|
|
83
|
+
const eventWithoutId = {
|
|
84
|
+
schemaVersion: LAB_EVENT_SCHEMA_VERSION, eventKind: "observation" as const, recordedAt, producer: LAB_PRODUCER, producerVersion: opts.producerVersion ?? LAB_PRODUCER_VERSION,
|
|
85
|
+
evidenceLayer: "live_route_compatibility" as const, scenarioId: caseRecord.id, scenarioVersion: String(authority.manifestDefaults.version), scenarioManifestDigest: scenarioDigest,
|
|
86
|
+
suiteId: caseRecord.suite, suiteVersion: String(authority.manifestDefaults.suiteVersion), suiteManifestDigest: suiteDigest, fixtureDigests, subject, subjectId,
|
|
87
|
+
startedAt, completedAt, executionMode: "live" as const, attempt: 1, limits, outcome: outcomeFromLiveResult(result),
|
|
88
|
+
// Sanitize before truncating: the reverse order can split a redaction
|
|
89
|
+
// target and persist a fragment of the raw value.
|
|
90
|
+
assertions: result.assertionResults.map((row) => ({ id: row.id, operator: row.operator, required: row.required, passed: row.passed, expectedSummary: "see_assertion_report", observedSummary: truncateUtf8(sanitizeDiagnostic(row.observedSummary), 512), ...(row.reason ? { reason: row.reason } : {}) })),
|
|
91
|
+
...(caseRecord.expectedFailure ? { expectedFailure: { ...caseRecord.expectedFailure } } : {}),
|
|
92
|
+
environment: { runtime: { platform: process.platform, arch: process.arch, bunVersion: process.versions.bun ?? Bun.version } }, artifactRefs: artifacts,
|
|
93
|
+
...(failure ? { failure } : {}),
|
|
94
|
+
};
|
|
95
|
+
return { event: assignEventId(eventWithoutId) as ObservationEvent, artifacts };
|
|
96
|
+
} finally { if (ownsStore) store.close(); }
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function persistLiveResult(result: LiveScenarioRunResult, caseRecord: CaseRecord, authority: CaseAuthority, opts: PersistLiveOptions = {}): PersistedLiveObservation {
|
|
100
|
+
const paths = ensureLabDirs(opts.configDir); const ownsStore = !opts.artifactStore; const store = opts.artifactStore ?? createArtifactStore(paths.artifactsDir);
|
|
101
|
+
try { const { event } = observationFromLiveResult(result, caseRecord, authority, { ...opts, artifactStore: store }); appendLabEvent(paths.ledgerPath, event); return { event, ledgerPath: paths.ledgerPath }; }
|
|
102
|
+
finally { if (ownsStore) store.close(); }
|
|
103
|
+
}
|
package/src/lab/paths.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { chmodSync, mkdirSync, statSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { getConfigDir } from "../config";
|
|
4
|
+
|
|
5
|
+
function ensureRestrictedDir(dir: string): void {
|
|
6
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
7
|
+
if (process.platform === "win32") return;
|
|
8
|
+
const mode = statSync(dir).mode & 0o777;
|
|
9
|
+
if (mode !== 0o700) chmodSync(dir, 0o700);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Canonical Compatibility Lab state root under the OpenCodex config dir. */
|
|
13
|
+
export function labRoot(configDir = getConfigDir()): string {
|
|
14
|
+
return join(configDir, "lab");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function labLedgerPath(configDir = getConfigDir()): string {
|
|
18
|
+
return join(labRoot(configDir), "compatibility.jsonl");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function labSqlitePath(configDir = getConfigDir()): string {
|
|
22
|
+
return join(labRoot(configDir), "compatibility.sqlite");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function labArtifactsDir(configDir = getConfigDir()): string {
|
|
26
|
+
return join(labRoot(configDir), "artifacts");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function labScratchDir(configDir = getConfigDir()): string {
|
|
30
|
+
return join(labRoot(configDir), "scratch");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function labExportDir(configDir = getConfigDir()): string {
|
|
34
|
+
return join(labRoot(configDir), "export");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Opaque per-installation salt for local fingerprinting (never exported as evidence). */
|
|
38
|
+
export function labInstallationSaltPath(configDir = getConfigDir()): string {
|
|
39
|
+
return join(labRoot(configDir), "installation-salt.bin");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Ensure lab directories exist with restrictive permissions where the platform allows. */
|
|
43
|
+
export function ensureLabDirs(configDir = getConfigDir()): {
|
|
44
|
+
root: string;
|
|
45
|
+
ledgerPath: string;
|
|
46
|
+
sqlitePath: string;
|
|
47
|
+
artifactsDir: string;
|
|
48
|
+
scratchDir: string;
|
|
49
|
+
exportDir: string;
|
|
50
|
+
} {
|
|
51
|
+
const root = labRoot(configDir);
|
|
52
|
+
const artifactsDir = labArtifactsDir(configDir);
|
|
53
|
+
const scratchDir = labScratchDir(configDir);
|
|
54
|
+
const exportDir = labExportDir(configDir);
|
|
55
|
+
ensureRestrictedDir(root);
|
|
56
|
+
ensureRestrictedDir(artifactsDir);
|
|
57
|
+
return {
|
|
58
|
+
root,
|
|
59
|
+
ledgerPath: labLedgerPath(configDir),
|
|
60
|
+
sqlitePath: labSqlitePath(configDir),
|
|
61
|
+
artifactsDir,
|
|
62
|
+
scratchDir,
|
|
63
|
+
exportDir,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
import { Database } from "bun:sqlite";
|
|
2
|
+
import { existsSync, unlinkSync } from "node:fs";
|
|
3
|
+
import { createArtifactStore, loadClaimSourceManifest } from "../artifacts/store";
|
|
4
|
+
import { ArtifactFsError } from "../artifacts/secure-fs";
|
|
5
|
+
import { sanitizeDiagnostic } from "../artifacts/sanitize";
|
|
6
|
+
import { LAB_PROJECTION_SPEC_VERSION } from "../constants";
|
|
7
|
+
import { expandScenario, loadCaseAuthority } from "../conformance/manifest";
|
|
8
|
+
import { scenarioManifestDigest, jcsStringify } from "../digest";
|
|
9
|
+
import {
|
|
10
|
+
parseSuiteManifestFromArtifact,
|
|
11
|
+
type ScenarioRequirements,
|
|
12
|
+
} from "./verification";
|
|
13
|
+
import type { ClaimSnapshotEvent, LabEvent, LedgerCorruption } from "../events/types";
|
|
14
|
+
import { buildInvalidationIndex, isEventExcluded } from "../ledger/invalidation";
|
|
15
|
+
import { replayLabLedger } from "../ledger/store";
|
|
16
|
+
import { ensureLabDirs } from "../paths";
|
|
17
|
+
import { LAB_SQLITE_DDL, LAB_SQLITE_SCHEMA_VERSION } from "./schema";
|
|
18
|
+
import {
|
|
19
|
+
claimKeyString,
|
|
20
|
+
excludeEventIds,
|
|
21
|
+
projectVerdicts,
|
|
22
|
+
projectionKeyString,
|
|
23
|
+
resolveClaimStates,
|
|
24
|
+
} from "./verdicts";
|
|
25
|
+
|
|
26
|
+
export interface RebuildResult {
|
|
27
|
+
events: number;
|
|
28
|
+
verdicts: number;
|
|
29
|
+
corruptions: LedgerCorruption[];
|
|
30
|
+
sqlitePath: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function wipeSqlite(path: string): void {
|
|
34
|
+
for (const candidate of [path, `${path}-wal`, `${path}-shm`]) {
|
|
35
|
+
let removed = false;
|
|
36
|
+
for (let attempt = 0; attempt < 8; attempt++) {
|
|
37
|
+
try {
|
|
38
|
+
if (existsSync(candidate)) unlinkSync(candidate);
|
|
39
|
+
removed = true;
|
|
40
|
+
break;
|
|
41
|
+
} catch (err) {
|
|
42
|
+
const code = err && typeof err === "object" && "code" in err
|
|
43
|
+
? String((err as { code: unknown }).code)
|
|
44
|
+
: "";
|
|
45
|
+
if (code !== "EBUSY" && code !== "EPERM") throw err;
|
|
46
|
+
if (attempt < 7) Bun.sleepSync(20 * (attempt + 1));
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (!removed) {
|
|
50
|
+
throw new Error(`failed to remove stale projection file after retries: ${candidate}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function resetProjectionSchema(db: Database): void {
|
|
56
|
+
db.exec(`
|
|
57
|
+
DROP TABLE IF EXISTS verdicts;
|
|
58
|
+
DROP TABLE IF EXISTS corruption;
|
|
59
|
+
DROP TABLE IF EXISTS artifacts;
|
|
60
|
+
DROP TABLE IF EXISTS purges;
|
|
61
|
+
DROP TABLE IF EXISTS invalidations;
|
|
62
|
+
DROP TABLE IF EXISTS claims;
|
|
63
|
+
DROP TABLE IF EXISTS observations;
|
|
64
|
+
DROP TABLE IF EXISTS subjects;
|
|
65
|
+
DROP TABLE IF EXISTS events;
|
|
66
|
+
DROP TABLE IF EXISTS schema_meta;
|
|
67
|
+
`);
|
|
68
|
+
db.exec(LAB_SQLITE_DDL);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
interface ArtifactValidationResult {
|
|
72
|
+
unusableObservationIds: Set<string>;
|
|
73
|
+
unusableClaimEventIds: Set<string>;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Deterministic rebuild:
|
|
78
|
+
* delete compatibility.sqlite → replay JSONL → validate artifacts → project.
|
|
79
|
+
*/
|
|
80
|
+
export function rebuildLabProjection(configDir?: string): RebuildResult {
|
|
81
|
+
const paths = ensureLabDirs(configDir);
|
|
82
|
+
wipeSqlite(paths.sqlitePath);
|
|
83
|
+
|
|
84
|
+
const replay = replayLabLedger(paths.ledgerPath);
|
|
85
|
+
const corruptions: LedgerCorruption[] = [...replay.corruptions];
|
|
86
|
+
const index = buildInvalidationIndex(replay.events);
|
|
87
|
+
corruptions.push(...index.corruptions);
|
|
88
|
+
|
|
89
|
+
const artifactStore = createArtifactStore(paths.artifactsDir);
|
|
90
|
+
const validation = validateRequiredArtifacts(replay.events, index, artifactStore, corruptions);
|
|
91
|
+
|
|
92
|
+
const authority = loadCaseAuthority();
|
|
93
|
+
const scenarioRequirementsByDigest = new Map<string, ScenarioRequirements>();
|
|
94
|
+
for (const caseRecord of authority.cases) {
|
|
95
|
+
const expanded = expandScenario(caseRecord, authority);
|
|
96
|
+
scenarioRequirementsByDigest.set(scenarioManifestDigest(expanded), {
|
|
97
|
+
inboundProtocols: [...caseRecord.requirements.inboundProtocols],
|
|
98
|
+
upstreamProtocols: [...caseRecord.requirements.upstreamProtocols],
|
|
99
|
+
surfaces: [...caseRecord.requirements.surfaces],
|
|
100
|
+
freshness: { ...authority.manifestDefaults.freshness },
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const loadSuiteManifest = (digest: string) => {
|
|
105
|
+
try {
|
|
106
|
+
const bytes = artifactStore.get(digest, { artifactClass: "suite_manifest" });
|
|
107
|
+
const parsed = JSON.parse(new TextDecoder().decode(bytes));
|
|
108
|
+
return parseSuiteManifestFromArtifact(parsed);
|
|
109
|
+
} catch {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
const loadScenarioManifest = (digest: string) => {
|
|
114
|
+
try {
|
|
115
|
+
const bytes = artifactStore.get(digest, { artifactClass: "scenario_manifest" });
|
|
116
|
+
return JSON.parse(new TextDecoder().decode(bytes)) as Record<string, unknown>;
|
|
117
|
+
} catch {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
const loadScenarioRequirements = (digest: string) => scenarioRequirementsByDigest.get(digest) ?? null;
|
|
122
|
+
|
|
123
|
+
const db = new Database(paths.sqlitePath);
|
|
124
|
+
let transactionOpen = false;
|
|
125
|
+
try {
|
|
126
|
+
db.exec("PRAGMA journal_mode=DELETE;");
|
|
127
|
+
db.exec("PRAGMA foreign_keys=OFF;");
|
|
128
|
+
db.exec("BEGIN IMMEDIATE;");
|
|
129
|
+
transactionOpen = true;
|
|
130
|
+
resetProjectionSchema(db);
|
|
131
|
+
|
|
132
|
+
db.prepare(
|
|
133
|
+
"INSERT OR REPLACE INTO schema_meta(key, value) VALUES (?, ?)",
|
|
134
|
+
).run("schema_version", String(LAB_SQLITE_SCHEMA_VERSION));
|
|
135
|
+
db.prepare(
|
|
136
|
+
"INSERT OR REPLACE INTO schema_meta(key, value) VALUES (?, ?)",
|
|
137
|
+
).run("projection_spec_version", LAB_PROJECTION_SPEC_VERSION);
|
|
138
|
+
db.prepare(
|
|
139
|
+
"INSERT OR REPLACE INTO schema_meta(key, value) VALUES (?, ?)",
|
|
140
|
+
).run("built_at_ms", String(Date.now()));
|
|
141
|
+
|
|
142
|
+
const insertCorruption = db.prepare(
|
|
143
|
+
"INSERT INTO corruption(kind, line_number, event_id, detail) VALUES (?, ?, ?, ?)",
|
|
144
|
+
);
|
|
145
|
+
for (const c of corruptions) {
|
|
146
|
+
insertCorruption.run(c.kind, c.lineNumber ?? null, c.eventId ?? null, c.detail);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const insertEvent = db.prepare(
|
|
150
|
+
`INSERT INTO events(event_id, event_kind, recorded_at, producer, producer_version, payload_json, excluded, exclusion_reason)
|
|
151
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
152
|
+
);
|
|
153
|
+
const insertSubject = db.prepare(
|
|
154
|
+
`INSERT OR IGNORE INTO subjects(subject_id, subject_kind, subject_json) VALUES (?, ?, ?)`,
|
|
155
|
+
);
|
|
156
|
+
const insertObs = db.prepare(
|
|
157
|
+
`INSERT INTO observations(
|
|
158
|
+
event_id, subject_id, evidence_layer, suite_id, suite_version, suite_manifest_digest,
|
|
159
|
+
scenario_id, scenario_version, scenario_manifest_digest, outcome, completed_at, execution_mode
|
|
160
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
161
|
+
);
|
|
162
|
+
const insertClaim = db.prepare(
|
|
163
|
+
`INSERT INTO claims(
|
|
164
|
+
event_id, subject_id, capability, polarity, source_manifest_digest,
|
|
165
|
+
effective_at, recorded_at, supersedes_json, current, usable
|
|
166
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
167
|
+
);
|
|
168
|
+
const insertInv = db.prepare(
|
|
169
|
+
`INSERT INTO invalidations(event_id, reason, targets_json, recorded_at, applied) VALUES (?, ?, ?, ?, ?)`,
|
|
170
|
+
);
|
|
171
|
+
const insertPurge = db.prepare(
|
|
172
|
+
`INSERT INTO purges(event_id, target_event_ids_json, target_artifact_digests_json, purge_actions_json, recorded_at)
|
|
173
|
+
VALUES (?, ?, ?, ?, ?)`,
|
|
174
|
+
);
|
|
175
|
+
const insertArtifact = db.prepare(
|
|
176
|
+
`INSERT INTO artifacts(digest, artifact_class, media_type, byte_count, status, last_error)
|
|
177
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
178
|
+
ON CONFLICT(digest) DO UPDATE SET
|
|
179
|
+
artifact_class = COALESCE(excluded.artifact_class, artifacts.artifact_class),
|
|
180
|
+
media_type = COALESCE(excluded.media_type, artifacts.media_type),
|
|
181
|
+
byte_count = COALESCE(excluded.byte_count, artifacts.byte_count),
|
|
182
|
+
status = CASE
|
|
183
|
+
WHEN artifacts.status = 'purged_unavailable' OR excluded.status = 'purged_unavailable'
|
|
184
|
+
THEN 'purged_unavailable'
|
|
185
|
+
WHEN artifacts.status = 'corrupt' OR excluded.status = 'corrupt'
|
|
186
|
+
THEN 'corrupt'
|
|
187
|
+
ELSE 'present'
|
|
188
|
+
END,
|
|
189
|
+
last_error = COALESCE(excluded.last_error, artifacts.last_error)`,
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
const excluded = excludeEventIds(index);
|
|
193
|
+
const usableClaimEvents = replay.events.filter(
|
|
194
|
+
(e): e is ClaimSnapshotEvent =>
|
|
195
|
+
e.eventKind === "claim_snapshot" && !isEventExcluded(e.eventId, index),
|
|
196
|
+
);
|
|
197
|
+
const claimStates = resolveClaimStates(usableClaimEvents, {
|
|
198
|
+
unusableClaimEventIds: validation.unusableClaimEventIds,
|
|
199
|
+
purgedEventIds: index.purgedEventIds,
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
for (const event of replay.events) {
|
|
203
|
+
const isExcluded = excluded.has(event.eventId);
|
|
204
|
+
let exclusionReason: string | null = null;
|
|
205
|
+
if (index.purgedEventIds.has(event.eventId)) exclusionReason = "purged";
|
|
206
|
+
else if (index.invalidatedBy.has(event.eventId)) exclusionReason = "invalidated";
|
|
207
|
+
|
|
208
|
+
insertEvent.run(
|
|
209
|
+
event.eventId,
|
|
210
|
+
event.eventKind,
|
|
211
|
+
event.recordedAt,
|
|
212
|
+
event.producer,
|
|
213
|
+
event.producerVersion,
|
|
214
|
+
jcsStringify(event),
|
|
215
|
+
isExcluded ? 1 : 0,
|
|
216
|
+
exclusionReason,
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
if (event.eventKind === "observation") {
|
|
220
|
+
insertSubject.run(event.subjectId, event.subject.subjectKind, jcsStringify(event.subject));
|
|
221
|
+
const usable = !isExcluded && !validation.unusableObservationIds.has(event.eventId);
|
|
222
|
+
if (usable) {
|
|
223
|
+
insertObs.run(
|
|
224
|
+
event.eventId,
|
|
225
|
+
event.subjectId,
|
|
226
|
+
event.evidenceLayer,
|
|
227
|
+
event.suiteId,
|
|
228
|
+
event.suiteVersion,
|
|
229
|
+
event.suiteManifestDigest,
|
|
230
|
+
event.scenarioId,
|
|
231
|
+
event.scenarioVersion,
|
|
232
|
+
event.scenarioManifestDigest,
|
|
233
|
+
event.outcome,
|
|
234
|
+
event.completedAt,
|
|
235
|
+
event.executionMode,
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
for (const ref of event.artifactRefs) {
|
|
239
|
+
const purged = index.purgedArtifactDigests.has(ref.digest);
|
|
240
|
+
const corrupt = validation.unusableObservationIds.has(event.eventId);
|
|
241
|
+
insertArtifact.run(
|
|
242
|
+
ref.digest,
|
|
243
|
+
ref.artifactClass,
|
|
244
|
+
ref.mediaType,
|
|
245
|
+
ref.byteCount,
|
|
246
|
+
purged ? "purged_unavailable" : corrupt ? "corrupt" : "present",
|
|
247
|
+
corrupt ? "required artifact unusable" : null,
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
} else if (event.eventKind === "claim_snapshot") {
|
|
251
|
+
insertSubject.run(event.subjectId, event.subject.subjectKind, jcsStringify(event.subject));
|
|
252
|
+
const key = claimKeyString(event.subjectId, event.capability);
|
|
253
|
+
const state = claimStates.states.get(key);
|
|
254
|
+
const current = state?.current?.eventId === event.eventId ? 1 : 0;
|
|
255
|
+
const claimCorruption = corruptions.find(
|
|
256
|
+
(c) => c.kind === "claim_corruption" && c.eventId === event.eventId,
|
|
257
|
+
);
|
|
258
|
+
const usable = !isExcluded && !state?.corruption &&
|
|
259
|
+
!validation.unusableClaimEventIds.has(event.eventId) ? 1 : 0;
|
|
260
|
+
|
|
261
|
+
if (!isExcluded) {
|
|
262
|
+
insertArtifact.run(
|
|
263
|
+
event.sourceManifestDigest,
|
|
264
|
+
"claim_source_manifest",
|
|
265
|
+
"application/json",
|
|
266
|
+
null,
|
|
267
|
+
claimCorruption ? "corrupt" : "present",
|
|
268
|
+
claimCorruption?.detail ?? null,
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
insertClaim.run(
|
|
273
|
+
event.eventId,
|
|
274
|
+
event.subjectId,
|
|
275
|
+
event.capability,
|
|
276
|
+
event.polarity,
|
|
277
|
+
event.sourceManifestDigest,
|
|
278
|
+
event.effectiveAt,
|
|
279
|
+
event.recordedAt,
|
|
280
|
+
jcsStringify(event.supersedes),
|
|
281
|
+
current,
|
|
282
|
+
usable,
|
|
283
|
+
);
|
|
284
|
+
} else if (event.eventKind === "invalidation") {
|
|
285
|
+
const applied = !corruptions.some((c) => c.eventId === event.eventId && c.kind === "invalid_reference");
|
|
286
|
+
insertInv.run(
|
|
287
|
+
event.eventId,
|
|
288
|
+
event.reason,
|
|
289
|
+
jcsStringify(event.targetEventIds),
|
|
290
|
+
event.recordedAt,
|
|
291
|
+
applied ? 1 : 0,
|
|
292
|
+
);
|
|
293
|
+
} else if (event.eventKind === "purge_tombstone") {
|
|
294
|
+
insertPurge.run(
|
|
295
|
+
event.eventId,
|
|
296
|
+
jcsStringify(event.targetEventIds),
|
|
297
|
+
jcsStringify(event.targetArtifactDigests),
|
|
298
|
+
jcsStringify(event.purgeActions),
|
|
299
|
+
event.recordedAt,
|
|
300
|
+
);
|
|
301
|
+
for (const digest of event.targetArtifactDigests) {
|
|
302
|
+
insertArtifact.run(digest, null, null, null, "purged_unavailable", null);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const { verdicts, corruptions: verdictCorruptions } = projectVerdicts(replay.events, {
|
|
308
|
+
index,
|
|
309
|
+
unusableObservationIds: validation.unusableObservationIds,
|
|
310
|
+
unusableClaimEventIds: validation.unusableClaimEventIds,
|
|
311
|
+
loadSuiteManifest,
|
|
312
|
+
loadScenarioManifest,
|
|
313
|
+
loadScenarioRequirements,
|
|
314
|
+
});
|
|
315
|
+
for (const c of verdictCorruptions) {
|
|
316
|
+
if (!corruptions.some((x) => x.detail === c.detail && x.eventId === c.eventId)) {
|
|
317
|
+
corruptions.push(c);
|
|
318
|
+
insertCorruption.run(c.kind, c.lineNumber ?? null, c.eventId ?? null, c.detail);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const insertVerdict = db.prepare(
|
|
323
|
+
`INSERT INTO verdicts(
|
|
324
|
+
projection_key, subject_id, evidence_layer, suite_id, suite_version, suite_manifest_digest,
|
|
325
|
+
projection_spec_version, verdict, as_of, scenario_manifest_digests_json, claim_source_digest,
|
|
326
|
+
contributing_event_ids_json, contradicting_event_ids_json, notes_json
|
|
327
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
328
|
+
);
|
|
329
|
+
|
|
330
|
+
for (const v of verdicts) {
|
|
331
|
+
insertVerdict.run(
|
|
332
|
+
projectionKeyString(v.key),
|
|
333
|
+
v.key.subjectId,
|
|
334
|
+
v.key.evidenceLayer,
|
|
335
|
+
v.key.suiteId,
|
|
336
|
+
v.key.suiteVersion,
|
|
337
|
+
v.key.suiteManifestDigest,
|
|
338
|
+
v.key.projectionSpecVersion,
|
|
339
|
+
v.verdict,
|
|
340
|
+
v.asOf,
|
|
341
|
+
jcsStringify(v.scenarioManifestDigests),
|
|
342
|
+
v.claimSourceDigest ?? null,
|
|
343
|
+
jcsStringify(v.contributingEventIds),
|
|
344
|
+
jcsStringify(v.contradictingEventIds),
|
|
345
|
+
jcsStringify(v.notes),
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
db.exec("COMMIT;");
|
|
350
|
+
transactionOpen = false;
|
|
351
|
+
return {
|
|
352
|
+
events: replay.events.length,
|
|
353
|
+
verdicts: verdicts.length,
|
|
354
|
+
corruptions,
|
|
355
|
+
sqlitePath: paths.sqlitePath,
|
|
356
|
+
};
|
|
357
|
+
} catch (err) {
|
|
358
|
+
if (transactionOpen) {
|
|
359
|
+
try {
|
|
360
|
+
db.exec("ROLLBACK;");
|
|
361
|
+
} catch {
|
|
362
|
+
// Preserve the original rebuild failure.
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
throw err;
|
|
366
|
+
} finally {
|
|
367
|
+
try {
|
|
368
|
+
db.exec("PRAGMA foreign_keys=ON;");
|
|
369
|
+
} catch {
|
|
370
|
+
// Closing the disposable DB is still safe if pragma restoration fails.
|
|
371
|
+
}
|
|
372
|
+
db.close();
|
|
373
|
+
artifactStore.close();
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function validateRequiredArtifacts(
|
|
378
|
+
events: LabEvent[],
|
|
379
|
+
index: ReturnType<typeof buildInvalidationIndex>,
|
|
380
|
+
artifactStore: ReturnType<typeof createArtifactStore>,
|
|
381
|
+
corruptions: LedgerCorruption[],
|
|
382
|
+
): ArtifactValidationResult {
|
|
383
|
+
const unusableObservationIds = new Set<string>();
|
|
384
|
+
const unusableClaimEventIds = new Set<string>();
|
|
385
|
+
|
|
386
|
+
for (const event of events) {
|
|
387
|
+
if (isEventExcluded(event.eventId, index)) continue;
|
|
388
|
+
if (event.eventKind === "observation") {
|
|
389
|
+
const required: Array<{ digest: string; artifactClass: "scenario_manifest" | "suite_manifest" | "fixture" }> = [
|
|
390
|
+
{ digest: event.scenarioManifestDigest, artifactClass: "scenario_manifest" },
|
|
391
|
+
{ digest: event.suiteManifestDigest, artifactClass: "suite_manifest" },
|
|
392
|
+
...event.fixtureDigests.map((digest) => ({ digest, artifactClass: "fixture" as const })),
|
|
393
|
+
];
|
|
394
|
+
let unusable = false;
|
|
395
|
+
for (const { digest, artifactClass } of required) {
|
|
396
|
+
if (index.purgedArtifactDigests.has(digest)) {
|
|
397
|
+
corruptions.push({
|
|
398
|
+
kind: "missing_artifact",
|
|
399
|
+
eventId: event.eventId,
|
|
400
|
+
detail: `required artifact purged: ${digest}`,
|
|
401
|
+
});
|
|
402
|
+
unusable = true;
|
|
403
|
+
continue;
|
|
404
|
+
}
|
|
405
|
+
try {
|
|
406
|
+
artifactStore.get(digest, { artifactClass });
|
|
407
|
+
} catch (err) {
|
|
408
|
+
const detail = sanitizeDiagnostic(err);
|
|
409
|
+
corruptions.push({
|
|
410
|
+
kind: err instanceof ArtifactFsError &&
|
|
411
|
+
(err.code === "artifact_mismatch" || err.message.includes("mismatch"))
|
|
412
|
+
? "artifact_mismatch"
|
|
413
|
+
: "missing_artifact",
|
|
414
|
+
eventId: event.eventId,
|
|
415
|
+
detail,
|
|
416
|
+
});
|
|
417
|
+
unusable = true;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
if (unusable) unusableObservationIds.add(event.eventId);
|
|
421
|
+
continue;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
if (event.eventKind === "claim_snapshot") {
|
|
425
|
+
if (index.purgedArtifactDigests.has(event.sourceManifestDigest)) {
|
|
426
|
+
unusableClaimEventIds.add(event.eventId);
|
|
427
|
+
corruptions.push({
|
|
428
|
+
kind: "claim_corruption",
|
|
429
|
+
eventId: event.eventId,
|
|
430
|
+
detail: `claim source artifact purged: ${event.sourceManifestDigest}`,
|
|
431
|
+
});
|
|
432
|
+
continue;
|
|
433
|
+
}
|
|
434
|
+
const loaded = loadClaimSourceManifest(artifactStore, event.sourceManifestDigest, {
|
|
435
|
+
subjectId: event.subjectId,
|
|
436
|
+
capability: event.capability,
|
|
437
|
+
});
|
|
438
|
+
if (!loaded.ok) {
|
|
439
|
+
unusableClaimEventIds.add(event.eventId);
|
|
440
|
+
corruptions.push({
|
|
441
|
+
kind: "claim_corruption",
|
|
442
|
+
eventId: event.eventId,
|
|
443
|
+
detail: loaded.corruption,
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
return { unusableObservationIds, unusableClaimEventIds };
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/** Snapshot derived verdict rows for rebuild-determinism tests (excludes asOf wall clock). */
|
|
453
|
+
export function readVerdictSnapshot(sqlitePath: string): unknown[] {
|
|
454
|
+
const db = new Database(sqlitePath, { readonly: true });
|
|
455
|
+
try {
|
|
456
|
+
return db
|
|
457
|
+
.query(
|
|
458
|
+
`SELECT projection_key, subject_id, evidence_layer, suite_id, suite_version,
|
|
459
|
+
suite_manifest_digest, projection_spec_version, verdict,
|
|
460
|
+
scenario_manifest_digests_json, claim_source_digest,
|
|
461
|
+
contributing_event_ids_json, contradicting_event_ids_json, notes_json
|
|
462
|
+
FROM verdicts ORDER BY projection_key`,
|
|
463
|
+
)
|
|
464
|
+
.all();
|
|
465
|
+
} finally {
|
|
466
|
+
db.close();
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
export function readCorruptionRows(sqlitePath: string): unknown[] {
|
|
471
|
+
const db = new Database(sqlitePath, { readonly: true });
|
|
472
|
+
try {
|
|
473
|
+
return db.query(`SELECT kind, line_number, event_id, detail FROM corruption ORDER BY id`).all();
|
|
474
|
+
} finally {
|
|
475
|
+
db.close();
|
|
476
|
+
}
|
|
477
|
+
}
|