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.
Files changed (204) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/LICENSE +201 -0
  3. package/README.md +274 -2
  4. package/dist/cli.d.ts +2 -0
  5. package/dist/cli.js +8542 -0
  6. package/dist/convert.d.ts +31 -0
  7. package/dist/convert.js +431 -0
  8. package/dist/index.d.ts +7 -0
  9. package/dist/index.js +8402 -0
  10. package/dist/log-baseline.d.ts +26 -0
  11. package/dist/log-baseline.js +104 -0
  12. package/dist/log-content.d.ts +16 -0
  13. package/dist/log-content.js +80 -0
  14. package/dist/log-io.d.ts +10 -0
  15. package/dist/log-io.js +121 -0
  16. package/dist/log-read.d.ts +24 -0
  17. package/dist/log-read.js +117 -0
  18. package/dist/log-reconcile.d.ts +5 -0
  19. package/dist/log-reconcile.js +38 -0
  20. package/dist/log-records.d.ts +35 -0
  21. package/dist/log-records.js +85 -0
  22. package/dist/log-recovery.d.ts +18 -0
  23. package/dist/log-recovery.js +123 -0
  24. package/dist/log-stage.d.ts +14 -0
  25. package/dist/log-stage.js +138 -0
  26. package/dist/log-state.d.ts +89 -0
  27. package/dist/log-state.js +92 -0
  28. package/dist/log-switch.d.ts +3 -0
  29. package/dist/log-switch.js +98 -0
  30. package/dist/log-types.d.ts +129 -0
  31. package/dist/log-types.js +8 -0
  32. package/dist/log.d.ts +8 -0
  33. package/dist/log.js +62 -0
  34. package/dist/migrate.d.ts +29 -0
  35. package/dist/migrate.js +173 -0
  36. package/dist/normalization-runtime.d.ts +1 -0
  37. package/dist/normalization-runtime.js +3150 -0
  38. package/dist/portable.d.ts +9 -0
  39. package/dist/portable.js +26 -0
  40. package/dist/readers/canonical.d.ts +13 -0
  41. package/dist/readers/canonical.js +54 -0
  42. package/dist/readers/cline.d.ts +10 -0
  43. package/dist/readers/cline.js +107 -0
  44. package/dist/readers/cursor.d.ts +20 -0
  45. package/dist/readers/cursor.js +374 -0
  46. package/dist/readers/deepagents.d.ts +41 -0
  47. package/dist/readers/deepagents.js +163 -0
  48. package/dist/readers/fx.d.ts +11 -0
  49. package/dist/readers/fx.js +573 -0
  50. package/dist/readers/github-copilot.d.ts +8 -0
  51. package/dist/readers/github-copilot.js +216 -0
  52. package/dist/readers/grok-build.d.ts +12 -0
  53. package/dist/readers/grok-build.js +391 -0
  54. package/dist/readers/guards.d.ts +4 -0
  55. package/dist/readers/guards.js +121 -0
  56. package/dist/readers/pi-compaction.d.ts +7 -0
  57. package/dist/readers/pi-compaction.js +112 -0
  58. package/dist/readers/pi-context.d.ts +13 -0
  59. package/dist/readers/pi-context.js +64 -0
  60. package/dist/store.d.ts +25 -0
  61. package/dist/store.js +218 -0
  62. package/dist/types.d.ts +91 -0
  63. package/dist/types.js +1 -0
  64. package/dist/writers/claude-code.d.ts +5 -0
  65. package/dist/writers/claude-code.js +51 -0
  66. package/dist/writers/cline.d.ts +3 -0
  67. package/dist/writers/cline.js +77 -0
  68. package/dist/writers/codex.d.ts +3 -0
  69. package/dist/writers/codex.js +32 -0
  70. package/dist/writers/cursor.d.ts +3 -0
  71. package/dist/writers/cursor.js +197 -0
  72. package/dist/writers/deepagents.d.ts +3 -0
  73. package/dist/writers/deepagents.js +116 -0
  74. package/dist/writers/entropy.d.ts +5 -0
  75. package/dist/writers/entropy.js +33 -0
  76. package/dist/writers/fx.d.ts +3 -0
  77. package/dist/writers/fx.js +135 -0
  78. package/dist/writers/github-copilot.d.ts +3 -0
  79. package/dist/writers/github-copilot.js +71 -0
  80. package/dist/writers/grok-build.d.ts +3 -0
  81. package/dist/writers/grok-build.js +43 -0
  82. package/dist/writers/helpers.d.ts +17 -0
  83. package/dist/writers/helpers.js +75 -0
  84. package/dist/writers/omp.d.ts +3 -0
  85. package/dist/writers/omp.js +7 -0
  86. package/dist/writers/opencode.d.ts +3 -0
  87. package/dist/writers/opencode.js +65 -0
  88. package/dist/writers/pi.d.ts +3 -0
  89. package/dist/writers/pi.js +45 -0
  90. package/docs/cline-native-format.md +43 -0
  91. package/docs/copilot-native-format.md +78 -0
  92. package/docs/cursor-native-format.md +103 -0
  93. package/docs/deepagents-native-format.md +98 -0
  94. package/docs/evidence-api.md +170 -0
  95. package/docs/fx-native-format.md +87 -0
  96. package/docs/harness-parity-bootstrap.md +78 -0
  97. package/docs/harness-support-parity.md +107 -0
  98. package/docs/inspirations.md +103 -0
  99. package/docs/ledger-migration.md +141 -0
  100. package/docs/pi-compaction-context.md +75 -0
  101. package/docs/pi-provider-context.md +76 -0
  102. package/docs/research/2026-10-06-huihua-and-oar.md +176 -0
  103. package/docs/research/ledger/README.md +96 -0
  104. package/docs/research/ledger/confer/2026-09-13-round-10/README.md +61 -0
  105. package/docs/research/ledger/confer/2026-09-13-round-2/cass.md +72 -0
  106. package/docs/research/ledger/confer/2026-09-13-round-3/README.md +70 -0
  107. package/docs/research/ledger/confer/2026-09-13-round-5/README.md +61 -0
  108. package/docs/research/ledger/confer/2026-09-13-round-6/README.md +68 -0
  109. package/docs/research/ledger/confer/2026-09-13-round-7/README.md +64 -0
  110. package/docs/research/ledger/confer/2026-09-13-round-9/README.md +59 -0
  111. package/docs/research/ledger/confer/2026-09-14-wake/wake.md +146 -0
  112. package/docs/research/ledger/docs/inspirations.md +320 -0
  113. package/docs/research/ledger/docs/pr2-pr6-integration.md +53 -0
  114. package/docs/research/ledger/docs/research/2026-09-07-confer-inspirations.md +163 -0
  115. package/docs/research/ledger/docs/research/2026-09-07-letta-continuous-service.md +104 -0
  116. package/docs/research/ledger/docs/research/2026-09-08-claude-history-retrieval.md +117 -0
  117. package/docs/research/ledger/docs/research/2026-09-09-agentsview-native-discovery.md +87 -0
  118. package/docs/research/ledger/docs/research/2026-09-09-daemon-observation.md +231 -0
  119. package/docs/research/ledger/docs/research/2026-09-09-discovery-pagination.md +114 -0
  120. package/docs/research/ledger/docs/research/2026-09-09-harness-transcript-contract.md +145 -0
  121. package/docs/research/ledger/docs/research/2026-09-09-native-history-discovery.md +180 -0
  122. package/docs/research/ledger/docs/research/2026-09-09-native-history-implementation.md +137 -0
  123. package/docs/research/ledger/docs/research/2026-09-09-other-inspirations.md +279 -0
  124. package/docs/research/ledger/docs/research/2026-09-09-trajectory-and-letta-code.md +128 -0
  125. package/docs/research/ledger/docs/research/2026-09-13-diagnostics-coverage.md +116 -0
  126. package/docs/research/ledger/docs/research/2026-09-13-fts-rebuild.md +83 -0
  127. package/docs/research/ledger/docs/research/2026-09-13-read-path-split.md +74 -0
  128. package/docs/research/ledger/docs/research/2026-09-13-runtime-rollout.md +102 -0
  129. package/docs/research/ledger/docs/research/2026-09-13-scan-outcomes.md +80 -0
  130. package/docs/research/ledger/docs/research/2026-09-13-search-quality.md +105 -0
  131. package/docs/research/ledger/docs/research/2026-09-13-source-coverage.md +64 -0
  132. package/docs/research/ledger/docs/research/2026-09-13-sync-wall-budget.md +82 -0
  133. package/docs/research/ledger/docs/research/2026-09-13-typed-read-gate.md +81 -0
  134. package/docs/research/ledger/docs/research/2026-09-13-writer-lease.md +84 -0
  135. package/docs/research/ledger/docs/research/2026-09-16-c1-copy-fallback.md +227 -0
  136. package/docs/research/ledger/docs/research/2026-09-16-c10-kimi.md +414 -0
  137. package/docs/research/ledger/docs/research/2026-09-16-c2-derive-counter.md +216 -0
  138. package/docs/research/ledger/docs/research/2026-09-16-c3-echo-filter.md +305 -0
  139. package/docs/research/ledger/docs/research/2026-09-16-c4-title-index.md +349 -0
  140. package/docs/research/ledger/docs/research/2026-09-16-c5-trigram.md +268 -0
  141. package/docs/research/ledger/docs/research/2026-09-16-c6-recency-prior.md +438 -0
  142. package/docs/research/ledger/docs/research/2026-09-16-c7-tombstones.md +467 -0
  143. package/docs/research/ledger/docs/research/2026-09-16-c8-remote-truth.md +390 -0
  144. package/docs/research/ledger/docs/research/2026-09-16-c9-grok.md +380 -0
  145. package/docs/research/ledger/docs/research/2026-09-16-wake-port-program.md +38 -0
  146. package/docs/research/ledger/docs/research/2026-09-17-cursor-blobstore.md +157 -0
  147. package/docs/research/ledger/manifest.json +540 -0
  148. package/legacy/NOTICE.md +50 -0
  149. package/legacy/bin/ledger-core.ts +7 -0
  150. package/legacy/bin/ledger.ts +7 -0
  151. package/legacy/bin/traj-bridge.ts +7 -0
  152. package/legacy/scripts/import-claude-history.ts +130 -0
  153. package/legacy/scripts/native-history-smoke.ts +52 -0
  154. package/legacy/scripts/observe-daemon.ts +570 -0
  155. package/legacy/scripts/retitle-sessions.md +19 -0
  156. package/legacy/scripts/verify-runtime-rollout.ts +151 -0
  157. package/legacy/src/canonical.ts +108 -0
  158. package/legacy/src/cli.ts +1293 -0
  159. package/legacy/src/db.ts +2254 -0
  160. package/legacy/src/diagnostics.ts +87 -0
  161. package/legacy/src/discovery.ts +56 -0
  162. package/legacy/src/embeddings.ts +382 -0
  163. package/legacy/src/filter.ts +602 -0
  164. package/legacy/src/gemini_embeddings.ts +103 -0
  165. package/legacy/src/identity.ts +9 -0
  166. package/legacy/src/index.ts +10 -0
  167. package/legacy/src/ingest.ts +791 -0
  168. package/legacy/src/managed.ts +178 -0
  169. package/legacy/src/native/common.ts +68 -0
  170. package/legacy/src/native/cursor.ts +647 -0
  171. package/legacy/src/native/grok.ts +540 -0
  172. package/legacy/src/native/kimi.ts +706 -0
  173. package/legacy/src/native/opencode.ts +470 -0
  174. package/legacy/src/native/sqlite-ro.ts +281 -0
  175. package/legacy/src/native.ts +97 -0
  176. package/legacy/src/normalizer.ts +457 -0
  177. package/legacy/src/parentage.ts +562 -0
  178. package/legacy/src/pressure.ts +162 -0
  179. package/legacy/src/private-storage.ts +74 -0
  180. package/legacy/src/reader.ts +1255 -0
  181. package/legacy/src/scanner.ts +591 -0
  182. package/legacy/src/schema-gate.ts +263 -0
  183. package/legacy/src/schema.ts +829 -0
  184. package/legacy/src/semantic-top-k.ts +71 -0
  185. package/legacy/src/session-hash.ts +49 -0
  186. package/legacy/src/store-status.ts +218 -0
  187. package/legacy/src/sync-preparation.ts +45 -0
  188. package/legacy/src/sync-truth.ts +843 -0
  189. package/legacy/src/sync.ts +1219 -0
  190. package/legacy/src/types.ts +436 -0
  191. package/legacy/src/vocab.txt +30522 -0
  192. package/legacy/src/watch.ts +673 -0
  193. package/legacy/src/writer-lease.ts +310 -0
  194. package/ops/README.md +120 -0
  195. package/ops/install-cli.sh +63 -0
  196. package/ops/install-release.sh +49 -0
  197. package/ops/linux/install-systemd-user.sh +103 -0
  198. package/ops/macos/install-launchd.sh +72 -0
  199. package/ops/sync-settings.sh +37 -0
  200. package/package.json +19 -5
  201. package/skills/trajectories/SKILL.md +138 -0
  202. package/skills/trajectories/scripts/trajectories_recent.sh +26 -0
  203. package/skills/trajectories/scripts/trajectories_search.sh +26 -0
  204. 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
+ }