dsh-research-report 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.
Files changed (71) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/LICENSE +201 -0
  3. package/README.es.md +155 -0
  4. package/README.hi.md +155 -0
  5. package/README.md +155 -0
  6. package/README.pt.md +155 -0
  7. package/README.zh.md +155 -0
  8. package/THIRD_PARTY_NOTICES.md +21 -0
  9. package/cordis.patch.yml +24 -0
  10. package/lib/index.js +2143 -0
  11. package/lib/types/assemble.d.ts +123 -0
  12. package/lib/types/assemble.d.ts.map +1 -0
  13. package/lib/types/assemble.js +239 -0
  14. package/lib/types/assemble.js.map +1 -0
  15. package/lib/types/config.d.ts +48 -0
  16. package/lib/types/config.d.ts.map +1 -0
  17. package/lib/types/config.js +60 -0
  18. package/lib/types/config.js.map +1 -0
  19. package/lib/types/gather.d.ts +119 -0
  20. package/lib/types/gather.d.ts.map +1 -0
  21. package/lib/types/gather.js +165 -0
  22. package/lib/types/gather.js.map +1 -0
  23. package/lib/types/index.d.ts +51 -0
  24. package/lib/types/index.d.ts.map +1 -0
  25. package/lib/types/index.js +71 -0
  26. package/lib/types/index.js.map +1 -0
  27. package/lib/types/ledger.d.ts +159 -0
  28. package/lib/types/ledger.d.ts.map +1 -0
  29. package/lib/types/ledger.js +276 -0
  30. package/lib/types/ledger.js.map +1 -0
  31. package/lib/types/provider-local.d.ts +137 -0
  32. package/lib/types/provider-local.d.ts.map +1 -0
  33. package/lib/types/provider-local.js +418 -0
  34. package/lib/types/provider-local.js.map +1 -0
  35. package/lib/types/service.d.ts +302 -0
  36. package/lib/types/service.d.ts.map +1 -0
  37. package/lib/types/service.js +31 -0
  38. package/lib/types/service.js.map +1 -0
  39. package/lib/types/tools/evidence-add.d.ts +36 -0
  40. package/lib/types/tools/evidence-add.d.ts.map +1 -0
  41. package/lib/types/tools/evidence-add.js +107 -0
  42. package/lib/types/tools/evidence-add.js.map +1 -0
  43. package/lib/types/tools/ledger-query.d.ts +42 -0
  44. package/lib/types/tools/ledger-query.d.ts.map +1 -0
  45. package/lib/types/tools/ledger-query.js +154 -0
  46. package/lib/types/tools/ledger-query.js.map +1 -0
  47. package/lib/types/tools/research-report.d.ts +67 -0
  48. package/lib/types/tools/research-report.d.ts.map +1 -0
  49. package/lib/types/tools/research-report.js +345 -0
  50. package/lib/types/tools/research-report.js.map +1 -0
  51. package/lib/types/verify.d.ts +128 -0
  52. package/lib/types/verify.d.ts.map +1 -0
  53. package/lib/types/verify.js +208 -0
  54. package/lib/types/verify.js.map +1 -0
  55. package/lib/types/version.d.ts +7 -0
  56. package/lib/types/version.d.ts.map +1 -0
  57. package/lib/types/version.js +7 -0
  58. package/lib/types/version.js.map +1 -0
  59. package/package.json +147 -0
  60. package/src/assemble.ts +302 -0
  61. package/src/config.ts +97 -0
  62. package/src/gather.ts +239 -0
  63. package/src/index.ts +139 -0
  64. package/src/ledger.ts +344 -0
  65. package/src/provider-local.ts +489 -0
  66. package/src/service.ts +322 -0
  67. package/src/tools/evidence-add.ts +132 -0
  68. package/src/tools/ledger-query.ts +191 -0
  69. package/src/tools/research-report.ts +424 -0
  70. package/src/verify.ts +285 -0
  71. package/src/version.ts +7 -0
