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,263 @@
|
|
|
1
|
+
import { existsSync, statSync } from "node:fs";
|
|
2
|
+
import { Database } from "bun:sqlite";
|
|
3
|
+
import { DERIVE_COUNTERS, REQUIRED_READ_SCHEMA, SCHEMA_VERSION } from "./schema.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The typed read gate: one ladder, one implementation, shared by every way a
|
|
7
|
+
* read verb opens a store (`openReadOnlyStore` and `readStoreStatus`). Both
|
|
8
|
+
* used to test for a `sessions` table and a version, which let a foreign file
|
|
9
|
+
* through to die later on a raw `no such table: records` / `no such column:
|
|
10
|
+
* updated_at` — an error with no code, no payload, and no remedy.
|
|
11
|
+
*
|
|
12
|
+
* Order is deliberate: a store is classified as not initialized before it is
|
|
13
|
+
* opened, a file this build cannot read is diagnosed before its version is
|
|
14
|
+
* judged (the version is unreadable too), a newer version is refused before its
|
|
15
|
+
* shape is judged (it is expected to differ), and only then is the read schema
|
|
16
|
+
* checked. `user_version < SCHEMA_VERSION` with the read schema present stays
|
|
17
|
+
* `ok`: an old store keeps serving reads, and nothing here migrates it.
|
|
18
|
+
*
|
|
19
|
+
* The derive-rule counters (schema.ts) are judged in the same pass and on the
|
|
20
|
+
* same principle, one axis over: a stored counter *newer* than this build
|
|
21
|
+
* derives is refused (the index was produced by rules this build does not have),
|
|
22
|
+
* while an older counter — or none at all, which is every store written before
|
|
23
|
+
* the counter existed — stays `ok` beside it. A *pending re-derivation flag* is
|
|
24
|
+
* not a refusal either: the index is stale, not unreadable, and Wake's readers
|
|
25
|
+
* serve the same stale index while a scan is pending. Reads never write, so
|
|
26
|
+
* neither gate ever clears a flag or stamps a counter.
|
|
27
|
+
*/
|
|
28
|
+
export type ReadStoreClassification =
|
|
29
|
+
| { state: "not_initialized"; reason: "missing" | "zero_bytes" }
|
|
30
|
+
| { state: "unreadable"; phase: "open" | "read"; detail: string }
|
|
31
|
+
| { state: "foreign" }
|
|
32
|
+
| { state: "schema_mismatch"; schemaVersion: number }
|
|
33
|
+
| { state: "format_mismatch"; counter: string; stored: string; supported: string }
|
|
34
|
+
| { state: "incomplete"; firstGap: string; schemaVersion: number }
|
|
35
|
+
/** `db` is the caller's to close. */
|
|
36
|
+
| { state: "ok"; schemaVersion: number; db: Database };
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Every state in which a store exists but cannot serve a read: the four
|
|
40
|
+
* refusals both gates map to their own error type and wording.
|
|
41
|
+
*/
|
|
42
|
+
export type ReadStoreRefusal = Exclude<
|
|
43
|
+
ReadStoreClassification,
|
|
44
|
+
{ state: "ok" } | { state: "not_initialized" }
|
|
45
|
+
>;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Classify the store at `dbPath`. A missing path and a zero-byte file both read
|
|
49
|
+
* as "not initialized" rather than as a foreign or damaged store.
|
|
50
|
+
*/
|
|
51
|
+
export function classifyReadStore(dbPath: string): ReadStoreClassification {
|
|
52
|
+
if (!existsSync(dbPath)) return { state: "not_initialized", reason: "missing" };
|
|
53
|
+
|
|
54
|
+
let db: Database;
|
|
55
|
+
try {
|
|
56
|
+
// A zero-byte file is what an interrupted creation leaves behind, and
|
|
57
|
+
// SQLite opens one as an empty database: classify it before the open.
|
|
58
|
+
if (statSync(dbPath).size === 0) return { state: "not_initialized", reason: "zero_bytes" };
|
|
59
|
+
db = new Database(dbPath, { readonly: true });
|
|
60
|
+
} catch (err) {
|
|
61
|
+
return {
|
|
62
|
+
state: "unreadable",
|
|
63
|
+
phase: "open",
|
|
64
|
+
detail: err instanceof Error ? err.message : String(err),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
// Bound the wait on a busy store instead of failing a read that would
|
|
70
|
+
// succeed moments later. Connection-local, so both gates inherit it.
|
|
71
|
+
db.run("PRAGMA busy_timeout = 5000;");
|
|
72
|
+
const classification = classifyOpenStore(db);
|
|
73
|
+
if (classification.state !== "ok") db.close();
|
|
74
|
+
return classification;
|
|
75
|
+
} catch (err) {
|
|
76
|
+
// The engine failed a probe: a corrupt file, or a WAL store whose sidecars
|
|
77
|
+
// are gone (a read-only connection may not create them).
|
|
78
|
+
db.close();
|
|
79
|
+
return {
|
|
80
|
+
state: "unreadable",
|
|
81
|
+
phase: "read",
|
|
82
|
+
detail: err instanceof Error ? err.message : String(err),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function classifyOpenStore(db: Database): ReadStoreClassification {
|
|
88
|
+
const schemaVersion =
|
|
89
|
+
db.query<{ user_version: number }, []>("PRAGMA user_version;").get()?.user_version ?? 0;
|
|
90
|
+
const hasSessionsTable = !!db
|
|
91
|
+
.query<{ name: string }, []>(
|
|
92
|
+
"SELECT name FROM sqlite_master WHERE type='table' AND name='sessions' LIMIT 1;",
|
|
93
|
+
)
|
|
94
|
+
.get();
|
|
95
|
+
|
|
96
|
+
if (!hasSessionsTable) return { state: "foreign" };
|
|
97
|
+
if (schemaVersion > SCHEMA_VERSION) return { state: "schema_mismatch", schemaVersion };
|
|
98
|
+
|
|
99
|
+
const derivedByNewerRules = newerDerivedFormat(db);
|
|
100
|
+
if (derivedByNewerRules) return { state: "format_mismatch", ...derivedByNewerRules };
|
|
101
|
+
|
|
102
|
+
const firstGap = firstMissingPair(db);
|
|
103
|
+
if (firstGap !== null) return { state: "incomplete", firstGap, schemaVersion };
|
|
104
|
+
|
|
105
|
+
return { state: "ok", schemaVersion, db };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* The first derive-rule counter this store records as newer than this build
|
|
110
|
+
* derives, or null when none does.
|
|
111
|
+
*
|
|
112
|
+
* Three answers here are decisions, not defaults:
|
|
113
|
+
*
|
|
114
|
+
* - A store with no `schema_meta` table, a differing table shape, or no row for
|
|
115
|
+
* a counter records nothing and stays readable. Absence means "derived by a
|
|
116
|
+
* build that had no counter", and every such build derived the index the same
|
|
117
|
+
* way; refusing would refuse every store that predates the counter, which is
|
|
118
|
+
* every store that exists when the counter lands.
|
|
119
|
+
* - A stored value that is *older* is tolerated: the index is stale, not
|
|
120
|
+
* foreign, and Wake's readers serve a stale index while a re-scan is pending.
|
|
121
|
+
* The next writer pass re-derives it.
|
|
122
|
+
* - A value this build cannot parse counts as newer. A counter that cannot be
|
|
123
|
+
* compared cannot be vouched for; the writer's next open stamps a comparable
|
|
124
|
+
* one, and until then the read refuses *by name* rather than serving an index
|
|
125
|
+
* whose rules are unknown.
|
|
126
|
+
*/
|
|
127
|
+
function newerDerivedFormat(
|
|
128
|
+
db: Database,
|
|
129
|
+
): { counter: string; stored: string; supported: string } | null {
|
|
130
|
+
if (!hasMetaColumns(db)) return null;
|
|
131
|
+
const read = db.query<{ value: string }, [string]>(
|
|
132
|
+
"SELECT value FROM schema_meta WHERE key = ?;",
|
|
133
|
+
);
|
|
134
|
+
for (const counter of DERIVE_COUNTERS) {
|
|
135
|
+
const stored = read.get(counter.format)?.value ?? null;
|
|
136
|
+
if (stored === null || stored === counter.current) continue;
|
|
137
|
+
if (isNewerCounter(stored, counter.current)) {
|
|
138
|
+
return { counter: counter.format, stored, supported: counter.current };
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Decimal-integer counter values, per the contract in schema.ts. */
|
|
145
|
+
const COUNTER_VALUE = /^[0-9]+$/;
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Whether a stored counter is newer than the one this build derives. The values
|
|
149
|
+
* are decimal integers by contract, so the comparison is numeric — a string
|
|
150
|
+
* comparison would call `"10"` older than `"2"`. Anything unparsable counts as
|
|
151
|
+
* newer: on the store's side that is the conservative direction (see above), and
|
|
152
|
+
* on ours it is a build bug that must fail loudly rather than tolerate silently.
|
|
153
|
+
*/
|
|
154
|
+
function isNewerCounter(stored: string, supported: string): boolean {
|
|
155
|
+
if (!COUNTER_VALUE.test(stored) || !COUNTER_VALUE.test(supported)) return true;
|
|
156
|
+
return Number(stored) > Number(supported);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Whether `schema_meta` is present with the two columns the counters are read
|
|
161
|
+
* through. A store that never had the table, or has one of another shape, is
|
|
162
|
+
* treated as recording no counters rather than as unreadable: the counter
|
|
163
|
+
* decides what a *derived index* is worth, and a store whose bookkeeping table
|
|
164
|
+
* is foreign can still satisfy the read schema below (which is what refuses a
|
|
165
|
+
* store that is genuinely malformed).
|
|
166
|
+
*/
|
|
167
|
+
function hasMetaColumns(db: Database): boolean {
|
|
168
|
+
const columns = db
|
|
169
|
+
.query<{ name: string }, []>("PRAGMA table_info(schema_meta);")
|
|
170
|
+
.all()
|
|
171
|
+
.map((column) => column.name);
|
|
172
|
+
return columns.includes("key") && columns.includes("value");
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* One line for both gates: the refusal names the counter, the value the store
|
|
177
|
+
* records, and the value this build derives. A genuinely newer value needs an
|
|
178
|
+
* upgrade; an unparsable one only needs any writer pass, which stamps a
|
|
179
|
+
* comparable counter and re-derives under this build's rules.
|
|
180
|
+
*/
|
|
181
|
+
export function formatMismatchMessage(
|
|
182
|
+
dbPath: string,
|
|
183
|
+
mismatch: { counter: string; stored: string; supported: string },
|
|
184
|
+
): string {
|
|
185
|
+
const remedy = COUNTER_VALUE.test(mismatch.stored)
|
|
186
|
+
? " Upgrade Trajectories to read this store."
|
|
187
|
+
: " Run any Ledger writer pass (for example `trajectories sync`) to re-stamp it.";
|
|
188
|
+
return (
|
|
189
|
+
`Database at ${dbPath} was derived under newer rules` +
|
|
190
|
+
` (${mismatch.counter}: ${mismatch.stored}; this build derives ${mismatch.supported}).` +
|
|
191
|
+
remedy
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* One line per caller: the refusal has to name the store, the gap, and the way
|
|
197
|
+
* out, in the same words in both gates. A store carrying a Ledger user_version
|
|
198
|
+
* is an older Ledger store a writer pass will migrate; a zero version with
|
|
199
|
+
* missing shape is foreign or damaged, and rebuilding is the only way out.
|
|
200
|
+
*/
|
|
201
|
+
export function incompleteStoreMessage(
|
|
202
|
+
dbPath: string,
|
|
203
|
+
firstGap: string,
|
|
204
|
+
schemaVersion: number,
|
|
205
|
+
): string {
|
|
206
|
+
if (schemaVersion > 0) {
|
|
207
|
+
return (
|
|
208
|
+
`Database at ${dbPath} has an older Ledger store schema (v${schemaVersion})` +
|
|
209
|
+
` that predates some read paths: missing ${firstGap}.` +
|
|
210
|
+
" Run `trajectories sync` or `trajectories build` to migrate it, or point --db at a current store."
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
return (
|
|
214
|
+
`Database at ${dbPath} has an incomplete store schema: missing ${firstGap}.` +
|
|
215
|
+
" The store may be foreign or damaged — re-build it from sources (`trajectories sync`/`trajectories build`)" +
|
|
216
|
+
" or point --db at a Ledger store."
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* The first missing pair, in table-then-column order, so the same store always
|
|
222
|
+
* reports the same gap. Pairs are compared by code unit rather than locale, and
|
|
223
|
+
* a presence-only pair (null column) sorts first inside its table, which makes
|
|
224
|
+
* a missing table name itself instead of an arbitrary column of it.
|
|
225
|
+
*/
|
|
226
|
+
function firstMissingPair(db: Database): string | null {
|
|
227
|
+
const columnsByTable = new Map<string, Set<string>>();
|
|
228
|
+
for (const [table] of REQUIRED_READ_SCHEMA) {
|
|
229
|
+
if (!columnsByTable.has(table)) {
|
|
230
|
+
columnsByTable.set(
|
|
231
|
+
table,
|
|
232
|
+
new Set(
|
|
233
|
+
db
|
|
234
|
+
.query<{ name: string }, []>(`PRAGMA table_info(${table});`)
|
|
235
|
+
.all()
|
|
236
|
+
.map((row) => row.name),
|
|
237
|
+
),
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
for (const [table, column] of GAP_ORDER) {
|
|
243
|
+
const present = columnsByTable.get(table)!;
|
|
244
|
+
if (!present.size) return table;
|
|
245
|
+
if (column !== null && !present.has(column)) return `${table}.${column}`;
|
|
246
|
+
}
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* `REQUIRED_READ_SCHEMA` in gap-report order: table, then column. Columns are
|
|
252
|
+
* compared by code unit rather than locale so every process reports the same
|
|
253
|
+
* gap for the same store.
|
|
254
|
+
*/
|
|
255
|
+
const GAP_ORDER = [...REQUIRED_READ_SCHEMA].sort(
|
|
256
|
+
(a, b) => {
|
|
257
|
+
if (a[0] !== b[0]) return a[0] < b[0] ? -1 : 1;
|
|
258
|
+
const aColumn = a[1] ?? "";
|
|
259
|
+
const bColumn = b[1] ?? "";
|
|
260
|
+
if (aColumn === bColumn) return 0;
|
|
261
|
+
return aColumn < bColumn ? -1 : 1;
|
|
262
|
+
},
|
|
263
|
+
);
|