@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.
- package/dist/importer.d.ts.map +1 -1
- package/dist/importer.js +22 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/jsonl-importer-dao.d.ts +15 -2
- package/dist/jsonl-importer-dao.d.ts.map +1 -1
- package/dist/jsonl-importer-dao.js +52 -1
- package/dist/opencode-importer.d.ts.map +1 -1
- package/dist/opencode-importer.js +41 -0
- package/dist/types.d.ts +19 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/importer.ts +38 -1
- package/src/index.ts +1 -0
- package/src/jsonl-importer-dao.ts +67 -1
- package/src/opencode-importer.ts +41 -1
- package/src/types.ts +20 -0
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":"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
|
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,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":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAI7D,OAAO,KAAK,EAAE,aAAa,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;
|
|
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,
|
|
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
|
package/dist/types.d.ts.map
CHANGED
|
@@ -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.
|
|
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.
|
|
51
|
-
"@gobing-ai/ts-runtime": "^0.4.
|
|
52
|
-
"@gobing-ai/ts-utils": "^0.4.
|
|
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 {
|
|
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
|
@@ -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,
|
package/src/opencode-importer.ts
CHANGED
|
@@ -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
|
}
|