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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"importer.d.ts","sourceRoot":"","sources":["../src/importer.ts"],"names":[],"mappings":"AAsBA,OAAO,KAAK,EAAe,aAAa,EAAE,YAAY,EAAc,gBAAgB,EAAoB,MAAM,SAAS,CAAC;AAOxH;;;;;;;GAOG;AACH,wBAAsB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,gBAAgB,EAAE,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,YAAY,CAAC,CAgJrH"}
1
+ {"version":3,"file":"importer.d.ts","sourceRoot":"","sources":["../src/importer.ts"],"names":[],"mappings":"AAuBA,OAAO,KAAK,EAER,aAAa,EACb,YAAY,EAGZ,gBAAgB,EAEnB,MAAM,SAAS,CAAC;AAOjB;;;;;;;GAOG;AACH,wBAAsB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,gBAAgB,EAAE,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,YAAY,CAAC,CA4KrH"}
package/dist/importer.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { ambientRuntimePaths, createNodeFileSystem, resolvePath, walkDir, } from '@gobing-ai/ts-runtime';
2
2
  import { sha256 } from './hash.js';
3
- import { applyHistoryImportSchema, ensureTargetTables, insertLedger, insertRecord, ledgerExists, readCheckpoint, resetCheckpoints, targetTableFor, writeCheckpoint, } from './jsonl-importer-dao.js';
3
+ import { applyHistoryImportSchema, ensureTargetTables, insertLedger, insertRecord, ledgerExists, readCheckpoint, reconcileFullImport, resetCheckpoints, targetTableFor, writeCheckpoint, } from './jsonl-importer-dao.js';
4
4
  import { redactRecord } from './redaction.js';
5
5
  import { resolveSourceDefinition } from './sources.js';
