@gobing-ai/ts-llm-jsonl-importer 0.4.21 → 0.4.23
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/dist/importer.js +27 -3
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/jsonl-importer-dao.d.ts +21 -0
- package/dist/jsonl-importer-dao.d.ts.map +1 -1
- package/dist/jsonl-importer-dao.js +87 -0
- package/package.json +4 -4
- package/src/importer.ts +26 -3
- package/src/index.ts +1 -1
- package/src/jsonl-importer-dao.ts +127 -0
package/dist/importer.js
CHANGED
|
@@ -180,7 +180,10 @@ function normalizeRecord(definition, raw, context) {
|
|
|
180
180
|
}
|
|
181
181
|
async function discoverFiles(definition, roots, files, fileSystem, paths) {
|
|
182
182
|
if (files !== undefined && files.length > 0) {
|
|
183
|
-
return files
|
|
183
|
+
return files
|
|
184
|
+
.map((file) => resolvePath(file))
|
|
185
|
+
.map((file) => normalizeSourceFilePath(file, fileSystem))
|
|
186
|
+
.sort();
|
|
184
187
|
}
|
|
185
188
|
// Root resolution splits by provenance (ADR-023 A1 / task 0042):
|
|
186
189
|
// - explicit caller-supplied `roots` → cwd semantics: resolve against `paths.cwd` when
|
|
@@ -201,16 +204,37 @@ async function discoverFiles(definition, roots, files, fileSystem, paths) {
|
|
|
201
204
|
continue;
|
|
202
205
|
if (stat.isFile()) {
|
|
203
206
|
if (matchesPattern(root, definition.filePatterns))
|
|
204
|
-
found.add(root);
|
|
207
|
+
found.add(normalizeSourceFilePath(root, fileSystem));
|
|
205
208
|
continue;
|
|
206
209
|
}
|
|
207
210
|
for (const file of await walkDir(root, fileSystem)) {
|
|
208
211
|
if (matchesPattern(file, definition.filePatterns))
|
|
209
|
-
found.add(file);
|
|
212
|
+
found.add(normalizeSourceFilePath(file, fileSystem));
|
|
210
213
|
}
|
|
211
214
|
}
|
|
212
215
|
return [...found].sort();
|
|
213
216
|
}
|
|
217
|
+
/**
|
|
218
|
+
* Normalize a discovered source file path to its canonical real-path identity.
|
|
219
|
+
*
|
|
220
|
+
* WHY: the same physical session file is reachable via a symlinked path and via its
|
|
221
|
+
* real path (every agent history dir under `$HOME` is a symlink into the dotfiles
|
|
222
|
+
* tree here). Without normalization, the checkpoint, ledger, and record_hash keys
|
|
223
|
+
* diverge, yielding duplicate checkpoint rows and silent full-corpus re-imports.
|
|
224
|
+
* `realPath` is optional on `FileSystem`: injected and in-memory test doubles may
|
|
225
|
+
* omit it, and a path that does not exist on disk has no realpath. Fall back to the
|
|
226
|
+
* original path in either case — never throw, never bypass the injected FileSystem.
|
|
227
|
+
*/
|
|
228
|
+
function normalizeSourceFilePath(path, fileSystem) {
|
|
229
|
+
if (!fileSystem.realPath)
|
|
230
|
+
return path;
|
|
231
|
+
try {
|
|
232
|
+
return fileSystem.realPath(path);
|
|
233
|
+
}
|
|
234
|
+
catch {
|
|
235
|
+
return path;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
214
238
|
function matchesPattern(path, patterns) {
|
|
215
239
|
return patterns.some((pattern) => {
|
|
216
240
|
if (pattern === '*.jsonl')
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export { HistoryImportError } from './errors';
|
|
2
2
|
export { sha256, stableJson } from './hash';
|
|
3
3
|
export { runJsonlImport } from './importer';
|
|
4
|
-
export { applyHistoryImportSchema } from './jsonl-importer-dao';
|
|
4
|
+
export { applyHistoryImportSchema, normalizeSourceFilePaths } from './jsonl-importer-dao';
|
|
5
5
|
export { DEFAULT_REDACTION_RULES, redactRecord, redactValue } from './redaction';
|
|
6
6
|
export { HISTORY_IMPORT_SCHEMA_SQL } from './schema-sql';
|
|
7
7
|
export { getSourceDefinition, resolveSourceDefinition, SOURCE_DEFINITIONS, VALID_TABLE_NAME, validateSourceDefinition, } from './sources';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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,MAAM,sBAAsB,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,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"}
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export { HistoryImportError } from './errors.js';
|
|
2
2
|
export { sha256, stableJson } from './hash.js';
|
|
3
3
|
export { runJsonlImport } from './importer.js';
|
|
4
|
-
export { applyHistoryImportSchema } from './jsonl-importer-dao.js';
|
|
4
|
+
export { applyHistoryImportSchema, normalizeSourceFilePaths } from './jsonl-importer-dao.js';
|
|
5
5
|
export { DEFAULT_REDACTION_RULES, redactRecord, redactValue } from './redaction.js';
|
|
6
6
|
export { HISTORY_IMPORT_SCHEMA_SQL } from './schema-sql.js';
|
|
7
7
|
export { getSourceDefinition, resolveSourceDefinition, SOURCE_DEFINITIONS, VALID_TABLE_NAME, validateSourceDefinition, } from './sources.js';
|
|
@@ -25,6 +25,27 @@ declare function writeCheckpoint(db: ImportOptions['db'], source: string, source
|
|
|
25
25
|
declare function ledgerExists(db: ImportOptions['db'], recordHash: string): Promise<boolean>;
|
|
26
26
|
declare function insertRecord(db: ImportOptions['db'], targetTable: string, recordHash: string, sourceFile: string, sourceLine: number, splitIndex: number, payload: JsonObject, now: ImportOptions['now']): Promise<void>;
|
|
27
27
|
declare function insertLedger(db: ImportOptions['db'], recordHash: string, source: string, sourceFile: string, sourceLine: number, splitIndex: number, targetTable: string, now: ImportOptions['now']): Promise<void>;
|
|
28
|
+
/**
|
|
29
|
+
* Idempotent migration that rewrites `source_file` to its realpath across the checkpoint,
|
|
30
|
+
* ledger, and any contract table present (R4).
|
|
31
|
+
*
|
|
32
|
+
* WHY: `record_hash` is `sha256({source, sourceFile, sourceLine, splitIndex, record})`, so
|
|
33
|
+
* `sourceFile` is inside the hash. Normalizing its representation at discovery (R1) changes
|
|
34
|
+
* every future hash; pre-normalization rows carry unnormalized paths and their old hashes
|
|
35
|
+
* remain valid as dedupe keys. This migration rewrites the `source_file` *column* so old and
|
|
36
|
+
* new rows agree on path identity, and collapses duplicate checkpoint rows produced when one
|
|
37
|
+
* physical file was imported via both a symlinked and a real path — keeping the highest
|
|
38
|
+
* `last_imported_line` so an incremental resume does not re-import already-seen content.
|
|
39
|
+
*
|
|
40
|
+
* `record_hash` is intentionally NOT touched: it is path-representation dependent by
|
|
41
|
+
* construction, pre-migration rows are grandfathered, and recomputing it would require the
|
|
42
|
+
* original record payload that the ledger does not store.
|
|
43
|
+
*
|
|
44
|
+
* @param resolveRealPath - resolves a `source_file` to its canonical real path. Falls back to
|
|
45
|
+
* the original value when it returns null/undefined or throws. Decouples the DAO from the
|
|
46
|
+
* runtime `FileSystem` seam.
|
|
47
|
+
*/
|
|
48
|
+
export declare function normalizeSourceFilePaths(db: ImportOptions['db'], resolveRealPath: (sourceFile: string) => string | null | undefined): Promise<void>;
|
|
28
49
|
export type { CheckpointRow };
|
|
29
50
|
export { ETL_TABLE_DDL, ensureTargetTables, insertLedger, insertRecord, ledgerExists, readCheckpoint, resetCheckpoints, targetTableFor, timestamp, writeCheckpoint, };
|
|
30
51
|
//# 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,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":"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"}
|
|
@@ -165,4 +165,91 @@ async function insertLedger(db, recordHash, source, sourceFile, sourceLine, spli
|
|
|
165
165
|
await db.run(`INSERT INTO history_import_ledger (record_hash, source, source_file, source_line, split_index, target_table, imported_at)
|
|
166
166
|
VALUES (?, ?, ?, ?, ?, ?, ?)`, recordHash, source, sourceFile, sourceLine, splitIndex, targetTable, timestamp(now));
|
|
167
167
|
}
|
|
168
|
+
/**
|
|
169
|
+
* Idempotent migration that rewrites `source_file` to its realpath across the checkpoint,
|
|
170
|
+
* ledger, and any contract table present (R4).
|
|
171
|
+
*
|
|
172
|
+
* WHY: `record_hash` is `sha256({source, sourceFile, sourceLine, splitIndex, record})`, so
|
|
173
|
+
* `sourceFile` is inside the hash. Normalizing its representation at discovery (R1) changes
|
|
174
|
+
* every future hash; pre-normalization rows carry unnormalized paths and their old hashes
|
|
175
|
+
* remain valid as dedupe keys. This migration rewrites the `source_file` *column* so old and
|
|
176
|
+
* new rows agree on path identity, and collapses duplicate checkpoint rows produced when one
|
|
177
|
+
* physical file was imported via both a symlinked and a real path — keeping the highest
|
|
178
|
+
* `last_imported_line` so an incremental resume does not re-import already-seen content.
|
|
179
|
+
*
|
|
180
|
+
* `record_hash` is intentionally NOT touched: it is path-representation dependent by
|
|
181
|
+
* construction, pre-migration rows are grandfathered, and recomputing it would require the
|
|
182
|
+
* original record payload that the ledger does not store.
|
|
183
|
+
*
|
|
184
|
+
* @param resolveRealPath - resolves a `source_file` to its canonical real path. Falls back to
|
|
185
|
+
* the original value when it returns null/undefined or throws. Decouples the DAO from the
|
|
186
|
+
* runtime `FileSystem` seam.
|
|
187
|
+
*/
|
|
188
|
+
export async function normalizeSourceFilePaths(db, resolveRealPath) {
|
|
189
|
+
await normalizeCheckpointPaths(db, resolveRealPath);
|
|
190
|
+
await normalizeColumnPaths(db, resolveRealPath, 'history_import_ledger');
|
|
191
|
+
for (const table of TYPED_TABLE_COLUMNS_SOURCE_FILE) {
|
|
192
|
+
if (await tableExists(db, table)) {
|
|
193
|
+
await normalizeColumnPaths(db, resolveRealPath, table);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
for (const table of await listEtlTables(db)) {
|
|
197
|
+
await normalizeColumnPaths(db, resolveRealPath, table);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
/** Typed contract tables that carry a `source_file` column (0466 forensic contract). */
|
|
201
|
+
const TYPED_TABLE_COLUMNS_SOURCE_FILE = ['history_message', 'history_tool_call'];
|
|
202
|
+
async function tableExists(db, table) {
|
|
203
|
+
const row = await db.queryFirst("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?", table);
|
|
204
|
+
return row !== undefined && row !== null;
|
|
205
|
+
}
|
|
206
|
+
async function listEtlTables(db) {
|
|
207
|
+
const rows = await db.queryAll("SELECT name FROM sqlite_master WHERE type = 'table' AND name LIKE 'history_etl_%'");
|
|
208
|
+
return rows.map((row) => row.name);
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Normalize `source_file` in the checkpoint table, collapsing duplicate rows per
|
|
212
|
+
* `(source, realpath)` by keeping the highest `last_imported_line`.
|
|
213
|
+
*/
|
|
214
|
+
async function normalizeCheckpointPaths(db, resolveRealPath) {
|
|
215
|
+
const rows = await db.queryAll('SELECT source, source_file, last_imported_line, updated_at FROM history_import_checkpoint');
|
|
216
|
+
// Collapse per (source, realPath) keeping the highest last_imported_line.
|
|
217
|
+
const collapsed = new Map();
|
|
218
|
+
for (const row of rows) {
|
|
219
|
+
const canonical = resolveWithFallback(resolveRealPath, row.source_file);
|
|
220
|
+
const key = `${row.source}\u0000${canonical}`;
|
|
221
|
+
const existing = collapsed.get(key);
|
|
222
|
+
if (existing === undefined || row.last_imported_line > existing.last_imported_line) {
|
|
223
|
+
collapsed.set(key, {
|
|
224
|
+
source: row.source,
|
|
225
|
+
source_file: canonical,
|
|
226
|
+
last_imported_line: row.last_imported_line,
|
|
227
|
+
updated_at: row.updated_at,
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
await db.exec('DELETE FROM history_import_checkpoint');
|
|
232
|
+
for (const row of collapsed.values()) {
|
|
233
|
+
await db.run(`INSERT INTO history_import_checkpoint (source, source_file, last_imported_line, updated_at)
|
|
234
|
+
VALUES (?, ?, ?, ?)`, row.source, row.source_file, row.last_imported_line, row.updated_at);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
/** Normalize `source_file` in a table that stores it as a plain column. */
|
|
238
|
+
async function normalizeColumnPaths(db, resolveRealPath, table) {
|
|
239
|
+
const rows = await db.queryAll(`SELECT rowid AS rowid, source_file FROM ${table} WHERE source_file IS NOT NULL`);
|
|
240
|
+
for (const row of rows) {
|
|
241
|
+
const canonical = resolveWithFallback(resolveRealPath, row.source_file);
|
|
242
|
+
if (canonical !== row.source_file) {
|
|
243
|
+
await db.run(`UPDATE ${table} SET source_file = ? WHERE rowid = ?`, canonical, row.rowid);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
function resolveWithFallback(resolveRealPath, sourceFile) {
|
|
248
|
+
try {
|
|
249
|
+
return resolveRealPath(sourceFile) ?? sourceFile;
|
|
250
|
+
}
|
|
251
|
+
catch {
|
|
252
|
+
return sourceFile;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
168
255
|
export { ETL_TABLE_DDL, ensureTargetTables, insertLedger, insertRecord, ledgerExists, readCheckpoint, resetCheckpoints, targetTableFor, timestamp, writeCheckpoint, };
|
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.23",
|
|
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.23",
|
|
51
|
+
"@gobing-ai/ts-runtime": "^0.4.23",
|
|
52
|
+
"@gobing-ai/ts-utils": "^0.4.23",
|
|
53
53
|
"zod": "^4.1.0"
|
|
54
54
|
},
|
|
55
55
|
"devDependencies": {
|
package/src/importer.ts
CHANGED
|
@@ -251,7 +251,10 @@ async function discoverFiles(
|
|
|
251
251
|
paths: RuntimePaths | undefined,
|
|
252
252
|
): Promise<readonly string[]> {
|
|
253
253
|
if (files !== undefined && files.length > 0) {
|
|
254
|
-
return files
|
|
254
|
+
return files
|
|
255
|
+
.map((file) => resolvePath(file))
|
|
256
|
+
.map((file) => normalizeSourceFilePath(file, fileSystem))
|
|
257
|
+
.sort();
|
|
255
258
|
}
|
|
256
259
|
|
|
257
260
|
// Root resolution splits by provenance (ADR-023 A1 / task 0042):
|
|
@@ -271,16 +274,36 @@ async function discoverFiles(
|
|
|
271
274
|
const stat = await fileSystem.stat(root);
|
|
272
275
|
if (stat === null) continue;
|
|
273
276
|
if (stat.isFile()) {
|
|
274
|
-
if (matchesPattern(root, definition.filePatterns)) found.add(root);
|
|
277
|
+
if (matchesPattern(root, definition.filePatterns)) found.add(normalizeSourceFilePath(root, fileSystem));
|
|
275
278
|
continue;
|
|
276
279
|
}
|
|
277
280
|
for (const file of await walkDir(root, fileSystem)) {
|
|
278
|
-
if (matchesPattern(file, definition.filePatterns)) found.add(file);
|
|
281
|
+
if (matchesPattern(file, definition.filePatterns)) found.add(normalizeSourceFilePath(file, fileSystem));
|
|
279
282
|
}
|
|
280
283
|
}
|
|
281
284
|
return [...found].sort();
|
|
282
285
|
}
|
|
283
286
|
|
|
287
|
+
/**
|
|
288
|
+
* Normalize a discovered source file path to its canonical real-path identity.
|
|
289
|
+
*
|
|
290
|
+
* WHY: the same physical session file is reachable via a symlinked path and via its
|
|
291
|
+
* real path (every agent history dir under `$HOME` is a symlink into the dotfiles
|
|
292
|
+
* tree here). Without normalization, the checkpoint, ledger, and record_hash keys
|
|
293
|
+
* diverge, yielding duplicate checkpoint rows and silent full-corpus re-imports.
|
|
294
|
+
* `realPath` is optional on `FileSystem`: injected and in-memory test doubles may
|
|
295
|
+
* omit it, and a path that does not exist on disk has no realpath. Fall back to the
|
|
296
|
+
* original path in either case — never throw, never bypass the injected FileSystem.
|
|
297
|
+
*/
|
|
298
|
+
function normalizeSourceFilePath(path: string, fileSystem: FileSystem): string {
|
|
299
|
+
if (!fileSystem.realPath) return path;
|
|
300
|
+
try {
|
|
301
|
+
return fileSystem.realPath(path);
|
|
302
|
+
} catch {
|
|
303
|
+
return path;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
284
307
|
function matchesPattern(path: string, patterns: readonly string[]): boolean {
|
|
285
308
|
return patterns.some((pattern) => {
|
|
286
309
|
if (pattern === '*.jsonl') return path.endsWith('.jsonl');
|
package/src/index.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export { HistoryImportError } from './errors';
|
|
2
2
|
export { sha256, stableJson } from './hash';
|
|
3
3
|
export { runJsonlImport } from './importer';
|
|
4
|
-
export { applyHistoryImportSchema } from './jsonl-importer-dao';
|
|
4
|
+
export { applyHistoryImportSchema, normalizeSourceFilePaths } from './jsonl-importer-dao';
|
|
5
5
|
export { DEFAULT_REDACTION_RULES, redactRecord, redactValue } from './redaction';
|
|
6
6
|
export { HISTORY_IMPORT_SCHEMA_SQL } from './schema-sql';
|
|
7
7
|
export {
|
|
@@ -241,6 +241,133 @@ async function insertLedger(
|
|
|
241
241
|
);
|
|
242
242
|
}
|
|
243
243
|
|
|
244
|
+
/**
|
|
245
|
+
* Idempotent migration that rewrites `source_file` to its realpath across the checkpoint,
|
|
246
|
+
* ledger, and any contract table present (R4).
|
|
247
|
+
*
|
|
248
|
+
* WHY: `record_hash` is `sha256({source, sourceFile, sourceLine, splitIndex, record})`, so
|
|
249
|
+
* `sourceFile` is inside the hash. Normalizing its representation at discovery (R1) changes
|
|
250
|
+
* every future hash; pre-normalization rows carry unnormalized paths and their old hashes
|
|
251
|
+
* remain valid as dedupe keys. This migration rewrites the `source_file` *column* so old and
|
|
252
|
+
* new rows agree on path identity, and collapses duplicate checkpoint rows produced when one
|
|
253
|
+
* physical file was imported via both a symlinked and a real path — keeping the highest
|
|
254
|
+
* `last_imported_line` so an incremental resume does not re-import already-seen content.
|
|
255
|
+
*
|
|
256
|
+
* `record_hash` is intentionally NOT touched: it is path-representation dependent by
|
|
257
|
+
* construction, pre-migration rows are grandfathered, and recomputing it would require the
|
|
258
|
+
* original record payload that the ledger does not store.
|
|
259
|
+
*
|
|
260
|
+
* @param resolveRealPath - resolves a `source_file` to its canonical real path. Falls back to
|
|
261
|
+
* the original value when it returns null/undefined or throws. Decouples the DAO from the
|
|
262
|
+
* runtime `FileSystem` seam.
|
|
263
|
+
*/
|
|
264
|
+
export async function normalizeSourceFilePaths(
|
|
265
|
+
db: ImportOptions['db'],
|
|
266
|
+
resolveRealPath: (sourceFile: string) => string | null | undefined,
|
|
267
|
+
): Promise<void> {
|
|
268
|
+
await normalizeCheckpointPaths(db, resolveRealPath);
|
|
269
|
+
await normalizeColumnPaths(db, resolveRealPath, 'history_import_ledger');
|
|
270
|
+
for (const table of TYPED_TABLE_COLUMNS_SOURCE_FILE) {
|
|
271
|
+
if (await tableExists(db, table)) {
|
|
272
|
+
await normalizeColumnPaths(db, resolveRealPath, table);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
for (const table of await listEtlTables(db)) {
|
|
276
|
+
await normalizeColumnPaths(db, resolveRealPath, table);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** Typed contract tables that carry a `source_file` column (0466 forensic contract). */
|
|
281
|
+
const TYPED_TABLE_COLUMNS_SOURCE_FILE = ['history_message', 'history_tool_call'] as const;
|
|
282
|
+
|
|
283
|
+
async function tableExists(db: ImportOptions['db'], table: string): Promise<boolean> {
|
|
284
|
+
const row = await db.queryFirst<{ name: string }>(
|
|
285
|
+
"SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?",
|
|
286
|
+
table,
|
|
287
|
+
);
|
|
288
|
+
return row !== undefined && row !== null;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
async function listEtlTables(db: ImportOptions['db']): Promise<string[]> {
|
|
292
|
+
const rows = await db.queryAll<{ name: string }>(
|
|
293
|
+
"SELECT name FROM sqlite_master WHERE type = 'table' AND name LIKE 'history_etl_%'",
|
|
294
|
+
);
|
|
295
|
+
return rows.map((row) => row.name);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Normalize `source_file` in the checkpoint table, collapsing duplicate rows per
|
|
300
|
+
* `(source, realpath)` by keeping the highest `last_imported_line`.
|
|
301
|
+
*/
|
|
302
|
+
async function normalizeCheckpointPaths(
|
|
303
|
+
db: ImportOptions['db'],
|
|
304
|
+
resolveRealPath: (sourceFile: string) => string | null | undefined,
|
|
305
|
+
): Promise<void> {
|
|
306
|
+
const rows = await db.queryAll<{
|
|
307
|
+
source: string;
|
|
308
|
+
source_file: string;
|
|
309
|
+
last_imported_line: number;
|
|
310
|
+
updated_at: string;
|
|
311
|
+
}>('SELECT source, source_file, last_imported_line, updated_at FROM history_import_checkpoint');
|
|
312
|
+
// Collapse per (source, realPath) keeping the highest last_imported_line.
|
|
313
|
+
const collapsed = new Map<
|
|
314
|
+
string,
|
|
315
|
+
{ source: string; source_file: string; last_imported_line: number; updated_at: string }
|
|
316
|
+
>();
|
|
317
|
+
for (const row of rows) {
|
|
318
|
+
const canonical = resolveWithFallback(resolveRealPath, row.source_file);
|
|
319
|
+
const key = `${row.source}\u0000${canonical}`;
|
|
320
|
+
const existing = collapsed.get(key);
|
|
321
|
+
if (existing === undefined || row.last_imported_line > existing.last_imported_line) {
|
|
322
|
+
collapsed.set(key, {
|
|
323
|
+
source: row.source,
|
|
324
|
+
source_file: canonical,
|
|
325
|
+
last_imported_line: row.last_imported_line,
|
|
326
|
+
updated_at: row.updated_at,
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
await db.exec('DELETE FROM history_import_checkpoint');
|
|
331
|
+
for (const row of collapsed.values()) {
|
|
332
|
+
await db.run(
|
|
333
|
+
`INSERT INTO history_import_checkpoint (source, source_file, last_imported_line, updated_at)
|
|
334
|
+
VALUES (?, ?, ?, ?)`,
|
|
335
|
+
row.source,
|
|
336
|
+
row.source_file,
|
|
337
|
+
row.last_imported_line,
|
|
338
|
+
row.updated_at,
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/** Normalize `source_file` in a table that stores it as a plain column. */
|
|
344
|
+
async function normalizeColumnPaths(
|
|
345
|
+
db: ImportOptions['db'],
|
|
346
|
+
resolveRealPath: (sourceFile: string) => string | null | undefined,
|
|
347
|
+
table: string,
|
|
348
|
+
): Promise<void> {
|
|
349
|
+
const rows = await db.queryAll<{ rowid: number; source_file: string }>(
|
|
350
|
+
`SELECT rowid AS rowid, source_file FROM ${table} WHERE source_file IS NOT NULL`,
|
|
351
|
+
);
|
|
352
|
+
for (const row of rows) {
|
|
353
|
+
const canonical = resolveWithFallback(resolveRealPath, row.source_file);
|
|
354
|
+
if (canonical !== row.source_file) {
|
|
355
|
+
await db.run(`UPDATE ${table} SET source_file = ? WHERE rowid = ?`, canonical, row.rowid);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function resolveWithFallback(
|
|
361
|
+
resolveRealPath: (sourceFile: string) => string | null | undefined,
|
|
362
|
+
sourceFile: string,
|
|
363
|
+
): string {
|
|
364
|
+
try {
|
|
365
|
+
return resolveRealPath(sourceFile) ?? sourceFile;
|
|
366
|
+
} catch {
|
|
367
|
+
return sourceFile;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
244
371
|
export type { CheckpointRow };
|
|
245
372
|
export {
|
|
246
373
|
ETL_TABLE_DDL,
|