@gobing-ai/ts-llm-jsonl-importer 0.4.23 → 0.4.25

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.
@@ -1 +1 @@
1
- {"version":3,"file":"importer.d.ts","sourceRoot":"","sources":["../src/importer.ts"],"names":[],"mappings":"AAsBA,OAAO,KAAK,EAAe,aAAa,EAAE,YAAY,EAAc,gBAAgB,EAAoB,MAAM,SAAS,CAAC;AAOxH;;;;;;;GAOG;AACH,wBAAsB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,gBAAgB,EAAE,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,YAAY,CAAC,CA2IrH"}
1
+ {"version":3,"file":"importer.d.ts","sourceRoot":"","sources":["../src/importer.ts"],"names":[],"mappings":"AAuBA,OAAO,KAAK,EAER,aAAa,EACb,YAAY,EAGZ,gBAAgB,EAEnB,MAAM,SAAS,CAAC;AAOjB;;;;;;;GAOG;AACH,wBAAsB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,gBAAgB,EAAE,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,YAAY,CAAC,CA4KrH"}
package/dist/importer.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { ambientRuntimePaths, createNodeFileSystem, resolvePath, walkDir, } from '@gobing-ai/ts-runtime';
2
2
  import { sha256 } from './hash.js';
3
- import { applyHistoryImportSchema, ensureTargetTables, insertLedger, insertRecord, ledgerExists, readCheckpoint, resetCheckpoints, targetTableFor, writeCheckpoint, } from './jsonl-importer-dao.js';
3
+ import { applyHistoryImportSchema, ensureTargetTables, insertLedger, insertRecord, ledgerExists, readCheckpoint, reconcileFullImport, resetCheckpoints, targetTableFor, writeCheckpoint, } from './jsonl-importer-dao.js';
4
4
  import { redactRecord } from './redaction.js';
5
5
  import { resolveSourceDefinition } from './sources.js';
