trajectories 0.0.0 → 0.1.1
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/CHANGELOG.md +23 -0
- package/LICENSE +201 -0
- package/README.md +274 -2
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +8542 -0
- package/dist/convert.d.ts +31 -0
- package/dist/convert.js +431 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +8402 -0
- package/dist/log-baseline.d.ts +26 -0
- package/dist/log-baseline.js +104 -0
- package/dist/log-content.d.ts +16 -0
- package/dist/log-content.js +80 -0
- package/dist/log-io.d.ts +10 -0
- package/dist/log-io.js +121 -0
- package/dist/log-read.d.ts +24 -0
- package/dist/log-read.js +117 -0
- package/dist/log-reconcile.d.ts +5 -0
- package/dist/log-reconcile.js +38 -0
- package/dist/log-records.d.ts +35 -0
- package/dist/log-records.js +85 -0
- package/dist/log-recovery.d.ts +18 -0
- package/dist/log-recovery.js +123 -0
- package/dist/log-stage.d.ts +14 -0
- package/dist/log-stage.js +138 -0
- package/dist/log-state.d.ts +89 -0
- package/dist/log-state.js +92 -0
- package/dist/log-switch.d.ts +3 -0
- package/dist/log-switch.js +98 -0
- package/dist/log-types.d.ts +129 -0
- package/dist/log-types.js +8 -0
- package/dist/log.d.ts +8 -0
- package/dist/log.js +62 -0
- package/dist/migrate.d.ts +29 -0
- package/dist/migrate.js +173 -0
- package/dist/normalization-runtime.d.ts +1 -0
- package/dist/normalization-runtime.js +3150 -0
- package/dist/portable.d.ts +9 -0
- package/dist/portable.js +26 -0
- package/dist/readers/canonical.d.ts +13 -0
- package/dist/readers/canonical.js +54 -0
- package/dist/readers/cline.d.ts +10 -0
- package/dist/readers/cline.js +107 -0
- package/dist/readers/cursor.d.ts +20 -0
- package/dist/readers/cursor.js +374 -0
- package/dist/readers/deepagents.d.ts +41 -0
- package/dist/readers/deepagents.js +163 -0
- package/dist/readers/fx.d.ts +11 -0
- package/dist/readers/fx.js +573 -0
- package/dist/readers/github-copilot.d.ts +8 -0
- package/dist/readers/github-copilot.js +216 -0
- package/dist/readers/grok-build.d.ts +12 -0
- package/dist/readers/grok-build.js +391 -0
- package/dist/readers/guards.d.ts +4 -0
- package/dist/readers/guards.js +121 -0
- package/dist/readers/pi-compaction.d.ts +7 -0
- package/dist/readers/pi-compaction.js +112 -0
- package/dist/readers/pi-context.d.ts +13 -0
- package/dist/readers/pi-context.js +64 -0
- package/dist/store.d.ts +25 -0
- package/dist/store.js +218 -0
- package/dist/types.d.ts +91 -0
- package/dist/types.js +1 -0
- package/dist/writers/claude-code.d.ts +5 -0
- package/dist/writers/claude-code.js +51 -0
- package/dist/writers/cline.d.ts +3 -0
- package/dist/writers/cline.js +77 -0
- package/dist/writers/codex.d.ts +3 -0
- package/dist/writers/codex.js +32 -0
- package/dist/writers/cursor.d.ts +3 -0
- package/dist/writers/cursor.js +197 -0
- package/dist/writers/deepagents.d.ts +3 -0
- package/dist/writers/deepagents.js +116 -0
- package/dist/writers/entropy.d.ts +5 -0
- package/dist/writers/entropy.js +33 -0
- package/dist/writers/fx.d.ts +3 -0
- package/dist/writers/fx.js +135 -0
- package/dist/writers/github-copilot.d.ts +3 -0
- package/dist/writers/github-copilot.js +71 -0
- package/dist/writers/grok-build.d.ts +3 -0
- package/dist/writers/grok-build.js +43 -0
- package/dist/writers/helpers.d.ts +17 -0
- package/dist/writers/helpers.js +75 -0
- package/dist/writers/omp.d.ts +3 -0
- package/dist/writers/omp.js +7 -0
- package/dist/writers/opencode.d.ts +3 -0
- package/dist/writers/opencode.js +65 -0
- package/dist/writers/pi.d.ts +3 -0
- package/dist/writers/pi.js +45 -0
- package/docs/cline-native-format.md +43 -0
- package/docs/copilot-native-format.md +78 -0
- package/docs/cursor-native-format.md +103 -0
- package/docs/deepagents-native-format.md +98 -0
- package/docs/evidence-api.md +170 -0
- package/docs/fx-native-format.md +87 -0
- package/docs/harness-parity-bootstrap.md +78 -0
- package/docs/harness-support-parity.md +107 -0
- package/docs/inspirations.md +103 -0
- package/docs/ledger-migration.md +141 -0
- package/docs/pi-compaction-context.md +75 -0
- package/docs/pi-provider-context.md +76 -0
- package/docs/research/2026-10-06-huihua-and-oar.md +176 -0
- package/docs/research/ledger/README.md +96 -0
- package/docs/research/ledger/confer/2026-09-13-round-10/README.md +61 -0
- package/docs/research/ledger/confer/2026-09-13-round-2/cass.md +72 -0
- package/docs/research/ledger/confer/2026-09-13-round-3/README.md +70 -0
- package/docs/research/ledger/confer/2026-09-13-round-5/README.md +61 -0
- package/docs/research/ledger/confer/2026-09-13-round-6/README.md +68 -0
- package/docs/research/ledger/confer/2026-09-13-round-7/README.md +64 -0
- package/docs/research/ledger/confer/2026-09-13-round-9/README.md +59 -0
- package/docs/research/ledger/confer/2026-09-14-wake/wake.md +146 -0
- package/docs/research/ledger/docs/inspirations.md +320 -0
- package/docs/research/ledger/docs/pr2-pr6-integration.md +53 -0
- package/docs/research/ledger/docs/research/2026-09-07-confer-inspirations.md +163 -0
- package/docs/research/ledger/docs/research/2026-09-07-letta-continuous-service.md +104 -0
- package/docs/research/ledger/docs/research/2026-09-08-claude-history-retrieval.md +117 -0
- package/docs/research/ledger/docs/research/2026-09-09-agentsview-native-discovery.md +87 -0
- package/docs/research/ledger/docs/research/2026-09-09-daemon-observation.md +231 -0
- package/docs/research/ledger/docs/research/2026-09-09-discovery-pagination.md +114 -0
- package/docs/research/ledger/docs/research/2026-09-09-harness-transcript-contract.md +145 -0
- package/docs/research/ledger/docs/research/2026-09-09-native-history-discovery.md +180 -0
- package/docs/research/ledger/docs/research/2026-09-09-native-history-implementation.md +137 -0
- package/docs/research/ledger/docs/research/2026-09-09-other-inspirations.md +279 -0
- package/docs/research/ledger/docs/research/2026-09-09-trajectory-and-letta-code.md +128 -0
- package/docs/research/ledger/docs/research/2026-09-13-diagnostics-coverage.md +116 -0
- package/docs/research/ledger/docs/research/2026-09-13-fts-rebuild.md +83 -0
- package/docs/research/ledger/docs/research/2026-09-13-read-path-split.md +74 -0
- package/docs/research/ledger/docs/research/2026-09-13-runtime-rollout.md +102 -0
- package/docs/research/ledger/docs/research/2026-09-13-scan-outcomes.md +80 -0
- package/docs/research/ledger/docs/research/2026-09-13-search-quality.md +105 -0
- package/docs/research/ledger/docs/research/2026-09-13-source-coverage.md +64 -0
- package/docs/research/ledger/docs/research/2026-09-13-sync-wall-budget.md +82 -0
- package/docs/research/ledger/docs/research/2026-09-13-typed-read-gate.md +81 -0
- package/docs/research/ledger/docs/research/2026-09-13-writer-lease.md +84 -0
- package/docs/research/ledger/docs/research/2026-09-16-c1-copy-fallback.md +227 -0
- package/docs/research/ledger/docs/research/2026-09-16-c10-kimi.md +414 -0
- package/docs/research/ledger/docs/research/2026-09-16-c2-derive-counter.md +216 -0
- package/docs/research/ledger/docs/research/2026-09-16-c3-echo-filter.md +305 -0
- package/docs/research/ledger/docs/research/2026-09-16-c4-title-index.md +349 -0
- package/docs/research/ledger/docs/research/2026-09-16-c5-trigram.md +268 -0
- package/docs/research/ledger/docs/research/2026-09-16-c6-recency-prior.md +438 -0
- package/docs/research/ledger/docs/research/2026-09-16-c7-tombstones.md +467 -0
- package/docs/research/ledger/docs/research/2026-09-16-c8-remote-truth.md +390 -0
- package/docs/research/ledger/docs/research/2026-09-16-c9-grok.md +380 -0
- package/docs/research/ledger/docs/research/2026-09-16-wake-port-program.md +38 -0
- package/docs/research/ledger/docs/research/2026-09-17-cursor-blobstore.md +157 -0
- package/docs/research/ledger/manifest.json +540 -0
- package/legacy/NOTICE.md +50 -0
- package/legacy/bin/ledger-core.ts +7 -0
- package/legacy/bin/ledger.ts +7 -0
- package/legacy/bin/traj-bridge.ts +7 -0
- package/legacy/scripts/import-claude-history.ts +130 -0
- package/legacy/scripts/native-history-smoke.ts +52 -0
- package/legacy/scripts/observe-daemon.ts +570 -0
- package/legacy/scripts/retitle-sessions.md +19 -0
- package/legacy/scripts/verify-runtime-rollout.ts +151 -0
- package/legacy/src/canonical.ts +108 -0
- package/legacy/src/cli.ts +1293 -0
- package/legacy/src/db.ts +2254 -0
- package/legacy/src/diagnostics.ts +87 -0
- package/legacy/src/discovery.ts +56 -0
- package/legacy/src/embeddings.ts +382 -0
- package/legacy/src/filter.ts +602 -0
- package/legacy/src/gemini_embeddings.ts +103 -0
- package/legacy/src/identity.ts +9 -0
- package/legacy/src/index.ts +10 -0
- package/legacy/src/ingest.ts +791 -0
- package/legacy/src/managed.ts +178 -0
- package/legacy/src/native/common.ts +68 -0
- package/legacy/src/native/cursor.ts +647 -0
- package/legacy/src/native/grok.ts +540 -0
- package/legacy/src/native/kimi.ts +706 -0
- package/legacy/src/native/opencode.ts +470 -0
- package/legacy/src/native/sqlite-ro.ts +281 -0
- package/legacy/src/native.ts +97 -0
- package/legacy/src/normalizer.ts +457 -0
- package/legacy/src/parentage.ts +562 -0
- package/legacy/src/pressure.ts +162 -0
- package/legacy/src/private-storage.ts +74 -0
- package/legacy/src/reader.ts +1255 -0
- package/legacy/src/scanner.ts +591 -0
- package/legacy/src/schema-gate.ts +263 -0
- package/legacy/src/schema.ts +829 -0
- package/legacy/src/semantic-top-k.ts +71 -0
- package/legacy/src/session-hash.ts +49 -0
- package/legacy/src/store-status.ts +218 -0
- package/legacy/src/sync-preparation.ts +45 -0
- package/legacy/src/sync-truth.ts +843 -0
- package/legacy/src/sync.ts +1219 -0
- package/legacy/src/types.ts +436 -0
- package/legacy/src/vocab.txt +30522 -0
- package/legacy/src/watch.ts +673 -0
- package/legacy/src/writer-lease.ts +310 -0
- package/ops/README.md +120 -0
- package/ops/install-cli.sh +63 -0
- package/ops/install-release.sh +49 -0
- package/ops/linux/install-systemd-user.sh +103 -0
- package/ops/macos/install-launchd.sh +72 -0
- package/ops/sync-settings.sh +37 -0
- package/package.json +19 -5
- package/skills/trajectories/SKILL.md +138 -0
- package/skills/trajectories/scripts/trajectories_recent.sh +26 -0
- package/skills/trajectories/scripts/trajectories_search.sh +26 -0
- package/index.js +0 -1
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/** Narrow scoring state: no transcript bodies or stored vector blobs. */
|
|
2
|
+
export interface SemanticCandidate {
|
|
3
|
+
record_id: string;
|
|
4
|
+
session_id: string;
|
|
5
|
+
score: number;
|
|
6
|
+
ordinal: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function better(a: SemanticCandidate, b: SemanticCandidate): boolean {
|
|
10
|
+
return a.score > b.score || (a.score === b.score && a.ordinal < b.ordinal);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Exact top-K distinct sessions using a worst-first heap. Once a session is
|
|
15
|
+
* evicted, its earlier best score cannot beat the monotonically improving
|
|
16
|
+
* cutoff; forgetting that session is safe. Space is O(K), update O(log K).
|
|
17
|
+
*/
|
|
18
|
+
export class SemanticTopK {
|
|
19
|
+
private heap: SemanticCandidate[] = [];
|
|
20
|
+
private positions = new Map<string, number>();
|
|
21
|
+
constructor(readonly limit: number) {
|
|
22
|
+
if (!Number.isSafeInteger(limit) || limit < 1) throw new RangeError("Semantic limit must be positive.");
|
|
23
|
+
}
|
|
24
|
+
get size(): number { return this.heap.length; }
|
|
25
|
+
private swap(a: number, b: number): void {
|
|
26
|
+
[this.heap[a], this.heap[b]] = [this.heap[b], this.heap[a]];
|
|
27
|
+
this.positions.set(this.heap[a].session_id, a);
|
|
28
|
+
this.positions.set(this.heap[b].session_id, b);
|
|
29
|
+
}
|
|
30
|
+
private down(index: number): void {
|
|
31
|
+
for (;;) {
|
|
32
|
+
const left = index * 2 + 1;
|
|
33
|
+
if (left >= this.heap.length) return;
|
|
34
|
+
const right = left + 1;
|
|
35
|
+
const worst = right < this.heap.length && better(this.heap[left], this.heap[right]) ? right : left;
|
|
36
|
+
if (!better(this.heap[index], this.heap[worst])) return;
|
|
37
|
+
this.swap(index, worst);
|
|
38
|
+
index = worst;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
offer(candidate: SemanticCandidate): void {
|
|
42
|
+
if (!Number.isFinite(candidate.score)) return;
|
|
43
|
+
const existing = this.positions.get(candidate.session_id);
|
|
44
|
+
if (existing !== undefined) {
|
|
45
|
+
if (better(candidate, this.heap[existing])) {
|
|
46
|
+
this.heap[existing] = candidate;
|
|
47
|
+
this.down(existing);
|
|
48
|
+
}
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if (this.heap.length < this.limit) {
|
|
52
|
+
let index = this.heap.length;
|
|
53
|
+
this.heap.push(candidate);
|
|
54
|
+
this.positions.set(candidate.session_id, index);
|
|
55
|
+
while (index > 0) {
|
|
56
|
+
const parent = Math.floor((index - 1) / 2);
|
|
57
|
+
if (!better(this.heap[parent], this.heap[index])) break;
|
|
58
|
+
this.swap(parent, index);
|
|
59
|
+
index = parent;
|
|
60
|
+
}
|
|
61
|
+
} else if (better(candidate, this.heap[0])) {
|
|
62
|
+
this.positions.delete(this.heap[0].session_id);
|
|
63
|
+
this.heap[0] = candidate;
|
|
64
|
+
this.positions.set(candidate.session_id, 0);
|
|
65
|
+
this.down(0);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
sorted(): SemanticCandidate[] {
|
|
69
|
+
return [...this.heap].sort((a, b) => b.score - a.score || a.ordinal - b.ordinal);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { nativeId } from "./identity.js";
|
|
3
|
+
import type { CanonicalDbRecord } from "./types.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Version 2 is a commutative cryptographic accumulator rather than one opaque
|
|
7
|
+
* streaming digest. Each record leaf includes its durable order and identity,
|
|
8
|
+
* while modular addition makes the session state extendable even when a later
|
|
9
|
+
* transcript line canonically sorts beside an earlier tool call. Version 1
|
|
10
|
+
* denotes the pre-incident whole-session SHA-256.
|
|
11
|
+
*/
|
|
12
|
+
export const SESSION_HASH_VERSION = 2;
|
|
13
|
+
const DIGEST_MODULUS = 1n << 256n;
|
|
14
|
+
|
|
15
|
+
type SessionHashRecord = Pick<
|
|
16
|
+
CanonicalDbRecord,
|
|
17
|
+
"id" | "record_type" | "source_order_id" | "component_index" | "content_hash"
|
|
18
|
+
>;
|
|
19
|
+
|
|
20
|
+
function sha256(parts: Array<string | Buffer>): string {
|
|
21
|
+
const hash = createHash("sha256");
|
|
22
|
+
for (const part of parts) hash.update(part);
|
|
23
|
+
return hash.digest("hex");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function initialSessionHash(source: string, sessionId: string): string {
|
|
27
|
+
return sha256([`ledger-session-hash:v${SESSION_HASH_VERSION}:${source}:${nativeId(source, sessionId)}\n`]);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function extendSessionHash(previous: string, records: Iterable<SessionHashRecord>): string {
|
|
31
|
+
if (!/^[a-f0-9]{64}$/.test(previous)) throw new Error("Invalid prior session hash state");
|
|
32
|
+
let state = BigInt(`0x${previous}`);
|
|
33
|
+
for (const record of records) {
|
|
34
|
+
const leaf = sha256([
|
|
35
|
+
`ledger-session-record:v${SESSION_HASH_VERSION}\n`,
|
|
36
|
+
`${record.record_type}:${record.source_order_id}:${record.component_index}:${record.id}:${record.content_hash}\n`,
|
|
37
|
+
]);
|
|
38
|
+
state = (state + BigInt(`0x${leaf}`)) % DIGEST_MODULUS;
|
|
39
|
+
}
|
|
40
|
+
return state.toString(16).padStart(64, "0");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function hashSessionRecords(
|
|
44
|
+
source: string,
|
|
45
|
+
sessionId: string,
|
|
46
|
+
records: Iterable<SessionHashRecord>,
|
|
47
|
+
): string {
|
|
48
|
+
return extendSessionHash(initialSessionHash(source, sessionId), records);
|
|
49
|
+
}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import type { DiscoverySummary } from "./discovery.js";
|
|
2
|
+
import { readDiagnosticsSummary, readSourceCoverage, readDiscoverySummary } from "./db.js";
|
|
3
|
+
import { echoCount } from "./filter.js";
|
|
4
|
+
import { FTS_FORMAT, SCHEMA_VERSION } from "./schema.js";
|
|
5
|
+
import { classifyReadStore, formatMismatchMessage, incompleteStoreMessage, type ReadStoreRefusal } from "./schema-gate.js";
|
|
6
|
+
import type { DiagnosticsSummary, SourceCoverage } from "./types.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Read-only snapshot of a Ledger store. `null` fields mean "unknown because
|
|
10
|
+
* no initialized store exists at this path"; they are never guessed.
|
|
11
|
+
*/
|
|
12
|
+
export interface StoreStatus {
|
|
13
|
+
path: string;
|
|
14
|
+
initialized: boolean;
|
|
15
|
+
schemaVersion: number | null;
|
|
16
|
+
sessions: number | null;
|
|
17
|
+
records: number | null;
|
|
18
|
+
/**
|
|
19
|
+
* Canonical records the self-referential echo filter keeps out of the index
|
|
20
|
+
* (filter.ts): the transcripts of agents asking Ledger itself. It is the
|
|
21
|
+
* derived set the index's own writes maintain, so it describes the index that
|
|
22
|
+
* is answering reads — not a claim about what a future derivation would do.
|
|
23
|
+
* Null when that description cannot be made: no store exists at this path,
|
|
24
|
+
* the store predates the filter's table, the index was derived under other
|
|
25
|
+
* rules (`fts_format`), or the derivation this build stamps at open is still
|
|
26
|
+
* pending (`fts_reindex` — deferred under critical pressure, or interrupted)
|
|
27
|
+
* — unknown, not zero.
|
|
28
|
+
*/
|
|
29
|
+
echoFiltered: number | null;
|
|
30
|
+
/** Null when no store exists or the store predates the diagnostics table. */
|
|
31
|
+
diagnostics: DiagnosticsSummary | null;
|
|
32
|
+
/**
|
|
33
|
+
* Sessions deleted by the user through the explicit verb, and the records
|
|
34
|
+
* those deletions still hold back — the disclosure that makes the two counts
|
|
35
|
+
* above honest, since they deliberately include the rows a `suppress`
|
|
36
|
+
* deletion keeps (reader.ts's `countSessions` carries the argument: the store
|
|
37
|
+
* *holds* them and the audit is why). Both are 0, never null, for a store
|
|
38
|
+
* that predates the tombstone table: no build could have written a tombstone
|
|
39
|
+
* into it, so the set is empty rather than unknown.
|
|
40
|
+
*/
|
|
41
|
+
tombstones: number | null;
|
|
42
|
+
tombstonedRecords: number | null;
|
|
43
|
+
/**
|
|
44
|
+
* Durable per-source scan coverage, in source order. Null when no store
|
|
45
|
+
* exists or the store predates the coverage table; empty when the table
|
|
46
|
+
* exists but no pass has scanned yet — unknown is not the same as none.
|
|
47
|
+
*/
|
|
48
|
+
coverage: SourceCoverage[] | null;
|
|
49
|
+
/** Absent until a scheduled discovery pass is recorded; never implies full archival retention. */
|
|
50
|
+
discovery?: DiscoverySummary;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export type StoreStatusErrorCode =
|
|
54
|
+
| "STORE_UNREADABLE"
|
|
55
|
+
| "SCHEMA_MISMATCH"
|
|
56
|
+
| "FORMAT_MISMATCH"
|
|
57
|
+
| "STORE_SCHEMA_INCOMPLETE";
|
|
58
|
+
|
|
59
|
+
export class StoreStatusError extends Error {
|
|
60
|
+
readonly code: StoreStatusErrorCode;
|
|
61
|
+
|
|
62
|
+
constructor(code: StoreStatusErrorCode, message: string) {
|
|
63
|
+
super(message);
|
|
64
|
+
this.name = "StoreStatusError";
|
|
65
|
+
this.code = code;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Inspect a store path without initializing, migrating, or creating anything.
|
|
71
|
+
* A missing or empty store is normal and returns initialized=false; callers
|
|
72
|
+
* decide how to render it. Anything unreadable, unsupported, or missing the
|
|
73
|
+
* read schema raises StoreStatusError — the same typed ladder the read verbs
|
|
74
|
+
* refuse with (`classifyReadStore`), so the diagnostic verb cannot report a
|
|
75
|
+
* store the read path would refuse, or vice versa.
|
|
76
|
+
*/
|
|
77
|
+
export function readStoreStatus(dbPath: string): StoreStatus {
|
|
78
|
+
const store = classifyReadStore(dbPath);
|
|
79
|
+
|
|
80
|
+
if (store.state === "not_initialized") {
|
|
81
|
+
return {
|
|
82
|
+
path: dbPath,
|
|
83
|
+
initialized: false,
|
|
84
|
+
schemaVersion: null,
|
|
85
|
+
sessions: null,
|
|
86
|
+
records: null,
|
|
87
|
+
echoFiltered: null,
|
|
88
|
+
diagnostics: null,
|
|
89
|
+
coverage: null,
|
|
90
|
+
tombstones: null,
|
|
91
|
+
tombstonedRecords: null,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
if (store.state !== "ok") throw statusRefusal(dbPath, store);
|
|
95
|
+
|
|
96
|
+
const db = store.db;
|
|
97
|
+
try {
|
|
98
|
+
const sessions =
|
|
99
|
+
db.query<{ count: number }, []>("SELECT COUNT(*) AS count FROM sessions").get()?.count ?? 0;
|
|
100
|
+
const records =
|
|
101
|
+
db.query<{ count: number }, []>("SELECT COUNT(*) AS count FROM records").get()?.count ?? 0;
|
|
102
|
+
// A store written by an older Ledger is readable but has no diagnostics
|
|
103
|
+
// table; report that as unknown coverage rather than as zero findings.
|
|
104
|
+
const hasDiagnostics = !!db
|
|
105
|
+
.query<{ name: string }, []>(
|
|
106
|
+
"SELECT name FROM sqlite_master WHERE type='table' AND name='ingest_diagnostics' LIMIT 1;",
|
|
107
|
+
)
|
|
108
|
+
.get();
|
|
109
|
+
// Same rule for durable coverage, one schema later: a pre-v12 store has
|
|
110
|
+
// never recorded a verdict, so its answer is unknown, not "no sources".
|
|
111
|
+
const hasCoverage = !!db
|
|
112
|
+
.query<{ name: string }, []>(
|
|
113
|
+
"SELECT name FROM sqlite_master WHERE type='table' AND name='source_coverage' LIMIT 1;",
|
|
114
|
+
)
|
|
115
|
+
.get();
|
|
116
|
+
// The echo set is a claim about the index, so it is published only when the
|
|
117
|
+
// derivation under *this* build's rules completed. A `fts_format` naming
|
|
118
|
+
// other rules means the served index was re-derived by another build, whose
|
|
119
|
+
// re-projection cannot maintain the set (it rewrites the index whole); a
|
|
120
|
+
// pending `fts_reindex` means the derivation this build stamps at open has
|
|
121
|
+
// not run yet — deferred under critical pressure, or left by a crash — with
|
|
122
|
+
// the lookups still in the served index. Publishing the empty set as a
|
|
123
|
+
// count there would read as "this store filters nothing" while searches
|
|
124
|
+
// echo; unknown is the honest answer, and the next writer pass derives it.
|
|
125
|
+
const hasMeta = !!db
|
|
126
|
+
.query<{ name: string }, []>(
|
|
127
|
+
"SELECT name FROM sqlite_master WHERE type='table' AND name='schema_meta' LIMIT 1;",
|
|
128
|
+
)
|
|
129
|
+
.get();
|
|
130
|
+
const derivation = hasMeta
|
|
131
|
+
? db
|
|
132
|
+
.query<{ format: string | null; pending: number | null }, []>(
|
|
133
|
+
`SELECT (SELECT value FROM schema_meta WHERE key = 'fts_format') AS format,
|
|
134
|
+
(SELECT 1 FROM schema_meta WHERE key = 'fts_reindex') AS pending;`,
|
|
135
|
+
)
|
|
136
|
+
.get()
|
|
137
|
+
: null;
|
|
138
|
+
const echoFiltered =
|
|
139
|
+
derivation?.format === FTS_FORMAT && !derivation.pending ? echoCount(db) : null;
|
|
140
|
+
// The tombstones themselves, and what they still hold back. A store without
|
|
141
|
+
// the table predates the verb, so its answer is an empty set rather than
|
|
142
|
+
// unknown — this is the one field here whose absence is a fact, not a gap.
|
|
143
|
+
const hasTombstones = !!db
|
|
144
|
+
.query<{ name: string }, []>(
|
|
145
|
+
"SELECT name FROM sqlite_master WHERE type='table' AND name='session_tombstones' LIMIT 1;",
|
|
146
|
+
)
|
|
147
|
+
.get();
|
|
148
|
+
const tombstones = hasTombstones
|
|
149
|
+
? db.query<{ count: number }, []>("SELECT COUNT(*) AS count FROM session_tombstones").get()?.count ?? 0
|
|
150
|
+
: 0;
|
|
151
|
+
const tombstonedRecords = hasTombstones
|
|
152
|
+
? db
|
|
153
|
+
.query<{ count: number }, []>(
|
|
154
|
+
`SELECT COUNT(*) AS count FROM records r
|
|
155
|
+
WHERE EXISTS (SELECT 1 FROM session_tombstones t WHERE t.session_id = r.session_id)`,
|
|
156
|
+
)
|
|
157
|
+
.get()?.count ?? 0
|
|
158
|
+
: 0;
|
|
159
|
+
const discovery = readDiscoverySummary(db);
|
|
160
|
+
return {
|
|
161
|
+
...(discovery ? { discovery } : {}),
|
|
162
|
+
path: dbPath,
|
|
163
|
+
initialized: true,
|
|
164
|
+
schemaVersion: store.schemaVersion,
|
|
165
|
+
sessions,
|
|
166
|
+
records,
|
|
167
|
+
echoFiltered,
|
|
168
|
+
diagnostics: hasDiagnostics ? readDiagnosticsSummary(db) : null,
|
|
169
|
+
tombstones,
|
|
170
|
+
tombstonedRecords,
|
|
171
|
+
coverage: hasCoverage
|
|
172
|
+
// Projected field by field rather than spread minus a key: this is a
|
|
173
|
+
// published payload, so a column added later must be published
|
|
174
|
+
// deliberately, not by leaking through a rest element.
|
|
175
|
+
? readSourceCoverage(db).map((row) => ({
|
|
176
|
+
source: row.source,
|
|
177
|
+
root: row.root,
|
|
178
|
+
status: row.status,
|
|
179
|
+
discovered: row.discovered,
|
|
180
|
+
detail: row.detail,
|
|
181
|
+
updated_at: row.updated_at,
|
|
182
|
+
}))
|
|
183
|
+
: null,
|
|
184
|
+
};
|
|
185
|
+
} finally {
|
|
186
|
+
db.close();
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** A classified refusal, in this verb's own wording. */
|
|
191
|
+
function statusRefusal(dbPath: string, store: ReadStoreRefusal): StoreStatusError {
|
|
192
|
+
switch (store.state) {
|
|
193
|
+
case "foreign":
|
|
194
|
+
return new StoreStatusError(
|
|
195
|
+
"STORE_UNREADABLE",
|
|
196
|
+
`Database at ${dbPath} exists but is not an initialized Ledger store (no sessions table).`,
|
|
197
|
+
);
|
|
198
|
+
case "unreadable":
|
|
199
|
+
return new StoreStatusError(
|
|
200
|
+
"STORE_UNREADABLE",
|
|
201
|
+
store.phase === "open"
|
|
202
|
+
? `Cannot open database at ${dbPath}: ${store.detail}`
|
|
203
|
+
: `Cannot read database at ${dbPath}: ${store.detail}. The file may be corrupt or not a SQLite database.`,
|
|
204
|
+
);
|
|
205
|
+
case "schema_mismatch":
|
|
206
|
+
return new StoreStatusError(
|
|
207
|
+
"SCHEMA_MISMATCH",
|
|
208
|
+
`Database schema version ${store.schemaVersion} at ${dbPath} is newer than supported schema version ${SCHEMA_VERSION}.`,
|
|
209
|
+
);
|
|
210
|
+
case "format_mismatch":
|
|
211
|
+
return new StoreStatusError("FORMAT_MISMATCH", formatMismatchMessage(dbPath, store));
|
|
212
|
+
case "incomplete":
|
|
213
|
+
return new StoreStatusError(
|
|
214
|
+
"STORE_SCHEMA_INCOMPLETE",
|
|
215
|
+
incompleteStoreMessage(dbPath, store.firstGap, store.schemaVersion),
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { TrajectoryDb } from "./db.js";
|
|
2
|
+
import { sessionNeedsIngest } from "./ingest.js";
|
|
3
|
+
import { scanSessions, type SessionScanOptions } from "./scanner.js";
|
|
4
|
+
|
|
5
|
+
export interface SyncPreparationJob {
|
|
6
|
+
protocol_version: 2;
|
|
7
|
+
db_path: string;
|
|
8
|
+
sources?: string[];
|
|
9
|
+
roots?: Record<string, string>;
|
|
10
|
+
scan_limit: number;
|
|
11
|
+
max_record_bytes: number;
|
|
12
|
+
}
|
|
13
|
+
export interface SyncPreparationReply {
|
|
14
|
+
protocol_version: 2;
|
|
15
|
+
type: "sync_preparation_result";
|
|
16
|
+
discovered: number;
|
|
17
|
+
init_ms: number;
|
|
18
|
+
scan_ms: number;
|
|
19
|
+
queue_ms: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Initialization, migration, native discovery and queue admission are all
|
|
24
|
+
* synchronous-I/O-bearing work. Production runs this entire phase in a leaf
|
|
25
|
+
* child, so a blocked scan or migration cannot wedge the lease-holding parent.
|
|
26
|
+
* The child inherits write authority only after its caller checks the lease.
|
|
27
|
+
*/
|
|
28
|
+
export async function prepareSyncStore(job: SyncPreparationJob): Promise<SyncPreparationReply> {
|
|
29
|
+
const started = performance.now();
|
|
30
|
+
const db = new TrajectoryDb(job.db_path);
|
|
31
|
+
const init_ms = performance.now() - started;
|
|
32
|
+
try {
|
|
33
|
+
const scanStarted = performance.now();
|
|
34
|
+
const options: SessionScanOptions = {
|
|
35
|
+
sources: job.sources, roots: job.roots, limit: job.scan_limit,
|
|
36
|
+
discovery: db.getDiscoveryCheckpoints(),
|
|
37
|
+
};
|
|
38
|
+
const result = await scanSessions(options);
|
|
39
|
+
const scan_ms = performance.now() - scanStarted;
|
|
40
|
+
const queueStarted = performance.now();
|
|
41
|
+
db.admitDiscovery(result, item => sessionNeedsIngest(db, item, { maxRecordBytes: job.max_record_bytes }), new Date());
|
|
42
|
+
return { protocol_version: 2, type: "sync_preparation_result", discovered: result.sessions.length,
|
|
43
|
+
init_ms, scan_ms, queue_ms: performance.now() - queueStarted };
|
|
44
|
+
} finally { db.close(); }
|
|
45
|
+
}
|