6
6
  /** Run the JSONL import pipeline for one source.
@@ -29,6 +29,9 @@ export async function runJsonlImport(source, options) {
29
29
  let skippedDuplicates = 0;
30
30
  let unknownRecords = 0;
31
31
  let checkpointUpdates = 0;
32
+ // Full-mode desired set (R1, task 0504): every record hash the current source produces.
33
+ // ReconcileFullImport diffs this against the persisted ledger to retire stale rows.
34
+ const desiredHashes = new Set();
32
35
  for (const file of files) {
33
36
  const checkpoint = mode === 'incremental' ? await readCheckpoint(options.db, resolvedSource, file) : 0;
34
37
  let lineNumber = 0;
@@ -48,6 +51,10 @@ export async function runJsonlImport(source, options) {
48
51
  splitIndex: 0,
49
52
  });
50
53
  let lineSucceeded = false;
54
+ // Atomic per-record acceptance (R2, task 0504): a schema-invalid split rejects the
55
+ // WHOLE line, so no partially accepted rows (or orphaned tool-call rows with a
56
+ // dangling message_hash) can be left behind by one bad split.
57
+ let lineRejected = false;
51
58
  const prepared = [];
52
59
  for (let splitIndex = 0; splitIndex < splitRecords.length; splitIndex += 1) {
53
60
  const split = splitRecords[splitIndex];
@@ -66,6 +73,7 @@ export async function runJsonlImport(source, options) {
66
73
  sourceLine: lineNumber,
67
74
  reason: parsed.error.issues.map((issue) => issue.message).join('; '),
68
75
  });
76
+ lineRejected = true;
69
77
  continue;
70
78
  }
71
79
  const redacted = redactRecord(parsed.data, options.redactionRules);
@@ -77,12 +85,17 @@ export async function runJsonlImport(source, options) {
77
85
  splitIndex,
78
86
  record: redacted,
79
87
  });
88
+ desiredHashes.add(recordHash);
80
89
  if (await ledgerExists(options.db, recordHash)) {
81
90
  skippedDuplicates += 1;
82
91
  continue;
83
92
  }
84
93
  prepared.push({ split, splitIndex, normalized: redacted, recordHash });
85
94
  }
95
+ // A rejected line is reported (validationErrors above) but never persisted — no
96
+ // second pass, no checkpoint advance, no partial rows from this record.
97
+ if (lineRejected)
98
+ continue;
86
99
  // Second pass: resolve _messageSplitIndex → message_hash, then insert.
87
100
  const recordHashBySplitIndex = new Map(prepared.map((e) => [e.splitIndex, e.recordHash]));
88
101
  for (const entry of prepared) {
@@ -111,6 +124,13 @@ export async function runJsonlImport(source, options) {
111
124
  }
112
125
  }
113
126
  }
127
+ // Full-mode reconciliation (R1, task 0504): diff the desired hash set against the
128
+ // persisted ledger and retire stale derived rows (target, tool, ledger, checkpoint) in
129
+ // one source-scoped batch. Dry-run computes the identical counts without mutation.
130
+ let reconciliation;
131
+ if (mode === 'full') {
132
+ reconciliation = await reconcileFullImport(options.db, resolvedSource, desiredHashes, files, options.dryRun ?? false);
133
+ }
114
134
  return {
115
135
  source: resolvedSource,
116
136
  mode,
@@ -122,6 +142,7 @@ export async function runJsonlImport(source, options) {
122
142
  parseErrors,
123
143
  validationErrors,
124
144
  checkpointUpdates,
145
+ reconciliation,
125
146
  };
126
147
  }
127
148
  function parseJsonLine(line, sourceFile, sourceLine, parseErrors) {
package/dist/index.d.ts CHANGED
@@ -6,5 +6,5 @@ export { type OpenCodeImportOptions, runOpenCodeImport } from './opencode-import
6
6
  export { DEFAULT_REDACTION_RULES, redactRecord, redactValue } from './redaction';
7
7
  export { HISTORY_IMPORT_SCHEMA_SQL } from './schema-sql';
8
8
  export { getSourceDefinition, resolveSourceDefinition, SOURCE_DEFINITIONS, VALID_TABLE_NAME, validateSourceDefinition, } from './sources';
9
- export type { FieldTransform, ImportIssue, ImportMode, ImportOptions, ImportResult, JsonObject, LlmJsonlSource, RedactionRule, SourceDefinition, SplitConfig, SplitEntry, TransformContext, } from './types';
9
+ export type { FieldTransform, ImportIssue, ImportMode, ImportOptions, ImportResult, JsonObject, LlmJsonlSource, ReconcileSummary, RedactionRule, SourceDefinition, SplitConfig, SplitEntry, TransformContext, } from './types';
10
10
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAC;AAC9C,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAC;AAC5C,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAC5C,OAAO,EAAE,wBAAwB,EAAE,wBAAwB,EAAE,MAAM,sBAAsB,CAAC;AAC1F,OAAO,EAAE,KAAK,qBAAqB,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AACpF,OAAO,EAAE,uBAAuB,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AACjF,OAAO,EAAE,yBAAyB,EAAE,MAAM,cAAc,CAAC;AACzD,OAAO,EACH,mBAAmB,EACnB,uBAAuB,EACvB,kBAAkB,EAClB,gBAAgB,EAChB,wBAAwB,GAC3B,MAAM,WAAW,CAAC;AACnB,YAAY,EACR,cAAc,EACd,WAAW,EACX,UAAU,EACV,aAAa,EACb,YAAY,EACZ,UAAU,EACV,cAAc,EACd,aAAa,EACb,gBAAgB,EAChB,WAAW,EACX,UAAU,EACV,gBAAgB,GACnB,MAAM,SAAS,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAC;AAC9C,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAC;AAC5C,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAC5C,OAAO,EAAE,wBAAwB,EAAE,wBAAwB,EAAE,MAAM,sBAAsB,CAAC;AAC1F,OAAO,EAAE,KAAK,qBAAqB,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AACpF,OAAO,EAAE,uBAAuB,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AACjF,OAAO,EAAE,yBAAyB,EAAE,MAAM,cAAc,CAAC;AACzD,OAAO,EACH,mBAAmB,EACnB,uBAAuB,EACvB,kBAAkB,EAClB,gBAAgB,EAChB,wBAAwB,GAC3B,MAAM,WAAW,CAAC;AACnB,YAAY,EACR,cAAc,EACd,WAAW,EACX,UAAU,EACV,aAAa,EACb,YAAY,EACZ,UAAU,EACV,cAAc,EACd,gBAAgB,EAChB,aAAa,EACb,gBAAgB,EAChB,WAAW,EACX,UAAU,EACV,gBAAgB,GACnB,MAAM,SAAS,CAAC"}
@@ -1,5 +1,5 @@
1
1
  import type { DbAdapter, DbBatchOp } from '@gobing-ai/ts-db';
2
- import type { ImportOptions, JsonObject, SourceDefinition } from './types';
2
+ import type { ImportOptions, JsonObject, ReconcileSummary, SourceDefinition } from './types';
3
3
  interface CheckpointRow {
4
4
  readonly last_imported_line: number;
5
5
  }
@@ -47,6 +47,19 @@ declare function insertLedger(db: ImportOptions['db'], recordHash: string, sourc
47
47
  * runtime `FileSystem` seam.
48
48
  */
49
49
  export declare function normalizeSourceFilePaths(db: ImportOptions['db'], resolveRealPath: (sourceFile: string) => string | null | undefined): Promise<void>;
50
+ /**
51
+ * Reconcile a source's persisted derived rows against the desired set produced by a full
52
+ * import (task 0504 R1). Rows whose `record_hash` is no longer reproduced by current source
53
+ * data or mapper output are stale: their target rows (typed contract tables — including tool
54
+ * calls — and ETL tables), ledger rows, and checkpoints for vanished source files are removed
55
+ * in ONE source-scoped batch (atomic). Dry-run returns the exact same counts without mutating
56
+ * the database, so `--dry-run` is a safe preview and a second full write reports zero changes.
57
+ *
58
+ * Deletes are keyed by `record_hash` (PK) and checkpoint `(source, source_file)` (PK) — never
59
+ * by an unindexed ledger predicate — so reconciliation cost scales with the stale count, not
60
+ * with ledger size.
61
+ */
62
+ declare function reconcileFullImport(db: ImportOptions['db'], source: string, desiredHashes: ReadonlySet<string>, discoveredFiles: readonly string[], dryRun: boolean): Promise<ReconcileSummary>;
50
63
  /** OpenCode message row projected from its SQLite store. */
