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
package/src/service.ts ADDED
@@ -0,0 +1,322 @@
1
+ /**
2
+ * Service Definition of the verifiable research-report seam (`ctx.researchReport`).
3
+ *
4
+ * Three roles in one package (they evolve together): this file owns the
5
+ * Definition — the frozen `assemble` contract, the evidence/claim vocabulary,
6
+ * and the typed session events; `provider-local.ts` owns the local Provider
7
+ * (filesystem ledger + byte-level verification); `tools/` owns the model-facing
8
+ * Consumers.
9
+ *
10
+ * The `ReportSectionInput` / `EvidenceInput` / `AssembleReportRequest` /
11
+ * `AssembleReportResult` block below is BYTE-FROZEN: sibling plugins
12
+ * (dsh-industry-research) consume `ctx.researchReport.assemble` against this
13
+ * exact text. `scripts/verify-frozen-contract.mjs` gates drift.
14
+ *
15
+ * @module dsh-research-report/service
16
+ */
17
+
18
+ import { Context, Service } from '@deepseek-ai/cordis'
19
+
20
+ // ── Frozen contract (do not edit — see the module doc) ──────────────────────
21
+
22
+ export interface ReportSectionInput {
23
+ heading: string
24
+ /** Paragraphs; each claim string may carry citations. */
25
+ paragraphs: Array<{ text: string; claimIds?: string[] }>
26
+ }
27
+ export interface EvidenceInput {
28
+ id: string
29
+ title: string
30
+ /** Where the evidence came from (URL or workspace path). */
31
+ origin: string
32
+ /** Verbatim content snapshot used for byte-level checks. */
33
+ content: string
34
+ /** ISO-8601 time the snapshot was captured. */
35
+ capturedAt: string
36
+ }
37
+ export interface AssembleReportRequest {
38
+ title: string
39
+ topic: string
40
+ evidence: EvidenceInput[]
41
+ sections: ReportSectionInput[]
42
+ /** Every claim id referenced in sections must be registered here. */
43
+ claims: Array<{ id: string; text: string; evidenceIds: string[] }>
44
+ }
45
+ export interface AssembleReportResult {
46
+ /** Workspace path of the sealed report directory (report.md + manifest.json). */
47
+ reportDir: string
48
+ /** SHA-256 content hash of manifest.json. */
49
+ sealHash: string
50
+ /** Per-claim verification verdicts. */
51
+ verdicts: Array<{ claimId: string; status: 'verified' | 'unverified' | 'contradicted'; note?: string }>
52
+ }
53
+
54
+ // ── Extended internal vocabulary (not part of the frozen block) ─────────────
55
+
56
+ /** The three-state verification verdict of one claim. */
57
+ export type VerdictStatus = 'verified' | 'unverified' | 'contradicted'
58
+
59
+ /** One claim's verification outcome (same fields as the frozen result item). */
60
+ export interface ClaimVerdict {
61
+ /** The claim this verdict belongs to. */
62
+ claimId: string
63
+ /** Byte-level (and optional numeric-bridge) outcome. */
64
+ status: VerdictStatus
65
+ /** Human-readable evidence note (missing citations, contradiction detail, …). */
66
+ note?: string
67
+ }
68
+
69
+ /**
70
+ * Claim registration accepted by the tools layer: the frozen claim shape plus
71
+ * the OPTIONAL numeric-bridge extension consumed when the claim's numbers cite
72
+ * a structured workspace dataset and `ctx.dataQuality` is mounted.
73
+ */
74
+ export interface ClaimRegistration {
75
+ /** Stable claim id chosen by the caller. */
76
+ id: string
77
+ /** The claim text; numbers and quoted spans are byte-checked against evidence. */
78
+ text: string
79
+ /** Ledger evidence ids this claim binds to. */
80
+ evidenceIds: string[]
81
+ /** Workspace-relative dataset path (CSV/JSON) for the optional numeric bridge. */
82
+ dataset?: string
83
+ /** Dataset citations for `ctx.dataQuality.verifyCitations` (requires `dataset`). */
84
+ citations?: Array<{
85
+ /** Stable id chosen by the caller, echoed back in results. */
86
+ id: string
87
+ /** JSON-path-ish locator, e.g. "rows[3].nav". */
88
+ path: string
89
+ /** The value as cited in the claim. */
90
+ value: number | string
91
+ /** Optional relative tolerance for numeric comparison, e.g. 0.01 = 1%. */
92
+ tolerance?: number
93
+ }>
94
+ }
95
+
96
+ /** Input for registering one evidence snapshot in the ledger. */
97
+ export interface AddEvidenceInput {
98
+ /** Caller-chosen evidence id; omitted derives a content-addressed `ev-<hash12>` id. */
99
+ id?: string
100
+ /** Display title. */
101
+ title: string
102
+ /** Where the evidence came from (URL or workspace path). */
103
+ origin: string
104
+ /** Verbatim content snapshot used for byte-level checks. */
105
+ content: string
106
+ /** ISO-8601 capture time; defaults to the registration clock. */
107
+ capturedAt?: string
108
+ }
109
+
110
+ /** Durable ledger facts of one evidence snapshot. */
111
+ export interface EvidenceRecord {
112
+ /** Ledger id (caller-chosen or `ev-<hash12>`). */
113
+ id: string
114
+ /** SHA-256 hex of the snapshot content (the object address). */
115
+ hash: string
116
+ /** Display title. */
117
+ title: string
118
+ /** Where the evidence came from (URL or workspace path). */
119
+ origin: string
120
+ /** ISO-8601 capture time. */
121
+ capturedAt: string
122
+ /** UTF-8 byte length of the snapshot. */
123
+ bytes: number
124
+ }
125
+
126
+ /** Integrity state of a stored snapshot, recomputed on every read. */
127
+ export type EvidenceIntegrity = 'ok' | 'tampered' | 'missing'
128
+
129
+ /** Read view of one ledger evidence item; content stays in the object store. */
130
+ export interface EvidenceView extends EvidenceRecord {
131
+ /** `ok` when the object bytes still hash to the indexed value. */
132
+ integrity: EvidenceIntegrity
133
+ }
134
+
135
+ /** Stored verdict record (latest write wins on read). */
136
+ export interface StoredVerdict extends ClaimVerdict {
137
+ /** ISO-8601 time the verdict was written. */
138
+ at: string
139
+ }
140
+
141
+ /** Read view of one registered claim and its latest verdict, when any. */
142
+ export interface ClaimView {
143
+ /** The claim id. */
144
+ id: string
145
+ /** The claim text. */
146
+ text: string
147
+ /** Bound evidence ids. */
148
+ evidenceIds: string[]
149
+ /** Optional numeric-bridge dataset citation carried from registration. */
150
+ dataset?: string
151
+ /** Latest stored verdict; absent when the claim was never verified. */
152
+ verdict?: StoredVerdict
153
+ }
154
+
155
+ /** Aggregate ledger counts for summary queries. */
156
+ export interface LedgerSummary {
157
+ /** Registered evidence count. */
158
+ evidenceCount: number
159
+ /** Registered claim count. */
160
+ claimCount: number
161
+ /** Claims carrying a stored verdict. */
162
+ verdictCount: number
163
+ /** Evidence items whose object failed the read-time re-hash. */
164
+ tamperedCount: number
165
+ }
166
+
167
+ /** Optional assemble-time context: the owning session for event logging. */
168
+ export interface AssembleContext {
169
+ /** The session whose log receives the research-report/* events, when known. */
170
+ session?: import('@deepseek-ai/dsh-session').Session
171
+ }
172
+
173
+ // ── Session events (typed, merge-extended) ──────────────────────────────────
174
+
175
+ declare module '@deepseek-ai/dsh-session/types' {
176
+ interface SessionEventMap {
177
+ /**
178
+ * One evidence snapshot entered the ledger (id ↔ content hash binding).
179
+ * Log-only audit record; the ledger itself is the durable source of truth.
180
+ * @mode emit
181
+ * @param id - ledger evidence id.
182
+ * @param hash - SHA-256 of the snapshot bytes.
183
+ * @param origin - where the evidence came from (URL or workspace path).
184
+ * @param title - display title.
185
+ * @param capturedAt - ISO-8601 capture time.
186
+ * @param bytes - UTF-8 byte length.
187
+ * @param deduplicated - true when the content was already stored.
188
+ */
189
+ 'research-report/evidence': {
190
+ id: string
191
+ hash: string
192
+ origin: string
193
+ title: string
194
+ capturedAt: string
195
+ bytes: number
196
+ deduplicated: boolean
197
+ }
198
+ /**
199
+ * One claim's verification verdict was written back to the ledger.
200
+ * Log-only audit record; `verdicts.jsonl` is the durable source of truth.
201
+ * @mode emit
202
+ * @param claimId - the verified claim.
203
+ * @param status - verified | unverified | contradicted.
204
+ * @param note - human-readable evidence note.
205
+ * @param evidenceIds - the bindings the verdict was computed over.
206
+ */
207
+ 'research-report/verify': {
208
+ claimId: string
209
+ status: VerdictStatus
210
+ note?: string
211
+ evidenceIds: string[]
212
+ }
213
+ /**
214
+ * A report directory was sealed (manifest written, hash computed).
215
+ * Log-only audit record; `manifest.json` is the durable source of truth.
216
+ * @mode emit
217
+ * @param reportDir - workspace path of the sealed report directory.
218
+ * @param sealHash - SHA-256 of manifest.json.
219
+ * @param topic - the report topic.
220
+ * @param title - the report title.
221
+ * @param verdicts - the per-claim outcomes the seal covers.
222
+ */
223
+ 'research-report/seal': {
224
+ reportDir: string
225
+ sealHash: string
226
+ topic: string
227
+ title: string
228
+ verdicts: ClaimVerdict[]
229
+ }
230
+ }
231
+ }
232
+
233
+ // ── Background-job kind ─────────────────────────────────────────────────────
234
+
235
+ declare module '@deepseek-ai/dsh-jobs' {
236
+ interface JobKindMap {
237
+ /** Report assembly jobs started by the research_report tool. */
238
+ 'research-report': 'research-report'
239
+ }
240
+ }
241
+
242
+ // ── The service ─────────────────────────────────────────────────────────────
243
+
244
+ declare module '@deepseek-ai/cordis' {
245
+ interface Context {
246
+ researchReport: ResearchReportService
247
+ }
248
+ }
249
+
250
+ /**
251
+ * The verifiable research-report service (`ctx.researchReport`).
252
+ *
253
+ * `assemble` is the frozen cross-plugin surface: validate the request, verify
254
+ * every claim against the ledger's immutable snapshots, render `report.md`
255
+ * (unverified/contradicted claims stay visibly marked in the body), write
256
+ * `manifest.json`, and seal the directory with the manifest's SHA-256.
257
+ */
258
+ export abstract class ResearchReportService extends Service {
259
+ constructor(ctx: Context) {
260
+ super(ctx, 'researchReport')
261
+ }
262
+
263
+ /**
264
+ * Assemble and seal one report.
265
+ * @param request - the frozen assemble request (see the module-level block).
266
+ * @returns the sealed directory, its seal hash, and the per-claim verdicts.
267
+ */
268
+ abstract assemble(request: AssembleReportRequest): Promise<AssembleReportResult>
269
+
270
+ /**
271
+ * Register one evidence snapshot (content-addressed; same content dedupes).
272
+ * @param input - the snapshot and its provenance.
273
+ * @returns the durable record and whether this call created it.
274
+ */
275
+ abstract addEvidence(input: AddEvidenceInput): Promise<{ record: EvidenceRecord; deduplicated: boolean }>
276
+
277
+ /**
278
+ * Re-run verification for one registered claim and write back the verdict.
279
+ * @param claimId - the claim to verify.
280
+ * @returns the fresh verdict.
281
+ */
282
+ abstract verifyClaim(claimId: string): Promise<ClaimVerdict>
283
+
284
+ /**
285
+ * Read one evidence item; the snapshot is re-hashed on every read.
286
+ * @param evidenceId - the ledger id.
287
+ * @returns the view (with integrity), or undefined when unknown.
288
+ */
289
+ abstract getEvidence(evidenceId: string): Promise<EvidenceView | undefined>
290
+
291
+ /**
292
+ * Read the raw snapshot bytes of one evidence item (verification path).
293
+ * @param evidenceId - the ledger id.
294
+ * @returns the content plus integrity state, or undefined when unknown.
295
+ */
296
+ abstract readEvidenceContent(evidenceId: string): Promise<{ content: string; integrity: EvidenceIntegrity } | undefined>
297
+
298
+ /**
299
+ * Read one registered claim with its latest verdict.
300
+ * @param claimId - the claim id.
301
+ * @returns the view, or undefined when unknown.
302
+ */
303
+ abstract getClaim(claimId: string): Promise<ClaimView | undefined>
304
+
305
+ /**
306
+ * List every registered evidence item (re-hashed on read).
307
+ * @returns all evidence views in registration order.
308
+ */
309
+ abstract listEvidence(): Promise<EvidenceView[]>
310
+
311
+ /**
312
+ * List every registered claim.
313
+ * @returns all claim views in registration order.
314
+ */
315
+ abstract listClaims(): Promise<ClaimView[]>
316
+
317
+ /**
318
+ * Aggregate ledger counts.
319
+ * @returns the summary (evidence/claim/verdict/tamper counts).
320
+ */
321
+ abstract summarize(): Promise<LedgerSummary>
322
+ }
@@ -0,0 +1,132 @@
1
+ /**
2
+ * The `evidence_add` model tool (Consumer): register one evidence snapshot in
3
+ * the ledger. `content` given inline is used verbatim; absent, the origin is
4
+ * captured — a URL through the `ctx.web` seam, a workspace path from disk.
5
+ * @module dsh-research-report/tools/evidence-add
6
+ */
7
+
8
+ import { defineTool } from '@deepseek-ai/dsh-tools'
9
+ import { CaptureError } from '../gather.ts'
10
+ import { ResearchReportError } from '../provider-local.ts'
11
+ import type { LocalResearchReportService } from '../provider-local.ts'
12
+
13
+ /** The canonical success value. */
14
+ interface EvidenceAddSuccess {
15
+ ok: true
16
+ evidenceId: string
17
+ hash: string
18
+ bytes: number
19
+ title: string
20
+ origin: string
21
+ capturedAt: string
22
+ deduplicated: boolean
23
+ }
24
+
25
+ /** The canonical domain-failure value (capability absence still throws). */
26
+ interface EvidenceAddFailure {
27
+ ok: false
28
+ error: { code: string; message: string }
29
+ }
30
+
31
+ /** The `evidence_add` canonical value. */
32
+ export type EvidenceAddValue = EvidenceAddSuccess | EvidenceAddFailure
33
+
34
+ /** The tool's output schema (both branches). */
35
+ const OUTPUT_SCHEMA = {
36
+ type: 'object',
37
+ additionalProperties: false,
38
+ properties: {
39
+ ok: { type: 'boolean', required: true },
40
+ evidenceId: { type: 'string' },
41
+ hash: { type: 'string' },
42
+ bytes: { type: 'integer' },
43
+ title: { type: 'string' },
44
+ origin: { type: 'string' },
45
+ capturedAt: { type: 'string' },
46
+ deduplicated: { type: 'boolean' },
47
+ error: {
48
+ type: 'object',
49
+ additionalProperties: false,
50
+ properties: {
51
+ code: { type: 'string', required: true },
52
+ message: { type: 'string', required: true },
53
+ },
54
+ },
55
+ },
56
+ } as const
57
+
58
+ /**
59
+ * Build the `evidence_add` tool bound to the local provider.
60
+ * @param service - the local research-report provider.
61
+ * @returns the tool definition.
62
+ */
63
+ export function makeEvidenceAddTool(service: LocalResearchReportService) {
64
+ return defineTool({
65
+ name: 'evidence_add',
66
+ description: [
67
+ 'Register one evidence snapshot in the verifiable research ledger (dsh-research-report).',
68
+ '',
69
+ 'Pass `content` inline when you already hold the text; otherwise the origin is captured for you — a URL is fetched through the harness web capability (ctx.web), a workspace-relative path is read from disk. Snapshots are content-addressed and immutable: the same content is stored once, and any later byte change is detected as tampering during verification.',
70
+ '',
71
+ 'Returns the evidence id and its SHA-256 hash. Bind the id to claims in research_report.',
72
+ ].join('\n'),
73
+ parameters: {
74
+ origin: {
75
+ type: 'string',
76
+ required: true,
77
+ description: 'Where the evidence comes from: an http(s) URL or a workspace-relative path.',
78
+ },
79
+ content: {
80
+ type: 'string',
81
+ description: 'The verbatim snapshot text. When omitted, the origin is captured (URL fetched / file read).',
82
+ },
83
+ title: {
84
+ type: 'string',
85
+ description: 'Display title (defaults to the origin).',
86
+ },
87
+ },
88
+ output: {
89
+ schema: OUTPUT_SCHEMA,
90
+ render: (_args, value) => {
91
+ const result = value as EvidenceAddValue
92
+ if (!result.ok) {
93
+ return [{ type: 'text', text: `evidence_add failed (${result.error.code}): ${result.error.message}` }]
94
+ }
95
+ return [{
96
+ type: 'text',
97
+ text: `evidence registered: ${result.evidenceId}${result.deduplicated ? ' (already stored — deduplicated)' : ''}\nsha256: ${result.hash}\norigin: ${result.origin}\nbytes: ${result.bytes}`,
98
+ }]
99
+ },
100
+ },
101
+ async execute(args, exec) {
102
+ exec.signal.throwIfAborted()
103
+ const session = exec.agent?.session
104
+ try {
105
+ const added = args.content !== undefined
106
+ ? await service.addEvidence(
107
+ { title: args.title ?? args.origin, origin: args.origin, content: args.content },
108
+ session,
109
+ )
110
+ : await service.captureAndRegister(args.origin, args.title, exec.signal, session)
111
+ return {
112
+ ok: true as const,
113
+ evidenceId: added.record.id,
114
+ hash: added.record.hash,
115
+ bytes: added.record.bytes,
116
+ title: added.record.title,
117
+ origin: added.record.origin,
118
+ capturedAt: added.record.capturedAt,
119
+ deduplicated: added.deduplicated,
120
+ }
121
+ } catch (error) {
122
+ // The web capability being absent for a URL origin is a deployment
123
+ // fact: fail loudly (isError) so the model stops retrying URLs.
124
+ if (error instanceof CaptureError && error.code === 'WEB_UNAVAILABLE') throw error
125
+ if (error instanceof CaptureError || error instanceof ResearchReportError) {
126
+ return { ok: false as const, error: { code: error.code, message: error.message } }
127
+ }
128
+ throw error
129
+ }
130
+ },
131
+ })
132
+ }
@@ -0,0 +1,191 @@
1
+ /**
2
+ * The `ledger_query` model tool (Consumer): read-only queries over the
3
+ * evidence ledger — bindings, verdicts, and the live integrity re-check.
4
+ * @module dsh-research-report/tools/ledger-query
5
+ */
6
+
7
+ import { defineTool } from '@deepseek-ai/dsh-tools'
8
+ import type { LocalResearchReportService } from '../provider-local.ts'
9
+ import type { ClaimView, EvidenceView } from '../service.ts'
10
+
11
+ /** The evidence branch of the canonical value. */
12
+ interface EvidenceBranch {
13
+ kind: 'evidence'
14
+ evidence: EvidenceView
15
+ }
16
+
17
+ /** The claim branch of the canonical value. */
18
+ interface ClaimBranch {
19
+ kind: 'claim'
20
+ claim: ClaimView
21
+ }
22
+
23
+ /** The summary branch (no id given). */
24
+ interface SummaryBranch {
25
+ kind: 'summary'
26
+ evidenceCount: number
27
+ claimCount: number
28
+ verdictCount: number
29
+ tamperedCount: number
30
+ evidenceIds: string[]
31
+ claimIds: string[]
32
+ }
33
+
34
+ /** The not-found branch (unknown id — query semantics, not an error). */
35
+ interface NotFoundBranch {
36
+ kind: 'not-found'
37
+ message: string
38
+ }
39
+
40
+ /** The `ledger_query` canonical value. */
41
+ export type LedgerQueryValue = EvidenceBranch | ClaimBranch | SummaryBranch | NotFoundBranch
42
+
43
+ /** The evidence view schema fragment. */
44
+ const evidenceViewSchema = {
45
+ type: 'object',
46
+ additionalProperties: false,
47
+ properties: {
48
+ id: { type: 'string', required: true },
49
+ hash: { type: 'string', required: true },
50
+ title: { type: 'string', required: true },
51
+ origin: { type: 'string', required: true },
52
+ capturedAt: { type: 'string', required: true },
53
+ bytes: { type: 'integer', required: true },
54
+ integrity: { type: 'string', required: true, enum: ['ok', 'tampered', 'missing'] },
55
+ },
56
+ } as const
57
+
58
+ /** The claim view schema fragment. */
59
+ const claimViewSchema = {
60
+ type: 'object',
61
+ additionalProperties: false,
62
+ properties: {
63
+ id: { type: 'string', required: true },
64
+ text: { type: 'string', required: true },
65
+ evidenceIds: { type: 'array', required: true, items: { type: 'string' } },
66
+ dataset: { type: 'string' },
67
+ verdict: {
68
+ type: 'object',
69
+ additionalProperties: false,
70
+ properties: {
71
+ claimId: { type: 'string', required: true },
72
+ status: { type: 'string', required: true, enum: ['verified', 'unverified', 'contradicted'] },
73
+ note: { type: 'string' },
74
+ at: { type: 'string', required: true },
75
+ },
76
+ },
77
+ },
78
+ } as const
79
+
80
+ /** The tool's output schema (all four branches). */
81
+ const OUTPUT_SCHEMA = {
82
+ type: 'object',
83
+ additionalProperties: false,
84
+ properties: {
85
+ kind: { type: 'string', required: true, enum: ['evidence', 'claim', 'summary', 'not-found'] },
86
+ evidence: evidenceViewSchema,
87
+ claim: claimViewSchema,
88
+ evidenceCount: { type: 'integer' },
89
+ claimCount: { type: 'integer' },
90
+ verdictCount: { type: 'integer' },
91
+ tamperedCount: { type: 'integer' },
92
+ evidenceIds: { type: 'array', items: { type: 'string' } },
93
+ claimIds: { type: 'array', items: { type: 'string' } },
94
+ message: { type: 'string' },
95
+ },
96
+ } as const
97
+
98
+ /** Render the canonical value as model-facing text. */
99
+ function renderValue(value: LedgerQueryValue): { type: 'text'; text: string }[] {
100
+ switch (value.kind) {
101
+ case 'evidence': {
102
+ const item = value.evidence
103
+ const lines = [
104
+ `evidence ${item.id}${item.integrity === 'ok' ? '' : ` — INTEGRITY ${item.integrity.toUpperCase()}`}`,
105
+ ` title: ${item.title}`,
106
+ ` origin: ${item.origin}`,
107
+ ` sha256: ${item.hash}`,
108
+ ` captured: ${item.capturedAt} (${item.bytes} bytes)`,
109
+ ]
110
+ if (item.integrity !== 'ok') {
111
+ lines.push(` WARNING: the stored bytes no longer match the indexed hash — any claim bound to this evidence verifies as contradicted`)
112
+ }
113
+ return [{ type: 'text', text: lines.join('\n') }]
114
+ }
115
+ case 'claim': {
116
+ const claim = value.claim
117
+ const lines = [
118
+ `claim ${claim.id}`,
119
+ ` text: ${claim.text}`,
120
+ ` evidence: ${claim.evidenceIds.join(', ') || '(none)'}`,
121
+ ]
122
+ if (claim.dataset !== undefined) lines.push(` dataset: ${claim.dataset}`)
123
+ if (claim.verdict === undefined) {
124
+ lines.push(' verdict: (never verified)')
125
+ } else {
126
+ lines.push(` verdict: ${claim.verdict.status} at ${claim.verdict.at}${claim.verdict.note === undefined ? '' : ` — ${claim.verdict.note}`}`)
127
+ }
128
+ return [{ type: 'text', text: lines.join('\n') }]
129
+ }
130
+ case 'summary':
131
+ return [{
132
+ type: 'text',
133
+ text: [
134
+ `ledger summary: ${value.evidenceCount} evidence, ${value.claimCount} claims, ${value.verdictCount} verdicts, ${value.tamperedCount} integrity failures`,
135
+ `evidence ids: ${value.evidenceIds.join(', ') || '(none)'}`,
136
+ `claim ids: ${value.claimIds.join(', ') || '(none)'}`,
137
+ ].join('\n'),
138
+ }]
139
+ case 'not-found':
140
+ return [{ type: 'text', text: value.message }]
141
+ }
142
+ }
143
+
144
+ /**
145
+ * Build the `ledger_query` tool bound to the local provider.
146
+ * @param service - the local research-report provider.
147
+ * @returns the tool definition.
148
+ */
149
+ export function makeLedgerQueryTool(service: LocalResearchReportService) {
150
+ return defineTool({
151
+ name: 'ledger_query',
152
+ description: [
153
+ 'Read-only query over the verifiable research ledger (dsh-research-report): claim ↔ evidence bindings and verification verdicts.',
154
+ 'Pass claimId or evidenceId for one entry (evidence is re-hashed on read — integrity tampered/missing is reported explicitly), or neither for a ledger summary.',
155
+ ].join('\n'),
156
+ parameters: {
157
+ claimId: { type: 'string', description: 'Query one claim: its bindings and latest verdict.' },
158
+ evidenceId: { type: 'string', description: 'Query one evidence item: provenance, hash, live integrity.' },
159
+ },
160
+ output: {
161
+ schema: OUTPUT_SCHEMA,
162
+ render: (_args, value) => renderValue(value as LedgerQueryValue),
163
+ },
164
+ async execute(args, exec): Promise<LedgerQueryValue> {
165
+ exec.signal.throwIfAborted()
166
+ if (args.claimId !== undefined) {
167
+ const claim = await service.getClaim(args.claimId)
168
+ return claim === undefined
169
+ ? { kind: 'not-found', message: `no claim "${args.claimId}" in the ledger` }
170
+ : { kind: 'claim', claim }
171
+ }
172
+ if (args.evidenceId !== undefined) {
173
+ const evidence = await service.getEvidence(args.evidenceId)
174
+ return evidence === undefined
175
+ ? { kind: 'not-found', message: `no evidence "${args.evidenceId}" in the ledger` }
176
+ : { kind: 'evidence', evidence }
177
+ }
178
+ const [summary, evidence, claims] = await Promise.all([
179
+ service.summarize(),
180
+ service.listEvidence(),
181
+ service.listClaims(),
182
+ ])
183
+ return {
184
+ kind: 'summary',
185
+ ...summary,
186
+ evidenceIds: evidence.map(item => item.id),
187
+ claimIds: claims.map(claim => claim.id),
188
+ }
189
+ },
190
+ })
191
+ }