6
6
  /** Run the JSONL import pipeline for one source.
@@ -29,6 +29,9 @@ export async function runJsonlImport(source, options) {
29
29
  let skippedDuplicates = 0;
30
30
  let unknownRecords = 0;
31
31
  let checkpointUpdates = 0;
32
+ // Full-mode desired set (R1, task 0504): every record hash the current source produces.
33
+ // ReconcileFullImport diffs this against the persisted ledger to retire stale rows.
34
+ const desiredHashes = new Set();
32
35
  for (const file of files) {
33
36
  const checkpoint = mode === 'incremental' ? await readCheckpoint(options.db, resolvedSource, file) : 0;
34
37
  let lineNumber = 0;
@@ -41,8 +44,17 @@ export async function runJsonlImport(source, options) {
41
44
  const raw = parseJsonLine(line, file, lineNumber, parseErrors);
42
45
  if (raw === undefined)
43
46
  continue;
44
- const splitRecords = splitRawRecord(definition, raw);
47
+ const splitRecords = splitRawRecord(definition, raw, {
48
+ source: resolvedSource,
49
+ sourceFile: file,
50
+ sourceLine: lineNumber,
51
+ splitIndex: 0,
52
+ });
45
53
  let lineSucceeded = false;
54
+ // Atomic per-record acceptance (R2, task 0504): a schema-invalid split rejects the
55
+ // WHOLE line, so no partially accepted rows (or orphaned tool-call rows with a
56
+ // dangling message_hash) can be left behind by one bad split.
57
+ let lineRejected = false;
46
58
  const prepared = [];
47
59
  for (let splitIndex = 0; splitIndex < splitRecords.length; splitIndex += 1) {
48
60
  const split = splitRecords[splitIndex];
@@ -61,6 +73,7 @@ export async function runJsonlImport(source, options) {
61
73
  sourceLine: lineNumber,
62
74
  reason: parsed.error.issues.map((issue) => issue.message).join('; '),
63
75
  });
76
+ lineRejected = true;
64
77
  continue;
65
78
  }
66
79
  const redacted = redactRecord(parsed.data, options.redactionRules);
@@ -72,12 +85,17 @@ export async function runJsonlImport(source, options) {
72
85
  splitIndex,
73
86
  record: redacted,
74
87
  });
88
+ desiredHashes.add(recordHash);
75
89
  if (await ledgerExists(options.db, recordHash)) {
76
90
  skippedDuplicates += 1;
77
91
  continue;
78
92
  }
79
93
  prepared.push({ split, splitIndex, normalized: redacted, recordHash });
80
94
  }
95
+ // A rejected line is reported (validationErrors above) but never persisted — no
96
+ // second pass, no checkpoint advance, no partial rows from this record.
97
+ if (lineRejected)
98
+ continue;
81
99
  // Second pass: resolve _messageSplitIndex → message_hash, then insert.
82
100
  const recordHashBySplitIndex = new Map(prepared.map((e) => [e.splitIndex, e.recordHash]));
83
101
  for (const entry of prepared) {
@@ -106,6 +124,13 @@ export async function runJsonlImport(source, options) {
106
124
  }
107
125
  }
108
126
  }
127
+ // Full-mode reconciliation (R1, task 0504): diff the desired hash set against the
128
+ // persisted ledger and retire stale derived rows (target, tool, ledger, checkpoint) in
129
+ // one source-scoped batch. Dry-run computes the identical counts without mutation.
130
+ let reconciliation;
131
+ if (mode === 'full') {
132
+ reconciliation = await reconcileFullImport(options.db, resolvedSource, desiredHashes, files, options.dryRun ?? false);
133
+ }
109
134
  return {
110
135
  source: resolvedSource,
111
136
  mode,
@@ -117,6 +142,7 @@ export async function runJsonlImport(source, options) {
117
142
  parseErrors,
118
143
  validationErrors,
119
144
  checkpointUpdates,
145
+ reconciliation,
120
146
  };
121
147
  }
122
148
  function parseJsonLine(line, sourceFile, sourceLine, parseErrors) {
@@ -133,7 +159,7 @@ function parseJsonLine(line, sourceFile, sourceLine, parseErrors) {
133
159
  return undefined;
134
160
  }
135
161
  }
136
- function splitRawRecord(definition, raw) {
162
+ function splitRawRecord(definition, raw, context) {
137
163
  const targetTable = targetTableFor(definition.targetTable);
138
164
  if (definition.splitConfig.mode === 'one-to-one') {
139
165
  return [{ targetTable, raw }];
@@ -141,7 +167,7 @@ function splitRawRecord(definition, raw) {
141
167
  if (definition.splitConfig.mode === 'custom') {
142
168
  const config = definition.splitConfig;
143
169
  const configTable = config.targetTable !== undefined ? targetTableFor(config.targetTable) : undefined;
144
- return config.split(raw).map((entry) => {
170
+ return config.split(raw, context).map((entry) => {
145
171
  // Normalize: SplitEntry has a `record` property, bare object is the record itself.
146
172
  const hasTargetTable = 'targetTable' in entry && typeof entry.targetTable === 'string';
147
173
  const record = hasTargetTable ? entry.record : entry;
package/dist/index.d.ts CHANGED
@@ -2,8 +2,9 @@ export { HistoryImportError } from './errors';
2
2
  export { sha256, stableJson } from './hash';
3
3
  export { runJsonlImport } from './importer';
4
4
  export { applyHistoryImportSchema, normalizeSourceFilePaths } from './jsonl-importer-dao';
5
+ export { type OpenCodeImportOptions, runOpenCodeImport } from './opencode-importer';
5
6
  export { DEFAULT_REDACTION_RULES, redactRecord, redactValue } from './redaction';
6
7
  export { HISTORY_IMPORT_SCHEMA_SQL } from './schema-sql';
7
8
  export { getSourceDefinition, resolveSourceDefinition, SOURCE_DEFINITIONS, VALID_TABLE_NAME, validateSourceDefinition, } from './sources';
8
- export type { FieldTransform, ImportIssue, ImportMode, ImportOptions, ImportResult, JsonObject, LlmJsonlSource, RedactionRule, SourceDefinition, SplitConfig, SplitEntry, TransformContext, } from './types';
9
+ export type { FieldTransform, ImportIssue, ImportMode, ImportOptions, ImportResult, JsonObject, LlmJsonlSource, ReconcileSummary, RedactionRule, SourceDefinition, SplitConfig, SplitEntry, TransformContext, } from './types';
9
10
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAC;AAC9C,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAC;AAC5C,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAC5C,OAAO,EAAE,wBAAwB,EAAE,wBAAwB,EAAE,MAAM,sBAAsB,CAAC;AAC1F,OAAO,EAAE,uBAAuB,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AACjF,OAAO,EAAE,yBAAyB,EAAE,MAAM,cAAc,CAAC;AACzD,OAAO,EACH,mBAAmB,EACnB,uBAAuB,EACvB,kBAAkB,EAClB,gBAAgB,EAChB,wBAAwB,GAC3B,MAAM,WAAW,CAAC;AACnB,YAAY,EACR,cAAc,EACd,WAAW,EACX,UAAU,EACV,aAAa,EACb,YAAY,EACZ,UAAU,EACV,cAAc,EACd,aAAa,EACb,gBAAgB,EAChB,WAAW,EACX,UAAU,EACV,gBAAgB,GACnB,MAAM,SAAS,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAC;AAC9C,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAC;AAC5C,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAC5C,OAAO,EAAE,wBAAwB,EAAE,wBAAwB,EAAE,MAAM,sBAAsB,CAAC;AAC1F,OAAO,EAAE,KAAK,qBAAqB,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AACpF,OAAO,EAAE,uBAAuB,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AACjF,OAAO,EAAE,yBAAyB,EAAE,MAAM,cAAc,CAAC;AACzD,OAAO,EACH,mBAAmB,EACnB,uBAAuB,EACvB,kBAAkB,EAClB,gBAAgB,EAChB,wBAAwB,GAC3B,MAAM,WAAW,CAAC;AACnB,YAAY,EACR,cAAc,EACd,WAAW,EACX,UAAU,EACV,aAAa,EACb,YAAY,EACZ,UAAU,EACV,cAAc,EACd,gBAAgB,EAChB,aAAa,EACb,gBAAgB,EAChB,WAAW,EACX,UAAU,EACV,gBAAgB,GACnB,MAAM,SAAS,CAAC"}
package/dist/index.js CHANGED
@@ -2,6 +2,7 @@ export { HistoryImportError } from './errors.js';
2
2
  export { sha256, stableJson } from './hash.js';
3
3
  export { runJsonlImport } from './importer.js';
4
4
  export { applyHistoryImportSchema, normalizeSourceFilePaths } from './jsonl-importer-dao.js';
5
+ export { runOpenCodeImport } from './opencode-importer.js';
5
6
  export { DEFAULT_REDACTION_RULES, redactRecord, redactValue } from './redaction.js';
6
7
  export { HISTORY_IMPORT_SCHEMA_SQL } from './schema-sql.js';
7
8
  export { getSourceDefinition, resolveSourceDefinition, SOURCE_DEFINITIONS, VALID_TABLE_NAME, validateSourceDefinition, } from './sources.js';
@@ -1,4 +1,5 @@
1
- import type { ImportOptions, JsonObject, SourceDefinition } from './types';
1
+ import type { DbAdapter, DbBatchOp } from '@gobing-ai/ts-db';
2
+ import type { ImportOptions, JsonObject, ReconcileSummary, SourceDefinition } from './types';
2
3
  interface CheckpointRow {
3
4
  readonly last_imported_line: number;
4
5
  }
@@ -46,6 +47,57 @@ declare function insertLedger(db: ImportOptions['db'], recordHash: string, sourc
46
47
  * runtime `FileSystem` seam.
47
48
  */
