@gobing-ai/ts-llm-jsonl-importer 0.4.28 → 0.4.30
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/README.md +2 -1
- package/dist/importer.d.ts.map +1 -1
- package/dist/importer.js +26 -12
- package/dist/jsonl-importer-dao.d.ts +32 -7
- package/dist/jsonl-importer-dao.d.ts.map +1 -1
- package/dist/jsonl-importer-dao.js +81 -25
- package/dist/redaction.d.ts.map +1 -1
- package/dist/redaction.js +15 -0
- package/dist/schema-sql.d.ts +9 -1
- package/dist/schema-sql.d.ts.map +1 -1
- package/dist/schema-sql.js +9 -64
- package/package.json +4 -4
- package/src/importer.ts +52 -31
- package/src/jsonl-importer-dao.ts +109 -37
- package/src/redaction.ts +15 -0
- package/src/schema-sql.ts +9 -64
package/README.md
CHANGED
|
@@ -145,7 +145,8 @@ Downstream consumers should treat `source_file`, `source_line`, and `split_index
|
|
|
145
145
|
|
|
146
146
|
Beyond the built-in source keys, `runJsonlImport()` accepts a fully-specified `SourceDefinition`
|
|
147
147
|
for any custom agent history format. Unknown strings throw `HistoryImportError`; custom definitions
|
|
148
|
-
are validated before any I/O (target-table names must match `VALID_TABLE_NAME` = `/^
|
|
148
|
+
are validated before any I/O (target-table names must match `VALID_TABLE_NAME` = `/^history_[a-z_]+$/`,
|
|
149
|
+
covering `history_etl_*` blob tables and the typed `history_message` / `history_tool_call` tables,
|
|
149
150
|
and required fields must be present).
|
|
150
151
|
|
|
151
152
|
```ts
|
package/dist/importer.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"importer.d.ts","sourceRoot":"","sources":["../src/importer.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"importer.d.ts","sourceRoot":"","sources":["../src/importer.ts"],"names":[],"mappings":"AAwBA,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,CAgMrH"}
|
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,
|
|
3
|
+
import { applyHistoryImportSchema, checkpointUpsertOp, ensureTargetTables, ledgerExistingHashes, ledgerInsertOp, readCheckpoint, reconcileFullImport, recordInsertOp, resetCheckpoints, targetTableFor, } 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.
|
|
@@ -86,19 +86,26 @@ export async function runJsonlImport(source, options) {
|
|
|
86
86
|
record: redacted,
|
|
87
87
|
});
|
|
88
88
|
desiredHashes.add(recordHash);
|
|
89
|
-
if (await ledgerExists(options.db, recordHash)) {
|
|
90
|
-
skippedDuplicates += 1;
|
|
91
|
-
continue;
|
|
92
|
-
}
|
|
93
89
|
prepared.push({ split, splitIndex, normalized: redacted, recordHash });
|
|
94
90
|
}
|
|
95
91
|
// A rejected line is reported (validationErrors above) but never persisted — no
|
|
96
92
|
// second pass, no checkpoint advance, no partial rows from this record.
|
|
97
93
|
if (lineRejected)
|
|
98
94
|
continue;
|
|
99
|
-
//
|
|
95
|
+
// Chunked ledger lookup (task 0060 F9): one `IN (...)` query per ≤200 hashes
|
|
96
|
+
// instead of a SELECT per record; duplicates are dropped before the write pass.
|
|
97
|
+
const existing = await ledgerExistingHashes(options.db, prepared.map((e) => e.recordHash));
|
|
98
|
+
const accepted = prepared.filter((entry) => {
|
|
99
|
+
if (existing.has(entry.recordHash)) {
|
|
100
|
+
skippedDuplicates += 1;
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
return true;
|
|
104
|
+
});
|
|
105
|
+
// Second pass: resolve _messageSplitIndex → message_hash, then batch.
|
|
100
106
|
const recordHashBySplitIndex = new Map(prepared.map((e) => [e.splitIndex, e.recordHash]));
|
|
101
|
-
|
|
107
|
+
const ops = [];
|
|
108
|
+
for (const entry of accepted) {
|
|
102
109
|
const { split, splitIndex, normalized, recordHash } = entry;
|
|
103
110
|
if (normalized.disposition === 'unknown') {
|
|
104
111
|
unknownRecords += 1;
|
|
@@ -113,14 +120,21 @@ export async function runJsonlImport(source, options) {
|
|
|
113
120
|
}
|
|
114
121
|
}
|
|
115
122
|
if (!options.dryRun) {
|
|
116
|
-
|
|
117
|
-
|
|
123
|
+
// Record row + ledger row join one batch op pair — a crash between the two
|
|
124
|
+
// writes is impossible (task 0060 F9).
|
|
125
|
+
ops.push(recordInsertOp(split.targetTable, recordHash, file, lineNumber, splitIndex, normalized, options.now));
|
|
126
|
+
ops.push(ledgerInsertOp(recordHash, resolvedSource, file, lineNumber, splitIndex, split.targetTable, options.now));
|
|
118
127
|
}
|
|
119
128
|
importedRecords += 1;
|
|
120
129
|
}
|
|
121
|
-
if (
|
|
122
|
-
|
|
123
|
-
|
|
130
|
+
if (!options.dryRun && lineSucceeded) {
|
|
131
|
+
// Checkpoint upsert joins the same batch so an accepted line can never be
|
|
132
|
+
// re-imported after a mid-batch crash.
|
|
133
|
+
ops.push(checkpointUpsertOp(resolvedSource, file, lineNumber, options.now));
|
|
134
|
+
if (ops.length > 0) {
|
|
135
|
+
await options.db.batch(ops);
|
|
136
|
+
checkpointUpdates += 1;
|
|
137
|
+
}
|
|
124
138
|
}
|
|
125
139
|
}
|
|
126
140
|
}
|
|
@@ -6,24 +6,49 @@ interface CheckpointRow {
|
|
|
6
6
|
declare function targetTableFor(table: string): string;
|
|
7
7
|
declare function timestamp(now: ImportOptions['now']): string;
|
|
8
8
|
declare function ETL_TABLE_DDL(table: string): string;
|
|
9
|
-
/**
|
|
9
|
+
/**
|
|
10
|
+
* Apply importer-owned schema to the target database.
|
|
11
|
+
*
|
|
12
|
+
* Creates the checkpoint, ledger, and typed contract tables from the static SQL, then materializes
|
|
13
|
+
* every built-in `history_etl_*` table by looping {@link SOURCE_DEFINITIONS} through
|
|
14
|
+
* {@link ensureTargetTables}. This keeps the migration contract "schema applied ⇒ all built-in ETL
|
|
15
|
+
* tables exist" for callers who apply the schema and then raw-insert without a prior import.
|
|
16
|
+
* Adding a new built-in source to {@link SOURCE_DEFINITIONS} is now the only edit needed.
|
|
17
|
+
*/
|
|
10
18
|
export declare function applyHistoryImportSchema(db: ImportOptions['db']): Promise<void>;
|
|
11
19
|
/**
|
|
12
20
|
* Ensure the ETL table(s) for a source definition exist.
|
|
13
21
|
*
|
|
14
|
-
* WHY: the static {@link HISTORY_IMPORT_SCHEMA_SQL} only
|
|
15
|
-
* `history_etl_*`
|
|
16
|
-
* (and
|
|
17
|
-
* target table(s) created on demand. The
|
|
22
|
+
* WHY: the static {@link HISTORY_IMPORT_SCHEMA_SQL} creates only the checkpoint, ledger, and
|
|
23
|
+
* typed contract tables. Every `history_etl_*` blob table — built-in and custom — is created here
|
|
24
|
+
* (and by {@link applyHistoryImportSchema} looping {@link SOURCE_DEFINITIONS}). Built-in definitions
|
|
25
|
+
* with a `splitConfig.targetTable` override also need their target table(s) created on demand. The
|
|
26
|
+
* table name is already gated by
|
|
18
27
|
* {@link VALID_TABLE_NAME} in {@link validateSourceDefinition} / {@link targetTableFor},
|
|
19
|
-
* so it is safe to interpolate into DDL. `CREATE TABLE IF NOT EXISTS`
|
|
20
|
-
* idempotent
|
|
28
|
+
* so it is safe to interpolate into DDL. `CREATE TABLE IF NOT EXISTS` is
|
|
29
|
+
* idempotent, so apply-then-import and a second apply are both safe.
|
|
21
30
|
*/
|
|
22
31
|
declare function ensureTargetTables(db: ImportOptions['db'], definition: SourceDefinition): Promise<void>;
|
|
23
32
|
declare function readCheckpoint(db: ImportOptions['db'], source: string, sourceFile: string): Promise<number>;
|
|
24
33
|
declare function resetCheckpoints(db: ImportOptions['db'], source: string, files: readonly string[]): Promise<void>;
|
|
25
34
|
declare function writeCheckpoint(db: ImportOptions['db'], source: string, sourceFile: string, line: number, now: ImportOptions['now']): Promise<void>;
|
|
26
35
|
declare function ledgerExists(db: ImportOptions['db'], recordHash: string): Promise<boolean>;
|
|
36
|
+
/**
|
|
37
|
+
* INSERT op for one accepted record row (task 0060 F9). Pure builder so the
|
|
38
|
+
* per-record batch path and the single-write `insertRecord` can never drift.
|
|
39
|
+
* Typed tables validate unknown keys here; the caller must have ensured the
|
|
40
|
+
* target table exists (`ensureTargetTables` runs at import start).
|
|
41
|
+
*/
|
|
42
|
+
export declare function recordInsertOp(targetTable: string, recordHash: string, sourceFile: string, sourceLine: number, splitIndex: number, payload: JsonObject, now: ImportOptions['now']): DbBatchOp;
|
|
43
|
+
/** INSERT op for one ledger row (task 0060 F9). */
|
|
44
|
+
export declare function ledgerInsertOp(recordHash: string, source: string, sourceFile: string, sourceLine: number, splitIndex: number, targetTable: string, now: ImportOptions['now']): DbBatchOp;
|
|
45
|
+
/** UPSERT op for one checkpoint row (task 0060 F9: checkpoint joins the record batch). */
|
|
46
|
+
export declare function checkpointUpsertOp(source: string, sourceFile: string, line: number, now: ImportOptions['now']): DbBatchOp;
|
|
47
|
+
/**
|
|
48
|
+
* Query which of the given hashes already exist in the ledger, in chunks of at
|
|
49
|
+
* most 200 per `IN (...)` (task 0060 F9 — replaces the per-record SELECT loop).
|
|
50
|
+
*/
|
|
51
|
+
export declare function ledgerExistingHashes(db: ImportOptions['db'], hashes: readonly string[]): Promise<Set<string>>;
|
|
27
52
|
declare function insertRecord(db: ImportOptions['db'], targetTable: string, recordHash: string, sourceFile: string, sourceLine: number, splitIndex: number, payload: JsonObject, now: ImportOptions['now']): Promise<void>;
|
|
28
53
|
declare function insertLedger(db: ImportOptions['db'], recordHash: string, source: string, sourceFile: string, sourceLine: number, splitIndex: number, targetTable: string, now: ImportOptions['now']): Promise<void>;
|
|
29
54
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"jsonl-importer-dao.d.ts","sourceRoot":"","sources":["../src/jsonl-importer-dao.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAI7D,OAAO,KAAK,EAAE,aAAa,EAAE,UAAU,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAE7F,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
|
|
1
|
+
{"version":3,"file":"jsonl-importer-dao.d.ts","sourceRoot":"","sources":["../src/jsonl-importer-dao.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { HistoryImportError } from './errors.js';
|
|
2
2
|
import { HISTORY_IMPORT_SCHEMA_SQL } from './schema-sql.js';
|
|
3
|
-
import { VALID_TABLE_NAME } from './sources.js';
|
|
3
|
+
import { SOURCE_DEFINITIONS, VALID_TABLE_NAME } from './sources.js';
|
|
4
4
|
/** Column allowlist per typed contract table; order is the INSERT column order. */
|
|
5
5
|
const TYPED_TABLE_COLUMNS = {
|
|
6
6
|
history_message: [
|
|
@@ -69,7 +69,15 @@ function ETL_TABLE_DDL(table) {
|
|
|
69
69
|
imported_at TEXT NOT NULL
|
|
70
70
|
);`;
|
|
71
71
|
}
|
|
72
|
-
/**
|
|
72
|
+
/**
|
|
73
|
+
* Apply importer-owned schema to the target database.
|
|
74
|
+
*
|
|
75
|
+
* Creates the checkpoint, ledger, and typed contract tables from the static SQL, then materializes
|
|
76
|
+
* every built-in `history_etl_*` table by looping {@link SOURCE_DEFINITIONS} through
|
|
77
|
+
* {@link ensureTargetTables}. This keeps the migration contract "schema applied ⇒ all built-in ETL
|
|
78
|
+
* tables exist" for callers who apply the schema and then raw-insert without a prior import.
|
|
79
|
+
* Adding a new built-in source to {@link SOURCE_DEFINITIONS} is now the only edit needed.
|
|
80
|
+
*/
|
|
73
81
|
export async function applyHistoryImportSchema(db) {
|
|
74
82
|
for (const statement of HISTORY_IMPORT_SCHEMA_SQL.split(';')) {
|
|
75
83
|
const sql = statement.trim();
|
|
@@ -77,17 +85,21 @@ export async function applyHistoryImportSchema(db) {
|
|
|
77
85
|
await db.exec(sql);
|
|
78
86
|
}
|
|
79
87
|
}
|
|
88
|
+
for (const definition of Object.values(SOURCE_DEFINITIONS)) {
|
|
89
|
+
await ensureTargetTables(db, definition);
|
|
90
|
+
}
|
|
80
91
|
}
|
|
81
92
|
/**
|
|
82
93
|
* Ensure the ETL table(s) for a source definition exist.
|
|
83
94
|
*
|
|
84
|
-
* WHY: the static {@link HISTORY_IMPORT_SCHEMA_SQL} only
|
|
85
|
-
* `history_etl_*`
|
|
86
|
-
* (and
|
|
87
|
-
* target table(s) created on demand. The
|
|
95
|
+
* WHY: the static {@link HISTORY_IMPORT_SCHEMA_SQL} creates only the checkpoint, ledger, and
|
|
96
|
+
* typed contract tables. Every `history_etl_*` blob table — built-in and custom — is created here
|
|
97
|
+
* (and by {@link applyHistoryImportSchema} looping {@link SOURCE_DEFINITIONS}). Built-in definitions
|
|
98
|
+
* with a `splitConfig.targetTable` override also need their target table(s) created on demand. The
|
|
99
|
+
* table name is already gated by
|
|
88
100
|
* {@link VALID_TABLE_NAME} in {@link validateSourceDefinition} / {@link targetTableFor},
|
|
89
|
-
* so it is safe to interpolate into DDL. `CREATE TABLE IF NOT EXISTS`
|
|
90
|
-
* idempotent
|
|
101
|
+
* so it is safe to interpolate into DDL. `CREATE TABLE IF NOT EXISTS` is
|
|
102
|
+
* idempotent, so apply-then-import and a second apply are both safe.
|
|
91
103
|
*/
|
|
92
104
|
async function ensureTargetTables(db, definition) {
|
|
93
105
|
const tables = new Set([targetTableFor(definition.targetTable)]);
|
|
@@ -121,8 +133,13 @@ async function ledgerExists(db, recordHash) {
|
|
|
121
133
|
const row = await db.queryFirst('SELECT record_hash FROM history_import_ledger WHERE record_hash = ?', recordHash);
|
|
122
134
|
return row !== undefined && row !== null;
|
|
123
135
|
}
|
|
124
|
-
|
|
125
|
-
|
|
136
|
+
/**
|
|
137
|
+
* INSERT op for one accepted record row (task 0060 F9). Pure builder so the
|
|
138
|
+
* per-record batch path and the single-write `insertRecord` can never drift.
|
|
139
|
+
* Typed tables validate unknown keys here; the caller must have ensured the
|
|
140
|
+
* target table exists (`ensureTargetTables` runs at import start).
|
|
141
|
+
*/
|
|
142
|
+
export function recordInsertOp(targetTable, recordHash, sourceFile, sourceLine, splitIndex, payload, now) {
|
|
126
143
|
const table = targetTableFor(targetTable);
|
|
127
144
|
const typedColumns = TYPED_TABLE_COLUMNS[table];
|
|
128
145
|
if (typedColumns !== undefined) {
|
|
@@ -142,24 +159,63 @@ async function insertRecord(db, targetTable, recordHash, sourceFile, sourceLine,
|
|
|
142
159
|
const v = payload[col];
|
|
143
160
|
return v === undefined ? null : v;
|
|
144
161
|
});
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
ON CONFLICT(record_hash) DO NOTHING`, ...values);
|
|
162
|
+
return {
|
|
163
|
+
sql: `INSERT INTO ${table} (${typedColumns.join(', ')})
|
|
164
|
+
VALUES (${typedColumns.map(() => '?').join(', ')})
|
|
165
|
+
ON CONFLICT(record_hash) DO NOTHING`,
|
|
166
|
+
params: values,
|
|
167
|
+
};
|
|
152
168
|
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
await db.exec(ETL_TABLE_DDL(table));
|
|
157
|
-
_ensuredTables.add(table);
|
|
158
|
-
}
|
|
159
|
-
await db.run(`INSERT INTO ${table} (record_hash, source_file, source_line, split_index, payload_json, imported_at)
|
|
169
|
+
// Generic ETL insert path: store as payload_json blob.
|
|
170
|
+
return {
|
|
171
|
+
sql: `INSERT INTO ${table} (record_hash, source_file, source_line, split_index, payload_json, imported_at)
|
|
160
172
|
VALUES (?, ?, ?, ?, ?, ?)
|
|
161
|
-
ON CONFLICT(record_hash) DO NOTHING`,
|
|
173
|
+
ON CONFLICT(record_hash) DO NOTHING`,
|
|
174
|
+
params: [recordHash, sourceFile, sourceLine, splitIndex, JSON.stringify(payload), timestamp(now)],
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
/** INSERT op for one ledger row (task 0060 F9). */
|
|
178
|
+
export function ledgerInsertOp(recordHash, source, sourceFile, sourceLine, splitIndex, targetTable, now) {
|
|
179
|
+
return {
|
|
180
|
+
sql: `INSERT INTO history_import_ledger (record_hash, source, source_file, source_line, split_index, target_table, imported_at)
|
|
181
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
182
|
+
params: [recordHash, source, sourceFile, sourceLine, splitIndex, targetTable, timestamp(now)],
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
/** UPSERT op for one checkpoint row (task 0060 F9: checkpoint joins the record batch). */
|
|
186
|
+
export function checkpointUpsertOp(source, sourceFile, line, now) {
|
|
187
|
+
return {
|
|
188
|
+
sql: `INSERT INTO history_import_checkpoint (source, source_file, last_imported_line, updated_at)
|
|
189
|
+
VALUES (?, ?, ?, ?)
|
|
190
|
+
ON CONFLICT(source, source_file) DO UPDATE SET
|
|
191
|
+
last_imported_line = excluded.last_imported_line,
|
|
192
|
+
updated_at = excluded.updated_at`,
|
|
193
|
+
params: [source, sourceFile, line, timestamp(now)],
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Query which of the given hashes already exist in the ledger, in chunks of at
|
|
198
|
+
* most 200 per `IN (...)` (task 0060 F9 — replaces the per-record SELECT loop).
|
|
199
|
+
*/
|
|
200
|
+
export async function ledgerExistingHashes(db, hashes) {
|
|
201
|
+
const existing = new Set();
|
|
202
|
+
for (let i = 0; i < hashes.length; i += 200) {
|
|
203
|
+
const chunk = hashes.slice(i, i + 200);
|
|
204
|
+
const placeholders = chunk.map(() => '?').join(', ');
|
|
205
|
+
const rows = await db.queryAll(`SELECT record_hash FROM history_import_ledger WHERE record_hash IN (${placeholders})`, ...chunk);
|
|
206
|
+
for (const row of rows)
|
|
207
|
+
existing.add(row.record_hash);
|
|
208
|
+
}
|
|
209
|
+
return existing;
|
|
210
|
+
}
|
|
211
|
+
async function insertRecord(db, targetTable, recordHash, sourceFile, sourceLine, splitIndex, payload, now) {
|
|
212
|
+
const op = recordInsertOp(targetTable, recordHash, sourceFile, sourceLine, splitIndex, payload, now);
|
|
213
|
+
if (TYPED_TABLE_COLUMNS[targetTableFor(targetTable)] === undefined) {
|
|
214
|
+
// Custom ETL tables are created on demand; `CREATE TABLE IF NOT EXISTS` is idempotent,
|
|
215
|
+
// so there is no process-global cache to go stale across adapter instances (task 0060 F8).
|
|
216
|
+
await db.exec(ETL_TABLE_DDL(targetTableFor(targetTable)));
|
|
162
217
|
}
|
|
218
|
+
await db.run(op.sql, ...op.params);
|
|
163
219
|
}
|
|
164
220
|
async function insertLedger(db, recordHash, source, sourceFile, sourceLine, splitIndex, targetTable, now) {
|
|
165
221
|
await db.run(`INSERT INTO history_import_ledger (record_hash, source, source_file, source_line, split_index, target_table, imported_at)
|
package/dist/redaction.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"redaction.d.ts","sourceRoot":"","sources":["../src/redaction.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAEzD,qFAAqF;AACrF,eAAO,MAAM,uBAAuB,EAAE,SAAS,aAAa,
|
|
1
|
+
{"version":3,"file":"redaction.d.ts","sourceRoot":"","sources":["../src/redaction.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAEzD,qFAAqF;AACrF,eAAO,MAAM,uBAAuB,EAAE,SAAS,aAAa,EA+B3D,CAAC;AAEF,qEAAqE;AACrE,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,EAAE,KAAK,GAAE,SAAS,aAAa,EAA4B,GAAG,OAAO,CAa9G;AAED,mFAAmF;AACnF,wBAAgB,YAAY,CAAC,OAAO,SAAS,UAAU,EACnD,MAAM,EAAE,OAAO,EACf,KAAK,GAAE,SAAS,aAAa,EAA4B,GAC1D,OAAO,CAET"}
|
package/dist/redaction.js
CHANGED
|
@@ -15,6 +15,21 @@ export const DEFAULT_REDACTION_RULES = [
|
|
|
15
15
|
pattern: /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/gi,
|
|
16
16
|
replacement: '[REDACTED:email]',
|
|
17
17
|
},
|
|
18
|
+
{
|
|
19
|
+
name: 'xai-key',
|
|
20
|
+
pattern: /\bxai-[A-Za-z0-9_-]{12,}\b/g,
|
|
21
|
+
replacement: '[REDACTED:token]',
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
name: 'aws-access-key-id',
|
|
25
|
+
pattern: /\bAKIA[0-9A-Z]{16}\b/g,
|
|
26
|
+
replacement: '[REDACTED:aws-key]',
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
name: 'bearer-token',
|
|
30
|
+
pattern: /\bBearer\s+[A-Za-z0-9._\-=]+\b/g,
|
|
31
|
+
replacement: '[REDACTED:bearer]',
|
|
32
|
+
},
|
|
18
33
|
];
|
|
19
34
|
/** Redact supported scalar and composite JSON values recursively. */
|
|
20
35
|
export function redactValue(value, rules = DEFAULT_REDACTION_RULES) {
|
package/dist/schema-sql.d.ts
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/**
|
|
2
|
+
* DDL string that creates the history import checkpoint, ledger, and typed contract tables.
|
|
3
|
+
*
|
|
4
|
+
* WHY: per-source `history_etl_*` blob tables are no longer hard-coded here. They are created
|
|
5
|
+
* dynamically by {@link ensureTargetTables} / {@link ETL_TABLE_DDL} looping every entry of
|
|
6
|
+
* {@link SOURCE_DEFINITIONS}, so adding a new built-in source needs no edit to this file. This
|
|
7
|
+
* single-owner rule closes the R16 drift where `omp` / `grok` / `agy` existed only via import-time
|
|
8
|
+
* DDL while the seven other sources were duplicated in the static string.
|
|
9
|
+
*/
|
|
2
10
|
export declare const HISTORY_IMPORT_SCHEMA_SQL: string;
|
|
3
11
|
//# sourceMappingURL=schema-sql.d.ts.map
|
package/dist/schema-sql.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schema-sql.d.ts","sourceRoot":"","sources":["../src/schema-sql.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"schema-sql.d.ts","sourceRoot":"","sources":["../src/schema-sql.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,eAAO,MAAM,yBAAyB,QA2E9B,CAAC"}
|
package/dist/schema-sql.js
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/**
|
|
2
|
+
* DDL string that creates the history import checkpoint, ledger, and typed contract tables.
|
|
3
|
+
*
|
|
4
|
+
* WHY: per-source `history_etl_*` blob tables are no longer hard-coded here. They are created
|
|
5
|
+
* dynamically by {@link ensureTargetTables} / {@link ETL_TABLE_DDL} looping every entry of
|
|
6
|
+
* {@link SOURCE_DEFINITIONS}, so adding a new built-in source needs no edit to this file. This
|
|
7
|
+
* single-owner rule closes the R16 drift where `omp` / `grok` / `agy` existed only via import-time
|
|
8
|
+
* DDL while the seven other sources were duplicated in the static string.
|
|
9
|
+
*/
|
|
2
10
|
export const HISTORY_IMPORT_SCHEMA_SQL = `
|
|
3
11
|
CREATE TABLE IF NOT EXISTS history_import_checkpoint (
|
|
4
12
|
source TEXT NOT NULL,
|
|
@@ -18,69 +26,6 @@ CREATE TABLE IF NOT EXISTS history_import_ledger (
|
|
|
18
26
|
imported_at TEXT NOT NULL
|
|
19
27
|
);
|
|
20
28
|
|
|
21
|
-
CREATE TABLE IF NOT EXISTS history_etl_pi (
|
|
22
|
-
record_hash TEXT PRIMARY KEY,
|
|
23
|
-
source_file TEXT NOT NULL,
|
|
24
|
-
source_line INTEGER NOT NULL,
|
|
25
|
-
split_index INTEGER NOT NULL,
|
|
26
|
-
payload_json TEXT NOT NULL,
|
|
27
|
-
imported_at TEXT NOT NULL
|
|
28
|
-
);
|
|
29
|
-
|
|
30
|
-
CREATE TABLE IF NOT EXISTS history_etl_claude (
|
|
31
|
-
record_hash TEXT PRIMARY KEY,
|
|
32
|
-
source_file TEXT NOT NULL,
|
|
33
|
-
source_line INTEGER NOT NULL,
|
|
34
|
-
split_index INTEGER NOT NULL,
|
|
35
|
-
payload_json TEXT NOT NULL,
|
|
36
|
-
imported_at TEXT NOT NULL
|
|
37
|
-
);
|
|
38
|
-
|
|
39
|
-
CREATE TABLE IF NOT EXISTS history_etl_codex (
|
|
40
|
-
record_hash TEXT PRIMARY KEY,
|
|
41
|
-
source_file TEXT NOT NULL,
|
|
42
|
-
source_line INTEGER NOT NULL,
|
|
43
|
-
split_index INTEGER NOT NULL,
|
|
44
|
-
payload_json TEXT NOT NULL,
|
|
45
|
-
imported_at TEXT NOT NULL
|
|
46
|
-
);
|
|
47
|
-
|
|
48
|
-
CREATE TABLE IF NOT EXISTS history_etl_gemini (
|
|
49
|
-
record_hash TEXT PRIMARY KEY,
|
|
50
|
-
source_file TEXT NOT NULL,
|
|
51
|
-
source_line INTEGER NOT NULL,
|
|
52
|
-
split_index INTEGER NOT NULL,
|
|
53
|
-
payload_json TEXT NOT NULL,
|
|
54
|
-
imported_at TEXT NOT NULL
|
|
55
|
-
);
|
|
56
|
-
|
|
57
|
-
CREATE TABLE IF NOT EXISTS history_etl_opencode (
|
|
58
|
-
record_hash TEXT PRIMARY KEY,
|
|
59
|
-
source_file TEXT NOT NULL,
|
|
60
|
-
source_line INTEGER NOT NULL,
|
|
61
|
-
split_index INTEGER NOT NULL,
|
|
62
|
-
payload_json TEXT NOT NULL,
|
|
63
|
-
imported_at TEXT NOT NULL
|
|
64
|
-
);
|
|
65
|
-
|
|
66
|
-
CREATE TABLE IF NOT EXISTS history_etl_antigravity (
|
|
67
|
-
record_hash TEXT PRIMARY KEY,
|
|
68
|
-
source_file TEXT NOT NULL,
|
|
69
|
-
source_line INTEGER NOT NULL,
|
|
70
|
-
split_index INTEGER NOT NULL,
|
|
71
|
-
payload_json TEXT NOT NULL,
|
|
72
|
-
imported_at TEXT NOT NULL
|
|
73
|
-
);
|
|
74
|
-
|
|
75
|
-
CREATE TABLE IF NOT EXISTS history_etl_openclaw (
|
|
76
|
-
record_hash TEXT PRIMARY KEY,
|
|
77
|
-
source_file TEXT NOT NULL,
|
|
78
|
-
source_line INTEGER NOT NULL,
|
|
79
|
-
split_index INTEGER NOT NULL,
|
|
80
|
-
payload_json TEXT NOT NULL,
|
|
81
|
-
imported_at TEXT NOT NULL
|
|
82
|
-
);
|
|
83
|
-
|
|
84
29
|
CREATE TABLE IF NOT EXISTS history_message (
|
|
85
30
|
record_hash TEXT PRIMARY KEY,
|
|
86
31
|
source TEXT NOT NULL,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gobing-ai/ts-llm-jsonl-importer",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.30",
|
|
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.
|
|
51
|
-
"@gobing-ai/ts-runtime": "^0.4.
|
|
52
|
-
"@gobing-ai/ts-utils": "^0.4.
|
|
50
|
+
"@gobing-ai/ts-db": "^0.4.30",
|
|
51
|
+
"@gobing-ai/ts-runtime": "^0.4.30",
|
|
52
|
+
"@gobing-ai/ts-utils": "^0.4.30",
|
|
53
53
|
"zod": "^4.1.0"
|
|
54
54
|
},
|
|
55
55
|
"devDependencies": {
|
package/src/importer.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { DbBatchOp } from '@gobing-ai/ts-db';
|
|
1
2
|
import {
|
|
2
3
|
ambientRuntimePaths,
|
|
3
4
|
createNodeFileSystem,
|
|
@@ -9,15 +10,15 @@ import {
|
|
|
9
10
|
import { sha256 } from './hash';
|
|
10
11
|
import {
|
|
11
12
|
applyHistoryImportSchema,
|
|
13
|
+
checkpointUpsertOp,
|
|
12
14
|
ensureTargetTables,
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
ledgerExists,
|
|
15
|
+
ledgerExistingHashes,
|
|
16
|
+
ledgerInsertOp,
|
|
16
17
|
readCheckpoint,
|
|
17
18
|
reconcileFullImport,
|
|
19
|
+
recordInsertOp,
|
|
18
20
|
resetCheckpoints,
|
|
19
21
|
targetTableFor,
|
|
20
|
-
writeCheckpoint,
|
|
21
22
|
} from './jsonl-importer-dao';
|
|
22
23
|
import { redactRecord } from './redaction';
|
|
23
24
|
import { resolveSourceDefinition } from './sources';
|
|
@@ -130,10 +131,6 @@ export async function runJsonlImport(source: string | SourceDefinition, options:
|
|
|
130
131
|
record: redacted,
|
|
131
132
|
});
|
|
132
133
|
desiredHashes.add(recordHash);
|
|
133
|
-
if (await ledgerExists(options.db, recordHash)) {
|
|
134
|
-
skippedDuplicates += 1;
|
|
135
|
-
continue;
|
|
136
|
-
}
|
|
137
134
|
prepared.push({ split, splitIndex, normalized: redacted, recordHash });
|
|
138
135
|
}
|
|
139
136
|
|
|
@@ -141,9 +138,24 @@ export async function runJsonlImport(source: string | SourceDefinition, options:
|
|
|
141
138
|
// second pass, no checkpoint advance, no partial rows from this record.
|
|
142
139
|
if (lineRejected) continue;
|
|
143
140
|
|
|
144
|
-
//
|
|
141
|
+
// Chunked ledger lookup (task 0060 F9): one `IN (...)` query per ≤200 hashes
|
|
142
|
+
// instead of a SELECT per record; duplicates are dropped before the write pass.
|
|
143
|
+
const existing = await ledgerExistingHashes(
|
|
144
|
+
options.db,
|
|
145
|
+
prepared.map((e) => e.recordHash),
|
|
146
|
+
);
|
|
147
|
+
const accepted = prepared.filter((entry) => {
|
|
148
|
+
if (existing.has(entry.recordHash)) {
|
|
149
|
+
skippedDuplicates += 1;
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
return true;
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// Second pass: resolve _messageSplitIndex → message_hash, then batch.
|
|
145
156
|
const recordHashBySplitIndex = new Map(prepared.map((e) => [e.splitIndex, e.recordHash] as const));
|
|
146
|
-
|
|
157
|
+
const ops: DbBatchOp[] = [];
|
|
158
|
+
for (const entry of accepted) {
|
|
147
159
|
const { split, splitIndex, normalized, recordHash } = entry;
|
|
148
160
|
if (normalized.disposition === 'unknown') {
|
|
149
161
|
unknownRecords += 1;
|
|
@@ -158,33 +170,42 @@ export async function runJsonlImport(source: string | SourceDefinition, options:
|
|
|
158
170
|
}
|
|
159
171
|
}
|
|
160
172
|
if (!options.dryRun) {
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
173
|
+
// Record row + ledger row join one batch op pair — a crash between the two
|
|
174
|
+
// writes is impossible (task 0060 F9).
|
|
175
|
+
ops.push(
|
|
176
|
+
recordInsertOp(
|
|
177
|
+
split.targetTable,
|
|
178
|
+
recordHash,
|
|
179
|
+
file,
|
|
180
|
+
lineNumber,
|
|
181
|
+
splitIndex,
|
|
182
|
+
normalized,
|
|
183
|
+
options.now,
|
|
184
|
+
),
|
|
170
185
|
);
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
186
|
+
ops.push(
|
|
187
|
+
ledgerInsertOp(
|
|
188
|
+
recordHash,
|
|
189
|
+
resolvedSource,
|
|
190
|
+
file,
|
|
191
|
+
lineNumber,
|
|
192
|
+
splitIndex,
|
|
193
|
+
split.targetTable,
|
|
194
|
+
options.now,
|
|
195
|
+
),
|
|
180
196
|
);
|
|
181
197
|
}
|
|
182
198
|
importedRecords += 1;
|
|
183
199
|
}
|
|
184
200
|
|
|
185
|
-
if (
|
|
186
|
-
|
|
187
|
-
|
|
201
|
+
if (!options.dryRun && lineSucceeded) {
|
|
202
|
+
// Checkpoint upsert joins the same batch so an accepted line can never be
|
|
203
|
+
// re-imported after a mid-batch crash.
|
|
204
|
+
ops.push(checkpointUpsertOp(resolvedSource, file, lineNumber, options.now));
|
|
205
|
+
if (ops.length > 0) {
|
|
206
|
+
await options.db.batch(ops);
|
|
207
|
+
checkpointUpdates += 1;
|
|
208
|
+
}
|
|
188
209
|
}
|
|
189
210
|
}
|
|
190
211
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { DbAdapter, DbBatchOp } from '@gobing-ai/ts-db';
|
|
2
2
|
import { HistoryImportError } from './errors';
|
|
3
3
|
import { HISTORY_IMPORT_SCHEMA_SQL } from './schema-sql';
|
|
4
|
-
import { VALID_TABLE_NAME } from './sources';
|
|
4
|
+
import { SOURCE_DEFINITIONS, VALID_TABLE_NAME } from './sources';
|
|
5
5
|
import type { ImportOptions, JsonObject, ReconcileSummary, SourceDefinition } from './types';
|
|
6
6
|
|
|
7
7
|
interface CheckpointRow {
|
|
@@ -81,7 +81,15 @@ function ETL_TABLE_DDL(table: string): string {
|
|
|
81
81
|
);`;
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
/**
|
|
84
|
+
/**
|
|
85
|
+
* Apply importer-owned schema to the target database.
|
|
86
|
+
*
|
|
87
|
+
* Creates the checkpoint, ledger, and typed contract tables from the static SQL, then materializes
|
|
88
|
+
* every built-in `history_etl_*` table by looping {@link SOURCE_DEFINITIONS} through
|
|
89
|
+
* {@link ensureTargetTables}. This keeps the migration contract "schema applied ⇒ all built-in ETL
|
|
90
|
+
* tables exist" for callers who apply the schema and then raw-insert without a prior import.
|
|
91
|
+
* Adding a new built-in source to {@link SOURCE_DEFINITIONS} is now the only edit needed.
|
|
92
|
+
*/
|
|
85
93
|
export async function applyHistoryImportSchema(db: ImportOptions['db']): Promise<void> {
|
|
86
94
|
for (const statement of HISTORY_IMPORT_SCHEMA_SQL.split(';')) {
|
|
87
95
|
const sql = statement.trim();
|
|
@@ -89,18 +97,22 @@ export async function applyHistoryImportSchema(db: ImportOptions['db']): Promise
|
|
|
89
97
|
await db.exec(sql);
|
|
90
98
|
}
|
|
91
99
|
}
|
|
100
|
+
for (const definition of Object.values(SOURCE_DEFINITIONS)) {
|
|
101
|
+
await ensureTargetTables(db, definition);
|
|
102
|
+
}
|
|
92
103
|
}
|
|
93
104
|
|
|
94
105
|
/**
|
|
95
106
|
* Ensure the ETL table(s) for a source definition exist.
|
|
96
107
|
*
|
|
97
|
-
* WHY: the static {@link HISTORY_IMPORT_SCHEMA_SQL} only
|
|
98
|
-
* `history_etl_*`
|
|
99
|
-
* (and
|
|
100
|
-
* target table(s) created on demand. The
|
|
108
|
+
* WHY: the static {@link HISTORY_IMPORT_SCHEMA_SQL} creates only the checkpoint, ledger, and
|
|
109
|
+
* typed contract tables. Every `history_etl_*` blob table — built-in and custom — is created here
|
|
110
|
+
* (and by {@link applyHistoryImportSchema} looping {@link SOURCE_DEFINITIONS}). Built-in definitions
|
|
111
|
+
* with a `splitConfig.targetTable` override also need their target table(s) created on demand. The
|
|
112
|
+
* table name is already gated by
|
|
101
113
|
* {@link VALID_TABLE_NAME} in {@link validateSourceDefinition} / {@link targetTableFor},
|
|
102
|
-
* so it is safe to interpolate into DDL. `CREATE TABLE IF NOT EXISTS`
|
|
103
|
-
* idempotent
|
|
114
|
+
* so it is safe to interpolate into DDL. `CREATE TABLE IF NOT EXISTS` is
|
|
115
|
+
* idempotent, so apply-then-import and a second apply are both safe.
|
|
104
116
|
*/
|
|
105
117
|
async function ensureTargetTables(db: ImportOptions['db'], definition: SourceDefinition): Promise<void> {
|
|
106
118
|
const tables = new Set<string>([targetTableFor(definition.targetTable)]);
|
|
@@ -157,10 +169,13 @@ async function ledgerExists(db: ImportOptions['db'], recordHash: string): Promis
|
|
|
157
169
|
return row !== undefined && row !== null;
|
|
158
170
|
}
|
|
159
171
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
172
|
+
/**
|
|
173
|
+
* INSERT op for one accepted record row (task 0060 F9). Pure builder so the
|
|
174
|
+
* per-record batch path and the single-write `insertRecord` can never drift.
|
|
175
|
+
* Typed tables validate unknown keys here; the caller must have ensured the
|
|
176
|
+
* target table exists (`ensureTargetTables` runs at import start).
|
|
177
|
+
*/
|
|
178
|
+
export function recordInsertOp(
|
|
164
179
|
targetTable: string,
|
|
165
180
|
recordHash: string,
|
|
166
181
|
sourceFile: string,
|
|
@@ -168,7 +183,7 @@ async function insertRecord(
|
|
|
168
183
|
splitIndex: number,
|
|
169
184
|
payload: JsonObject,
|
|
170
185
|
now: ImportOptions['now'],
|
|
171
|
-
):
|
|
186
|
+
): DbBatchOp {
|
|
172
187
|
const table = targetTableFor(targetTable);
|
|
173
188
|
const typedColumns = TYPED_TABLE_COLUMNS[table];
|
|
174
189
|
if (typedColumns !== undefined) {
|
|
@@ -189,34 +204,91 @@ async function insertRecord(
|
|
|
189
204
|
const v = payload[col];
|
|
190
205
|
return v === undefined ? null : v;
|
|
191
206
|
});
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
)
|
|
202
|
-
} else {
|
|
203
|
-
// Generic ETL insert path: store as payload_json blob.
|
|
204
|
-
if (!_ensuredTables.has(table)) {
|
|
205
|
-
await db.exec(ETL_TABLE_DDL(table));
|
|
206
|
-
_ensuredTables.add(table);
|
|
207
|
-
}
|
|
208
|
-
await db.run(
|
|
209
|
-
`INSERT INTO ${table} (record_hash, source_file, source_line, split_index, payload_json, imported_at)
|
|
207
|
+
return {
|
|
208
|
+
sql: `INSERT INTO ${table} (${typedColumns.join(', ')})
|
|
209
|
+
VALUES (${typedColumns.map(() => '?').join(', ')})
|
|
210
|
+
ON CONFLICT(record_hash) DO NOTHING`,
|
|
211
|
+
params: values,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
// Generic ETL insert path: store as payload_json blob.
|
|
215
|
+
return {
|
|
216
|
+
sql: `INSERT INTO ${table} (record_hash, source_file, source_line, split_index, payload_json, imported_at)
|
|
210
217
|
VALUES (?, ?, ?, ?, ?, ?)
|
|
211
218
|
ON CONFLICT(record_hash) DO NOTHING`,
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
219
|
+
params: [recordHash, sourceFile, sourceLine, splitIndex, JSON.stringify(payload), timestamp(now)],
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** INSERT op for one ledger row (task 0060 F9). */
|
|
224
|
+
export function ledgerInsertOp(
|
|
225
|
+
recordHash: string,
|
|
226
|
+
source: string,
|
|
227
|
+
sourceFile: string,
|
|
228
|
+
sourceLine: number,
|
|
229
|
+
splitIndex: number,
|
|
230
|
+
targetTable: string,
|
|
231
|
+
now: ImportOptions['now'],
|
|
232
|
+
): DbBatchOp {
|
|
233
|
+
return {
|
|
234
|
+
sql: `INSERT INTO history_import_ledger (record_hash, source, source_file, source_line, split_index, target_table, imported_at)
|
|
235
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
236
|
+
params: [recordHash, source, sourceFile, sourceLine, splitIndex, targetTable, timestamp(now)],
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** UPSERT op for one checkpoint row (task 0060 F9: checkpoint joins the record batch). */
|
|
241
|
+
export function checkpointUpsertOp(
|
|
242
|
+
source: string,
|
|
243
|
+
sourceFile: string,
|
|
244
|
+
line: number,
|
|
245
|
+
now: ImportOptions['now'],
|
|
246
|
+
): DbBatchOp {
|
|
247
|
+
return {
|
|
248
|
+
sql: `INSERT INTO history_import_checkpoint (source, source_file, last_imported_line, updated_at)
|
|
249
|
+
VALUES (?, ?, ?, ?)
|
|
250
|
+
ON CONFLICT(source, source_file) DO UPDATE SET
|
|
251
|
+
last_imported_line = excluded.last_imported_line,
|
|
252
|
+
updated_at = excluded.updated_at`,
|
|
253
|
+
params: [source, sourceFile, line, timestamp(now)],
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Query which of the given hashes already exist in the ledger, in chunks of at
|
|
259
|
+
* most 200 per `IN (...)` (task 0060 F9 — replaces the per-record SELECT loop).
|
|
260
|
+
*/
|
|
261
|
+
export async function ledgerExistingHashes(db: ImportOptions['db'], hashes: readonly string[]): Promise<Set<string>> {
|
|
262
|
+
const existing = new Set<string>();
|
|
263
|
+
for (let i = 0; i < hashes.length; i += 200) {
|
|
264
|
+
const chunk = hashes.slice(i, i + 200);
|
|
265
|
+
const placeholders = chunk.map(() => '?').join(', ');
|
|
266
|
+
const rows = await db.queryAll<{ record_hash: string }>(
|
|
267
|
+
`SELECT record_hash FROM history_import_ledger WHERE record_hash IN (${placeholders})`,
|
|
268
|
+
...chunk,
|
|
218
269
|
);
|
|
270
|
+
for (const row of rows) existing.add(row.record_hash);
|
|
271
|
+
}
|
|
272
|
+
return existing;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
async function insertRecord(
|
|
276
|
+
db: ImportOptions['db'],
|
|
277
|
+
targetTable: string,
|
|
278
|
+
recordHash: string,
|
|
279
|
+
sourceFile: string,
|
|
280
|
+
sourceLine: number,
|
|
281
|
+
splitIndex: number,
|
|
282
|
+
payload: JsonObject,
|
|
283
|
+
now: ImportOptions['now'],
|
|
284
|
+
): Promise<void> {
|
|
285
|
+
const op = recordInsertOp(targetTable, recordHash, sourceFile, sourceLine, splitIndex, payload, now);
|
|
286
|
+
if (TYPED_TABLE_COLUMNS[targetTableFor(targetTable)] === undefined) {
|
|
287
|
+
// Custom ETL tables are created on demand; `CREATE TABLE IF NOT EXISTS` is idempotent,
|
|
288
|
+
// so there is no process-global cache to go stale across adapter instances (task 0060 F8).
|
|
289
|
+
await db.exec(ETL_TABLE_DDL(targetTableFor(targetTable)));
|
|
219
290
|
}
|
|
291
|
+
await db.run(op.sql, ...op.params);
|
|
220
292
|
}
|
|
221
293
|
|
|
222
294
|
async function insertLedger(
|
package/src/redaction.ts
CHANGED
|
@@ -17,6 +17,21 @@ export const DEFAULT_REDACTION_RULES: readonly RedactionRule[] = [
|
|
|
17
17
|
pattern: /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/gi,
|
|
18
18
|
replacement: '[REDACTED:email]',
|
|
19
19
|
},
|
|
20
|
+
{
|
|
21
|
+
name: 'xai-key',
|
|
22
|
+
pattern: /\bxai-[A-Za-z0-9_-]{12,}\b/g,
|
|
23
|
+
replacement: '[REDACTED:token]',
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
name: 'aws-access-key-id',
|
|
27
|
+
pattern: /\bAKIA[0-9A-Z]{16}\b/g,
|
|
28
|
+
replacement: '[REDACTED:aws-key]',
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
name: 'bearer-token',
|
|
32
|
+
pattern: /\bBearer\s+[A-Za-z0-9._\-=]+\b/g,
|
|
33
|
+
replacement: '[REDACTED:bearer]',
|
|
34
|
+
},
|
|
20
35
|
];
|
|
21
36
|
|
|
22
37
|
/** Redact supported scalar and composite JSON values recursively. */
|
package/src/schema-sql.ts
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/**
|
|
2
|
+
* DDL string that creates the history import checkpoint, ledger, and typed contract tables.
|
|
3
|
+
*
|
|
4
|
+
* WHY: per-source `history_etl_*` blob tables are no longer hard-coded here. They are created
|
|
5
|
+
* dynamically by {@link ensureTargetTables} / {@link ETL_TABLE_DDL} looping every entry of
|
|
6
|
+
* {@link SOURCE_DEFINITIONS}, so adding a new built-in source needs no edit to this file. This
|
|
7
|
+
* single-owner rule closes the R16 drift where `omp` / `grok` / `agy` existed only via import-time
|
|
8
|
+
* DDL while the seven other sources were duplicated in the static string.
|
|
9
|
+
*/
|
|
2
10
|
export const HISTORY_IMPORT_SCHEMA_SQL = `
|
|
3
11
|
CREATE TABLE IF NOT EXISTS history_import_checkpoint (
|
|
4
12
|
source TEXT NOT NULL,
|
|
@@ -18,69 +26,6 @@ CREATE TABLE IF NOT EXISTS history_import_ledger (
|
|
|
18
26
|
imported_at TEXT NOT NULL
|
|
19
27
|
);
|
|
20
28
|
|
|
21
|
-
CREATE TABLE IF NOT EXISTS history_etl_pi (
|
|
22
|
-
record_hash TEXT PRIMARY KEY,
|
|
23
|
-
source_file TEXT NOT NULL,
|
|
24
|
-
source_line INTEGER NOT NULL,
|
|
25
|
-
split_index INTEGER NOT NULL,
|
|
26
|
-
payload_json TEXT NOT NULL,
|
|
27
|
-
imported_at TEXT NOT NULL
|
|
28
|
-
);
|
|
29
|
-
|
|
30
|
-
CREATE TABLE IF NOT EXISTS history_etl_claude (
|
|
31
|
-
record_hash TEXT PRIMARY KEY,
|
|
32
|
-
source_file TEXT NOT NULL,
|
|
33
|
-
source_line INTEGER NOT NULL,
|
|
34
|
-
split_index INTEGER NOT NULL,
|
|
35
|
-
payload_json TEXT NOT NULL,
|
|
36
|
-
imported_at TEXT NOT NULL
|
|
37
|
-
);
|
|
38
|
-
|
|
39
|
-
CREATE TABLE IF NOT EXISTS history_etl_codex (
|
|
40
|
-
record_hash TEXT PRIMARY KEY,
|
|
41
|
-
source_file TEXT NOT NULL,
|
|
42
|
-
source_line INTEGER NOT NULL,
|
|
43
|
-
split_index INTEGER NOT NULL,
|
|
44
|
-
payload_json TEXT NOT NULL,
|
|
45
|
-
imported_at TEXT NOT NULL
|
|
46
|
-
);
|
|
47
|
-
|
|
48
|
-
CREATE TABLE IF NOT EXISTS history_etl_gemini (
|
|
49
|
-
record_hash TEXT PRIMARY KEY,
|
|
50
|
-
source_file TEXT NOT NULL,
|
|
51
|
-
source_line INTEGER NOT NULL,
|
|
52
|
-
split_index INTEGER NOT NULL,
|
|
53
|
-
payload_json TEXT NOT NULL,
|
|
54
|
-
imported_at TEXT NOT NULL
|
|
55
|
-
);
|
|
56
|
-
|
|
57
|
-
CREATE TABLE IF NOT EXISTS history_etl_opencode (
|
|
58
|
-
record_hash TEXT PRIMARY KEY,
|
|
59
|
-
source_file TEXT NOT NULL,
|
|
60
|
-
source_line INTEGER NOT NULL,
|
|
61
|
-
split_index INTEGER NOT NULL,
|
|
62
|
-
payload_json TEXT NOT NULL,
|
|
63
|
-
imported_at TEXT NOT NULL
|
|
64
|
-
);
|
|
65
|
-
|
|
66
|
-
CREATE TABLE IF NOT EXISTS history_etl_antigravity (
|
|
67
|
-
record_hash TEXT PRIMARY KEY,
|
|
68
|
-
source_file TEXT NOT NULL,
|
|
69
|
-
source_line INTEGER NOT NULL,
|
|
70
|
-
split_index INTEGER NOT NULL,
|
|
71
|
-
payload_json TEXT NOT NULL,
|
|
72
|
-
imported_at TEXT NOT NULL
|
|
73
|
-
);
|
|
74
|
-
|
|
75
|
-
CREATE TABLE IF NOT EXISTS history_etl_openclaw (
|
|
76
|
-
record_hash TEXT PRIMARY KEY,
|
|
77
|
-
source_file TEXT NOT NULL,
|
|
78
|
-
source_line INTEGER NOT NULL,
|
|
79
|
-
split_index INTEGER NOT NULL,
|
|
80
|
-
payload_json TEXT NOT NULL,
|
|
81
|
-
imported_at TEXT NOT NULL
|
|
82
|
-
);
|
|
83
|
-
|
|
84
29
|
CREATE TABLE IF NOT EXISTS history_message (
|
|
85
30
|
record_hash TEXT PRIMARY KEY,
|
|
86
31
|
source TEXT NOT NULL,
|