@gsengai/core 0.1.0

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.
@@ -0,0 +1,629 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { existsSync } from "fs";
5
+ import { parseArgs } from "util";
6
+
7
+ // src/store.ts
8
+ import { randomUUID } from "crypto";
9
+ import { once } from "events";
10
+ import { createWriteStream, writeFileSync } from "fs";
11
+ import Database from "better-sqlite3";
12
+
13
+ // src/hash.ts
14
+ import { createHash } from "crypto";
15
+ var HASH_VERSION = 1;
16
+ function sha256Hex(data) {
17
+ const hash = createHash("sha256");
18
+ if (typeof data === "string") {
19
+ hash.update(data, "utf8");
20
+ } else {
21
+ hash.update(data);
22
+ }
23
+ return hash.digest("hex");
24
+ }
25
+ function normalizeText(raw) {
26
+ return raw.normalize("NFC").toLowerCase().replace(/\s+/gu, " ").trim();
27
+ }
28
+ function hashText(raw) {
29
+ return {
30
+ outputHash: sha256Hex(raw),
31
+ outputHashNormalized: sha256Hex(normalizeText(raw)),
32
+ hashVersion: HASH_VERSION
33
+ };
34
+ }
35
+
36
+ // src/canonical-json.ts
37
+ function canonicalJson(value) {
38
+ const out = JSON.stringify(value, (_key, val) => {
39
+ if (val !== null && typeof val === "object" && !Array.isArray(val)) {
40
+ const record = val;
41
+ const sorted = {};
42
+ for (const key of Object.keys(record).sort()) {
43
+ sorted[key] = record[key];
44
+ }
45
+ return sorted;
46
+ }
47
+ return val;
48
+ });
49
+ if (out === void 0) {
50
+ throw new TypeError("canonicalJson: value does not serialize to JSON");
51
+ }
52
+ return out;
53
+ }
54
+
55
+ // src/errors.ts
56
+ var NotImplementedError = class extends Error {
57
+ constructor(message) {
58
+ super(message);
59
+ this.name = "NotImplementedError";
60
+ }
61
+ };
62
+
63
+ // src/export.ts
64
+ var CSV_COLUMNS = [
65
+ "id",
66
+ "ts",
67
+ "modality",
68
+ "model",
69
+ "system_id",
70
+ "prompt_hash",
71
+ "output_hash",
72
+ "output_hash_normalized",
73
+ "hash_version",
74
+ "marking_methods",
75
+ "manifest_ref",
76
+ "disclosure_context",
77
+ "prev_hash",
78
+ "record_hash"
79
+ ];
80
+ function csvField(value) {
81
+ if (value === null) {
82
+ return "";
83
+ }
84
+ const s = String(value);
85
+ return /[",\r\n]/.test(s) ? `"${s.replaceAll('"', '""')}"` : s;
86
+ }
87
+ function recordToCsvRow(record) {
88
+ return CSV_COLUMNS.map(
89
+ (column) => csvField(
90
+ column === "marking_methods" ? JSON.stringify(record.marking_methods) : record[column]
91
+ )
92
+ ).join(",");
93
+ }
94
+ var PRD_S9_LIMITS = `> **What this does NOT do**
95
+ >
96
+ > - It does not make you compliant. It supports compliance with EU AI Act Article 50 and California SB 942; compliance depends on your system, your deployment context, and your processes.
97
+ > - It does not embed imperceptible watermarks (MVP). It implements the signed-metadata and logging/fingerprinting layers of a multi-layer marking strategy.
98
+ > - It cannot prevent downstream metadata stripping \u2014 manifests can be removed by re-encoding, screenshots, or platform uploads. That is exactly why the logging layer exists.
99
+ > - It does not detect third-party AI content.
100
+ > - It is not legal advice. Consult qualified counsel about your obligations.`;
101
+ function cell(value) {
102
+ if (value === null || value === "") {
103
+ return "";
104
+ }
105
+ return value.replaceAll("|", "\\|").replaceAll(/\r?\n/gu, " ");
106
+ }
107
+ function code(value) {
108
+ return value === null || value === "" ? "" : `\`${cell(value)}\``;
109
+ }
110
+ function describeFilter(filter) {
111
+ const parts = [];
112
+ if (filter?.systemId !== void 0) {
113
+ parts.push(`system_id = \`${cell(filter.systemId)}\``);
114
+ }
115
+ if (filter?.modality !== void 0) {
116
+ parts.push(`modality = \`${filter.modality}\``);
117
+ }
118
+ if (filter?.since !== void 0) {
119
+ parts.push(`ts \u2265 \`${filter.since}\``);
120
+ }
121
+ if (filter?.until !== void 0) {
122
+ parts.push(`ts \u2264 \`${filter.until}\``);
123
+ }
124
+ return parts.length === 0 ? "none (full store)" : parts.join(" \xB7 ");
125
+ }
126
+ function breakdown(records, label) {
127
+ const counts = /* @__PURE__ */ new Map();
128
+ for (const record of records) {
129
+ const key = label(record);
130
+ counts.set(key, (counts.get(key) ?? 0) + 1);
131
+ }
132
+ const rows = [...counts.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]));
133
+ return rows.map(([key, n]) => `| ${cell(key)} | ${n} |`).join("\n");
134
+ }
135
+ function integritySection(integrity) {
136
+ if (integrity.ok) {
137
+ return `**Chain verified** \u2014 ${integrity.checked} record(s) checked; the tamper-evident hash chain is intact.`;
138
+ }
139
+ return `**\u26A0 BROKEN at seq ${integrity.brokenAtSeq}** \u2014 tamper-evident hash-chain verification FAILED at record seq ${integrity.brokenAtSeq} (${integrity.checked} record(s) checked up to and including the break). Records at and after this point cannot be trusted as appended; investigate the store before relying on this log.`;
140
+ }
141
+ function renderAuditReport(ctx) {
142
+ const { records, integrity } = ctx;
143
+ const firstTs = records[0]?.ts ?? null;
144
+ const lastTs = records.at(-1)?.ts ?? null;
145
+ const recordRows = records.length === 0 ? "_No records match the filter._" : [
146
+ "| id | ts | modality | model | system_id | output_hash | output_hash_normalized | marking_methods | manifest_ref | disclosure_context |",
147
+ "| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |",
148
+ ...records.map(
149
+ (r) => `| ${code(r.id)} | ${r.ts} | ${r.modality} | ${cell(r.model)} | ${cell(r.system_id)} | ${code(r.output_hash)} | ${code(r.output_hash_normalized)} | ${cell(r.marking_methods.join(" + "))} | ${code(r.manifest_ref)} | ${cell(r.disclosure_context)} |`
150
+ )
151
+ ].join("\n");
152
+ const summaryBreakdowns = records.length === 0 ? "" : `
153
+ ### By modality
154
+
155
+ | Modality | Records |
156
+ | --- | --- |
157
+ ${breakdown(records, (r) => r.modality)}
158
+
159
+ ### By model
160
+
161
+ | Model | Records |
162
+ | --- | --- |
163
+ ${breakdown(records, (r) => r.model)}
164
+
165
+ ### By system_id
166
+
167
+ | system_id | Records |
168
+ | --- | --- |
169
+ ${breakdown(records, (r) => r.system_id)}
170
+
171
+ ### By marking methods
172
+
173
+ | Marking methods | Records |
174
+ | --- | --- |
175
+ ${breakdown(records, (r) => r.marking_methods.join(" + "))}
176
+ `;
177
+ return `# Evidence audit report (gsengai)
178
+
179
+ - Generated: ${ctx.generatedAt} (UTC)
180
+ - Store: \`${cell(ctx.storePath)}\`
181
+ - Filter: ${describeFilter(ctx.filter)}
182
+ - Record schema: v1 (PRD \xA74) \u2014 hashes and metadata only; raw content is never stored or exported
183
+
184
+ ## Integrity
185
+
186
+ ${integritySection(integrity)}
187
+
188
+ Chain verification always covers the entire store, regardless of the export filter above.
189
+
190
+ ## Summary
191
+
192
+ - Records (after filter): ${records.length}
193
+ - First record: ${firstTs ?? "\u2014"}
194
+ - Last record: ${lastTs ?? "\u2014"}
195
+ ${summaryBreakdowns}
196
+ ## Records
197
+
198
+ ${recordRows}
199
+
200
+ ## What this evidence is \u2014 and is not
201
+
202
+ This report is a machine-generated record of the outputs an AI system generated, marked,
203
+ and disclosed, as captured in an append-only, hash-chained evidence store. It contains
204
+ cryptographic hashes and metadata only \u2014 raw prompts and outputs are never stored and
205
+ never exported. It supports compliance with EU AI Act Article 50 and California SB 942;
206
+ it is not, by itself, a determination of compliance.
207
+
208
+ ${PRD_S9_LIMITS}
209
+ `;
210
+ }
211
+
212
+ // src/store.ts
213
+ var MODALITIES = /* @__PURE__ */ new Set(["text", "image", "audio", "video"]);
214
+ var SCHEMA_SQL = `
215
+ CREATE TABLE IF NOT EXISTS evidence (
216
+ seq INTEGER PRIMARY KEY,
217
+ id TEXT NOT NULL UNIQUE,
218
+ ts TEXT NOT NULL,
219
+ modality TEXT NOT NULL CHECK (modality IN ('text','image','audio','video')),
220
+ model TEXT NOT NULL,
221
+ system_id TEXT NOT NULL,
222
+ prompt_hash TEXT,
223
+ output_hash TEXT NOT NULL,
224
+ output_hash_normalized TEXT,
225
+ hash_version INTEGER NOT NULL,
226
+ marking_methods TEXT NOT NULL,
227
+ manifest_ref TEXT,
228
+ disclosure_context TEXT,
229
+ prev_hash TEXT,
230
+ record_hash TEXT NOT NULL
231
+ );
232
+ CREATE INDEX IF NOT EXISTS idx_evidence_output_hash
233
+ ON evidence (output_hash);
234
+ CREATE INDEX IF NOT EXISTS idx_evidence_output_hash_normalized
235
+ ON evidence (output_hash_normalized);
236
+ CREATE TRIGGER IF NOT EXISTS evidence_append_only_update
237
+ BEFORE UPDATE ON evidence
238
+ BEGIN
239
+ SELECT RAISE(ABORT, 'gsengai evidence store is append-only: UPDATE is not permitted');
240
+ END;
241
+ CREATE TRIGGER IF NOT EXISTS evidence_append_only_delete
242
+ BEFORE DELETE ON evidence
243
+ BEGIN
244
+ SELECT RAISE(ABORT, 'gsengai evidence store is append-only: DELETE is not permitted');
245
+ END;
246
+ `;
247
+ function rowToRecord(row) {
248
+ return {
249
+ id: row.id,
250
+ ts: row.ts,
251
+ modality: row.modality,
252
+ model: row.model,
253
+ system_id: row.system_id,
254
+ prompt_hash: row.prompt_hash,
255
+ output_hash: row.output_hash,
256
+ output_hash_normalized: row.output_hash_normalized,
257
+ hash_version: row.hash_version,
258
+ marking_methods: JSON.parse(row.marking_methods),
259
+ manifest_ref: row.manifest_ref,
260
+ disclosure_context: row.disclosure_context,
261
+ prev_hash: row.prev_hash,
262
+ record_hash: row.record_hash
263
+ };
264
+ }
265
+ function computeRecordHash(body) {
266
+ return sha256Hex(canonicalJson(body));
267
+ }
268
+ function requireNonEmptyString(value, name) {
269
+ if (typeof value !== "string" || value.length === 0) {
270
+ throw new TypeError(`gsengai: ${name} must be a non-empty string`);
271
+ }
272
+ return value;
273
+ }
274
+ function resolveOutputHashes(input) {
275
+ const provided = [input.outputText, input.outputBytes, input.outputHashes].filter(
276
+ (v) => v !== void 0
277
+ );
278
+ if (provided.length !== 1) {
279
+ throw new TypeError("gsengai: provide exactly one of outputText, outputBytes, or outputHashes");
280
+ }
281
+ if (input.outputText !== void 0) {
282
+ const hashes = hashText(input.outputText);
283
+ return {
284
+ output_hash: hashes.outputHash,
285
+ output_hash_normalized: hashes.outputHashNormalized,
286
+ hash_version: hashes.hashVersion
287
+ };
288
+ }
289
+ if (input.outputBytes !== void 0) {
290
+ return {
291
+ output_hash: sha256Hex(input.outputBytes),
292
+ output_hash_normalized: null,
293
+ hash_version: HASH_VERSION
294
+ };
295
+ }
296
+ const given = input.outputHashes;
297
+ return {
298
+ output_hash: requireNonEmptyString(given.outputHash, "outputHashes.outputHash"),
299
+ output_hash_normalized: given.outputHashNormalized ?? null,
300
+ hash_version: given.hashVersion ?? HASH_VERSION
301
+ };
302
+ }
303
+ function normalizeInstant(value, name) {
304
+ const ms = Date.parse(requireNonEmptyString(value, name));
305
+ if (Number.isNaN(ms)) {
306
+ throw new TypeError(`gsengai: ${name} must be a Date.parse-able timestamp`);
307
+ }
308
+ return new Date(ms).toISOString();
309
+ }
310
+ function buildFilter(filter) {
311
+ if (filter === void 0) {
312
+ return { where: "", params: {}, normalized: void 0 };
313
+ }
314
+ const conditions = [];
315
+ const params = {};
316
+ const normalized = {};
317
+ if (filter.systemId !== void 0) {
318
+ normalized.systemId = requireNonEmptyString(filter.systemId, "filter.systemId");
319
+ conditions.push("system_id = @systemId");
320
+ params.systemId = normalized.systemId;
321
+ }
322
+ if (filter.modality !== void 0) {
323
+ if (!MODALITIES.has(filter.modality)) {
324
+ throw new TypeError(
325
+ "gsengai: filter.modality must be one of 'text' | 'image' | 'audio' | 'video'"
326
+ );
327
+ }
328
+ normalized.modality = filter.modality;
329
+ conditions.push("modality = @modality");
330
+ params.modality = normalized.modality;
331
+ }
332
+ if (filter.since !== void 0) {
333
+ normalized.since = normalizeInstant(filter.since, "filter.since");
334
+ conditions.push("ts >= @since");
335
+ params.since = normalized.since;
336
+ }
337
+ if (filter.until !== void 0) {
338
+ normalized.until = normalizeInstant(filter.until, "filter.until");
339
+ conditions.push("ts <= @until");
340
+ params.until = normalized.until;
341
+ }
342
+ if (conditions.length === 0) {
343
+ return { where: "", params: {}, normalized: void 0 };
344
+ }
345
+ return { where: ` WHERE ${conditions.join(" AND ")}`, params, normalized };
346
+ }
347
+ function createEvidenceStore(options) {
348
+ if (options.storeRawContent) {
349
+ throw new NotImplementedError(
350
+ "gsengai: storeRawContent is not implemented in the MVP. Only hashes and metadata persist (PRD C4); encrypted raw-content capture is planned post-MVP."
351
+ );
352
+ }
353
+ const storePath = requireNonEmptyString(options.path, "path");
354
+ const db = new Database(storePath);
355
+ db.pragma("journal_mode = WAL");
356
+ db.exec(SCHEMA_SQL);
357
+ const insertStmt = db.prepare(`
358
+ INSERT INTO evidence (
359
+ id, ts, modality, model, system_id, prompt_hash, output_hash,
360
+ output_hash_normalized, hash_version, marking_methods, manifest_ref,
361
+ disclosure_context, prev_hash, record_hash
362
+ ) VALUES (
363
+ @id, @ts, @modality, @model, @system_id, @prompt_hash, @output_hash,
364
+ @output_hash_normalized, @hash_version, @marking_methods, @manifest_ref,
365
+ @disclosure_context, @prev_hash, @record_hash
366
+ )
367
+ `);
368
+ const lastHashStmt = db.prepare("SELECT record_hash FROM evidence ORDER BY seq DESC LIMIT 1");
369
+ const allStmt = db.prepare("SELECT * FROM evidence ORDER BY seq");
370
+ const byOutputHashStmt = db.prepare("SELECT * FROM evidence WHERE output_hash = ? ORDER BY seq");
371
+ const byTextStmt = db.prepare(
372
+ "SELECT * FROM evidence WHERE output_hash = @raw OR output_hash_normalized = @normalized ORDER BY seq"
373
+ );
374
+ const countStmt = db.prepare("SELECT COUNT(*) AS n FROM evidence");
375
+ function filteredRows(filter) {
376
+ const { where, params, normalized } = buildFilter(filter);
377
+ const stmt = db.prepare(`SELECT * FROM evidence${where} ORDER BY seq`);
378
+ return { rows: stmt.iterate(params), normalized };
379
+ }
380
+ function verifyChainImpl() {
381
+ let checked = 0;
382
+ let prevHash = null;
383
+ for (const row of allStmt.iterate()) {
384
+ checked += 1;
385
+ const { record_hash, ...body } = rowToRecord(row);
386
+ if (body.prev_hash !== prevHash || computeRecordHash(body) !== record_hash) {
387
+ return { ok: false, checked, brokenAtSeq: row.seq };
388
+ }
389
+ prevHash = record_hash;
390
+ }
391
+ return { ok: true, checked };
392
+ }
393
+ const runAppend = db.transaction(
394
+ (body) => {
395
+ const last = lastHashStmt.get();
396
+ const chained = {
397
+ ...body,
398
+ prev_hash: last ? last.record_hash : null
399
+ };
400
+ const record = { ...chained, record_hash: computeRecordHash(chained) };
401
+ insertStmt.run({ ...record, marking_methods: JSON.stringify(record.marking_methods) });
402
+ return record;
403
+ }
404
+ );
405
+ return {
406
+ append(input) {
407
+ const modality = input.modality;
408
+ if (typeof modality !== "string" || !MODALITIES.has(modality)) {
409
+ throw new TypeError(
410
+ "gsengai: modality must be one of 'text' | 'image' | 'audio' | 'video'"
411
+ );
412
+ }
413
+ const body = {
414
+ id: randomUUID(),
415
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
416
+ modality,
417
+ model: requireNonEmptyString(input.model, "model"),
418
+ system_id: requireNonEmptyString(input.systemId, "systemId"),
419
+ prompt_hash: input.promptHash ?? null,
420
+ ...resolveOutputHashes(input),
421
+ marking_methods: input.markingMethods ?? ["logging"],
422
+ manifest_ref: input.manifestRef ?? null,
423
+ disclosure_context: input.disclosureContext ?? null
424
+ };
425
+ return runAppend.immediate(body);
426
+ },
427
+ findByOutputHash(hash) {
428
+ const rows = byOutputHashStmt.all(hash);
429
+ return rows.map(rowToRecord);
430
+ },
431
+ findByText(text) {
432
+ const hashes = hashText(text);
433
+ const rows = byTextStmt.all({
434
+ raw: hashes.outputHash,
435
+ normalized: hashes.outputHashNormalized
436
+ });
437
+ return rows.map(rowToRecord);
438
+ },
439
+ async exportJsonl(path) {
440
+ const out = createWriteStream(path, { encoding: "utf8" });
441
+ let records = 0;
442
+ try {
443
+ for (const row of allStmt.iterate()) {
444
+ const line = `${canonicalJson(rowToRecord(row))}
445
+ `;
446
+ if (!out.write(line)) {
447
+ await once(out, "drain");
448
+ }
449
+ records += 1;
450
+ }
451
+ } finally {
452
+ out.end();
453
+ }
454
+ await once(out, "close");
455
+ return { records, path };
456
+ },
457
+ async exportCsv(path, filter) {
458
+ const { rows } = filteredRows(filter);
459
+ const out = createWriteStream(path, { encoding: "utf8" });
460
+ let records = 0;
461
+ try {
462
+ if (!out.write(`${CSV_COLUMNS.join(",")}
463
+ `)) {
464
+ await once(out, "drain");
465
+ }
466
+ for (const row of rows) {
467
+ const line = `${recordToCsvRow(rowToRecord(row))}
468
+ `;
469
+ if (!out.write(line)) {
470
+ await once(out, "drain");
471
+ }
472
+ records += 1;
473
+ }
474
+ } finally {
475
+ out.end();
476
+ }
477
+ await once(out, "close");
478
+ return { records, path };
479
+ },
480
+ buildAuditReport(opts = {}) {
481
+ const { rows, normalized } = filteredRows(opts.filter);
482
+ const records = [...rows].map(rowToRecord);
483
+ const integrity = verifyChainImpl();
484
+ const markdown = renderAuditReport({
485
+ records,
486
+ integrity,
487
+ storePath,
488
+ filter: normalized,
489
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString()
490
+ });
491
+ if (opts.path !== void 0) {
492
+ writeFileSync(opts.path, markdown, "utf8");
493
+ }
494
+ return { markdown, records: records.length, integrity, path: opts.path ?? null };
495
+ },
496
+ verifyChain: verifyChainImpl,
497
+ count() {
498
+ const row = countStmt.get();
499
+ return row.n;
500
+ },
501
+ close() {
502
+ db.close();
503
+ }
504
+ };
505
+ }
506
+
507
+ // src/cli.ts
508
+ var USAGE = `gsengai-audit \u2014 export audit artifacts from an gsengai evidence store.
509
+ Supports compliance with EU AI Act Article 50 and California SB 942. Exports
510
+ contain hashes and metadata only; raw content is never stored or exported.
511
+ Not legal advice.
512
+
513
+ Usage:
514
+ gsengai-audit export --store <evidence.db> --format csv|report --out <path>
515
+ [--system-id <id>] [--modality text|image|audio|video]
516
+ [--since <ISO timestamp>] [--until <ISO timestamp>]
517
+
518
+ Formats:
519
+ csv streaming CSV of evidence records (fixed schema-v1 column order)
520
+ report human-readable Markdown audit report (integrity, summary, records)
521
+
522
+ Tip: render the report Markdown to PDF/HTML yourself before handing it to
523
+ counsel (e.g. pandoc report.md -o report.pdf).`;
524
+ var MODALITIES2 = /* @__PURE__ */ new Set(["text", "image", "audio", "video"]);
525
+ function parseCliArgs(argv) {
526
+ return parseArgs({
527
+ args: argv,
528
+ allowPositionals: true,
529
+ options: {
530
+ store: { type: "string" },
531
+ format: { type: "string" },
532
+ out: { type: "string" },
533
+ "system-id": { type: "string" },
534
+ modality: { type: "string" },
535
+ since: { type: "string" },
536
+ until: { type: "string" },
537
+ help: { type: "boolean" }
538
+ }
539
+ });
540
+ }
541
+ async function runAuditCli(argv, io = console) {
542
+ let parsed;
543
+ try {
544
+ parsed = parseCliArgs(argv);
545
+ } catch (err) {
546
+ io.error(err instanceof Error ? err.message : String(err));
547
+ io.error(USAGE);
548
+ return 1;
549
+ }
550
+ const { values, positionals } = parsed;
551
+ if (values.help === true) {
552
+ io.log(USAGE);
553
+ return 0;
554
+ }
555
+ if (positionals[0] !== "export" || positionals.length !== 1) {
556
+ io.error("gsengai-audit: the only supported command is 'export'");
557
+ io.error(USAGE);
558
+ return 1;
559
+ }
560
+ const storePath = values.store;
561
+ const format = values.format;
562
+ const outPath = values.out;
563
+ if (storePath === void 0 || format === void 0 || outPath === void 0) {
564
+ io.error("gsengai-audit: --store, --format, and --out are required");
565
+ io.error(USAGE);
566
+ return 1;
567
+ }
568
+ if (format !== "csv" && format !== "report") {
569
+ io.error(`gsengai-audit: unknown --format '${format}' (expected csv or report)`);
570
+ return 1;
571
+ }
572
+ if (!existsSync(storePath)) {
573
+ io.error(`gsengai-audit: no evidence store found at '${storePath}'`);
574
+ return 1;
575
+ }
576
+ const modality = values.modality;
577
+ if (modality !== void 0 && !MODALITIES2.has(modality)) {
578
+ io.error(`gsengai-audit: unknown --modality '${modality}' (expected text|image|audio|video)`);
579
+ return 1;
580
+ }
581
+ const filter = {};
582
+ if (values["system-id"] !== void 0) {
583
+ filter.systemId = values["system-id"];
584
+ }
585
+ if (modality !== void 0) {
586
+ filter.modality = modality;
587
+ }
588
+ if (values.since !== void 0) {
589
+ filter.since = values.since;
590
+ }
591
+ if (values.until !== void 0) {
592
+ filter.until = values.until;
593
+ }
594
+ const store = createEvidenceStore({ path: storePath });
595
+ try {
596
+ if (format === "csv") {
597
+ const result = await store.exportCsv(outPath, filter);
598
+ io.log(`CSV export: ${result.records} record(s) -> ${result.path}`);
599
+ } else {
600
+ const report = store.buildAuditReport({ path: outPath, filter });
601
+ io.log(`Audit report: ${report.records} record(s) -> ${outPath}`);
602
+ if (report.integrity.ok) {
603
+ io.log(`Integrity: chain verified (${report.integrity.checked} record(s) checked)`);
604
+ } else {
605
+ io.error(
606
+ `WARNING: evidence hash chain BROKEN at seq ${report.integrity.brokenAtSeq} \u2014 the Integrity section of the report records the break.`
607
+ );
608
+ }
609
+ }
610
+ } catch (err) {
611
+ io.error(`gsengai-audit: export failed: ${err instanceof Error ? err.message : String(err)}`);
612
+ return 1;
613
+ } finally {
614
+ store.close();
615
+ }
616
+ return 0;
617
+ }
618
+
619
+ // src/gsengai-audit.ts
620
+ runAuditCli(process.argv.slice(2)).then(
621
+ (code2) => {
622
+ process.exitCode = code2;
623
+ },
624
+ (err) => {
625
+ console.error(err);
626
+ process.exitCode = 1;
627
+ }
628
+ );
629
+ //# sourceMappingURL=gsengai-audit.js.map