48
49
  export declare function normalizeSourceFilePaths(db: ImportOptions['db'], resolveRealPath: (sourceFile: string) => string | null | undefined): Promise<void>;
50
+ /**
51
+ * Reconcile a source's persisted derived rows against the desired set produced by a full
52
+ * import (task 0504 R1). Rows whose `record_hash` is no longer reproduced by current source
53
+ * data or mapper output are stale: their target rows (typed contract tables — including tool
54
+ * calls — and ETL tables), ledger rows, and checkpoints for vanished source files are removed
55
+ * in ONE source-scoped batch (atomic). Dry-run returns the exact same counts without mutating
56
+ * the database, so `--dry-run` is a safe preview and a second full write reports zero changes.
57
+ *
58
+ * Deletes are keyed by `record_hash` (PK) and checkpoint `(source, source_file)` (PK) — never
59
+ * by an unindexed ledger predicate — so reconciliation cost scales with the stale count, not
60
+ * with ledger size.
61
+ */
62
+ declare function reconcileFullImport(db: ImportOptions['db'], source: string, desiredHashes: ReadonlySet<string>, discoveredFiles: readonly string[], dryRun: boolean): Promise<ReconcileSummary>;
63
+ /** OpenCode message row projected from its SQLite store. */
64
+ export interface OpenCodeMessageRow {
65
+ id: string;
66
+ session_id: string;
67
+ time_created: number;
68
+ data: string;
69
+ directory: string;
70
+ }
71
+ /** OpenCode message-part row projected from its SQLite store. */
72
+ export interface OpenCodePartRow {
73
+ id: string;
74
+ message_id: string;
75
+ time_created: number;
76
+ data: string;
77
+ }
78
+ /** Previously imported OpenCode ledger entry. */
79
+ export interface OpenCodeExistingEntry {
80
+ record_hash: string;
81
+ target_table: string;
82
+ }
83
+ /** Normalized OpenCode entry awaiting a history-table write. */
84
+ export interface OpenCodeQueuedEntry {
85
+ targetTable: 'history_message' | 'history_tool_call';
86
+ splitIndex: number;
87
+ record: JsonObject;
88
+ recordHash: string;
89
+ sourceFile: string;
90
+ }
91
+ /** Read one ordered page of OpenCode messages after the supplied cursor. */
92
+ export declare function readOpenCodeMessages(db: DbAdapter, lastTime: number, lastId: string, limit: number): Promise<OpenCodeMessageRow[]>;
93
+ /** Read all parts belonging to the supplied OpenCode message IDs. */
94
+ export declare function readOpenCodeParts(db: DbAdapter, messageIds: readonly string[]): Promise<OpenCodePartRow[]>;
95
+ /** Load existing OpenCode ledger entries grouped by source message ID. */
96
+ export declare function readOpenCodeExistingEntries(db: DbAdapter): Promise<Map<string, OpenCodeExistingEntry[]>>;
97
+ /** Build deletion operations for records superseded by a forced import. */
98
+ export declare function openCodeDeleteOperations(entries: readonly OpenCodeExistingEntry[]): DbBatchOp[];
99
+ /** Build batched history, ledger, and checkpoint writes for OpenCode entries. */
100
+ export declare function openCodeBulkWriteOperations(entries: readonly OpenCodeQueuedEntry[], checkpointFiles: readonly string[], now: ImportOptions['now']): DbBatchOp[];
49
101
  export type { CheckpointRow };
50
- export { ETL_TABLE_DDL, ensureTargetTables, insertLedger, insertRecord, ledgerExists, readCheckpoint, resetCheckpoints, targetTableFor, timestamp, writeCheckpoint, };
102
+ export { ETL_TABLE_DDL, ensureTargetTables, insertLedger, insertRecord, ledgerExists, readCheckpoint, reconcileFullImport, resetCheckpoints, targetTableFor, timestamp, writeCheckpoint, };
51
103
  //# sourceMappingURL=jsonl-importer-dao.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"jsonl-importer-dao.d.ts","sourceRoot":"","sources":["../src/jsonl-importer-dao.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAE3E,UAAU,aAAa;IACnB,QAAQ,CAAC,kBAAkB,EAAE,MAAM,CAAC;CACvC;AAqDD,iBAAS,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAK7C;AAED,iBAAS,SAAS,CAAC,GAAG,EAAE,aAAa,CAAC,KAAK,CAAC,GAAG,MAAM,CAEpD;AAED,iBAAS,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAS5C;AAED,0DAA0D;AAC1D,wBAAsB,wBAAwB,CAAC,EAAE,EAAE,aAAa,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAOrF;AAED;;;;;;;;;;GAUG;AACH,iBAAe,kBAAkB,CAAC,EAAE,EAAE,aAAa,CAAC,IAAI,CAAC,EAAE,UAAU,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAUtG;AAED,iBAAe,cAAc,CAAC,EAAE,EAAE,aAAa,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAO1G;AAED,iBAAe,gBAAgB,CAAC,EAAE,EAAE,aAAa,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAIhH;AAED,iBAAe,eAAe,CAC1B,EAAE,EAAE,aAAa,CAAC,IAAI,CAAC,EACvB,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,MAAM,EACZ,GAAG,EAAE,aAAa,CAAC,KAAK,CAAC,GAC1B,OAAO,CAAC,IAAI,CAAC,CAYf;AAED,iBAAe,YAAY,CAAC,EAAE,EAAE,aAAa,CAAC,IAAI,CAAC,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAMzF;AAID,iBAAe,YAAY,CACvB,EAAE,EAAE,aAAa,CAAC,IAAI,CAAC,EACvB,WAAW,EAAE,MAAM,EACnB,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,UAAU,EACnB,GAAG,EAAE,aAAa,CAAC,KAAK,CAAC,GAC1B,OAAO,CAAC,IAAI,CAAC,CAiDf;AAED,iBAAe,YAAY,CACvB,EAAE,EAAE,aAAa,CAAC,IAAI,CAAC,EACvB,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,EAClB,WAAW,EAAE,MAAM,EACnB,GAAG,EAAE,aAAa,CAAC,KAAK,CAAC,GAC1B,OAAO,CAAC,IAAI,CAAC,CAYf;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAsB,wBAAwB,CAC1C,EAAE,EAAE,aAAa,CAAC,IAAI,CAAC,EACvB,eAAe,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,GAAG,SAAS,GACnE,OAAO,CAAC,IAAI,CAAC,CAWf;AA6FD,YAAY,EAAE,aAAa,EAAE,CAAC;AAC9B,OAAO,EACH,aAAa,EACb,kBAAkB,EAClB,YAAY,EACZ,YAAY,EACZ,YAAY,EACZ,cAAc,EACd,gBAAgB,EAChB,cAAc,EACd,SAAS,EACT,eAAe,GAClB,CAAC"}
