dsh-research-report 0.2.0 → 0.3.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.
- package/CHANGELOG.md +10 -0
- package/README.es.md +15 -0
- package/README.hi.md +15 -0
- package/README.md +16 -0
- package/README.pt.md +15 -0
- package/README.zh.md +15 -0
- package/lib/cli.js +103 -0
- package/lib/index.js +6 -594
- package/lib/types/cli.d.ts +14 -0
- package/lib/types/cli.d.ts.map +1 -0
- package/lib/types/cli.js +102 -0
- package/lib/types/cli.js.map +1 -0
- package/lib/types/index.d.ts +2 -2
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +1 -1
- package/lib/types/index.js.map +1 -1
- package/lib/types/verify-sealed.d.ts +60 -0
- package/lib/types/verify-sealed.d.ts.map +1 -1
- package/lib/types/verify-sealed.js +107 -0
- package/lib/types/verify-sealed.js.map +1 -1
- package/lib/types/version.d.ts +1 -1
- package/lib/types/version.js +1 -1
- package/lib/verify-sealed-CMJp1f_v.js +711 -0
- package/package.json +4 -1
- package/src/cli.ts +121 -0
- package/src/index.ts +2 -2
- package/src/verify-sealed.ts +157 -0
- package/src/version.ts +1 -1
package/lib/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
+
import { _ as sha256Of, a as renderSarif, c as VERSION, d as extractCitations, f as mapBridgeResults, g as LedgerError, h as EvidenceLedger, i as renderMachineCheckMarkdown, l as combineOutcomes, m as verifyClaimText, n as VERIFIER_TOOL, o as renderVerificationJson, p as normalizeNumber, r as buildVerificationReport, s as verifySealedReport, t as SARIF_SCHEMA, u as contextLabelOf } from "./verify-sealed-CMJp1f_v.js";
|
|
1
2
|
import path from "node:path";
|
|
2
3
|
import z from "@deepseek-ai/schemastery";
|
|
3
|
-
import { appendFile, mkdir, readFile,
|
|
4
|
+
import { appendFile, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
4
5
|
import { KNOWN_SESSION_EVENT_TYPES } from "@deepseek-ai/dsh-session";
|
|
5
|
-
import { createHash, randomBytes } from "node:crypto";
|
|
6
6
|
import { Service } from "@deepseek-ai/cordis";
|
|
7
7
|
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
8
8
|
//#region src/config.ts
|
|
@@ -62,302 +62,6 @@ function resolveConfig(config = {}) {
|
|
|
62
62
|
};
|
|
63
63
|
}
|
|
64
64
|
//#endregion
|
|
65
|
-
//#region src/ledger.ts
|
|
66
|
-
/**
|
|
67
|
-
* The evidence ledger: a content-addressed snapshot store with JSONL journals.
|
|
68
|
-
*
|
|
69
|
-
* Layout under the configured `ledgerRoot`:
|
|
70
|
-
* - `objects/<sha256>` — one immutable snapshot per content hash (same content
|
|
71
|
-
* is stored exactly once; an "update" is a new object, history is never
|
|
72
|
-
* rewritten).
|
|
73
|
-
* - `index.jsonl` — evidence registrations: id → hash, origin, capturedAt,
|
|
74
|
-
* title, bytes. Append-only.
|
|
75
|
-
* - `claims.jsonl` — claim registrations: id → text, evidenceIds, optional
|
|
76
|
-
* dataset bridge fields. Append-only.
|
|
77
|
-
* - `verdicts.jsonl` — verification verdicts; the latest line per claim wins.
|
|
78
|
-
*
|
|
79
|
-
* Tamper detection is the point of the design: every content read recomputes
|
|
80
|
-
* the SHA-256 of the object file and compares it against the indexed hash —
|
|
81
|
-
* a mismatch surfaces as `tampered`, a deleted object as `missing`.
|
|
82
|
-
*
|
|
83
|
-
* This module is pure Node (zero DSH imports) so it stays testable in
|
|
84
|
-
* isolation; policy (size caps, fetch) lives in the provider.
|
|
85
|
-
*
|
|
86
|
-
* @module dsh-research-report/ledger
|
|
87
|
-
*/
|
|
88
|
-
/** SHA-256 hex of one UTF-8 string. */
|
|
89
|
-
function sha256Of(content) {
|
|
90
|
-
return createHash("sha256").update(content, "utf8").digest("hex");
|
|
91
|
-
}
|
|
92
|
-
/** A loud ledger failure (audit state must never degrade silently). */
|
|
93
|
-
var LedgerError = class extends Error {
|
|
94
|
-
/** The machine-routable failure code. */
|
|
95
|
-
code;
|
|
96
|
-
constructor(code, message) {
|
|
97
|
-
super(message);
|
|
98
|
-
this.name = "LedgerError";
|
|
99
|
-
this.code = code;
|
|
100
|
-
}
|
|
101
|
-
};
|
|
102
|
-
/** Read one JSONL journal; a corrupt line fails loud with file and line number. */
|
|
103
|
-
async function readJournal(file) {
|
|
104
|
-
let text;
|
|
105
|
-
try {
|
|
106
|
-
text = await readFile(file, "utf8");
|
|
107
|
-
} catch (error) {
|
|
108
|
-
if (error.code === "ENOENT") return [];
|
|
109
|
-
throw new LedgerError("IO", `cannot read journal ${file}: ${error.message}`);
|
|
110
|
-
}
|
|
111
|
-
const lines = [];
|
|
112
|
-
const rows = text.split("\n");
|
|
113
|
-
for (let index = 0; index < rows.length; index++) {
|
|
114
|
-
const row = rows[index];
|
|
115
|
-
if (row.trim() === "") continue;
|
|
116
|
-
try {
|
|
117
|
-
lines.push(JSON.parse(row));
|
|
118
|
-
} catch {
|
|
119
|
-
throw new LedgerError("JOURNAL_CORRUPT", `corrupt JSONL at ${file}:${index + 1}`);
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
return lines;
|
|
123
|
-
}
|
|
124
|
-
/**
|
|
125
|
-
* The content-addressed evidence ledger. Writes are serialized through an
|
|
126
|
-
* internal promise queue so concurrent tool calls cannot interleave journals.
|
|
127
|
-
*/
|
|
128
|
-
var EvidenceLedger = class {
|
|
129
|
-
/** Absolute ledger root directory. */
|
|
130
|
-
root;
|
|
131
|
-
/** Write serialization chain (never rejects — each link absorbs the previous error). */
|
|
132
|
-
queue = Promise.resolve();
|
|
133
|
-
/**
|
|
134
|
-
* @param root - absolute ledger root directory.
|
|
135
|
-
*/
|
|
136
|
-
constructor(root) {
|
|
137
|
-
this.root = root;
|
|
138
|
-
}
|
|
139
|
-
get objectsDir() {
|
|
140
|
-
return path.join(this.root, "objects");
|
|
141
|
-
}
|
|
142
|
-
get indexFile() {
|
|
143
|
-
return path.join(this.root, "index.jsonl");
|
|
144
|
-
}
|
|
145
|
-
get claimsFile() {
|
|
146
|
-
return path.join(this.root, "claims.jsonl");
|
|
147
|
-
}
|
|
148
|
-
get verdictsFile() {
|
|
149
|
-
return path.join(this.root, "verdicts.jsonl");
|
|
150
|
-
}
|
|
151
|
-
get disproofsFile() {
|
|
152
|
-
return path.join(this.root, "disproofs.jsonl");
|
|
153
|
-
}
|
|
154
|
-
/** Run `work` after all previously queued writes settle. */
|
|
155
|
-
enqueue(work) {
|
|
156
|
-
const run = this.queue.then(work);
|
|
157
|
-
this.queue = run.then(() => void 0, () => void 0);
|
|
158
|
-
return run;
|
|
159
|
-
}
|
|
160
|
-
/** Ensure the directory layout exists. */
|
|
161
|
-
async ensureLayout() {
|
|
162
|
-
await mkdir(this.objectsDir, { recursive: true });
|
|
163
|
-
}
|
|
164
|
-
/** Write one snapshot object atomically (tmp + rename); no-op when present. */
|
|
165
|
-
async writeObject(hash, content) {
|
|
166
|
-
const target = path.join(this.objectsDir, hash);
|
|
167
|
-
try {
|
|
168
|
-
await readFile(target);
|
|
169
|
-
return;
|
|
170
|
-
} catch (error) {
|
|
171
|
-
if (error.code !== "ENOENT") throw new LedgerError("IO", `cannot stat object ${hash}: ${error.message}`);
|
|
172
|
-
}
|
|
173
|
-
const temporary = path.join(this.objectsDir, `.${hash}.tmp-${process.pid}-${randomBytes(4).toString("hex")}`);
|
|
174
|
-
await writeFile(temporary, content, "utf8");
|
|
175
|
-
try {
|
|
176
|
-
await rename(temporary, target);
|
|
177
|
-
} catch (error) {
|
|
178
|
-
try {
|
|
179
|
-
await readFile(target);
|
|
180
|
-
} catch {
|
|
181
|
-
throw new LedgerError("IO", `cannot commit object ${hash}: ${error.message}`);
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
/**
|
|
186
|
-
* Register one evidence snapshot. Same content dedupes to the stored object;
|
|
187
|
-
* a caller-chosen id that already exists with DIFFERENT content is refused
|
|
188
|
-
* loudly (history is never rewritten).
|
|
189
|
-
* @param input - id (optional), title, origin, content, capturedAt.
|
|
190
|
-
* @returns the record plus whether this call created it.
|
|
191
|
-
*/
|
|
192
|
-
async putEvidence(input) {
|
|
193
|
-
return this.enqueue(async () => {
|
|
194
|
-
await this.ensureLayout();
|
|
195
|
-
const hash = sha256Of(input.content);
|
|
196
|
-
const id = input.id ?? `ev-${hash.slice(0, 12)}`;
|
|
197
|
-
const existing = (await readJournal(this.indexFile)).find((line) => line.id === id);
|
|
198
|
-
if (existing !== void 0) {
|
|
199
|
-
if (existing.hash !== hash) throw new LedgerError("ID_CONFLICT", `evidence id "${id}" is already registered with different content (indexed ${existing.hash}, new ${hash}); choose a new id — snapshots are immutable`);
|
|
200
|
-
return {
|
|
201
|
-
record: existing,
|
|
202
|
-
created: false
|
|
203
|
-
};
|
|
204
|
-
}
|
|
205
|
-
await this.writeObject(hash, input.content);
|
|
206
|
-
const record = {
|
|
207
|
-
id,
|
|
208
|
-
hash,
|
|
209
|
-
title: input.title,
|
|
210
|
-
origin: input.origin,
|
|
211
|
-
capturedAt: input.capturedAt,
|
|
212
|
-
bytes: Buffer.byteLength(input.content, "utf8"),
|
|
213
|
-
...input.journal === void 0 ? {} : { journal: input.journal },
|
|
214
|
-
...input.year === void 0 ? {} : { year: input.year },
|
|
215
|
-
...input.sessionRef === void 0 ? {} : { sessionRef: input.sessionRef }
|
|
216
|
-
};
|
|
217
|
-
await appendFile(this.indexFile, `${JSON.stringify(record)}\n`, "utf8");
|
|
218
|
-
return {
|
|
219
|
-
record,
|
|
220
|
-
created: true
|
|
221
|
-
};
|
|
222
|
-
});
|
|
223
|
-
}
|
|
224
|
-
/**
|
|
225
|
-
* Register claims (id → text, evidenceIds). Re-registering a claim id with
|
|
226
|
-
* a different text or different bindings is refused loudly.
|
|
227
|
-
* @param claims - the registrations to append.
|
|
228
|
-
* @param registeredAt - ISO-8601 registration time.
|
|
229
|
-
* @returns the durable claim lines (existing lines for idempotent repeats).
|
|
230
|
-
*/
|
|
231
|
-
async registerClaims(claims, registeredAt) {
|
|
232
|
-
return this.enqueue(async () => {
|
|
233
|
-
await this.ensureLayout();
|
|
234
|
-
const journal = await readJournal(this.claimsFile);
|
|
235
|
-
const out = [];
|
|
236
|
-
for (const claim of claims) {
|
|
237
|
-
const existing = journal.find((line) => line.id === claim.id);
|
|
238
|
-
if (existing !== void 0) {
|
|
239
|
-
if (!(existing.text === claim.text && JSON.stringify(existing.evidenceIds) === JSON.stringify(claim.evidenceIds) && existing.dataset === claim.dataset)) throw new LedgerError("ID_CONFLICT", `claim id "${claim.id}" is already registered with different text or bindings; choose a new claim id — registrations are immutable`);
|
|
240
|
-
out.push(existing);
|
|
241
|
-
continue;
|
|
242
|
-
}
|
|
243
|
-
const line = {
|
|
244
|
-
...claim,
|
|
245
|
-
registeredAt
|
|
246
|
-
};
|
|
247
|
-
await appendFile(this.claimsFile, `${JSON.stringify(line)}\n`, "utf8");
|
|
248
|
-
journal.push(line);
|
|
249
|
-
out.push(line);
|
|
250
|
-
}
|
|
251
|
-
return out;
|
|
252
|
-
});
|
|
253
|
-
}
|
|
254
|
-
/**
|
|
255
|
-
* Append one verdict (latest per claim wins on read).
|
|
256
|
-
* @param verdict - claimId, status, optional note.
|
|
257
|
-
* @param at - ISO-8601 write time.
|
|
258
|
-
*/
|
|
259
|
-
async recordVerdict(verdict, at) {
|
|
260
|
-
await this.enqueue(async () => {
|
|
261
|
-
await this.ensureLayout();
|
|
262
|
-
const line = {
|
|
263
|
-
...verdict,
|
|
264
|
-
at
|
|
265
|
-
};
|
|
266
|
-
await appendFile(this.verdictsFile, `${JSON.stringify(line)}\n`, "utf8");
|
|
267
|
-
});
|
|
268
|
-
}
|
|
269
|
-
/**
|
|
270
|
-
* Read the evidence index.
|
|
271
|
-
* @returns every registration in append order.
|
|
272
|
-
*/
|
|
273
|
-
async listEvidence() {
|
|
274
|
-
return readJournal(this.indexFile);
|
|
275
|
-
}
|
|
276
|
-
/**
|
|
277
|
-
* Read one evidence registration.
|
|
278
|
-
* @param id - the ledger id.
|
|
279
|
-
* @returns the record, or undefined when unknown.
|
|
280
|
-
*/
|
|
281
|
-
async getEvidence(id) {
|
|
282
|
-
return (await this.listEvidence()).find((line) => line.id === id);
|
|
283
|
-
}
|
|
284
|
-
/**
|
|
285
|
-
* Read every claim registration.
|
|
286
|
-
* @returns every claim in append order.
|
|
287
|
-
*/
|
|
288
|
-
async listClaims() {
|
|
289
|
-
return readJournal(this.claimsFile);
|
|
290
|
-
}
|
|
291
|
-
/**
|
|
292
|
-
* Read one claim registration.
|
|
293
|
-
* @param id - the claim id.
|
|
294
|
-
* @returns the claim line, or undefined when unknown.
|
|
295
|
-
*/
|
|
296
|
-
async getClaim(id) {
|
|
297
|
-
return (await this.listClaims()).find((line) => line.id === id);
|
|
298
|
-
}
|
|
299
|
-
/**
|
|
300
|
-
* Fold the verdict journal to the latest verdict per claim.
|
|
301
|
-
* @returns claimId → latest stored verdict.
|
|
302
|
-
*/
|
|
303
|
-
async latestVerdicts() {
|
|
304
|
-
const journal = await readJournal(this.verdictsFile);
|
|
305
|
-
const latest = /* @__PURE__ */ new Map();
|
|
306
|
-
for (const line of journal) latest.set(line.claimId, line);
|
|
307
|
-
return latest;
|
|
308
|
-
}
|
|
309
|
-
/**
|
|
310
|
-
* Append one negative-knowledge (disproven) record, keyed by claim content
|
|
311
|
-
* hash so the same text cannot be re-reported under a different claim id.
|
|
312
|
-
* @param disproof - claimHash, claimId, text, evidenceHashes, note.
|
|
313
|
-
* @param at - ISO-8601 write time.
|
|
314
|
-
*/
|
|
315
|
-
async recordDisproof(disproof, at) {
|
|
316
|
-
await this.enqueue(async () => {
|
|
317
|
-
await this.ensureLayout();
|
|
318
|
-
const line = {
|
|
319
|
-
...disproof,
|
|
320
|
-
at
|
|
321
|
-
};
|
|
322
|
-
await appendFile(this.disproofsFile, `${JSON.stringify(line)}\n`, "utf8");
|
|
323
|
-
});
|
|
324
|
-
}
|
|
325
|
-
/**
|
|
326
|
-
* Fold the disproof journal to the latest disproof per claim content hash.
|
|
327
|
-
* @returns claimHash → latest stored disproof.
|
|
328
|
-
*/
|
|
329
|
-
async latestDisproofs() {
|
|
330
|
-
const journal = await readJournal(this.disproofsFile);
|
|
331
|
-
const latest = /* @__PURE__ */ new Map();
|
|
332
|
-
for (const line of journal) latest.set(line.claimHash, line);
|
|
333
|
-
return latest;
|
|
334
|
-
}
|
|
335
|
-
/**
|
|
336
|
-
* Read one snapshot and recompute its hash — the tamper-detection path.
|
|
337
|
-
* @param id - the ledger id.
|
|
338
|
-
* @returns content plus integrity (`ok` | `tampered` | `missing`), or
|
|
339
|
-
* undefined when the id is unknown.
|
|
340
|
-
*/
|
|
341
|
-
async readContent(id) {
|
|
342
|
-
const record = await this.getEvidence(id);
|
|
343
|
-
if (record === void 0) return void 0;
|
|
344
|
-
let content;
|
|
345
|
-
try {
|
|
346
|
-
content = await readFile(path.join(this.objectsDir, record.hash), "utf8");
|
|
347
|
-
} catch (error) {
|
|
348
|
-
if (error.code === "ENOENT") return {
|
|
349
|
-
content: "",
|
|
350
|
-
integrity: "missing"
|
|
351
|
-
};
|
|
352
|
-
throw new LedgerError("IO", `cannot read object ${record.hash}: ${error.message}`);
|
|
353
|
-
}
|
|
354
|
-
return {
|
|
355
|
-
content,
|
|
356
|
-
integrity: sha256Of(content) === record.hash ? "ok" : "tampered"
|
|
357
|
-
};
|
|
358
|
-
}
|
|
359
|
-
};
|
|
360
|
-
//#endregion
|
|
361
65
|
//#region src/assemble.ts
|
|
362
66
|
/**
|
|
363
67
|
* Report assembly and sealing — pure functions. The provider owns the ledger
|
|
@@ -416,10 +120,11 @@ function versionIdOf(at) {
|
|
|
416
120
|
* @returns 16 hex characters of the knobs' SHA-256.
|
|
417
121
|
*/
|
|
418
122
|
function configFingerprint(knobs) {
|
|
419
|
-
|
|
123
|
+
const stable = JSON.stringify({
|
|
420
124
|
maxEvidenceBytes: knobs.maxEvidenceBytes,
|
|
421
125
|
maxEvidencePerReport: knobs.maxEvidencePerReport
|
|
422
|
-
})
|
|
126
|
+
});
|
|
127
|
+
return sha256Of(stable).slice(0, 16);
|
|
423
128
|
}
|
|
424
129
|
/**
|
|
425
130
|
* Validate one assemble request; every violation throws (a loud rejection —
|
|
@@ -872,299 +577,6 @@ var ResearchReportService = class extends Service {
|
|
|
872
577
|
}
|
|
873
578
|
};
|
|
874
579
|
//#endregion
|
|
875
|
-
//#region src/verify.ts
|
|
876
|
-
/** Number literal: optional sign, grouped digits, decimals, trailing %. */
|
|
877
|
-
const NUMBER_PATTERN = /-?\d[\d,]*(?:\.\d+)?%?/gu;
|
|
878
|
-
/** Quoted spans: ASCII double quotes, CJK corner brackets, full-width quotes. */
|
|
879
|
-
const QUOTE_PATTERNS = [
|
|
880
|
-
/"([^"\n]{4,200})"/gu,
|
|
881
|
-
/「([^」\n]{2,200})」/gu,
|
|
882
|
-
/“([^“”\n]{4,200})”/gu
|
|
883
|
-
];
|
|
884
|
-
/** The tail run of label characters (letters / CJK) ending a context window. */
|
|
885
|
-
const LABEL_PATTERN = /[\p{L}\p{N}_()()%$-]{2,24}$/u;
|
|
886
|
-
/**
|
|
887
|
-
* Extract the trailing context label of a number citation: up to 24 characters
|
|
888
|
-
* before the number, trimmed to its trailing label run. Too-short labels are
|
|
889
|
-
* dropped (a weak label would false-positive the contradiction check).
|
|
890
|
-
* @param text - the full claim text.
|
|
891
|
-
* @param index - offset of the number in the text.
|
|
892
|
-
* @returns the label, or undefined when there is no usable one.
|
|
893
|
-
*/
|
|
894
|
-
function contextLabelOf(text, index) {
|
|
895
|
-
const window = text.slice(Math.max(0, index - 24), index).trimEnd();
|
|
896
|
-
const label = LABEL_PATTERN.exec(window)?.[0].trim();
|
|
897
|
-
if (label === void 0 || label.length < 2) return void 0;
|
|
898
|
-
return label;
|
|
899
|
-
}
|
|
900
|
-
/**
|
|
901
|
-
* Extract every checkable citation from one claim text: number literals (with
|
|
902
|
-
* their left-context labels) and quoted spans.
|
|
903
|
-
* @param claimText - the claim to analyze.
|
|
904
|
-
* @returns citations in first-seen order (duplicates kept once).
|
|
905
|
-
*/
|
|
906
|
-
function extractCitations(claimText) {
|
|
907
|
-
const citations = [];
|
|
908
|
-
const seen = /* @__PURE__ */ new Set();
|
|
909
|
-
for (const match of claimText.matchAll(NUMBER_PATTERN)) {
|
|
910
|
-
const text = match[0];
|
|
911
|
-
if (seen.has(`number:${text}`)) continue;
|
|
912
|
-
seen.add(`number:${text}`);
|
|
913
|
-
const context = contextLabelOf(claimText, match.index);
|
|
914
|
-
citations.push(context === void 0 ? {
|
|
915
|
-
kind: "number",
|
|
916
|
-
text
|
|
917
|
-
} : {
|
|
918
|
-
kind: "number",
|
|
919
|
-
text,
|
|
920
|
-
context
|
|
921
|
-
});
|
|
922
|
-
}
|
|
923
|
-
for (const pattern of QUOTE_PATTERNS) for (const match of claimText.matchAll(pattern)) {
|
|
924
|
-
const text = match[1];
|
|
925
|
-
if (seen.has(`quote:${text}`)) continue;
|
|
926
|
-
seen.add(`quote:${text}`);
|
|
927
|
-
citations.push({
|
|
928
|
-
kind: "quote",
|
|
929
|
-
text
|
|
930
|
-
});
|
|
931
|
-
}
|
|
932
|
-
return citations;
|
|
933
|
-
}
|
|
934
|
-
/** The number token scan window after a context label (bytes of text). */
|
|
935
|
-
const CONTEXT_SCAN_WINDOW = 24;
|
|
936
|
-
/** Normalize a number literal for comparison (drop grouping commas and %). */
|
|
937
|
-
function normalizeNumber(text) {
|
|
938
|
-
return text.replace(/[,%]/gu, "");
|
|
939
|
-
}
|
|
940
|
-
/** The first number literal found in `text` from `from` (fresh regex — no shared lastIndex). */
|
|
941
|
-
function numberAfter(text, from) {
|
|
942
|
-
const window = text.slice(from, from + CONTEXT_SCAN_WINDOW);
|
|
943
|
-
return /-?\d[\d,]*(?:\.\d+)?%?/u.exec(window)?.[0];
|
|
944
|
-
}
|
|
945
|
-
/** Cap how many problem citations one note enumerates. */
|
|
946
|
-
const NOTE_LIST_CAP = 5;
|
|
947
|
-
/**
|
|
948
|
-
* Run the byte-level check of one claim against its bound evidence snapshots.
|
|
949
|
-
* @param claimText - the claim text.
|
|
950
|
-
* @param evidenceContents - the verbatim snapshot contents of the bound evidence.
|
|
951
|
-
* @returns the outcome (never throws).
|
|
952
|
-
*/
|
|
953
|
-
function verifyClaimText(claimText, evidenceContents) {
|
|
954
|
-
if (evidenceContents.length === 0) return {
|
|
955
|
-
status: "unverified",
|
|
956
|
-
note: "claim binds no evidence snapshot",
|
|
957
|
-
missing: [],
|
|
958
|
-
contradictions: []
|
|
959
|
-
};
|
|
960
|
-
const haystack = evidenceContents.join("\n");
|
|
961
|
-
const citations = extractCitations(claimText);
|
|
962
|
-
if (citations.length === 0) return {
|
|
963
|
-
status: "unverified",
|
|
964
|
-
note: "claim carries no checkable citation (number or quoted span); byte-level verification needs a literal to locate",
|
|
965
|
-
missing: [],
|
|
966
|
-
contradictions: []
|
|
967
|
-
};
|
|
968
|
-
const missing = [];
|
|
969
|
-
const contradictions = [];
|
|
970
|
-
for (const citation of citations) {
|
|
971
|
-
if (citation.kind === "quote") {
|
|
972
|
-
if (!haystack.includes(citation.text)) missing.push(`"${citation.text}"`);
|
|
973
|
-
continue;
|
|
974
|
-
}
|
|
975
|
-
if (haystack.includes(citation.text)) continue;
|
|
976
|
-
if (citation.context !== void 0) {
|
|
977
|
-
let searchFrom = 0;
|
|
978
|
-
let different;
|
|
979
|
-
for (;;) {
|
|
980
|
-
const at = haystack.indexOf(citation.context, searchFrom);
|
|
981
|
-
if (at === -1) break;
|
|
982
|
-
const found = numberAfter(haystack, at + citation.context.length);
|
|
983
|
-
if (found !== void 0 && normalizeNumber(found) !== normalizeNumber(citation.text)) {
|
|
984
|
-
different = found;
|
|
985
|
-
break;
|
|
986
|
-
}
|
|
987
|
-
searchFrom = at + citation.context.length;
|
|
988
|
-
}
|
|
989
|
-
if (different !== void 0) {
|
|
990
|
-
contradictions.push(`${citation.context}: claim says ${citation.text}, snapshot says ${different}`);
|
|
991
|
-
continue;
|
|
992
|
-
}
|
|
993
|
-
}
|
|
994
|
-
missing.push(citation.text);
|
|
995
|
-
}
|
|
996
|
-
if (contradictions.length > 0) return {
|
|
997
|
-
status: "disproven",
|
|
998
|
-
note: `disproves the claim: ${contradictions.slice(0, NOTE_LIST_CAP).join("; ")}`,
|
|
999
|
-
missing,
|
|
1000
|
-
contradictions
|
|
1001
|
-
};
|
|
1002
|
-
if (missing.length > 0) return {
|
|
1003
|
-
status: "insufficient",
|
|
1004
|
-
note: `insufficient evidence: citation(s) not found in bound evidence: ${missing.slice(0, NOTE_LIST_CAP).join(", ")}`,
|
|
1005
|
-
missing,
|
|
1006
|
-
contradictions
|
|
1007
|
-
};
|
|
1008
|
-
return {
|
|
1009
|
-
status: "verified",
|
|
1010
|
-
note: `${citations.length} citation(s) located verbatim in the bound snapshot(s)`,
|
|
1011
|
-
missing,
|
|
1012
|
-
contradictions
|
|
1013
|
-
};
|
|
1014
|
-
}
|
|
1015
|
-
/**
|
|
1016
|
-
* Map one bridge result set onto the verdict vocabulary. `mismatch` maps to
|
|
1017
|
-
* `disproven` (the dataset content explicitly falsifies the citation);
|
|
1018
|
-
* `not-found`/`unverifiable` map to `unverified`.
|
|
1019
|
-
* @param result - the dataQuality outcome.
|
|
1020
|
-
* @returns the mapped status plus a human-readable note.
|
|
1021
|
-
*/
|
|
1022
|
-
function mapBridgeResults(result) {
|
|
1023
|
-
const mismatch = result.results.filter((entry) => entry.status === "mismatch");
|
|
1024
|
-
if (mismatch.length > 0) return {
|
|
1025
|
-
status: "disproven",
|
|
1026
|
-
note: `dataset cross-check disproves: ${mismatch.slice(0, NOTE_LIST_CAP).map((entry) => `${entry.id}: dataset has ${String(entry.actual ?? "?")}${entry.note === void 0 ? "" : ` (${entry.note})`}`).join("; ")}`
|
|
1027
|
-
};
|
|
1028
|
-
const unresolved = result.results.filter((entry) => entry.status === "not-found" || entry.status === "unverifiable");
|
|
1029
|
-
if (unresolved.length > 0) return {
|
|
1030
|
-
status: "unverified",
|
|
1031
|
-
note: `dataset cross-check unresolved: ${unresolved.slice(0, NOTE_LIST_CAP).map((entry) => `${entry.id}: ${entry.status}${entry.note === void 0 ? "" : ` (${entry.note})`}`).join("; ")}`
|
|
1032
|
-
};
|
|
1033
|
-
return {
|
|
1034
|
-
status: "verified",
|
|
1035
|
-
note: `${result.results.length} dataset citation(s) verified via ctx.dataQuality`
|
|
1036
|
-
};
|
|
1037
|
-
}
|
|
1038
|
-
/**
|
|
1039
|
-
* Combine the byte-level and bridge outcomes: `disproven` wins, then
|
|
1040
|
-
* `contradicted`, then `unverified`, then `insufficient`, then `verified`.
|
|
1041
|
-
* @param byte - the byte-level outcome.
|
|
1042
|
-
* @param bridge - the bridge outcome, when the numeric bridge ran.
|
|
1043
|
-
* @returns the combined status and a merged note.
|
|
1044
|
-
*/
|
|
1045
|
-
function combineOutcomes(byte, bridge) {
|
|
1046
|
-
if (bridge === void 0) return {
|
|
1047
|
-
status: byte.status,
|
|
1048
|
-
note: byte.note
|
|
1049
|
-
};
|
|
1050
|
-
const rank = {
|
|
1051
|
-
verified: 0,
|
|
1052
|
-
insufficient: 1,
|
|
1053
|
-
unverified: 2,
|
|
1054
|
-
contradicted: 3,
|
|
1055
|
-
disproven: 4
|
|
1056
|
-
};
|
|
1057
|
-
return {
|
|
1058
|
-
status: rank[bridge.status] > rank[byte.status] ? bridge.status : byte.status,
|
|
1059
|
-
note: `${byte.note} | ${bridge.note}`
|
|
1060
|
-
};
|
|
1061
|
-
}
|
|
1062
|
-
//#endregion
|
|
1063
|
-
//#region src/verify-sealed.ts
|
|
1064
|
-
/**
|
|
1065
|
-
* Read-only sealed-report verification: a deterministic, zero-network,
|
|
1066
|
-
* zero-model fallback that recomputes the seal and audit hashes and re-runs the
|
|
1067
|
-
* byte-level + integrity claim check. The optional model verifier (spawned over
|
|
1068
|
-
* `ctx.jobs`) is an enhancement over this machine check, never a replacement.
|
|
1069
|
-
* @module dsh-research-report/verify-sealed
|
|
1070
|
-
*/
|
|
1071
|
-
/** Read one optional journal; a missing journal is an empty string. */
|
|
1072
|
-
async function readOptional(deps, file) {
|
|
1073
|
-
try {
|
|
1074
|
-
return await deps.readFile(file);
|
|
1075
|
-
} catch {
|
|
1076
|
-
return "";
|
|
1077
|
-
}
|
|
1078
|
-
}
|
|
1079
|
-
/**
|
|
1080
|
-
* Deterministically verify a sealed report directory: recompute the seal hash,
|
|
1081
|
-
* the report hash, and the audit-journal hashes, re-run the byte-level +
|
|
1082
|
-
* integrity check for every claim, and confirm the gap/disproof sections exist.
|
|
1083
|
-
* Zero network, zero model.
|
|
1084
|
-
* @param reportDir - the sealed report directory.
|
|
1085
|
-
* @param expectedSealHash - the seal hash returned at assemble time.
|
|
1086
|
-
* @param deps - file and evidence readers.
|
|
1087
|
-
* @returns the verification result.
|
|
1088
|
-
*/
|
|
1089
|
-
async function verifySealedReport(reportDir, expectedSealHash, deps) {
|
|
1090
|
-
const manifestText = await deps.readFile(path.join(reportDir, "manifest.json"));
|
|
1091
|
-
const reportText = await deps.readFile(path.join(reportDir, "report.md"));
|
|
1092
|
-
const sealHash = sha256Of(manifestText);
|
|
1093
|
-
const manifest = JSON.parse(manifestText);
|
|
1094
|
-
const reportHashMatches = sha256Of(reportText) === manifest.reportSha256;
|
|
1095
|
-
const verificationText = await readOptional(deps, path.join(reportDir, "verification.jsonl"));
|
|
1096
|
-
const disconfirmationText = await readOptional(deps, path.join(reportDir, "disconfirmation.jsonl"));
|
|
1097
|
-
const verificationMatches = manifest.verification === void 0 ? verificationText === "" : sha256Of(verificationText) === manifest.verification.sha256;
|
|
1098
|
-
const disconfirmationMatches = manifest.disconfirmation === void 0 ? disconfirmationText === "" : sha256Of(disconfirmationText) === manifest.disconfirmation.sha256;
|
|
1099
|
-
const journalHashesMatch = verificationMatches && disconfirmationMatches;
|
|
1100
|
-
const claims = Array.isArray(manifest.claims) ? manifest.claims : [];
|
|
1101
|
-
const verdicts = Array.isArray(manifest.verdicts) ? manifest.verdicts : [];
|
|
1102
|
-
const claimChecks = [];
|
|
1103
|
-
for (const claim of claims) {
|
|
1104
|
-
const recorded = verdicts.find((verdict) => verdict.claimId === claim.id)?.status ?? "unverified";
|
|
1105
|
-
const contents = [];
|
|
1106
|
-
let integrityFailed = false;
|
|
1107
|
-
for (const evidenceId of claim.evidenceIds) {
|
|
1108
|
-
const read = await deps.readEvidenceContent(evidenceId);
|
|
1109
|
-
if (read === void 0 || read.integrity !== "ok") {
|
|
1110
|
-
integrityFailed = true;
|
|
1111
|
-
break;
|
|
1112
|
-
}
|
|
1113
|
-
contents.push(read.content);
|
|
1114
|
-
}
|
|
1115
|
-
const recomputed = integrityFailed ? "contradicted" : verifyClaimText(claim.text, contents).status;
|
|
1116
|
-
claimChecks.push({
|
|
1117
|
-
claimId: claim.id,
|
|
1118
|
-
recorded,
|
|
1119
|
-
recomputed,
|
|
1120
|
-
match: recorded === recomputed
|
|
1121
|
-
});
|
|
1122
|
-
}
|
|
1123
|
-
const gapSectionPresent = reportText.includes("## Appendix A: Claim verification");
|
|
1124
|
-
const disproofSectionPresent = reportText.includes("## Appendix D: Disconfirmation log");
|
|
1125
|
-
const sealHashMatches = sealHash === expectedSealHash;
|
|
1126
|
-
return {
|
|
1127
|
-
sealHash,
|
|
1128
|
-
sealHashMatches,
|
|
1129
|
-
reportHashMatches,
|
|
1130
|
-
journalHashesMatch,
|
|
1131
|
-
claimChecks,
|
|
1132
|
-
gapSectionPresent,
|
|
1133
|
-
disproofSectionPresent,
|
|
1134
|
-
ok: sealHashMatches && reportHashMatches && journalHashesMatch && gapSectionPresent && disproofSectionPresent
|
|
1135
|
-
};
|
|
1136
|
-
}
|
|
1137
|
-
/**
|
|
1138
|
-
* Render the machine-check section of `verifier-note.md` (deterministic).
|
|
1139
|
-
* @param result - the sealed-report verification result.
|
|
1140
|
-
* @returns the machine-check markdown (no trailing newline).
|
|
1141
|
-
*/
|
|
1142
|
-
function renderMachineCheckMarkdown(result) {
|
|
1143
|
-
const lines = [
|
|
1144
|
-
`- Seal hash (sha256 of manifest.json): \`${result.sealHash}\` → ${result.sealHashMatches ? "MATCH" : "MISMATCH"}`,
|
|
1145
|
-
`- Report hash (sha256 of report.md): → ${result.reportHashMatches ? "MATCH" : "MISMATCH"}`,
|
|
1146
|
-
`- Audit journal hashes (verification.jsonl + disconfirmation.jsonl): → ${result.journalHashesMatch ? "MATCH" : "MISMATCH"}`,
|
|
1147
|
-
`- Claims re-checked: ${result.claimChecks.filter((check) => check.match).length}/${result.claimChecks.length} status match`,
|
|
1148
|
-
`- Gap section (Appendix A): ${result.gapSectionPresent ? "present" : "MISSING"}`,
|
|
1149
|
-
`- Disproof section (Appendix D): ${result.disproofSectionPresent ? "present" : "MISSING"}`,
|
|
1150
|
-
`- Overall: ${result.ok ? "OK" : "FAILED"}`
|
|
1151
|
-
];
|
|
1152
|
-
const mismatches = result.claimChecks.filter((check) => !check.match);
|
|
1153
|
-
if (mismatches.length > 0) {
|
|
1154
|
-
lines.push("", "Claim re-check mismatches:");
|
|
1155
|
-
for (const check of mismatches) lines.push(`- ${check.claimId}: recorded ${check.recorded}, recomputed ${check.recomputed}`);
|
|
1156
|
-
}
|
|
1157
|
-
return lines.join("\n");
|
|
1158
|
-
}
|
|
1159
|
-
//#endregion
|
|
1160
|
-
//#region src/version.ts
|
|
1161
|
-
/**
|
|
1162
|
-
* Single source of truth for the plugin version (stamped by scripts/release.mjs).
|
|
1163
|
-
* @module dsh-research-report/version
|
|
1164
|
-
*/
|
|
1165
|
-
/** The published package version. */
|
|
1166
|
-
const VERSION = "0.2.0";
|
|
1167
|
-
//#endregion
|
|
1168
580
|
//#region src/provider-local.ts
|
|
1169
581
|
/**
|
|
1170
582
|
* The local Provider of the research-report seam: assembles the filesystem
|
|
@@ -2806,4 +2218,4 @@ function apply(ctx, config) {
|
|
|
2806
2218
|
});
|
|
2807
2219
|
}
|
|
2808
2220
|
//#endregion
|
|
2809
|
-
export { CONTRADICTED_MARK, CaptureError, Config, DISPROVEN_MARK, EvidenceLedger, GATHER_DEPTH_RESULTS, INSUFFICIENT_MARK, LedgerError, LocalResearchReportService, MANIFEST_SCHEMA, RequestValidationError, ResearchReportError, ResearchReportService, SealBlockedError, UNVERIFIED_MARK, VERSION, apply, buildManifest, captureFromFile, captureFromWeb, captureSnapshot, combineOutcomes, configFingerprint, contextLabelOf, extractCitations, gatherCandidates, inject, isDoiOrigin, isUrlOrigin, mapBridgeResults, name, normalizeDoi, normalizeNumber, renderMachineCheckMarkdown, renderReportMarkdown, resolveConfig, resolveWorkspacePath, serializeDisconfirmationJournal, serializeManifest, serializeVerificationJournal, sha256Of, slugify, toWorkspaceRelative, validateAssembleRequest, validateDoi, verifyClaimText, verifySealedReport, versionIdOf };
|
|
2221
|
+
export { CONTRADICTED_MARK, CaptureError, Config, DISPROVEN_MARK, EvidenceLedger, GATHER_DEPTH_RESULTS, INSUFFICIENT_MARK, LedgerError, LocalResearchReportService, MANIFEST_SCHEMA, RequestValidationError, ResearchReportError, ResearchReportService, SARIF_SCHEMA, SealBlockedError, UNVERIFIED_MARK, VERIFIER_TOOL, VERSION, apply, buildManifest, buildVerificationReport, captureFromFile, captureFromWeb, captureSnapshot, combineOutcomes, configFingerprint, contextLabelOf, extractCitations, gatherCandidates, inject, isDoiOrigin, isUrlOrigin, mapBridgeResults, name, normalizeDoi, normalizeNumber, renderMachineCheckMarkdown, renderReportMarkdown, renderSarif, renderVerificationJson, resolveConfig, resolveWorkspacePath, serializeDisconfirmationJournal, serializeManifest, serializeVerificationJournal, sha256Of, slugify, toWorkspaceRelative, validateAssembleRequest, validateDoi, verifyClaimText, verifySealedReport, versionIdOf };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Standalone sealed-report verifier CLI. One command recomputes the seal hash
|
|
3
|
+
* (SHA-256 of manifest.json), the report hash, and the audit-journal hashes,
|
|
4
|
+
* re-runs the byte-level + integrity claim check against the evidence ledger,
|
|
5
|
+
* and prints a SARIF 2.1.0 or JSON report. Zero network, zero model, zero
|
|
6
|
+
* @deepseek-ai imports — this entry bundles to a self-contained binary.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* dsh-research-verify --report <dir> [--seal <sha256>] [--ledger <dir>] [--format json|sarif]
|
|
10
|
+
*
|
|
11
|
+
* @module dsh-research-report/cli
|
|
12
|
+
*/
|
|
13
|
+
export {};
|
|
14
|
+
//# sourceMappingURL=cli.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../../src/cli.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG"}
|