@gobing-ai/ts-llm-jsonl-importer 0.4.24 → 0.4.26
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/mappers.d.ts +3 -1
- package/dist/mappers.d.ts.map +1 -1
- package/dist/mappers.js +67 -36
- 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/mappers.ts +65 -37
- 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, };
|
package/dist/mappers.d.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import type { JsonObject, SplitEntry, TransformContext } from './types';
|
|
3
|
+
/** Compute args_digest: sha256 of stable-jsonified, key-sorted, redacted args. */
|
|
4
|
+
export declare function argsDigest(args: unknown): string;
|
|
3
5
|
/** Map a Claude Code JSONL record into one or more ETL split entries. */
|
|
4
6
|
export declare function claudeSplit(raw: Record<string, unknown>): readonly SplitEntry[];
|
|
5
7
|
/** Map a Pi JSONL record into one or more ETL split entries. */
|
|
6
8
|
export declare function piSplit(raw: unknown): readonly SplitEntry[];
|
|
7
9
|
/** Map an OMP JSONL record into one or more ETL split entries. */
|
|
8
|
-
export declare function ompSplit(raw: Record<string, unknown
|
|
10
|
+
export declare function ompSplit(raw: Record<string, unknown>, context?: TransformContext): readonly SplitEntry[];
|
|
9
11
|
/** Map a Codex JSONL record into one or more ETL split entries. */
|
|
10
12
|
export declare function codexSplit(raw: Record<string, unknown>): readonly SplitEntry[];
|
|
11
13
|
/** Map an Antigravity (AGY) JSONL record into one or more ETL split entries. */
|
package/dist/mappers.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mappers.d.ts","sourceRoot":"","sources":["../src/mappers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,KAAK,EAAE,UAAU,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"mappers.d.ts","sourceRoot":"","sources":["../src/mappers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,KAAK,EAAE,UAAU,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AA+DxE,kFAAkF;AAClF,wBAAgB,UAAU,CAAC,IAAI,EAAE,OAAO,GAAG,MAAM,CAGhD;AAwBD,yEAAyE;AACzE,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,UAAU,EAAE,CA4G/E;AAMD,gEAAgE;AAChE,wBAAgB,OAAO,CAAC,GAAG,EAAE,OAAO,GAAG,SAAS,UAAU,EAAE,CAsE3D;AAMD,kEAAkE;AAClE,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO,CAAC,EAAE,gBAAgB,GAAG,SAAS,UAAU,EAAE,CA0GxG;AAoBD,mEAAmE;AACnE,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,UAAU,EAAE,CAiG9E;AAMD,gFAAgF;AAChF,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,UAAU,EAAE,CAgH5E;AAMD,gGAAgG;AAChG,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO,CAAC,EAAE,gBAAgB,GAAG,SAAS,UAAU,EAAE,CAgF3G;AAyMD,kEAAkE;AAClE,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,UAAU,EAAE,CAyS7E;AAwDD,wGAAwG;AACxG,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,UAAU,GAAG,MAAM,CAKxD;AAMD,gDAAgD;AAChD,eAAO,MAAM,gBAAgB,wBAAsE,CAAC;AACpG,4CAA4C;AAC5C,eAAO,MAAM,YAAY,wBAAsE,CAAC;AAChG,6CAA6C;AAC7C,eAAO,MAAM,aAAa,wBAAsE,CAAC;AACjG,+CAA+C;AAC/C,eAAO,MAAM,eAAe,wBAAsE,CAAC;AACnG,6CAA6C;AAC7C,eAAO,MAAM,aAAa,wBAAsE,CAAC;AACjG,8CAA8C;AAC9C,eAAO,MAAM,cAAc,wBAAsE,CAAC;AAClG,gDAAgD;AAChD,eAAO,MAAM,gBAAgB,wBAAsE,CAAC;AAQpG,oDAAoD;AACpD,eAAO,MAAM,aAAa,gCAAoB,CAAC;AAC/C,gDAAgD;AAChD,eAAO,MAAM,SAAS,gCAAoB,CAAC;AAC3C,iDAAiD;AACjD,eAAO,MAAM,UAAU,gCAAoB,CAAC;AAC5C,mDAAmD;AACnD,eAAO,MAAM,YAAY,gCAAoB,CAAC;AAC9C,iDAAiD;AACjD,eAAO,MAAM,UAAU,gCAAoB,CAAC;AAC5C,kDAAkD;AAClD,eAAO,MAAM,WAAW,gCAAoB,CAAC;AAC7C,oDAAoD;AACpD,eAAO,MAAM,aAAa,gCAAoB,CAAC"}
|
package/dist/mappers.js
CHANGED
|
@@ -57,7 +57,7 @@ function detectProvenance(cwd) {
|
|
|
57
57
|
return cwd.includes('/spur') || cwd.includes('/spur-new') ? 'spur-run' : 'ambient';
|
|
58
58
|
}
|
|
59
59
|
/** Compute args_digest: sha256 of stable-jsonified, key-sorted, redacted args. */
|
|
60
|
-
function argsDigest(args) {
|
|
60
|
+
export function argsDigest(args) {
|
|
61
61
|
const redacted = redactArgs(args);
|
|
62
62
|
return sha256(redacted);
|
|
63
63
|
}
|
|
@@ -262,44 +262,59 @@ export function piSplit(raw) {
|
|
|
262
262
|
// OMP mapper (near-identical to pi)
|
|
263
263
|
// ---------------------------------------------------------------------------
|
|
264
264
|
/** Map an OMP JSONL record into one or more ETL split entries. */
|
|
265
|
-
export function ompSplit(raw) {
|
|
265
|
+
export function ompSplit(raw, context) {
|
|
266
|
+
// Current OMP files are top-level event envelopes: `{type: "message", id, timestamp, parentId,
|
|
267
|
+
// message: {role, content, model, usage, cost, duration, ...}}`. The unique top-level `id` is
|
|
268
|
+
// an event id, never a session id; the session key and sequence come from the source file
|
|
269
|
+
// (context) when available. The legacy direct shape keeps role/type/content at the top level.
|
|
270
|
+
const msg = raw.message;
|
|
266
271
|
const recordType = String(raw.type ?? '');
|
|
272
|
+
const sessionId = sessionIdFromContext(context, raw);
|
|
273
|
+
const seq = context?.sourceLine ?? (typeof raw.seq === 'number' ? raw.seq : 0);
|
|
274
|
+
const ts = s(raw.ts, raw.timestamp, raw.createdAt) ?? new Date(0).toISOString();
|
|
275
|
+
// Meta lifecycle/custom records — including the current `custom.*` events and non-message
|
|
276
|
+
// lifecycle types — collapse to one meta row keyed by the source session, never the unique
|
|
277
|
+
// event id, and never a guessed role.
|
|
267
278
|
if (recordType === 'title' ||
|
|
268
279
|
recordType === 'title_change' ||
|
|
269
280
|
recordType === 'service_tier_change' ||
|
|
270
281
|
recordType === 'ttsr_injection' ||
|
|
271
282
|
recordType === 'session_init' ||
|
|
272
|
-
recordType === '
|
|
283
|
+
recordType === 'session' ||
|
|
284
|
+
recordType === 'model_change' ||
|
|
285
|
+
recordType === 'thinking_level_change' ||
|
|
286
|
+
recordType === 'compaction' ||
|
|
287
|
+
recordType === 'custom' ||
|
|
288
|
+
recordType === 'custom_message' ||
|
|
289
|
+
recordType.startsWith('custom.')) {
|
|
273
290
|
return [
|
|
274
291
|
{
|
|
275
292
|
targetTable: 'history_message',
|
|
276
293
|
record: {
|
|
277
|
-
session_id:
|
|
278
|
-
seq
|
|
294
|
+
session_id: sessionId,
|
|
295
|
+
seq,
|
|
279
296
|
role: 'meta',
|
|
280
297
|
record_type: recordType,
|
|
281
298
|
disposition: 'meta',
|
|
282
|
-
ts
|
|
299
|
+
ts,
|
|
283
300
|
provenance: 'ambient',
|
|
284
301
|
},
|
|
285
302
|
},
|
|
286
303
|
];
|
|
287
304
|
}
|
|
288
305
|
const entries = [];
|
|
289
|
-
const
|
|
290
|
-
const
|
|
291
|
-
const ts = s(raw.ts, raw.timestamp, raw.createdAt) ?? new Date(0).toISOString();
|
|
292
|
-
const role = mapRole(raw.type ?? raw.role);
|
|
293
|
-
const model = s(raw.model, o(raw.message).model);
|
|
306
|
+
const role = mapRole(msg?.role ?? raw.type ?? raw.role);
|
|
307
|
+
const model = s(raw.model, o(msg).model);
|
|
294
308
|
const cwd = s(raw.cwd, raw.dir);
|
|
295
|
-
const
|
|
296
|
-
const usage = msg?.usage;
|
|
309
|
+
const usage = (msg?.usage ?? raw.usage);
|
|
297
310
|
const inputTokens = (usage?.input ?? usage?.input_tokens ?? undefined);
|
|
298
311
|
const outputTokens = (usage?.output ?? usage?.output_tokens ?? undefined);
|
|
299
312
|
const cacheRead = (usage?.cacheRead ?? usage?.cache_read_tokens ?? undefined);
|
|
300
313
|
const cacheWrite = (usage?.cacheWrite ?? usage?.cache_write_tokens ?? undefined);
|
|
301
314
|
const costObj = (msg?.cost ?? raw.cost);
|
|
302
315
|
const costUsd = typeof costObj?.total === 'number' ? costObj.total : computeCost(inputTokens, outputTokens, model);
|
|
316
|
+
const contentBlocks = msg?.content ?? raw.content;
|
|
317
|
+
const durationMs = typeof msg?.duration === 'number' && Number.isFinite(msg.duration) ? msg.duration : undefined;
|
|
303
318
|
const messageSplitIndex = entries.length;
|
|
304
319
|
entries.push({
|
|
305
320
|
targetTable: 'history_message',
|
|
@@ -310,43 +325,59 @@ export function ompSplit(raw) {
|
|
|
310
325
|
record_type: recordType,
|
|
311
326
|
disposition: 'keep',
|
|
312
327
|
ts,
|
|
313
|
-
duration_ms:
|
|
328
|
+
duration_ms: durationMs,
|
|
314
329
|
model: model ?? null,
|
|
315
330
|
input_tokens: inputTokens ?? null,
|
|
316
331
|
output_tokens: outputTokens ?? null,
|
|
317
332
|
cache_read_tokens: cacheRead ?? null,
|
|
318
333
|
cache_write_tokens: cacheWrite ?? null,
|
|
319
334
|
cost_usd: costUsd ?? null,
|
|
320
|
-
content_text: s(raw.content, raw.text, msg?.content) ?? null,
|
|
335
|
+
content_text: extractContentText(contentBlocks) ?? s(raw.content, raw.text, msg?.content) ?? null,
|
|
321
336
|
cwd: cwd ?? null,
|
|
322
337
|
provenance: detectProvenance(cwd),
|
|
323
338
|
},
|
|
324
339
|
});
|
|
325
|
-
if (Array.isArray(
|
|
326
|
-
for (const block of
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
}
|
|
345
|
-
}
|
|
340
|
+
if (Array.isArray(contentBlocks) && role === 'assistant') {
|
|
341
|
+
for (const block of contentBlocks) {
|
|
342
|
+
const call = normalizeOmpToolCall(block);
|
|
343
|
+
if (call === null)
|
|
344
|
+
continue;
|
|
345
|
+
entries.push({
|
|
346
|
+
targetTable: 'history_tool_call',
|
|
347
|
+
record: {
|
|
348
|
+
_messageSplitIndex: messageSplitIndex,
|
|
349
|
+
session_id: sessionId,
|
|
350
|
+
seq,
|
|
351
|
+
tool_name: String(call.name ?? ''),
|
|
352
|
+
args_digest: argsDigest(call.input ?? call.arguments),
|
|
353
|
+
status: 'ok',
|
|
354
|
+
started_at: undefined,
|
|
355
|
+
completed_at: undefined,
|
|
356
|
+
duration_ms: undefined,
|
|
357
|
+
result_bytes: undefined,
|
|
358
|
+
error_text: undefined,
|
|
359
|
+
},
|
|
360
|
+
});
|
|
346
361
|
}
|
|
347
362
|
}
|
|
348
363
|
return entries;
|
|
349
364
|
}
|
|
365
|
+
/**
|
|
366
|
+
* Normalize an OMP assistant content block to a tool-call object, or null when the block is not a
|
|
367
|
+
* call. Supports both the legacy nested `{toolCall: {...}}` block and the current flat
|
|
368
|
+
* `{type: "toolCall", id, name, arguments}` block. Exactly one history_tool_call row is emitted
|
|
369
|
+
* per call block.
|
|
370
|
+
*/
|
|
371
|
+
function normalizeOmpToolCall(block) {
|
|
372
|
+
if (typeof block !== 'object' || block === null)
|
|
373
|
+
return null;
|
|
374
|
+
const nested = o(block.toolCall);
|
|
375
|
+
if (Object.keys(nested).length > 0)
|
|
376
|
+
return nested;
|
|
377
|
+
if (block.type === 'toolCall')
|
|
378
|
+
return block;
|
|
379
|
+
return null;
|
|
380
|
+
}
|
|
350
381
|
// ---------------------------------------------------------------------------
|
|
351
382
|
// Codex mapper
|
|
352
383
|
// ---------------------------------------------------------------------------
|
|
@@ -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.26",
|
|
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.26",
|
|
51
|
+
"@gobing-ai/ts-runtime": "^0.4.26",
|
|
52
|
+
"@gobing-ai/ts-utils": "^0.4.26",
|
|
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/mappers.ts
CHANGED
|
@@ -64,7 +64,7 @@ function detectProvenance(cwd?: string): 'ambient' | 'spur-run' {
|
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
/** Compute args_digest: sha256 of stable-jsonified, key-sorted, redacted args. */
|
|
67
|
-
function argsDigest(args: unknown): string {
|
|
67
|
+
export function argsDigest(args: unknown): string {
|
|
68
68
|
const redacted = redactArgs(args);
|
|
69
69
|
return sha256(redacted);
|
|
70
70
|
}
|
|
@@ -284,26 +284,44 @@ export function piSplit(raw: unknown): readonly SplitEntry[] {
|
|
|
284
284
|
// ---------------------------------------------------------------------------
|
|
285
285
|
|
|
286
286
|
/** Map an OMP JSONL record into one or more ETL split entries. */
|
|
287
|
-
export function ompSplit(raw: Record<string, unknown
|
|
287
|
+
export function ompSplit(raw: Record<string, unknown>, context?: TransformContext): readonly SplitEntry[] {
|
|
288
|
+
// Current OMP files are top-level event envelopes: `{type: "message", id, timestamp, parentId,
|
|
289
|
+
// message: {role, content, model, usage, cost, duration, ...}}`. The unique top-level `id` is
|
|
290
|
+
// an event id, never a session id; the session key and sequence come from the source file
|
|
291
|
+
// (context) when available. The legacy direct shape keeps role/type/content at the top level.
|
|
292
|
+
const msg = raw.message as Record<string, unknown> | undefined;
|
|
288
293
|
const recordType = String(raw.type ?? '');
|
|
294
|
+
const sessionId = sessionIdFromContext(context, raw);
|
|
295
|
+
const seq = context?.sourceLine ?? (typeof raw.seq === 'number' ? raw.seq : 0);
|
|
296
|
+
const ts = s(raw.ts, raw.timestamp, raw.createdAt) ?? new Date(0).toISOString();
|
|
297
|
+
|
|
298
|
+
// Meta lifecycle/custom records — including the current `custom.*` events and non-message
|
|
299
|
+
// lifecycle types — collapse to one meta row keyed by the source session, never the unique
|
|
300
|
+
// event id, and never a guessed role.
|
|
289
301
|
if (
|
|
290
302
|
recordType === 'title' ||
|
|
291
303
|
recordType === 'title_change' ||
|
|
292
304
|
recordType === 'service_tier_change' ||
|
|
293
305
|
recordType === 'ttsr_injection' ||
|
|
294
306
|
recordType === 'session_init' ||
|
|
295
|
-
recordType === '
|
|
307
|
+
recordType === 'session' ||
|
|
308
|
+
recordType === 'model_change' ||
|
|
309
|
+
recordType === 'thinking_level_change' ||
|
|
310
|
+
recordType === 'compaction' ||
|
|
311
|
+
recordType === 'custom' ||
|
|
312
|
+
recordType === 'custom_message' ||
|
|
313
|
+
recordType.startsWith('custom.')
|
|
296
314
|
) {
|
|
297
315
|
return [
|
|
298
316
|
{
|
|
299
317
|
targetTable: 'history_message',
|
|
300
318
|
record: {
|
|
301
|
-
session_id:
|
|
302
|
-
seq
|
|
319
|
+
session_id: sessionId,
|
|
320
|
+
seq,
|
|
303
321
|
role: 'meta',
|
|
304
322
|
record_type: recordType,
|
|
305
323
|
disposition: 'meta',
|
|
306
|
-
ts
|
|
324
|
+
ts,
|
|
307
325
|
provenance: 'ambient',
|
|
308
326
|
},
|
|
309
327
|
},
|
|
@@ -311,21 +329,18 @@ export function ompSplit(raw: Record<string, unknown>): readonly SplitEntry[] {
|
|
|
311
329
|
}
|
|
312
330
|
|
|
313
331
|
const entries: SplitEntry[] = [];
|
|
314
|
-
const
|
|
315
|
-
const
|
|
316
|
-
const ts = s(raw.ts, raw.timestamp, raw.createdAt) ?? new Date(0).toISOString();
|
|
317
|
-
const role = mapRole(raw.type ?? raw.role);
|
|
318
|
-
const model = s(raw.model, o(raw.message).model);
|
|
332
|
+
const role = mapRole(msg?.role ?? raw.type ?? raw.role);
|
|
333
|
+
const model = s(raw.model, o(msg).model);
|
|
319
334
|
const cwd = s(raw.cwd, raw.dir);
|
|
320
|
-
|
|
321
|
-
const msg = raw.message as Record<string, unknown> | undefined;
|
|
322
|
-
const usage = msg?.usage as Record<string, unknown> | undefined;
|
|
335
|
+
const usage = (msg?.usage ?? raw.usage) as Record<string, unknown> | undefined;
|
|
323
336
|
const inputTokens = (usage?.input ?? usage?.input_tokens ?? undefined) as number | undefined;
|
|
324
337
|
const outputTokens = (usage?.output ?? usage?.output_tokens ?? undefined) as number | undefined;
|
|
325
338
|
const cacheRead = (usage?.cacheRead ?? usage?.cache_read_tokens ?? undefined) as number | undefined;
|
|
326
339
|
const cacheWrite = (usage?.cacheWrite ?? usage?.cache_write_tokens ?? undefined) as number | undefined;
|
|
327
340
|
const costObj = (msg?.cost ?? raw.cost) as Record<string, unknown> | undefined;
|
|
328
341
|
const costUsd = typeof costObj?.total === 'number' ? costObj.total : computeCost(inputTokens, outputTokens, model);
|
|
342
|
+
const contentBlocks = msg?.content ?? raw.content;
|
|
343
|
+
const durationMs = typeof msg?.duration === 'number' && Number.isFinite(msg.duration) ? msg.duration : undefined;
|
|
329
344
|
|
|
330
345
|
const messageSplitIndex = entries.length;
|
|
331
346
|
|
|
@@ -338,46 +353,59 @@ export function ompSplit(raw: Record<string, unknown>): readonly SplitEntry[] {
|
|
|
338
353
|
record_type: recordType,
|
|
339
354
|
disposition: 'keep',
|
|
340
355
|
ts,
|
|
341
|
-
duration_ms:
|
|
356
|
+
duration_ms: durationMs,
|
|
342
357
|
model: model ?? null,
|
|
343
358
|
input_tokens: inputTokens ?? null,
|
|
344
359
|
output_tokens: outputTokens ?? null,
|
|
345
360
|
cache_read_tokens: cacheRead ?? null,
|
|
346
361
|
cache_write_tokens: cacheWrite ?? null,
|
|
347
362
|
cost_usd: costUsd ?? null,
|
|
348
|
-
content_text: s(raw.content, raw.text, msg?.content) ?? null,
|
|
363
|
+
content_text: extractContentText(contentBlocks) ?? s(raw.content, raw.text, msg?.content) ?? null,
|
|
349
364
|
cwd: cwd ?? null,
|
|
350
365
|
provenance: detectProvenance(cwd),
|
|
351
366
|
},
|
|
352
367
|
});
|
|
353
368
|
|
|
354
|
-
if (Array.isArray(
|
|
355
|
-
for (const block of
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
}
|
|
369
|
+
if (Array.isArray(contentBlocks) && role === 'assistant') {
|
|
370
|
+
for (const block of contentBlocks as Record<string, unknown>[]) {
|
|
371
|
+
const call = normalizeOmpToolCall(block);
|
|
372
|
+
if (call === null) continue;
|
|
373
|
+
entries.push({
|
|
374
|
+
targetTable: 'history_tool_call',
|
|
375
|
+
record: {
|
|
376
|
+
_messageSplitIndex: messageSplitIndex,
|
|
377
|
+
session_id: sessionId,
|
|
378
|
+
seq,
|
|
379
|
+
tool_name: String(call.name ?? ''),
|
|
380
|
+
args_digest: argsDigest(call.input ?? call.arguments),
|
|
381
|
+
status: 'ok',
|
|
382
|
+
started_at: undefined,
|
|
383
|
+
completed_at: undefined,
|
|
384
|
+
duration_ms: undefined,
|
|
385
|
+
result_bytes: undefined,
|
|
386
|
+
error_text: undefined,
|
|
387
|
+
},
|
|
388
|
+
});
|
|
375
389
|
}
|
|
376
390
|
}
|
|
377
391
|
|
|
378
392
|
return entries;
|
|
379
393
|
}
|
|
380
394
|
|
|
395
|
+
/**
|
|
396
|
+
* Normalize an OMP assistant content block to a tool-call object, or null when the block is not a
|
|
397
|
+
* call. Supports both the legacy nested `{toolCall: {...}}` block and the current flat
|
|
398
|
+
* `{type: "toolCall", id, name, arguments}` block. Exactly one history_tool_call row is emitted
|
|
399
|
+
* per call block.
|
|
400
|
+
*/
|
|
401
|
+
function normalizeOmpToolCall(block: Record<string, unknown>): Record<string, unknown> | null {
|
|
402
|
+
if (typeof block !== 'object' || block === null) return null;
|
|
403
|
+
const nested = o(block.toolCall);
|
|
404
|
+
if (Object.keys(nested).length > 0) return nested;
|
|
405
|
+
if (block.type === 'toolCall') return block;
|
|
406
|
+
return null;
|
|
407
|
+
}
|
|
408
|
+
|
|
381
409
|
// ---------------------------------------------------------------------------
|
|
382
410
|
// Codex mapper
|
|
383
411
|
// ---------------------------------------------------------------------------
|
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
|
}
|