1
+ {"version":3,"file":"jsonl-importer-dao.d.ts","sourceRoot":"","sources":["../src/jsonl-importer-dao.ts"],"names":[],"mappings":"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"}
@@ -252,4 +252,156 @@ function resolveWithFallback(resolveRealPath, sourceFile) {
252
252
  return sourceFile;
253
253
  }
254
254
  }
255
- export { ETL_TABLE_DDL, ensureTargetTables, insertLedger, insertRecord, ledgerExists, readCheckpoint, resetCheckpoints, targetTableFor, timestamp, writeCheckpoint, };
255
+ /**
256
+ * Reconcile a source's persisted derived rows against the desired set produced by a full
257
+ * import (task 0504 R1). Rows whose `record_hash` is no longer reproduced by current source
258
+ * data or mapper output are stale: their target rows (typed contract tables — including tool
259
+ * calls — and ETL tables), ledger rows, and checkpoints for vanished source files are removed
260
+ * in ONE source-scoped batch (atomic). Dry-run returns the exact same counts without mutating
261
+ * the database, so `--dry-run` is a safe preview and a second full write reports zero changes.
262
+ *
263
+ * Deletes are keyed by `record_hash` (PK) and checkpoint `(source, source_file)` (PK) — never
264
+ * by an unindexed ledger predicate — so reconciliation cost scales with the stale count, not
265
+ * with ledger size.
266
+ */
267
+ async function reconcileFullImport(db, source, desiredHashes, discoveredFiles, dryRun) {
268
+ const ledgerRows = await db.queryAll('SELECT record_hash, target_table FROM history_import_ledger WHERE source = ?', source);
269
+ const stale = [];
270
+ for (const row of ledgerRows) {
271
+ if (!desiredHashes.has(row.record_hash))
272
+ stale.push(row);
273
+ }
274
+ const checkpointRows = await db.queryAll('SELECT source_file FROM history_import_checkpoint WHERE source = ?', source);
275
+ const discovered = new Set(discoveredFiles);
276
+ const staleCheckpoints = checkpointRows
277
+ .map((row) => row.source_file)
278
+ .filter((sourceFile) => !discovered.has(sourceFile));
279
+ const summary = {
280
+ staleTargetRows: stale.length,
281
+ staleLedgerRows: stale.length,
282
+ staleCheckpointRows: staleCheckpoints.length,
283
+ };
284
+ if (dryRun || (stale.length === 0 && staleCheckpoints.length === 0))
285
+ return summary;
286
+ const operations = [];
287
+ for (const row of stale) {
288
+ operations.push({
289
+ sql: `DELETE FROM ${targetTableFor(row.target_table)} WHERE record_hash = ?`,
290
+ params: [row.record_hash],
291
+ });
292
+ operations.push({
293
+ sql: 'DELETE FROM history_import_ledger WHERE record_hash = ?',
294
+ params: [row.record_hash],
295
+ });
296
+ }
297
+ for (const sourceFile of staleCheckpoints) {
298
+ operations.push({
299
+ sql: 'DELETE FROM history_import_checkpoint WHERE source = ? AND source_file = ?',
300
+ params: [source, sourceFile],
301
+ });
302
+ }
303
+ await db.batch(operations);
304
+ return summary;
305
+ }
306
+ const OPENCODE_SOURCE = 'opencode';
307
+ /** Read one ordered page of OpenCode messages after the supplied cursor. */
308
+ export async function readOpenCodeMessages(db, lastTime, lastId, limit) {
309
+ return db.queryAll(`SELECT m.id, m.session_id, m.time_created, m.data, s.directory
310
+ FROM message m
311
+ JOIN session s ON s.id = m.session_id
312
+ WHERE m.time_created > ? OR (m.time_created = ? AND m.id > ?)
313
+ ORDER BY m.time_created, m.id
314
+ LIMIT ?`, lastTime, lastTime, lastId, limit);
315
+ }
316
+ /** Read all parts belonging to the supplied OpenCode message IDs. */
317
+ export async function readOpenCodeParts(db, messageIds) {
318
+ if (messageIds.length === 0)
319
+ return [];
320
+ return db.queryAll(`SELECT id, message_id, time_created, data
321
+ FROM part
322
+ WHERE message_id IN (${messageIds.map(() => '?').join(', ')})
323
+ ORDER BY time_created, id`, ...messageIds);
324
+ }
325
+ /** Load existing OpenCode ledger entries grouped by source message ID. */
326
+ export async function readOpenCodeExistingEntries(db) {
327
+ const rows = await db.queryAll('SELECT source_file, record_hash, target_table FROM history_import_ledger WHERE source = ?', OPENCODE_SOURCE);
328
+ const bySourceFile = new Map();
329
+ for (const row of rows) {
330
+ const entries = bySourceFile.get(row.source_file) ?? [];
331
+ entries.push({ record_hash: row.record_hash, target_table: row.target_table });
332
+ bySourceFile.set(row.source_file, entries);
333
+ }
334
+ return bySourceFile;
335
+ }
336
+ /** Build deletion operations for records superseded by a forced import. */
337
+ export function openCodeDeleteOperations(entries) {
338
+ return entries.flatMap((entry) => [
339
+ {
340
+ sql: `DELETE FROM ${targetTableFor(entry.target_table)} WHERE record_hash = ?`,
341
+ params: [entry.record_hash],
342
+ },
343
+ {
344
+ sql: 'DELETE FROM history_import_ledger WHERE record_hash = ?',
345
+ params: [entry.record_hash],
346
+ },
347
+ ]);
348
+ }
349
+ /** Build batched history, ledger, and checkpoint writes for OpenCode entries. */
350
+ export function openCodeBulkWriteOperations(entries, checkpointFiles, now) {
351
+ const importedAt = timestamp(now);
352
+ const operations = [];
353
+ for (const table of ['history_message', 'history_tool_call']) {
354
+ const tableEntries = entries.filter((entry) => entry.targetTable === table);
355
+ for (let offset = 0; offset < tableEntries.length; offset += 100) {
356
+ const chunk = tableEntries.slice(offset, offset + 100);
357
+ const columns = TYPED_TABLE_COLUMNS[table] ?? [];
358
+ operations.push({
359
+ sql: `WITH input(record_hash, payload_json, imported_at) AS (
360
+ VALUES ${chunk.map(() => '(?, ?, ?)').join(', ')}
361
+ )
362
+ INSERT INTO ${table} (${columns.join(', ')})
363
+ SELECT ${columns
364
+ .map((column) => column === 'record_hash'
365
+ ? 'record_hash'
366
+ : column === 'imported_at'
367
+ ? 'imported_at'
368
+ : `json_extract(payload_json, '$.${column}')`)
369
+ .join(', ')}
370
+ FROM input
371
+ WHERE true
372
+ ON CONFLICT(record_hash) DO NOTHING`,
373
+ params: chunk.flatMap((entry) => [entry.recordHash, JSON.stringify(entry.record), importedAt]),
374
+ });
375
+ }
376
+ }
377
+ for (let offset = 0; offset < entries.length; offset += 200) {
378
+ const chunk = entries.slice(offset, offset + 200);
379
+ operations.push({
380
+ sql: `INSERT INTO history_import_ledger
381
+ (record_hash, source, source_file, source_line, split_index, target_table, imported_at)
382
+ VALUES ${chunk.map(() => '(?, ?, ?, ?, ?, ?, ?)').join(', ')}`,
383
+ params: chunk.flatMap((entry) => [
384
+ entry.recordHash,
385
+ OPENCODE_SOURCE,
386
+ entry.sourceFile,
387
+ 1,
388
+ entry.splitIndex,
389
+ entry.targetTable,
390
+ importedAt,
391
+ ]),
392
+ });
393
+ }
394
+ for (let offset = 0; offset < checkpointFiles.length; offset += 200) {
395
+ const chunk = checkpointFiles.slice(offset, offset + 200);
396
+ operations.push({
397
+ sql: `INSERT INTO history_import_checkpoint (source, source_file, last_imported_line, updated_at)
398
+ VALUES ${chunk.map(() => '(?, ?, ?, ?)').join(', ')}
399
+ ON CONFLICT(source, source_file) DO UPDATE SET
400
+ last_imported_line = excluded.last_imported_line,
401
+ updated_at = excluded.updated_at`,
402
+ params: chunk.flatMap((sourceFile) => [OPENCODE_SOURCE, sourceFile, 1, importedAt]),
403
+ });
404
+ }
405
+ return operations;
406
+ }
407
+ export { ETL_TABLE_DDL, ensureTargetTables, insertLedger, insertRecord, ledgerExists, readCheckpoint, reconcileFullImport, resetCheckpoints, targetTableFor, timestamp, writeCheckpoint, };
package/dist/mappers.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { z } from 'zod';
2
- import type { JsonObject, SplitEntry } from './types';
2
+ import type { JsonObject, SplitEntry, TransformContext } from './types';
3
3
  /** Map a Claude Code JSONL record into one or more ETL split entries. */