@@ -0,0 +1,489 @@
1
+ /**
2
+ * The local Provider of the research-report seam: assembles the filesystem
3
+ * evidence ledger, the byte-level verifier, the optional numeric bridge, and
4
+ * the sealing renderer into the `ctx.researchReport` service implementation.
5
+ * @module dsh-research-report/provider-local
6
+ */
7
+
8
+ import { mkdir, writeFile } from 'node:fs/promises'
9
+ import path from 'node:path'
10
+ import type { Context } from '@deepseek-ai/cordis'
11
+ import { KNOWN_SESSION_EVENT_TYPES } from '@deepseek-ai/dsh-session'
12
+ import type { Session } from '@deepseek-ai/dsh-session'
13
+ import type { WebRuntime } from '@deepseek-ai/dsh-web'
14
+ import {
15
+ buildManifest,
16
+ configFingerprint,
17
+ renderReportMarkdown,
18
+ serializeManifest,
19
+ slugify,
20
+ validateAssembleRequest,
21
+ versionIdOf,
22
+ } from './assemble.ts'
23
+ import type { ReportPlan } from './assemble.ts'
24
+ import type { ResolvedConfig } from './config.ts'
25
+ import { captureSnapshot, gatherCandidates } from './gather.ts'
26
+ import type { CaptureDeps, GatherOutcome } from './gather.ts'
27
+ import { EvidenceLedger, LedgerError, sha256Of } from './ledger.ts'
28
+ import { ResearchReportService } from './service.ts'
29
+ import type {
30
+ AddEvidenceInput,
31
+ AssembleContext,
32
+ AssembleReportRequest,
33
+ AssembleReportResult,
34
+ ClaimRegistration,
35
+ ClaimVerdict,
36
+ ClaimView,
37
+ EvidenceIntegrity,
38
+ EvidenceRecord,
39
+ EvidenceView,
40
+ LedgerSummary,
41
+ VerdictStatus,
42
+ } from './service.ts'
43
+ import { combineOutcomes, mapBridgeResults, verifyClaimText } from './verify.ts'
44
+ import type { DataQualityBridge } from './verify.ts'
45
+ import { VERSION } from './version.ts'
46
+
47
+ /** A loud provider failure with a machine-routable code. */
48
+ export class ResearchReportError extends Error {
49
+ /** The machine-routable failure code. */
50
+ readonly code: 'EVIDENCE_TOO_LARGE' | 'CLAIM_UNKNOWN' | 'LEDGER'
51
+ constructor(code: ResearchReportError['code'], message: string) {
52
+ super(message)
53
+ this.name = 'ResearchReportError'
54
+ this.code = code
55
+ }
56
+ }
57
+
58
+ /**
59
+ * rc.6's persistence layer refuses a session log carrying an event type it
60
+ * does not know, and rc.6 offers no plugin event-registration surface — so the
61
+ * research-report/* events are appended only when the host build already knows
62
+ * them. The ledger journals are always the durable source of truth; these
63
+ * events are the in-log audit mirror and activate automatically once the host
64
+ * learns the vocabulary.
65
+ * @param session - the owning session, when known.
66
+ * @param type - the event type.
67
+ * @param append - the typed append thunk.
68
+ */
69
+ function appendAudit(session: Session | undefined, type: string, append: () => void): void {
70
+ if (session === undefined) return
71
+ if (!KNOWN_SESSION_EVENT_TYPES.has(type)) return
72
+ append()
73
+ }
74
+
75
+ /**
76
+ * The local `ctx.researchReport` implementation. Everything durable lives in
77
+ * the filesystem ledger; the service adds policy (caps), verification, and
78
+ * sealing on top.
79
+ */
80
+ export class LocalResearchReportService extends ResearchReportService {
81
+ /** The content-addressed ledger. */
82
+ private readonly ledger: EvidenceLedger
83
+ /** The resolved plugin config. */
84
+ private readonly config: ResolvedConfig
85
+ /** Absolute workspace root for local capture and path display. */
86
+ private readonly workspaceRoot: string
87
+
88
+ /**
89
+ * @param ctx - the plugin context.
90
+ * @param config - the resolved plugin config.
91
+ * @param workspaceRoot - absolute workspace root (the harness cwd).
92
+ */
93
+ constructor(ctx: Context, config: ResolvedConfig, workspaceRoot: string) {
94
+ super(ctx)
95
+ this.config = config
96
+ this.workspaceRoot = workspaceRoot
97
+ this.ledger = new EvidenceLedger(config.ledgerRoot)
98
+ }
99
+
100
+ /** The web seam, resolved at call time (HMR-safe; may be absent). */
101
+ private get web(): WebRuntime | undefined {
102
+ return this.ctx.get('web') as WebRuntime | undefined
103
+ }
104
+
105
+ /** The optional numeric bridge, resolved at call time (never injected). */
106
+ private get dataQuality(): DataQualityBridge | undefined {
107
+ return this.ctx.get('dataQuality') as unknown as DataQualityBridge | undefined
108
+ }
109
+
110
+ /** Capture dependencies for the gather/capture paths. */
111
+ private get captureDeps(): CaptureDeps {
112
+ return { web: this.web, fetchTimeoutMs: this.config.fetchTimeoutMs, workspaceRoot: this.workspaceRoot }
113
+ }
114
+
115
+ /**
116
+ * Register one evidence snapshot. Over-size content is refused loudly;
117
+ * same-content registrations dedupe.
118
+ * @param input - the snapshot and its provenance.
119
+ * @param session - the owning session (audit event), when known.
120
+ * @returns the durable record and whether this call created it.
121
+ */
122
+ async addEvidence(input: AddEvidenceInput, session?: Session): Promise<{ record: EvidenceRecord; deduplicated: boolean }> {
123
+ const bytes = Buffer.byteLength(input.content, 'utf8')
124
+ if (bytes > this.config.maxEvidenceBytes) {
125
+ throw new ResearchReportError(
126
+ 'EVIDENCE_TOO_LARGE',
127
+ `evidence content is ${bytes} bytes, above the configured maxEvidenceBytes ${this.config.maxEvidenceBytes}`,
128
+ )
129
+ }
130
+ const capturedAt = input.capturedAt ?? new Date().toISOString()
131
+ let outcome
132
+ try {
133
+ outcome = await this.ledger.putEvidence({
134
+ ...(input.id === undefined ? {} : { id: input.id }),
135
+ title: input.title,
136
+ origin: input.origin,
137
+ content: input.content,
138
+ capturedAt,
139
+ })
140
+ } catch (error) {
141
+ if (error instanceof LedgerError) throw new ResearchReportError('LEDGER', error.message)
142
+ throw error
143
+ }
144
+ appendAudit(session, 'research-report/evidence', () => {
145
+ session?.append('research-report/evidence', {
146
+ id: outcome.record.id,
147
+ hash: outcome.record.hash,
148
+ origin: outcome.record.origin,
149
+ title: outcome.record.title,
150
+ capturedAt: outcome.record.capturedAt,
151
+ bytes: outcome.record.bytes,
152
+ deduplicated: !outcome.created,
153
+ })
154
+ })
155
+ return { record: outcome.record, deduplicated: !outcome.created }
156
+ }
157
+
158
+ /**
159
+ * Capture one origin (URL via ctx.web, workspace path via fs) and register
160
+ * it. Provider-internal helper for the tools layer.
161
+ * @param origin - URL or workspace path.
162
+ * @param title - display title (defaults to the origin).
163
+ * @param signal - caller cancellation.
164
+ * @param session - the owning session (audit event), when known.
165
+ * @returns the durable record and whether this call created it.
166
+ */
167
+ async captureAndRegister(
168
+ origin: string,
169
+ title: string | undefined,
170
+ signal?: AbortSignal,
171
+ session?: Session,
172
+ ): Promise<{ record: EvidenceRecord; deduplicated: boolean }> {
173
+ const snapshot = await captureSnapshot(this.captureDeps, origin, signal)
174
+ return this.addEvidence({ title: title ?? snapshot.origin, origin: snapshot.origin, content: snapshot.content }, session)
175
+ }
176
+
177
+ /**
178
+ * Run one topic gather: search + snapshot capture + registration. Never
179
+ * auto-assembles; uncaptured sources land in the gap list.
180
+ * @param topic - the research topic.
181
+ * @param depth - quick | standard | deep.
182
+ * @param signal - caller cancellation.
183
+ * @param session - the owning session (audit events), when known.
184
+ * @returns candidates plus gaps.
185
+ */
186
+ async gather(topic: string, depth: 'quick' | 'standard' | 'deep', signal?: AbortSignal, session?: Session): Promise<GatherOutcome> {
187
+ return gatherCandidates(this.captureDeps, topic, depth, signal, async (input) => {
188
+ const { record } = await this.addEvidence(input, session)
189
+ return record
190
+ })
191
+ }
192
+
193
+ /**
194
+ * Verify one registered claim against its bound snapshots: integrity first
195
+ * (tampered/missing ⇒ contradicted), then the byte-level check, then the
196
+ * optional numeric bridge. The verdict is written back to the ledger.
197
+ * @param claimId - the claim to verify.
198
+ * @param session - the owning session (audit event), when known.
199
+ * @returns the fresh verdict.
200
+ */
201
+ async verifyClaim(claimId: string, session?: Session): Promise<ClaimVerdict> {
202
+ const claim = await this.ledger.getClaim(claimId)
203
+ if (claim === undefined) {
204
+ throw new ResearchReportError('CLAIM_UNKNOWN', `unknown claim id "${claimId}"`)
205
+ }
206
+ const verdict = await this.verifyRegistration(claim)
207
+ await this.ledger.recordVerdict(
208
+ {
209
+ claimId,
210
+ status: verdict.status,
211
+ ...(verdict.note === undefined ? {} : { note: verdict.note }),
212
+ },
213
+ new Date().toISOString(),
214
+ )
215
+ appendAudit(session, 'research-report/verify', () => {
216
+ session?.append('research-report/verify', {
217
+ claimId,
218
+ status: verdict.status,
219
+ ...(verdict.note === undefined ? {} : { note: verdict.note }),
220
+ evidenceIds: claim.evidenceIds,
221
+ })
222
+ })
223
+ return verdict
224
+ }
225
+
226
+ /** Compute the verdict for one claim registration (no writeback). */
227
+ private async verifyRegistration(claim: {
228
+ id: string
229
+ text: string
230
+ evidenceIds: string[]
231
+ dataset?: string
232
+ citations?: Array<{ id: string; path: string; value: number | string; tolerance?: number }>
233
+ }): Promise<ClaimVerdict> {
234
+ const contents: string[] = []
235
+ const broken: string[] = []
236
+ for (const evidenceId of claim.evidenceIds) {
237
+ const read = await this.ledger.readContent(evidenceId)
238
+ if (read === undefined) {
239
+ broken.push(`${evidenceId} (not in the ledger)`)
240
+ continue
241
+ }
242
+ if (read.integrity !== 'ok') {
243
+ broken.push(`${evidenceId} (${read.integrity}: object bytes no longer match the indexed hash)`)
244
+ continue
245
+ }
246
+ contents.push(read.content)
247
+ }
248
+ let byte
249
+ if (broken.length > 0) {
250
+ byte = {
251
+ status: 'contradicted' as const,
252
+ note: `bound evidence failed the integrity check: ${broken.join('; ')}`,
253
+ missing: [],
254
+ contradictions: broken,
255
+ }
256
+ } else {
257
+ byte = verifyClaimText(claim.text, contents)
258
+ }
259
+
260
+ let bridge: { status: VerdictStatus; note: string } | undefined
261
+ if (claim.dataset !== undefined && claim.citations !== undefined && claim.citations.length > 0) {
262
+ const dataQuality = this.dataQuality
263
+ if (dataQuality === undefined) {
264
+ bridge = {
265
+ status: 'unverified',
266
+ note: 'numeric dataset citations not cross-checked: ctx.dataQuality is not mounted (dsh-data-quality absent); byte-level check only',
267
+ }
268
+ } else {
269
+ try {
270
+ bridge = mapBridgeResults(await dataQuality.verifyCitations({ dataset: claim.dataset, citations: claim.citations }))
271
+ } catch (error) {
272
+ bridge = {
273
+ status: 'unverified',
274
+ note: `numeric dataset bridge failed: ${error instanceof Error ? error.message : String(error)}`,
275
+ }
276
+ }
277
+ }
278
+ }
279
+ const combined = combineOutcomes(byte, bridge)
280
+ return { claimId: claim.id, status: combined.status, note: combined.note }
281
+ }
282
+
283
+ /**
284
+ * Assemble and seal one report: validate (loud), register evidence and
285
+ * claims (idempotent; conflicts throw), verify every claim, render
286
+ * `report.md` with visible markers for unverified/contradicted claims, write
287
+ * `manifest.json`, and seal the versioned directory with the manifest hash.
288
+ * @param request - the frozen assemble request.
289
+ * @param context - optional assemble context (owning session for events).
290
+ * @returns the sealed directory, the seal hash, and the per-claim verdicts.
291
+ */
292
+ async assemble(request: AssembleReportRequest, context?: AssembleContext): Promise<AssembleReportResult> {
293
+ validateAssembleRequest(request, this.config)
294
+
295
+ // Evidence: the ledger is authoritative and immutable. New ids register;
296
+ // an existing id whose incoming content still matches the index is a
297
+ // no-op; a mismatch against an INTACT object is a loud conflict, while a
298
+ // mismatch explained by tampering flows into verification (contradicted).
299
+ const records: EvidenceRecord[] = []
300
+ for (const item of request.evidence) {
301
+ const existing = await this.ledger.getEvidence(item.id)
302
+ if (existing === undefined) {
303
+ const added = await this.addEvidence(
304
+ { id: item.id, title: item.title, origin: item.origin, content: item.content, capturedAt: item.capturedAt },
305
+ context?.session,
306
+ )
307
+ records.push(added.record)
308
+ continue
309
+ }
310
+ const incomingHash = sha256Of(item.content)
311
+ if (incomingHash !== existing.hash) {
312
+ const stored = await this.ledger.readContent(item.id)
313
+ if (stored !== undefined && stored.integrity === 'ok') {
314
+ throw new ResearchReportError(
315
+ 'LEDGER',
316
+ `evidence id "${item.id}" is already registered with different content; snapshots are immutable — choose a new id`,
317
+ )
318
+ }
319
+ // Tampered/missing object: verification over the current bytes will
320
+ // produce the contradicted verdict; the index keeps the original hash.
321
+ }
322
+ records.push(existing)
323
+ }
324
+
325
+ await this.ledger.registerClaims(
326
+ request.claims.map(claim => {
327
+ const registration = claim as ClaimRegistration
328
+ return {
329
+ id: registration.id,
330
+ text: registration.text,
331
+ evidenceIds: registration.evidenceIds,
332
+ ...(registration.dataset === undefined ? {} : { dataset: registration.dataset }),
333
+ ...(registration.citations === undefined ? {} : { citations: registration.citations }),
334
+ }
335
+ }),
336
+ new Date().toISOString(),
337
+ )
338
+
339
+ const verdicts: ClaimVerdict[] = []
340
+ for (const claim of request.claims) {
341
+ verdicts.push(await this.verifyClaim(claim.id, context?.session))
342
+ }
343
+
344
+ const generatedAt = new Date().toISOString()
345
+ const fingerprint = configFingerprint(this.config)
346
+ const plan: ReportPlan = { request, verdicts, evidence: records, generatedAt, fingerprint, pluginVersion: VERSION }
347
+ const reportText = renderReportMarkdown(plan)
348
+ const manifestText = serializeManifest(buildManifest(plan, sha256Of(reportText)))
349
+ const sealHash = sha256Of(manifestText)
350
+
351
+ const reportDir = await this.freshReportDir(request.topic, new Date(generatedAt))
352
+ await writeFile(path.join(reportDir, 'report.md'), reportText, 'utf8')
353
+ await writeFile(path.join(reportDir, 'manifest.json'), manifestText, 'utf8')
354
+
355
+ appendAudit(context?.session, 'research-report/seal', () => {
356
+ context?.session?.append('research-report/seal', {
357
+ reportDir,
358
+ sealHash,
359
+ topic: request.topic,
360
+ title: request.title,
361
+ verdicts,
362
+ })
363
+ })
364
+ return { reportDir, sealHash, verdicts }
365
+ }
366
+
367
+ /** Allocate the next versioned report directory for one topic (UTC clock). */
368
+ private async freshReportDir(topic: string, at: Date): Promise<string> {
369
+ const base = path.join(this.config.reportRoot, slugify(topic))
370
+ await mkdir(base, { recursive: true })
371
+ const stamp = versionIdOf(at)
372
+ for (let suffix = 0; ; suffix++) {
373
+ const candidate = path.join(base, suffix === 0 ? stamp : `${stamp}-${suffix + 1}`)
374
+ try {
375
+ await mkdir(candidate)
376
+ return candidate
377
+ } catch (error) {
378
+ if ((error as NodeJS.ErrnoException).code !== 'EEXIST') throw error
379
+ }
380
+ }
381
+ }
382
+
383
+ /**
384
+ * Read one evidence item (re-hashed on read).
385
+ * @param evidenceId - the ledger id.
386
+ * @returns the view, or undefined when unknown.
387
+ */
388
+ async getEvidence(evidenceId: string): Promise<EvidenceView | undefined> {
389
+ const record = await this.ledger.getEvidence(evidenceId)
390
+ if (record === undefined) return undefined
391
+ const read = await this.ledger.readContent(evidenceId)
392
+ const integrity: EvidenceIntegrity = read === undefined ? 'missing' : read.integrity
393
+ return { ...record, integrity }
394
+ }
395
+
396
+ /**
397
+ * Read one snapshot's bytes (re-hashed on read).
398
+ * @param evidenceId - the ledger id.
399
+ * @returns content plus integrity, or undefined when unknown.
400
+ */
401
+ async readEvidenceContent(evidenceId: string): Promise<{ content: string; integrity: EvidenceIntegrity } | undefined> {
402
+ return this.ledger.readContent(evidenceId)
403
+ }
404
+
405
+ /**
406
+ * Read one claim with its latest verdict.
407
+ * @param claimId - the claim id.
408
+ * @returns the view, or undefined when unknown.
409
+ */
410
+ async getClaim(claimId: string): Promise<ClaimView | undefined> {
411
+ const claim = await this.ledger.getClaim(claimId)
412
+ if (claim === undefined) return undefined
413
+ const verdict = (await this.ledger.latestVerdicts()).get(claimId)
414
+ return {
415
+ id: claim.id,
416
+ text: claim.text,
417
+ evidenceIds: claim.evidenceIds,
418
+ ...(claim.dataset === undefined ? {} : { dataset: claim.dataset }),
419
+ ...(verdict === undefined
420
+ ? {}
421
+ : {
422
+ verdict: {
423
+ claimId: verdict.claimId,
424
+ status: verdict.status,
425
+ ...(verdict.note === undefined ? {} : { note: verdict.note }),
426
+ at: verdict.at,
427
+ },
428
+ }),
429
+ }
430
+ }
431
+
432
+ /**
433
+ * List every registered evidence item (re-hashed on read).
434
+ * @returns all evidence views in registration order.
435
+ */
436
+ async listEvidence(): Promise<EvidenceView[]> {
437
+ const records = await this.ledger.listEvidence()
438
+ const views: EvidenceView[] = []
439
+ for (const record of records) {
440
+ const read = await this.ledger.readContent(record.id)
441
+ views.push({ ...record, integrity: read === undefined ? 'missing' : read.integrity })
442
+ }
443
+ return views
444
+ }
445
+
446
+ /**
447
+ * List every registered claim with its latest verdict.
448
+ * @returns all claim views in registration order.
449
+ */
450
+ async listClaims(): Promise<ClaimView[]> {
451
+ const claims = await this.ledger.listClaims()
452
+ const verdicts = await this.ledger.latestVerdicts()
453
+ return claims.map(claim => {
454
+ const verdict = verdicts.get(claim.id)
455
+ return {
456
+ id: claim.id,
457
+ text: claim.text,
458
+ evidenceIds: claim.evidenceIds,
459
+ ...(claim.dataset === undefined ? {} : { dataset: claim.dataset }),
460
+ ...(verdict === undefined
461
+ ? {}
462
+ : {
463
+ verdict: {
464
+ claimId: verdict.claimId,
465
+ status: verdict.status,
466
+ ...(verdict.note === undefined ? {} : { note: verdict.note }),
467
+ at: verdict.at,
468
+ },
469
+ }),
470
+ }
471
+ })
472
+ }
473
+
474
+ /**
475
+ * Aggregate ledger counts (evidence re-hashed for the tamper count).
476
+ * @returns the summary.
477
+ */
478
+ async summarize(): Promise<LedgerSummary> {
479
+ const evidence = await this.listEvidence()
480
+ const claims = await this.ledger.listClaims()
481
+ const verdicts = await this.ledger.latestVerdicts()
482
+ return {
483
+ evidenceCount: evidence.length,
484
+ claimCount: claims.length,
485
+ verdictCount: verdicts.size,
486
+ tamperedCount: evidence.filter(item => item.integrity !== 'ok').length,
487
+ }
488
+ }
489
+ }