51
64
  export interface OpenCodeMessageRow {
52
65
  id: string;
@@ -86,5 +99,5 @@ export declare function openCodeDeleteOperations(entries: readonly OpenCodeExist
86
99
  /** Build batched history, ledger, and checkpoint writes for OpenCode entries. */
87
100
  export declare function openCodeBulkWriteOperations(entries: readonly OpenCodeQueuedEntry[], checkpointFiles: readonly string[], now: ImportOptions['now']): DbBatchOp[];
88
101
  export type { CheckpointRow };
89
- export { ETL_TABLE_DDL, ensureTargetTables, insertLedger, insertRecord, ledgerExists, readCheckpoint, resetCheckpoints, targetTableFor, timestamp, writeCheckpoint, };
102
+ export { ETL_TABLE_DDL, ensureTargetTables, insertLedger, insertRecord, ledgerExists, readCheckpoint, reconcileFullImport, resetCheckpoints, targetTableFor, timestamp, writeCheckpoint, };
90
103
  //# sourceMappingURL=jsonl-importer-dao.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"jsonl-importer-dao.d.ts","sourceRoot":"","sources":["../src/jsonl-importer-dao.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"jsonl-importer-dao.d.ts","sourceRoot":"","sources":["../src/jsonl-importer-dao.ts"],"names":[],"mappings":"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"}
@@ -252,6 +252,57 @@ function resolveWithFallback(resolveRealPath, sourceFile) {
252
252
  return sourceFile;
253
253
  }
254
254
  }
255
+ /**
256
+ * Reconcile a source's persisted derived rows against the desired set produced by a full
257
+ * import (task 0504 R1). Rows whose `record_hash` is no longer reproduced by current source
258
+ * data or mapper output are stale: their target rows (typed contract tables — including tool
259
+ * calls — and ETL tables), ledger rows, and checkpoints for vanished source files are removed
260
+ * in ONE source-scoped batch (atomic). Dry-run returns the exact same counts without mutating
261
+ * the database, so `--dry-run` is a safe preview and a second full write reports zero changes.
262
+ *
263
+ * Deletes are keyed by `record_hash` (PK) and checkpoint `(source, source_file)` (PK) — never
264
+ * by an unindexed ledger predicate — so reconciliation cost scales with the stale count, not
265
+ * with ledger size.
266
+ */
267
+ async function reconcileFullImport(db, source, desiredHashes, discoveredFiles, dryRun) {
268
+ const ledgerRows = await db.queryAll('SELECT record_hash, target_table FROM history_import_ledger WHERE source = ?', source);
269
+ const stale = [];
270
+ for (const row of ledgerRows) {
271
+ if (!desiredHashes.has(row.record_hash))
272
+ stale.push(row);
273
+ }
274
+ const checkpointRows = await db.queryAll('SELECT source_file FROM history_import_checkpoint WHERE source = ?', source);
275
+ const discovered = new Set(discoveredFiles);
276
+ const staleCheckpoints = checkpointRows
277
+ .map((row) => row.source_file)
278
+ .filter((sourceFile) => !discovered.has(sourceFile));
279
+ const summary = {
280
+ staleTargetRows: stale.length,
281
+ staleLedgerRows: stale.length,
282
+ staleCheckpointRows: staleCheckpoints.length,
283
+ };
284
+ if (dryRun || (stale.length === 0 && staleCheckpoints.length === 0))
285
+ return summary;
286
+ const operations = [];
287
+ for (const row of stale) {
288
+ operations.push({
289
+ sql: `DELETE FROM ${targetTableFor(row.target_table)} WHERE record_hash = ?`,
290
+ params: [row.record_hash],
291
+ });
292
+ operations.push({
293
+ sql: 'DELETE FROM history_import_ledger WHERE record_hash = ?',
294
+ params: [row.record_hash],
295
+ });
296
+ }
297
+ for (const sourceFile of staleCheckpoints) {
298
+ operations.push({
299
+ sql: 'DELETE FROM history_import_checkpoint WHERE source = ? AND source_file = ?',
300
+ params: [source, sourceFile],
301
+ });
302
+ }
303
+ await db.batch(operations);
304
+ return summary;
305
+ }
255
306
  const OPENCODE_SOURCE = 'opencode';
256
307
  /** Read one ordered page of OpenCode messages after the supplied cursor. */
257
308
  export async function readOpenCodeMessages(db, lastTime, lastId, limit) {
@@ -353,4 +404,4 @@ export function openCodeBulkWriteOperations(entries, checkpointFiles, now) {
353
404
  }
354
405
  return operations;
355
406
  }
356
- export { ETL_TABLE_DDL, ensureTargetTables, insertLedger, insertRecord, ledgerExists, readCheckpoint, resetCheckpoints, targetTableFor, timestamp, writeCheckpoint, };
407
+ export { ETL_TABLE_DDL, ensureTargetTables, insertLedger, insertRecord, ledgerExists, readCheckpoint, reconcileFullImport, resetCheckpoints, targetTableFor, timestamp, writeCheckpoint, };
@@ -1 +1 @@
1
- {"version":3,"file":"opencode-importer.d.ts","sourceRoot":"","sources":["../src/opencode-importer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAmB,KAAK,SAAS,EAAkB,MAAM,kBAAkB,CAAC;AACnF,OAAO,EAGH,KAAK,UAAU,EAEf,KAAK,YAAY,EACpB,MAAM,uBAAuB,CAAC;AAc/B,OAAO,KAAK,EAAe,UAAU,EAAE,YAAY,EAAc,aAAa,EAAE,MAAM,SAAS,CAAC;AAYhG,8EAA8E;AAC9E,MAAM,WAAW,qBAAqB;IAClC,EAAE,EAAE,SAAS,CAAC;IACd,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,KAAK,CAAC,EAAE,YAAY,CAAC;IACrB,IAAI,CAAC,EAAE,UAAU,CAAC;IAClB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,cAAc,CAAC,EAAE,SAAS,aAAa,EAAE,CAAC;IAC1C,GAAG,CAAC,EAAE,MAAM,IAAI,CAAC;CACpB;AAED,wFAAwF;AACxF,wBAAsB,iBAAiB,CAAC,OAAO,EAAE,qBAAqB,GAAG,OAAO,CAAC,YAAY,CAAC,CA+F7F"}
1
+ {"version":3,"file":"opencode-importer.d.ts","sourceRoot":"","sources":["../src/opencode-importer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAmB,KAAK,SAAS,EAAkB,MAAM,kBAAkB,CAAC;AACnF,OAAO,EAGH,KAAK,UAAU,EAEf,KAAK,YAAY,EACpB,MAAM,uBAAuB,CAAC;AAc/B,OAAO,KAAK,EAAe,UAAU,EAAE,YAAY,EAAgC,aAAa,EAAE,MAAM,SAAS,CAAC;AAYlH,8EAA8E;AAC9E,MAAM,WAAW,qBAAqB;IAClC,EAAE,EAAE,SAAS,CAAC;IACd,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,KAAK,CAAC,EAAE,YAAY,CAAC;IACrB,IAAI,CAAC,EAAE,UAAU,CAAC;IAClB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,cAAc,CAAC,EAAE,SAAS,aAAa,EAAE,CAAC;IAC1C,GAAG,CAAC,EAAE,MAAM,IAAI,CAAC;CACpB;AAED,wFAAwF;AACxF,wBAAsB,iBAAiB,CAAC,OAAO,EAAE,qBAAqB,GAAG,OAAO,CAAC,YAAY,CAAC,CAuI7F"}
@@ -24,7 +24,20 @@ export async function runOpenCodeImport(options) {
24
24
  let lastTime = -1;
25
25
  let lastId = '';
26
26
  const existingBySourceFile = await readOpenCodeExistingEntries(options.db);
27
+ // Source files still present in the OpenCode store this pass (R1, task 0504). Files with
28
+ // no valid role produce no entries and are deliberately NOT marked seen, so their old
29
+ // derived rows are swept as no-longer-reproduced in full mode.
30
+ const seenSourceFiles = new Set();
27
31
  const operations = [];
32
+ // Full-mode reconciliation counters (R1, task 0504): stale rows from BOTH sources —
33
+ // per-file hash-diff deletes (changed messages) and the vanished-file sweep below. Dry-run
34
+ // counts the identical totals the write would delete, so `--dry-run` is an exact preview.
35
+ // Declared here (outside `try`) because the result is returned after `finally` closes the
36
+ // source database.
37
+ let staleTargetRows = 0;
38
+ let staleLedgerRows = 0;
39
+ let staleCheckpointRows = 0;
40
+ let reconciliation;
28
41
  try {
29
42
  while (true) {
30
43
  const messages = await readOpenCodeMessages(sourceDb, lastTime, lastId, PAGE_SIZE);
@@ -48,11 +61,18 @@ export async function runOpenCodeImport(options) {
48
61
  validationErrors.push({ sourceFile, sourceLine: 1, reason: 'OpenCode message has no valid role' });
49
62
  continue;
50
63
  }
64
+ seenSourceFiles.add(sourceFile);
51
65
  const existing = existingBySourceFile.get(sourceFile) ?? [];
52
66
  if (sameHashes(existing, entries)) {
53
67
  skippedDuplicates += entries.length;
54
68
  continue;
55
69
  }
70
+ if (mode === 'full') {
71
+ // Per-file mapper drift: the old derived rows for this source file are
72
+ // stale and are deleted before the new entries are written.
73
+ staleTargetRows += existing.length;
74
+ staleLedgerRows += existing.length;
75
+ }
56
76
  if (!options.dryRun) {
57
77
  operations.push(...openCodeDeleteOperations(existing));
58
78
  for (const entry of entries) {
@@ -73,6 +93,26 @@ export async function runOpenCodeImport(options) {
73
93
  lastTime = tail.time_created;
74
94
  lastId = tail.id;
75
95
  }
96
+ // Full-mode reconciliation (R1, task 0504): messages deleted from the OpenCode store
97
+ // leave stale derived rows behind. Sweep ledger/target rows and checkpoints for source
98
+ // files never seen this pass, merged into the SAME source-scoped batch as the writes.
99
+ if (mode === 'full') {
100
+ for (const [sourceFile, entries] of existingBySourceFile) {
101
+ if (seenSourceFiles.has(sourceFile))
102
+ continue;
103
+ staleLedgerRows += entries.length;
104
+ staleTargetRows += entries.length;
105
+ staleCheckpointRows += 1;
106
+ if (!options.dryRun) {
107
+ operations.push(...openCodeDeleteOperations(entries));
108
+ operations.push({
109
+ sql: 'DELETE FROM history_import_checkpoint WHERE source = ? AND source_file = ?',
110
+ params: [SOURCE, sourceFile],
111
+ });
112
+ }
113
+ }
114
+ reconciliation = { staleTargetRows, staleLedgerRows, staleCheckpointRows };
115
+ }
76
116
  if (operations.length > 0)
77
117
  await options.db.batch(operations);
78
118
  }
@@ -90,6 +130,7 @@ export async function runOpenCodeImport(options) {
90
130
  parseErrors,
91
131
  validationErrors,
92
132
  checkpointUpdates,
133
+ reconciliation,
93
134
  };
94
135
  }
95
136
  function groupParts(parts) {
package/dist/types.d.ts CHANGED
@@ -76,6 +76,20 @@ export interface ImportIssue {
76
76
  readonly sourceLine: number;
77
77
  readonly reason: string;
78
78
  }
79
+ /**
80
+ * Source-scoped full-mode reconciliation outcome (task 0504 R1). Computed by diffing the
81
+ * current source's desired record hashes against the persisted ledger; rows no longer
82
+ * reproduced by current source data or mapper output are stale and are removed in one
83
+ * source-scoped batch (dry-run reports the same counts without mutating the database).
84
+ */
85
+ export interface ReconcileSummary {
86
+ /** Rows deleted from typed/ETL target tables (history_message / history_tool_call / history_etl_*). */
87
+ readonly staleTargetRows: number;
88
+ /** Rows deleted from `history_import_ledger`. */
89
+ readonly staleLedgerRows: number;
90
+ /** Checkpoint rows deleted for source files no longer discovered. */
91
+ readonly staleCheckpointRows: number;
92
+ }
79
93
  /** Summary produced by the importer control function. */
80
94
  export interface ImportResult {
81
95
  /** Source identifier — the built-in name or custom definition name that produced this result. */
@@ -89,5 +103,10 @@ export interface ImportResult {
89
103
  readonly parseErrors: readonly ImportIssue[];
90
104
  readonly validationErrors: readonly ImportIssue[];
91
105
  readonly checkpointUpdates: number;
106
+ /**
107
+ * Full-mode reconciliation counts (only set when `mode === 'full'`). A second full run
108
+ * reports zero stale rows once the database matches the current source (task 0504 R1).
109
+ */
110
+ readonly reconciliation?: ReconcileSummary;
92
111
  }
93
112
  //# sourceMappingURL=types.d.ts.map
@@ -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.24",
3
+ "version": "0.4.25",
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.24",
51
- "@gobing-ai/ts-runtime": "^0.4.24",
52
- "@gobing-ai/ts-utils": "^0.4.24",
50
+ "@gobing-ai/ts-db": "^0.4.25",
51
+ "@gobing-ai/ts-runtime": "^0.4.25",
52
+ "@gobing-ai/ts-utils": "^0.4.25",
53
53
  "zod": "^4.1.0"
54
54
  },
55
55
  "devDependencies": {
package/src/importer.ts CHANGED
@@ -14,13 +14,22 @@ import {
14
14
  insertRecord,
15
15
  ledgerExists,
16
16
  readCheckpoint,
17
+ reconcileFullImport,
17
18
  resetCheckpoints,
18
19
  targetTableFor,
19
20
  writeCheckpoint,
20
21
  } from './jsonl-importer-dao';
21
22
  import { redactRecord } from './redaction';
22
23
  import { resolveSourceDefinition } from './sources';
23
- import type { ImportIssue, ImportOptions, ImportResult, JsonObject, SourceDefinition, TransformContext } from './types';
24
+ import type {
25
+ ImportIssue,
26
+ ImportOptions,
27
+ ImportResult,
28
+ JsonObject,
29
+ ReconcileSummary,
30
+ SourceDefinition,
31
+ TransformContext,
32
+ } from './types';
24
33
 
25
34
  interface SplitRecord {
26
35
  readonly targetTable: string;
@@ -55,6 +64,9 @@ export async function runJsonlImport(source: string | SourceDefinition, options:
55
64
  let skippedDuplicates = 0;
56
65
  let unknownRecords = 0;
57
66
  let checkpointUpdates = 0;
67
+ // Full-mode desired set (R1, task 0504): every record hash the current source produces.
68
+ // ReconcileFullImport diffs this against the persisted ledger to retire stale rows.
69
+ const desiredHashes = new Set<string>();
58
70
 
59
71
  for (const file of files) {
60
72
  const checkpoint = mode === 'incremental' ? await readCheckpoint(options.db, resolvedSource, file) : 0;
@@ -75,6 +87,10 @@ export async function runJsonlImport(source: string | SourceDefinition, options:
75
87
  splitIndex: 0,
76
88
  });
77
89
  let lineSucceeded = false;
90
+ // Atomic per-record acceptance (R2, task 0504): a schema-invalid split rejects the
91
+ // WHOLE line, so no partially accepted rows (or orphaned tool-call rows with a
92
+ // dangling message_hash) can be left behind by one bad split.
93
+ let lineRejected = false;
78
94
 
79
95
  // First pass: normalize, validate, redact, compute recordHash for every entry.
80
96
  interface PreparedEntry {
@@ -100,6 +116,7 @@ export async function runJsonlImport(source: string | SourceDefinition, options:
100
116
  sourceLine: lineNumber,
101
117
  reason: parsed.error.issues.map((issue) => issue.message).join('; '),
102
118
  });
119
+ lineRejected = true;
103
120
  continue;
104
121
  }
105
122
 
@@ -112,6 +129,7 @@ export async function runJsonlImport(source: string | SourceDefinition, options:
112
129
  splitIndex,
113
130
  record: redacted,
114
131
  });
132
+ desiredHashes.add(recordHash);
115
133
  if (await ledgerExists(options.db, recordHash)) {
116
134
  skippedDuplicates += 1;
117
135
  continue;
@@ -119,6 +137,10 @@ export async function runJsonlImport(source: string | SourceDefinition, options:
119
137
  prepared.push({ split, splitIndex, normalized: redacted, recordHash });
120
138
  }
121
139
 
140
+ // A rejected line is reported (validationErrors above) but never persisted — no
141
+ // second pass, no checkpoint advance, no partial rows from this record.
142
+ if (lineRejected) continue;
143
+
122
144
  // Second pass: resolve _messageSplitIndex → message_hash, then insert.
123
145
  const recordHashBySplitIndex = new Map(prepared.map((e) => [e.splitIndex, e.recordHash] as const));
124
146
  for (const entry of prepared) {
@@ -167,6 +189,20 @@ export async function runJsonlImport(source: string | SourceDefinition, options:
167
189
  }
168
190
  }
169
191
 
192
+ // Full-mode reconciliation (R1, task 0504): diff the desired hash set against the
193
+ // persisted ledger and retire stale derived rows (target, tool, ledger, checkpoint) in
194
+ // one source-scoped batch. Dry-run computes the identical counts without mutation.
195
+ let reconciliation: ReconcileSummary | undefined;
196
+ if (mode === 'full') {
197
+ reconciliation = await reconcileFullImport(
198
+ options.db,
199
+ resolvedSource,
200
+ desiredHashes,
201
+ files,
202
+ options.dryRun ?? false,
203
+ );
204
+ }
205
+
170
206
  return {
171
207
  source: resolvedSource,
172
208
  mode,
@@ -178,6 +214,7 @@ export async function runJsonlImport(source: string | SourceDefinition, options:
178
214
  parseErrors,
179
215
  validationErrors,
180
216
  checkpointUpdates,
217
+ reconciliation,
181
218
  };
182
219
  }
183
220
 
package/src/index.ts CHANGED
@@ -20,6 +20,7 @@ export type {
20
20
  ImportResult,
21
21
  JsonObject,
22
22
  LlmJsonlSource,
23
+ ReconcileSummary,
23
24
  RedactionRule,
24
25
  SourceDefinition,
25
26
  SplitConfig,
@@ -2,7 +2,7 @@ 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
4
  import { VALID_TABLE_NAME } from './sources';
5
- import type { ImportOptions, JsonObject, SourceDefinition } from './types';
5
+ import type { ImportOptions, JsonObject, ReconcileSummary, SourceDefinition } from './types';
6
6
 
7
7
  interface CheckpointRow {
8
8
  readonly last_imported_line: number;
@@ -369,6 +369,71 @@ function resolveWithFallback(
369
369
  }
370
370
  }
371
371
 
372
+ /**
373
+ * Reconcile a source's persisted derived rows against the desired set produced by a full
374
+ * import (task 0504 R1). Rows whose `record_hash` is no longer reproduced by current source
375
+ * data or mapper output are stale: their target rows (typed contract tables — including tool
376
+ * calls — and ETL tables), ledger rows, and checkpoints for vanished source files are removed
377
+ * in ONE source-scoped batch (atomic). Dry-run returns the exact same counts without mutating
378
+ * the database, so `--dry-run` is a safe preview and a second full write reports zero changes.
379
+ *
380
+ * Deletes are keyed by `record_hash` (PK) and checkpoint `(source, source_file)` (PK) — never
381
+ * by an unindexed ledger predicate — so reconciliation cost scales with the stale count, not
382
+ * with ledger size.
383
+ */
384
+ async function reconcileFullImport(
385
+ db: ImportOptions['db'],
386
+ source: string,
387
+ desiredHashes: ReadonlySet<string>,
388
+ discoveredFiles: readonly string[],
389
+ dryRun: boolean,
390
+ ): Promise<ReconcileSummary> {
391
+ const ledgerRows = await db.queryAll<{ record_hash: string; target_table: string }>(
392
+ 'SELECT record_hash, target_table FROM history_import_ledger WHERE source = ?',
393
+ source,
394
+ );
395
+ const stale: { record_hash: string; target_table: string }[] = [];
396
+ for (const row of ledgerRows) {
397
+ if (!desiredHashes.has(row.record_hash)) stale.push(row);
398
+ }
399
+
400
+ const checkpointRows = await db.queryAll<{ source_file: string }>(
401
+ 'SELECT source_file FROM history_import_checkpoint WHERE source = ?',
402
+ source,
403
+ );
404
+ const discovered = new Set(discoveredFiles);
405
+ const staleCheckpoints = checkpointRows
406
+ .map((row) => row.source_file)
407
+ .filter((sourceFile) => !discovered.has(sourceFile));
408
+
409
+ const summary: ReconcileSummary = {
410
+ staleTargetRows: stale.length,
411
+ staleLedgerRows: stale.length,
412
+ staleCheckpointRows: staleCheckpoints.length,
413
+ };
414
+ if (dryRun || (stale.length === 0 && staleCheckpoints.length === 0)) return summary;
415
+
416
+ const operations: DbBatchOp[] = [];
417
+ for (const row of stale) {
418
+ operations.push({
419
+ sql: `DELETE FROM ${targetTableFor(row.target_table)} WHERE record_hash = ?`,
420
+ params: [row.record_hash],
421
+ });
422
+ operations.push({
423
+ sql: 'DELETE FROM history_import_ledger WHERE record_hash = ?',
424
+ params: [row.record_hash],
425
+ });
426
+ }
427
+ for (const sourceFile of staleCheckpoints) {
428
+ operations.push({
429
+ sql: 'DELETE FROM history_import_checkpoint WHERE source = ? AND source_file = ?',
430
+ params: [source, sourceFile],
431
+ });
432
+ }
433
+ await db.batch(operations);
434
+ return summary;
435
+ }
436
+
372
437
  const OPENCODE_SOURCE = 'opencode';
373
438
 
374
439
  /** OpenCode message row projected from its SQLite store. */
@@ -538,6 +603,7 @@ export {
538
603
  insertRecord,
539
604
  ledgerExists,
540
605
  readCheckpoint,
606
+ reconcileFullImport,
541
607
  resetCheckpoints,
542
608
  targetTableFor,
543
609
  timestamp,
@@ -19,7 +19,7 @@ import {
19
19
  readOpenCodeParts,
20
20
  } from './jsonl-importer-dao';
21
21
  import { redactRecord } from './redaction';
22
- import type { ImportIssue, ImportMode, ImportResult, JsonObject, RedactionRule } from './types';
22
+ import type { ImportIssue, ImportMode, ImportResult, JsonObject, ReconcileSummary, RedactionRule } from './types';
23
23
 
24
24
  const SOURCE = 'opencode';
25
25
  const PAGE_SIZE = 250;
@@ -63,7 +63,20 @@ export async function runOpenCodeImport(options: OpenCodeImportOptions): Promise
63
63
  let lastTime = -1;
64
64
  let lastId = '';
65
65
  const existingBySourceFile = await readOpenCodeExistingEntries(options.db);
66
+ // Source files still present in the OpenCode store this pass (R1, task 0504). Files with
67
+ // no valid role produce no entries and are deliberately NOT marked seen, so their old
68
+ // derived rows are swept as no-longer-reproduced in full mode.
69
+ const seenSourceFiles = new Set<string>();
66
70
  const operations: DbBatchOp[] = [];
71
+ // Full-mode reconciliation counters (R1, task 0504): stale rows from BOTH sources —
72
+ // per-file hash-diff deletes (changed messages) and the vanished-file sweep below. Dry-run
73
+ // counts the identical totals the write would delete, so `--dry-run` is an exact preview.
74
+ // Declared here (outside `try`) because the result is returned after `finally` closes the
75
+ // source database.
76
+ let staleTargetRows = 0;
77
+ let staleLedgerRows = 0;
78
+ let staleCheckpointRows = 0;
79
+ let reconciliation: ReconcileSummary | undefined;
67
80
 
68
81
  try {
69
82
  while (true) {
@@ -91,6 +104,7 @@ export async function runOpenCodeImport(options: OpenCodeImportOptions): Promise
91
104
  validationErrors.push({ sourceFile, sourceLine: 1, reason: 'OpenCode message has no valid role' });
92
105
  continue;
93
106
  }
107
+ seenSourceFiles.add(sourceFile);
94
108
 
95
109
  const existing = existingBySourceFile.get(sourceFile) ?? [];
96
110
  if (sameHashes(existing, entries)) {
@@ -98,6 +112,12 @@ export async function runOpenCodeImport(options: OpenCodeImportOptions): Promise
98
112
  continue;
99
113
  }
100
114
 
115
+ if (mode === 'full') {
116
+ // Per-file mapper drift: the old derived rows for this source file are
117
+ // stale and are deleted before the new entries are written.
118
+ staleTargetRows += existing.length;
119
+ staleLedgerRows += existing.length;
120
+ }
101
121
  if (!options.dryRun) {
102
122
  operations.push(...openCodeDeleteOperations(existing));
103
123
  for (const entry of entries) {
@@ -122,6 +142,25 @@ export async function runOpenCodeImport(options: OpenCodeImportOptions): Promise
122
142
  lastTime = tail.time_created;
123
143
  lastId = tail.id;
124
144
  }
145
+ // Full-mode reconciliation (R1, task 0504): messages deleted from the OpenCode store
146
+ // leave stale derived rows behind. Sweep ledger/target rows and checkpoints for source
147
+ // files never seen this pass, merged into the SAME source-scoped batch as the writes.
148
+ if (mode === 'full') {
149
+ for (const [sourceFile, entries] of existingBySourceFile) {
150
+ if (seenSourceFiles.has(sourceFile)) continue;
151
+ staleLedgerRows += entries.length;
152
+ staleTargetRows += entries.length;
153
+ staleCheckpointRows += 1;
154
+ if (!options.dryRun) {
155
+ operations.push(...openCodeDeleteOperations(entries));
156
+ operations.push({
157
+ sql: 'DELETE FROM history_import_checkpoint WHERE source = ? AND source_file = ?',
158
+ params: [SOURCE, sourceFile],
159
+ });
160
+ }
161
+ }
162
+ reconciliation = { staleTargetRows, staleLedgerRows, staleCheckpointRows };
163
+ }
125
164
  if (operations.length > 0) await options.db.batch(operations);
126
165
  } finally {
127
166
  sourceDb.close();
@@ -138,6 +177,7 @@ export async function runOpenCodeImport(options: OpenCodeImportOptions): Promise
138
177
  parseErrors,
139
178
  validationErrors,
140
179
  checkpointUpdates,
180
+ reconciliation,
141
181
  };
142
182
  }
143
183
 
package/src/types.ts CHANGED
@@ -101,6 +101,21 @@ export interface ImportIssue {
101
101
  readonly reason: string;
102
102
  }
103
103
 
104
+ /**
105
+ * Source-scoped full-mode reconciliation outcome (task 0504 R1). Computed by diffing the
106
+ * current source's desired record hashes against the persisted ledger; rows no longer
107
+ * reproduced by current source data or mapper output are stale and are removed in one
108
+ * source-scoped batch (dry-run reports the same counts without mutating the database).
109
+ */
110
+ export interface ReconcileSummary {
111
+ /** Rows deleted from typed/ETL target tables (history_message / history_tool_call / history_etl_*). */
112
+ readonly staleTargetRows: number;
113
+ /** Rows deleted from `history_import_ledger`. */
114
+ readonly staleLedgerRows: number;
115
+ /** Checkpoint rows deleted for source files no longer discovered. */
116
+ readonly staleCheckpointRows: number;
117
+ }
118
+
104
119
  /** Summary produced by the importer control function. */
105
120
  export interface ImportResult {
106
121
  /** Source identifier — the built-in name or custom definition name that produced this result. */
@@ -114,4 +129,9 @@ export interface ImportResult {
114
129
  readonly parseErrors: readonly ImportIssue[];
115
130
  readonly validationErrors: readonly ImportIssue[];
116
131
  readonly checkpointUpdates: number;
132
+ /**
133
+ * Full-mode reconciliation counts (only set when `mode === 'full'`). A second full run
134
+ * reports zero stale rows once the database matches the current source (task 0504 R1).
135
+ */
136
+ readonly reconciliation?: ReconcileSummary;
117
137
  }