4
4
  export declare function claudeSplit(raw: Record<string, unknown>): readonly SplitEntry[];
5
5
  /** Map a Pi JSONL record into one or more ETL split entries. */
@@ -10,6 +10,8 @@ export declare function ompSplit(raw: Record<string, unknown>): readonly SplitEn
10
10
  export declare function codexSplit(raw: Record<string, unknown>): readonly SplitEntry[];
11
11
  /** Map an Antigravity (AGY) JSONL record into one or more ETL split entries. */
12
12
  export declare function agySplit(raw: Record<string, unknown>): readonly SplitEntry[];
13
+ /** Map Gemini CLI message, tool, and session-state events into the forensic contract tables. */
14
+ export declare function geminiSplit(raw: Record<string, unknown>, context?: TransformContext): readonly SplitEntry[];
13
15
  /** Map a Grok JSONL record into one or more ETL split entries. */
14
16
  export declare function grokSplit(raw: Record<string, unknown>): readonly SplitEntry[];
15
17
  /** Compute a stable field-shape key from a JSON object: sorted, lowercased, +-joined top-level keys. */
@@ -26,6 +28,8 @@ export declare const CODEX_FIELD_MAP: Record<string, string>;
26
28
  export declare const AGY_FIELD_MAP: Record<string, string>;
27
29
  /** Identity field map for the Grok mapper. */
28
30
  export declare const GROK_FIELD_MAP: Record<string, string>;
31
+ /** Identity field map for the Gemini mapper. */
32
+ export declare const GEMINI_FIELD_MAP: Record<string, string>;
29
33
  /** Passthrough Zod schema for the Claude mapper. */
30
34
  export declare const CLAUDE_SCHEMA: z.ZodObject<{}, z.core.$loose>;
31
35
  /** Passthrough Zod schema for the Pi mapper. */
