@gobing-ai/ts-llm-jsonl-importer 0.4.19 → 0.4.21

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":"schema-sql.d.ts","sourceRoot":"","sources":["../src/schema-sql.ts"],"names":[],"mappings":"AAAA,gGAAgG;AAChG,eAAO,MAAM,yBAAyB,QAiF9B,CAAC"}
1
+ {"version":3,"file":"schema-sql.d.ts","sourceRoot":"","sources":["../src/schema-sql.ts"],"names":[],"mappings":"AAAA,gGAAgG;AAChG,eAAO,MAAM,yBAAyB,QA0I9B,CAAC"}
@@ -80,4 +80,61 @@ CREATE TABLE IF NOT EXISTS history_etl_openclaw (
80
80
  payload_json TEXT NOT NULL,
81
81
  imported_at TEXT NOT NULL
82
82
  );
83
+
84
+ CREATE TABLE IF NOT EXISTS history_message (
85
+ record_hash TEXT PRIMARY KEY,
86
+ source TEXT NOT NULL,
87
+ source_file TEXT NOT NULL,
88
+ source_line INTEGER NOT NULL,
89
+ session_id TEXT NOT NULL,
90
+ seq INTEGER NOT NULL,
91
+ turn_index INTEGER,
92
+ role TEXT NOT NULL,
93
+ record_type TEXT NOT NULL,
94
+ disposition TEXT NOT NULL,
95
+ ts TEXT NOT NULL,
96
+ duration_ms INTEGER,
97
+ model TEXT,
98
+ input_tokens INTEGER,
99
+ output_tokens INTEGER,
100
+ cache_read_tokens INTEGER,
101
+ cache_write_tokens INTEGER,
102
+ cost_usd REAL,
103
+ content_text TEXT,
104
+ cwd TEXT,
105
+ provenance TEXT NOT NULL,
106
+ run_id TEXT,
107
+ task_wbs TEXT,
108
+ imported_at TEXT NOT NULL
109
+ );
110
+
111
+ CREATE TABLE IF NOT EXISTS history_tool_call (
112
+ record_hash TEXT PRIMARY KEY,
113
+ message_hash TEXT NOT NULL,
114
+ source TEXT NOT NULL,
115
+ source_file TEXT NOT NULL,
116
+ source_line INTEGER NOT NULL,
117
+ session_id TEXT NOT NULL,
118
+ seq INTEGER NOT NULL,
119
+ tool_name TEXT NOT NULL,
120
+ args_digest TEXT,
121
+ status TEXT NOT NULL,
122
+ started_at TEXT,
123
+ completed_at TEXT,
124
+ duration_ms INTEGER,
125
+ result_bytes INTEGER,
126
+ error_text TEXT,
127
+ imported_at TEXT NOT NULL
128
+ );
129
+
130
+ CREATE INDEX IF NOT EXISTS idx_history_message_session
131
+ ON history_message(source, session_id, seq);
132
+ CREATE INDEX IF NOT EXISTS idx_history_message_ts
133
+ ON history_message(ts);
134
+ CREATE INDEX IF NOT EXISTS idx_history_tool_call_session
135
+ ON history_tool_call(source, session_id, seq);
136
+ CREATE INDEX IF NOT EXISTS idx_history_tool_call_tool_name
137
+ ON history_tool_call(tool_name);
138
+ CREATE INDEX IF NOT EXISTS idx_history_tool_call_message_hash
139
+ ON history_tool_call(message_hash);
83
140
  `.trim();
@@ -1 +1 @@
1
- {"version":3,"file":"sources.d.ts","sourceRoot":"","sources":["../src/sources.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAA8B,cAAc,EAAE,gBAAgB,EAAoB,MAAM,SAAS,CAAC;AA8F9G;;;;;;;GAOG;AACH,eAAO,MAAM,gBAAgB,QAA0B,CAAC;AAExD,8DAA8D;AAC9D,eAAO,MAAM,kBAAkB,EAAE,QAAQ,CAAC,MAAM,CAAC,cAAc,EAAE,gBAAgB,CAAC,CAQjF,CAAC;AAEF;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,cAAc,GAAG,gBAAgB,CAM5E;AAED;;;;;;;;GAQG;AACH,wBAAgB,wBAAwB,CAAC,UAAU,EAAE,gBAAgB,GAAG,gBAAgB,CA+BvF;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,MAAM,GAAG,gBAAgB,GAAG,gBAAgB,CAK1F"}
1
+ {"version":3,"file":"sources.d.ts","sourceRoot":"","sources":["../src/sources.ts"],"names":[],"mappings":"AAsBA,OAAO,KAAK,EAA8B,cAAc,EAAE,gBAAgB,EAAoB,MAAM,SAAS,CAAC;AAsH9G;;;;;;;GAOG;AACH,eAAO,MAAM,gBAAgB,QAAsB,CAAC;AAEpD,8DAA8D;AAC9D,eAAO,MAAM,kBAAkB,EAAE,QAAQ,CAAC,MAAM,CAAC,cAAc,EAAE,gBAAgB,CAAC,CAmDjF,CAAC;AAEF;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,cAAc,GAAG,gBAAgB,CAM5E;AAED;;;;;;;;GAQG;AACH,wBAAgB,wBAAwB,CAAC,UAAU,EAAE,gBAAgB,GAAG,gBAAgB,CA+BvF;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,MAAM,GAAG,gBAAgB,GAAG,gBAAgB,CAK1F"}
package/dist/sources.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { z } from 'zod';
2
2
  import { HistoryImportError } from './errors.js';
3
+ import { AGY_FIELD_MAP, AGY_SCHEMA, agySplit, CLAUDE_FIELD_MAP, CLAUDE_SCHEMA, CODEX_FIELD_MAP, CODEX_SCHEMA, claudeSplit, codexSplit, GROK_FIELD_MAP, GROK_SCHEMA, grokSplit, OMP_FIELD_MAP, OMP_SCHEMA, ompSplit, PI_FIELD_MAP, PI_SCHEMA, piSplit, } from './mappers.js';
3
4
  const sourceRecordSchema = z
4
5
  .object({
5
6
  source_record_id: z.string().min(1),
@@ -72,6 +73,20 @@ function sourceDefinition(source, displayName, defaultRoots, filePatterns) {
72
73
  schema: sourceRecordSchema,
73
74
  };
74
75
  }
76
+ /** Build a source definition with a typed custom split for the forensic contract tables. */
77
+ function customSourceDefinition(source, displayName, defaultRoots, filePatterns, split, fieldMap, schema) {
78
+ return {
79
+ source,
80
+ displayName,
81
+ defaultRoots,
82
+ filePatterns,
83
+ targetTable: `history_etl_${source}`,
84
+ splitConfig: { mode: 'custom', split },
85
+ fieldMap,
86
+ fieldTransforms: {},
87
+ schema,
88
+ };
89
+ }
75
90
  /**
76
91
  * Pattern every importer-owned table must match.
77
92
  *
@@ -80,12 +95,15 @@ function sourceDefinition(source, displayName, defaultRoots, filePatterns) {
80
95
  * namespaces importer tables away from consumer schemas. Both built-in and
81
96
  * custom source definitions are held to this rule.
82
97
  */
83
- export const VALID_TABLE_NAME = /^history_etl_[a-z_]+$/;
98
+ export const VALID_TABLE_NAME = /^history_[a-z_]+$/;
84
99
  /** Built-in source definitions keyed by source identifier. */
85
100
  export const SOURCE_DEFINITIONS = {
86
- pi: sourceDefinition('pi', 'Pi', ['.pi/history', '.pi'], ['*.jsonl', '*.json']),
87
- claude: sourceDefinition('claude', 'Claude Code', ['.claude/projects', '.claude'], ['*.jsonl']),
88
- codex: sourceDefinition('codex', 'Codex', ['.codex/sessions', '.codex'], ['*.jsonl']),
101
+ pi: customSourceDefinition('pi', 'Pi', ['.pi/agent/sessions'], ['*.jsonl'], piSplit, PI_FIELD_MAP, PI_SCHEMA),
102
+ claude: customSourceDefinition('claude', 'Claude Code', ['.claude/projects'], ['*.jsonl'], claudeSplit, CLAUDE_FIELD_MAP, CLAUDE_SCHEMA),
103
+ codex: customSourceDefinition('codex', 'Codex', ['.codex/sessions'], ['*.jsonl'], codexSplit, CODEX_FIELD_MAP, CODEX_SCHEMA),
104
+ omp: customSourceDefinition('omp', 'OMP', ['.omp/agent/sessions'], ['*.jsonl'], ompSplit, OMP_FIELD_MAP, OMP_SCHEMA),
105
+ grok: customSourceDefinition('grok', 'Grok', ['.grok/sessions'], ['*.jsonl'], grokSplit, GROK_FIELD_MAP, GROK_SCHEMA),
106
+ agy: customSourceDefinition('agy', 'Antigravity CLI', ['.gemini/antigravity-cli/brain'], ['*.jsonl'], agySplit, AGY_FIELD_MAP, AGY_SCHEMA),
89
107
  gemini: sourceDefinition('gemini', 'Gemini CLI', ['.gemini', '.config/gemini'], ['*.jsonl']),
90
108
  opencode: sourceDefinition('opencode', 'OpenCode', ['.opencode', '.local/share/opencode'], ['*.jsonl']),
91
109
  antigravity: sourceDefinition('antigravity', 'Antigravity', ['.antigravity'], ['*.jsonl']),
@@ -158,6 +176,6 @@ export function resolveSourceDefinition(input) {
158
176
  }
159
177
  function assertValidTable(table, source) {
160
178
  if (!VALID_TABLE_NAME.test(table)) {
161
- throw new HistoryImportError(`Invalid history ETL target table for source "${source}": ${table}. Must match ${VALID_TABLE_NAME.source}.`, { source, table });
179
+ throw new HistoryImportError(`Invalid history target table for source "${source}": ${table}. Must match ${VALID_TABLE_NAME.source}.`, { source, table });
162
180
  }
163
181
  }
package/dist/types.d.ts CHANGED
@@ -2,9 +2,14 @@ import type { DbAdapter } from '@gobing-ai/ts-db';
2
2
  import type { FileSystem, RuntimePaths } from '@gobing-ai/ts-runtime';
3
3
  import type { z } from 'zod';
4
4
  /** Built-in source identifiers supported by the importer. */
5
- export type LlmJsonlSource = 'pi' | 'claude' | 'codex' | 'gemini' | 'opencode' | 'antigravity' | 'openclaw';
5
+ export type LlmJsonlSource = 'pi' | 'claude' | 'codex' | 'gemini' | 'opencode' | 'antigravity' | 'openclaw' | 'omp' | 'grok' | 'agy';
6
6
  /** Import mode controlling checkpoint behavior. */
7
7
  export type ImportMode = 'full' | 'incremental' | 'force-file';
8
+ /** One record emitted by a custom split, optionally routed to its own table. */
9
+ export interface SplitEntry {
10
+ readonly targetTable?: string;
11
+ readonly record: JsonObject;
12
+ }
8
13
  /** Split strategy for turning a raw JSONL object into one or more ETL records. */
9
14
  export type SplitConfig = {
10
15
  readonly mode: 'one-to-one';
@@ -14,7 +19,7 @@ export type SplitConfig = {
14
19
  readonly targetTable?: string;
15
20
  } | {
16
21
  readonly mode: 'custom';
17
- readonly split: (raw: JsonObject) => readonly JsonObject[];
22
+ readonly split: (raw: JsonObject) => readonly (JsonObject | SplitEntry)[];
18
23
  readonly targetTable?: string;
19
24
  };
20
25
  /** JSON-compatible record used at the importer boundary. */
@@ -80,6 +85,7 @@ export interface ImportResult {
80
85
  readonly processedLines: number;
81
86
  readonly importedRecords: number;
82
87
  readonly skippedDuplicates: number;
88
+ readonly unknownRecords: number;
83
89
  readonly parseErrors: readonly ImportIssue[];
84
90
  readonly validationErrors: readonly ImportIssue[];
85
91
  readonly checkpointUpdates: number;
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gobing-ai/ts-llm-jsonl-importer",
3
- "version": "0.4.19",
3
+ "version": "0.4.21",
4
4
  "description": "@gobing-ai/ts-llm-jsonl-importer — Generic JSONL importer for LLM agent history files.",
5
5
  "keywords": [
6
6
  "typescript",
@@ -47,9 +47,9 @@
47
47
  "release": "echo 'Manual publish is disabled. Releases go through GitHub Actions via Trusted Publishing — push a tag: git tag @gobing-ai/ts-llm-jsonl-importer-v<version> && git push --tags' && exit 1"
48
48
  },
49
49
  "dependencies": {
50
- "@gobing-ai/ts-db": "^0.4.19",
51
- "@gobing-ai/ts-runtime": "^0.4.19",
52
- "@gobing-ai/ts-utils": "^0.4.19",
50
+ "@gobing-ai/ts-db": "^0.4.21",
51
+ "@gobing-ai/ts-runtime": "^0.4.21",
52
+ "@gobing-ai/ts-utils": "^0.4.21",
53
53
  "zod": "^4.1.0"
54
54
  },
55
55
  "devDependencies": {
package/src/importer.ts CHANGED
@@ -53,6 +53,7 @@ export async function runJsonlImport(source: string | SourceDefinition, options:
53
53
  let processedLines = 0;
54
54
  let importedRecords = 0;
55
55
  let skippedDuplicates = 0;
56
+ let unknownRecords = 0;
56
57
  let checkpointUpdates = 0;
57
58
 
58
59
  for (const file of files) {
@@ -70,6 +71,14 @@ export async function runJsonlImport(source: string | SourceDefinition, options:
70
71
  const splitRecords = splitRawRecord(definition, raw);
71
72
  let lineSucceeded = false;
72
73
 
74
+ // First pass: normalize, validate, redact, compute recordHash for every entry.
75
+ interface PreparedEntry {
76
+ readonly split: SplitRecord;
77
+ readonly splitIndex: number;
78
+ readonly normalized: JsonObject;
79
+ readonly recordHash: string;
80
+ }
81
+ const prepared: PreparedEntry[] = [];
73
82
  for (let splitIndex = 0; splitIndex < splitRecords.length; splitIndex += 1) {
74
83
  const split = splitRecords[splitIndex];
75
84
  if (split === undefined) continue;
@@ -102,6 +111,25 @@ export async function runJsonlImport(source: string | SourceDefinition, options:
102
111
  skippedDuplicates += 1;
103
112
  continue;
104
113
  }
114
+ prepared.push({ split, splitIndex, normalized: redacted, recordHash });
115
+ }
116
+
117
+ // Second pass: resolve _messageSplitIndex → message_hash, then insert.
118
+ const recordHashBySplitIndex = new Map(prepared.map((e) => [e.splitIndex, e.recordHash] as const));
119
+ for (const entry of prepared) {
120
+ const { split, splitIndex, normalized, recordHash } = entry;
121
+ if (normalized.disposition === 'unknown') {
122
+ unknownRecords += 1;
123
+ }
124
+ // Resolve _messageSplitIndex if present (tool call → parent message linkage).
125
+ const msgSplitIdx = normalized._messageSplitIndex as number | undefined;
126
+ delete normalized._messageSplitIndex;
127
+ if (msgSplitIdx !== undefined && typeof msgSplitIdx === 'number') {
128
+ const msgHash = recordHashBySplitIndex.get(msgSplitIdx);
129
+ if (msgHash !== undefined) {
130
+ normalized.message_hash = msgHash;
131
+ }
132
+ }
105
133
  if (!options.dryRun) {
106
134
  await insertRecord(
107
135
  options.db,
@@ -110,7 +138,7 @@ export async function runJsonlImport(source: string | SourceDefinition, options:
110
138
  file,
111
139
  lineNumber,
112
140
  splitIndex,
113
- redacted,
141
+ normalized,
114
142
  options.now,
115
143
  );
116
144
  await insertLedger(
@@ -141,6 +169,7 @@ export async function runJsonlImport(source: string | SourceDefinition, options:
141
169
  processedLines,
142
170
  importedRecords,
143
171
  skippedDuplicates,
172
+ unknownRecords,
144
173
  parseErrors,
145
174
  validationErrors,
146
175
  checkpointUpdates,
@@ -173,10 +202,17 @@ function splitRawRecord(definition: SourceDefinition, raw: JsonObject): readonly
173
202
  }
174
203
  if (definition.splitConfig.mode === 'custom') {
175
204
  const config = definition.splitConfig;
176
- return config.split(raw).map((entry) => ({
177
- targetTable: targetTableFor(config.targetTable ?? definition.targetTable),
178
- raw: entry,
179
- }));
205
+ const configTable = config.targetTable !== undefined ? targetTableFor(config.targetTable) : undefined;
206
+ return config.split(raw).map((entry) => {
207
+ // Normalize: SplitEntry has a `record` property, bare object is the record itself.
208
+ const hasTargetTable =
209
+ 'targetTable' in entry && typeof (entry as { targetTable: unknown }).targetTable === 'string';
210
+ const record = hasTargetTable ? (entry as { record: JsonObject }).record : (entry as JsonObject);
211
+ const entryTable = hasTargetTable ? (entry as { targetTable?: string }).targetTable : undefined;
212
+ // Resolution order: entry → splitConfig → definition
213
+ const table = entryTable ?? configTable ?? targetTable;
214
+ return { targetTable: targetTableFor(table), raw: record };
215
+ });
180
216
  }
181
217
 
182
218
  const config = definition.splitConfig;
package/src/index.ts CHANGED
@@ -22,5 +22,6 @@ export type {
22
22
  RedactionRule,
23
23
  SourceDefinition,
24
24
  SplitConfig,
25
+ SplitEntry,
25
26
  TransformContext,
26
27
  } from './types';
@@ -7,6 +7,57 @@ interface CheckpointRow {
7
7
  readonly last_imported_line: number;
8
8
  }
9
9
 
10
+ /** Column allowlist per typed contract table; order is the INSERT column order. */
11
+ const TYPED_TABLE_COLUMNS: Readonly<Record<string, readonly string[]>> = {
12
+ history_message: [
13
+ 'record_hash',
14
+ 'source',
15
+ 'source_file',
16
+ 'source_line',
17
+ 'session_id',
18
+ 'seq',
19
+ 'turn_index',
20
+ 'role',
21
+ 'record_type',
22
+ 'disposition',
23
+ 'ts',
24
+ 'duration_ms',
25
+ 'model',
26
+ 'input_tokens',
27
+ 'output_tokens',
28
+ 'cache_read_tokens',
29
+ 'cache_write_tokens',
30
+ 'cost_usd',
31
+ 'content_text',
32
+ 'cwd',
33
+ 'provenance',
34
+ 'run_id',
35
+ 'task_wbs',
36
+ 'imported_at',
37
+ ],
38
+ history_tool_call: [
39
+ 'record_hash',
40
+ 'message_hash',
41
+ 'source',
42
+ 'source_file',
43
+ 'source_line',
44
+ 'session_id',
45
+ 'seq',
46
+ 'tool_name',
47
+ 'args_digest',
48
+ 'status',
49
+ 'started_at',
50
+ 'completed_at',
51
+ 'duration_ms',
52
+ 'result_bytes',
53
+ 'error_text',
54
+ 'imported_at',
55
+ ],
56
+ };
57
+
58
+ /** Keys that a typed table mapper may produce that are not columns. */
59
+ const TYPED_IGNORED_KEYS = new Set<string>(['_meta', 'split_index']);
60
+
10
61
  function targetTableFor(table: string): string {
11
62
  if (!VALID_TABLE_NAME.test(table)) {
12
63
  throw new HistoryImportError(`Invalid history ETL target table: ${table}`, { table });
@@ -43,12 +94,12 @@ export async function applyHistoryImportSchema(db: ImportOptions['db']): Promise
43
94
  * Ensure the ETL table(s) for a source definition exist.
44
95
  *
45
96
  * WHY: the static {@link HISTORY_IMPORT_SCHEMA_SQL} only creates the built-in
46
- * `history_etl_*` tables. Custom source definitions (and built-in definitions
47
- * with a `splitConfig.targetTable` override) need their target table(s) created
48
- * on demand. The table name is already gated by {@link VALID_TABLE_NAME} in
49
- * {@link validateSourceDefinition} / {@link targetTableFor}, so it is safe to
50
- * interpolate into DDL. `CREATE TABLE IF NOT EXISTS` makes this idempotent for
51
- * built-in tables that the static schema already created.
97
+ * `history_etl_*` tables and the typed contract tables. Custom source definitions
98
+ * (and built-in definitions with a `splitConfig.targetTable` override) need their
99
+ * target table(s) created on demand. The table name is already gated by
100
+ * {@link VALID_TABLE_NAME} in {@link validateSourceDefinition} / {@link targetTableFor},
101
+ * so it is safe to interpolate into DDL. `CREATE TABLE IF NOT EXISTS` makes this
102
+ * idempotent for built-in tables that the static schema already created.
52
103
  */
53
104
  async function ensureTargetTables(db: ImportOptions['db'], definition: SourceDefinition): Promise<void> {
54
105
  const tables = new Set<string>([targetTableFor(definition.targetTable)]);
@@ -56,6 +107,8 @@ async function ensureTargetTables(db: ImportOptions['db'], definition: SourceDef
56
107
  tables.add(targetTableFor(definition.splitConfig.targetTable));
57
108
  }
58
109
  for (const table of tables) {
110
+ // Typed tables are created by the static schema; use ETL DDL for custom tables.
111
+ if (TYPED_TABLE_COLUMNS[table] !== undefined) continue;
59
112
  await db.exec(ETL_TABLE_DDL(table));
60
113
  }
61
114
  }
@@ -103,6 +156,8 @@ async function ledgerExists(db: ImportOptions['db'], recordHash: string): Promis
103
156
  return row !== undefined && row !== null;
104
157
  }
105
158
 
159
+ const _ensuredTables = new Set<string>();
160
+
106
161
  async function insertRecord(
107
162
  db: ImportOptions['db'],
108
163
  targetTable: string,
@@ -114,17 +169,53 @@ async function insertRecord(
114
169
  now: ImportOptions['now'],
115
170
  ): Promise<void> {
116
171
  const table = targetTableFor(targetTable);
117
- await db.run(
118
- `INSERT INTO ${table} (record_hash, source_file, source_line, split_index, payload_json, imported_at)
119
- VALUES (?, ?, ?, ?, ?, ?)
120
- ON CONFLICT(record_hash) DO NOTHING`,
121
- recordHash,
122
- sourceFile,
123
- sourceLine,
124
- splitIndex,
125
- JSON.stringify(payload),
126
- timestamp(now),
127
- );
172
+ const typedColumns = TYPED_TABLE_COLUMNS[table];
173
+ if (typedColumns !== undefined) {
174
+ // Typed insert path: map each column from the payload, assert no unknown keys.
175
+ const unknownKeys = Object.keys(payload).filter((k) => !typedColumns.includes(k) && !TYPED_IGNORED_KEYS.has(k));
176
+ if (unknownKeys.length > 0) {
177
+ throw new HistoryImportError(
178
+ `Typed table "${table}" has unknown columns: ${unknownKeys.join(', ')}. ` +
179
+ `Expected one of: ${typedColumns.join(', ')}.`,
180
+ { table, unknownKeys, knownColumns: typedColumns },
181
+ );
182
+ }
183
+ // record_hash and imported_at come from the function parameters, not the payload.
184
+ const ts = timestamp(now);
185
+ const values = typedColumns.map((col) => {
186
+ if (col === 'record_hash') return recordHash;
187
+ if (col === 'imported_at') return ts;
188
+ const v = payload[col];
189
+ return v === undefined ? null : v;
190
+ });
191
+ if (!_ensuredTables.has(table)) {
192
+ // Typed tables are created by the static schema in applyHistoryImportSchema.
193
+ _ensuredTables.add(table);
194
+ }
195
+ await db.run(
196
+ `INSERT INTO ${table} (${typedColumns.join(', ')})
197
+ VALUES (${typedColumns.map(() => '?').join(', ')})
198
+ ON CONFLICT(record_hash) DO NOTHING`,
199
+ ...values,
200
+ );
201
+ } else {
202
+ // Generic ETL insert path: store as payload_json blob.
203
+ if (!_ensuredTables.has(table)) {
204
+ await db.exec(ETL_TABLE_DDL(table));
205
+ _ensuredTables.add(table);
206
+ }
207
+ await db.run(
208
+ `INSERT INTO ${table} (record_hash, source_file, source_line, split_index, payload_json, imported_at)
209
+ VALUES (?, ?, ?, ?, ?, ?)
210
+ ON CONFLICT(record_hash) DO NOTHING`,
211
+ recordHash,
212
+ sourceFile,
213
+ sourceLine,
214
+ splitIndex,
215
+ JSON.stringify(payload),
216
+ timestamp(now),
217
+ );
218
+ }
128
219
  }
129
220
 
130
221
  async function insertLedger(