@@ -38,4 +42,6 @@ export declare const CODEX_SCHEMA: z.ZodObject<{}, z.core.$loose>;
38
42
  export declare const AGY_SCHEMA: z.ZodObject<{}, z.core.$loose>;
39
43
  /** Passthrough Zod schema for the Grok mapper. */
40
44
  export declare const GROK_SCHEMA: z.ZodObject<{}, z.core.$loose>;
45
+ /** Passthrough Zod schema for the Gemini mapper. */
46
+ export declare const GEMINI_SCHEMA: z.ZodObject<{}, z.core.$loose>;
41
47
  //# sourceMappingURL=mappers.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"mappers.d.ts","sourceRoot":"","sources":["../src/mappers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,KAAK,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AA2FtD,yEAAyE;AACzE,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,UAAU,EAAE,CA4G/E;AAMD,gEAAgE;AAChE,wBAAgB,OAAO,CAAC,GAAG,EAAE,OAAO,GAAG,SAAS,UAAU,EAAE,CAsE3D;AAMD,kEAAkE;AAClE,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,UAAU,EAAE,CA4F5E;AAMD,mEAAmE;AACnE,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,UAAU,EAAE,CAiG9E;AAMD,gFAAgF;AAChF,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,UAAU,EAAE,CAgH5E;AAyJD,kEAAkE;AAClE,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,UAAU,EAAE,CAoR7E;AAwDD,wGAAwG;AACxG,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,UAAU,GAAG,MAAM,CAKxD;AAMD,gDAAgD;AAChD,eAAO,MAAM,gBAAgB,wBAAsE,CAAC;AACpG,4CAA4C;AAC5C,eAAO,MAAM,YAAY,wBAAsE,CAAC;AAChG,6CAA6C;AAC7C,eAAO,MAAM,aAAa,wBAAsE,CAAC;AACjG,+CAA+C;AAC/C,eAAO,MAAM,eAAe,wBAAsE,CAAC;AACnG,6CAA6C;AAC7C,eAAO,MAAM,aAAa,wBAAsE,CAAC;AACjG,8CAA8C;AAC9C,eAAO,MAAM,cAAc,wBAAsE,CAAC;AAQlG,oDAAoD;AACpD,eAAO,MAAM,aAAa,gCAAoB,CAAC;AAC/C,gDAAgD;AAChD,eAAO,MAAM,SAAS,gCAAoB,CAAC;AAC3C,iDAAiD;AACjD,eAAO,MAAM,UAAU,gCAAoB,CAAC;AAC5C,mDAAmD;AACnD,eAAO,MAAM,YAAY,gCAAoB,CAAC;AAC9C,iDAAiD;AACjD,eAAO,MAAM,UAAU,gCAAoB,CAAC;AAC5C,kDAAkD;AAClD,eAAO,MAAM,WAAW,gCAAoB,CAAC"}
1
+ {"version":3,"file":"mappers.d.ts","sourceRoot":"","sources":["../src/mappers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,KAAK,EAAE,UAAU,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AA2FxE,yEAAyE;AACzE,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,UAAU,EAAE,CA4G/E;AAMD,gEAAgE;AAChE,wBAAgB,OAAO,CAAC,GAAG,EAAE,OAAO,GAAG,SAAS,UAAU,EAAE,CAsE3D;AAMD,kEAAkE;AAClE,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,UAAU,EAAE,CA4F5E;AAMD,mEAAmE;AACnE,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,UAAU,EAAE,CAiG9E;AAMD,gFAAgF;AAChF,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,UAAU,EAAE,CAgH5E;AAMD,gGAAgG;AAChG,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO,CAAC,EAAE,gBAAgB,GAAG,SAAS,UAAU,EAAE,CAgF3G;AAyMD,kEAAkE;AAClE,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,UAAU,EAAE,CAyS7E;AAwDD,wGAAwG;AACxG,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,UAAU,GAAG,MAAM,CAKxD;AAMD,gDAAgD;AAChD,eAAO,MAAM,gBAAgB,wBAAsE,CAAC;AACpG,4CAA4C;AAC5C,eAAO,MAAM,YAAY,wBAAsE,CAAC;AAChG,6CAA6C;AAC7C,eAAO,MAAM,aAAa,wBAAsE,CAAC;AACjG,+CAA+C;AAC/C,eAAO,MAAM,eAAe,wBAAsE,CAAC;AACnG,6CAA6C;AAC7C,eAAO,MAAM,aAAa,wBAAsE,CAAC;AACjG,8CAA8C;AAC9C,eAAO,MAAM,cAAc,wBAAsE,CAAC;AAClG,gDAAgD;AAChD,eAAO,MAAM,gBAAgB,wBAAsE,CAAC;AAQpG,oDAAoD;AACpD,eAAO,MAAM,aAAa,gCAAoB,CAAC;AAC/C,gDAAgD;AAChD,eAAO,MAAM,SAAS,gCAAoB,CAAC;AAC3C,iDAAiD;AACjD,eAAO,MAAM,UAAU,gCAAoB,CAAC;AAC5C,mDAAmD;AACnD,eAAO,MAAM,YAAY,gCAAoB,CAAC;AAC9C,iDAAiD;AACjD,eAAO,MAAM,UAAU,gCAAoB,CAAC;AAC5C,kDAAkD;AAClD,eAAO,MAAM,WAAW,gCAAoB,CAAC;AAC7C,oDAAoD;AACpD,eAAO,MAAM,aAAa,gCAAoB,CAAC"}
package/dist/mappers.js CHANGED
@@ -366,9 +366,9 @@ export function codexSplit(raw) {
366
366
  record: {
367
367
  session_id: sessionId,
368
368
  seq,
369
- role: 'unknown',
369
+ role: 'meta',
370
370
  record_type: 'short_format',
371
- disposition: 'unknown',
371
+ disposition: 'meta',
372
372
  ts,
373
373
  provenance: 'ambient',
374
374
  },
@@ -552,6 +552,109 @@ export function agySplit(raw) {
552
552
  return entries;
553
553
  }
554
554
  // ---------------------------------------------------------------------------
555
+ // Gemini mapper
556
+ // ---------------------------------------------------------------------------
557
+ /** Map Gemini CLI message, tool, and session-state events into the forensic contract tables. */
558
+ export function geminiSplit(raw, context) {
559
+ const recordType = String(raw.type ?? (raw.kind !== undefined ? 'session' : raw.$set !== undefined ? 'state' : ''));
560
+ const state = o(raw.$set);
561
+ const sessionId = sessionIdFromContext(context, raw);
562
+ const seq = context?.sourceLine ?? 0;
563
+ const ts = s(raw.timestamp, raw.startTime, raw.lastUpdated, state.lastUpdated) ?? new Date(0).toISOString();
564
+ if (recordType === 'session' || recordType === 'state') {
565
+ return [
566
+ {
567
+ targetTable: 'history_message',
568
+ record: {
569
+ session_id: sessionId,
570
+ seq,
571
+ role: 'meta',
572
+ record_type: recordType,
573
+ disposition: 'meta',
574
+ ts,
575
+ content_text: s(state.summary) ?? null,
576
+ provenance: detectProvenance(context?.sourceFile),
577
+ },
578
+ },
579
+ ];
580
+ }
581
+ const role = recordType === 'gemini' ? 'assistant' : recordType === 'user' ? 'user' : 'system';
582
+ const tokens = o(raw.tokens);
583
+ const content = geminiContent(raw);
584
+ const entries = [
585
+ {
586
+ targetTable: 'history_message',
587
+ record: {
588
+ session_id: sessionId,
589
+ seq,
590
+ role,
591
+ record_type: recordType,
592
+ disposition: 'keep',
593
+ ts,
594
+ duration_ms: null,
595
+ model: s(raw.model) ?? null,
596
+ input_tokens: numberOrNull(tokens.input),
597
+ output_tokens: numberOrNull(tokens.output),
598
+ cache_read_tokens: numberOrNull(tokens.cached),
599
+ cache_write_tokens: null,
600
+ cost_usd: null,
601
+ content_text: content,
602
+ cwd: null,
603
+ provenance: detectProvenance(context?.sourceFile),
604
+ },
605
+ },
606
+ ];
607
+ if (Array.isArray(raw.toolCalls)) {
608
+ for (const value of raw.toolCalls) {
609
+ if (value === null || typeof value !== 'object' || Array.isArray(value))
610
+ continue;
611
+ const tool = value;
612
+ const status = s(tool.status) ?? 'unknown';
613
+ const timestamp = s(tool.timestamp);
614
+ const result = tool.result ?? tool.resultDisplay;
615
+ entries.push({
616
+ targetTable: 'history_tool_call',
617
+ record: {
618
+ _messageSplitIndex: 0,
619
+ session_id: sessionId,
620
+ seq,
621
+ tool_name: s(tool.name, tool.displayName) ?? 'unknown',
622
+ args_digest: argsDigest(tool.args),
623
+ status,
624
+ started_at: timestamp,
625
+ completed_at: timestamp,
626
+ duration_ms: null,
627
+ result_bytes: result === undefined ? null : new TextEncoder().encode(JSON.stringify(result)).byteLength,
628
+ error_text: status === 'error' ? (s(tool.resultDisplay, tool.result) ?? null) : null,
629
+ },
630
+ });
631
+ }
632
+ }
633
+ return entries;
634
+ }
635
+ function sessionIdFromContext(context, raw) {
636
+ if (context !== undefined) {
637
+ const file = context.sourceFile.split('/').at(-1);
638
+ if (file !== undefined)
639
+ return file.replace(/\.jsonl$/, '');
640
+ }
641
+ return s(raw.sessionId, raw.id) ?? 'unknown';
642
+ }
643
+ function geminiContent(raw) {
644
+ const content = extractContentText(raw.content);
645
+ const thoughts = Array.isArray(raw.thoughts)
646
+ ? raw.thoughts
647
+ .map((value) => (value !== null && typeof value === 'object' ? s(o(value).description) : undefined))
648
+ .filter((value) => value !== undefined)
649
+ .join('\n')
650
+ : '';
651
+ return ([content, thoughts].filter((value) => value !== undefined && value.length > 0).join('\n') ||
652
+ null);
653
+ }
654
+ function numberOrNull(value) {
655
+ return typeof value === 'number' && Number.isFinite(value) ? value : null;
656
+ }
657
+ // ---------------------------------------------------------------------------
555
658
  // Grok mapper
556
659
  // ---------------------------------------------------------------------------
557
660
  /**
@@ -566,13 +669,29 @@ function normalizeGrokRecord(raw) {
566
669
  const update = params?.update;
567
670
  const isUpdatesShape = typeof raw.method === 'string' && params !== undefined && update !== undefined;
568
671
  const body = isUpdatesShape ? update : raw;
569
- const recordType = String((isUpdatesShape ? update?.sessionUpdate : raw.type) ?? raw.sessionUpdate ?? '');
570
- const sessionId = s(params?.sessionId, raw.session_id, raw.sessionId, raw.params?.sessionId) ?? 'unknown';
571
- const ts = normalizeTs(raw.ts ?? raw.timestamp ?? body.ts ?? body.timestamp);
672
+ const recordType = String((isUpdatesShape ? update?.sessionUpdate : raw.type) ??
673
+ raw.sessionUpdate ??
674
+ raw.eventType ??
675
+ (raw.file_snapshots !== undefined || raw.after_snapshots !== undefined
676
+ ? 'file_snapshot'
677
+ : raw.prompt !== undefined
678
+ ? 'user_message'
679
+ : raw.answer !== undefined || raw.question !== undefined
680
+ ? 'assistant'
681
+ : ''));
682
+ const sessionId = s(params?.sessionId, raw.session_id, raw.sessionId, raw.parentSessionId, raw.btwSessionId, raw.params?.sessionId) ?? 'unknown';
683
+ const ts = normalizeTs(raw.ts ?? raw.timestamp ?? raw.created_at ?? raw.askedAt ?? body.ts ?? body.timestamp);
572
684
  const seq = typeof raw.seq === 'number' ? raw.seq : typeof body.seq === 'number' ? body.seq : 0;
573
685
  const meta = (body._meta ?? params?._meta ?? raw._meta);
574
686
  const model = s(meta?.modelId, body.model, raw.model);
575
- const contentText = extractGrokContent(body.content ?? body.text ?? raw.content ?? raw.text);
687
+ const contentText = extractGrokContent(body.content ??
688
+ body.text ??
689
+ raw.content ??
690
+ raw.text ??
691
+ raw.prompt ??
692
+ (raw.answer !== undefined || raw.question !== undefined
693
+ ? [raw.question, raw.answer].filter((value) => typeof value === 'string').join('\n')
694
+ : undefined));
576
695
  const usage = (body.usage ?? raw.usage);
577
696
  const toolMeta = meta?.['x.ai/tool'];
578
697
  const toolName = s(body.title, body.tool_name, body.name, toolMeta?.name, toolMeta?.label, raw.tool_name, raw.name) ?? '';
@@ -756,6 +875,27 @@ export function grokSplit(raw) {
756
875
  return entries;
757
876
  }
758
877
  if (recordType === 'tool_completed') {
878
+ entries.push({
879
+ targetTable: 'history_message',
880
+ record: {
881
+ session_id: sessionId,
882
+ seq,
883
+ role: 'assistant',
884
+ record_type: recordType,
885
+ disposition: 'keep',
886
+ ts,
887
+ duration_ms: n.durationMs ?? null,
888
+ model: n.model ?? null,
889
+ input_tokens: null,
890
+ output_tokens: null,
891
+ cache_read_tokens: null,
892
+ cache_write_tokens: null,
893
+ cost_usd: null,
894
+ content_text: n.contentText,
895
+ cwd: null,
896
+ provenance: 'ambient',
897
+ },
898
+ });
759
899
  entries.push({
760
900
  targetTable: 'history_tool_call',
761
901
  record: {
@@ -1017,6 +1157,8 @@ export const CODEX_FIELD_MAP = identityFieldMap(MESSAGE_MAPPER_KEYS.concat(TOOL_
1017
1157
  export const AGY_FIELD_MAP = identityFieldMap(MESSAGE_MAPPER_KEYS.concat(TOOL_CALL_MAPPER_KEYS));
1018
1158
  /** Identity field map for the Grok mapper. */
1019
1159
  export const GROK_FIELD_MAP = identityFieldMap(MESSAGE_MAPPER_KEYS.concat(TOOL_CALL_MAPPER_KEYS));
1160
+ /** Identity field map for the Gemini mapper. */
1161
+ export const GEMINI_FIELD_MAP = identityFieldMap(MESSAGE_MAPPER_KEYS.concat(TOOL_CALL_MAPPER_KEYS));
1020
1162
  // ---------------------------------------------------------------------------
1021
1163
  // Zod schemas for the six source mappers
1022
1164
  // ---------------------------------------------------------------------------
@@ -1033,3 +1175,5 @@ export const CODEX_SCHEMA = passthroughSchema;
1033
1175
  export const AGY_SCHEMA = passthroughSchema;
1034
1176
  /** Passthrough Zod schema for the Grok mapper. */
1035
1177
  export const GROK_SCHEMA = passthroughSchema;
1178
+ /** Passthrough Zod schema for the Gemini mapper. */
1179
+ export const GEMINI_SCHEMA = passthroughSchema;
@@ -0,0 +1,17 @@
1
+ import { type DbAdapter } from '@gobing-ai/ts-db';
2
+ import { type FileSystem, type RuntimePaths } from '@gobing-ai/ts-runtime';
3
+ import type { ImportMode, ImportResult, RedactionRule } from './types';
4
+ /** Dependencies and mode controls for importing OpenCode's SQLite history. */
5
+ export interface OpenCodeImportOptions {
6
+ db: DbAdapter;
7
+ sourceDatabase?: string;
8
+ fileSystem?: FileSystem;
9
+ paths?: RuntimePaths;
10
+ mode?: ImportMode;
11
+ dryRun?: boolean;
12
+ redactionRules?: readonly RedactionRule[];
13
+ now?: () => Date;
14
+ }
15
+ /** Import OpenCode's current SQLite history store into the forensic contract tables. */
16
+ export declare function runOpenCodeImport(options: OpenCodeImportOptions): Promise<ImportResult>;
17
+ //# sourceMappingURL=opencode-importer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"opencode-importer.d.ts","sourceRoot":"","sources":["../src/opencode-importer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAmB,KAAK,SAAS,EAAkB,MAAM,kBAAkB,CAAC;AACnF,OAAO,EAGH,KAAK,UAAU,EAEf,KAAK,YAAY,EACpB,MAAM,uBAAuB,CAAC;AAc/B,OAAO,KAAK,EAAe,UAAU,EAAE,YAAY,EAAgC,aAAa,EAAE,MAAM,SAAS,CAAC;AAYlH,8EAA8E;AAC9E,MAAM,WAAW,qBAAqB;IAClC,EAAE,EAAE,SAAS,CAAC;IACd,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,KAAK,CAAC,EAAE,YAAY,CAAC;IACrB,IAAI,CAAC,EAAE,UAAU,CAAC;IAClB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,cAAc,CAAC,EAAE,SAAS,aAAa,EAAE,CAAC;IAC1C,GAAG,CAAC,EAAE,MAAM,IAAI,CAAC;CACpB;AAED,wFAAwF;AACxF,wBAAsB,iBAAiB,CAAC,OAAO,EAAE,qBAAqB,GAAG,OAAO,CAAC,YAAY,CAAC,CAuI7F"}