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/verify.ts ADDED
@@ -0,0 +1,285 @@
1
+ /**
2
+ * Claim verification: the built-in byte-level check plus the optional numeric
3
+ * bridge to `ctx.dataQuality`.
4
+ *
5
+ * Byte-level semantics (v1 is deliberately NON-semantic): every number and
6
+ * quoted span in the claim text must be locatable VERBATIM in the bound
7
+ * evidence snapshots. A citation that cannot be located makes the claim
8
+ * `unverified`; a number whose left-context label appears in the snapshot
9
+ * followed by a DIFFERENT number makes the claim `contradicted`. The check
10
+ * proves "the claimed literals exist in the captured bytes", nothing more.
11
+ *
12
+ * The `CitationCheckRequest` / `CitationCheckResult` block below is BYTE-FROZEN:
13
+ * it is the structural surface of the optional `ctx.dataQuality` service
14
+ * (provided by the sibling dsh-data-quality plugin, consumed via `ctx.get` —
15
+ * never imported, never injected). `scripts/verify-frozen-contract.mjs` gates
16
+ * drift.
17
+ *
18
+ * @module dsh-research-report/verify
19
+ */
20
+
21
+ // ── Frozen contract (do not edit — see the module doc) ──────────────────────
22
+
23
+ export interface CitationCheckRequest {
24
+ /** Workspace-relative path of the source dataset snapshot (CSV/JSON). */
25
+ dataset: string
26
+ /** Citations to verify against the dataset. */
27
+ citations: Array<{
28
+ /** Stable id chosen by the caller, echoed back in results. */
29
+ id: string
30
+ /** JSON-path-ish locator, e.g. "rows[3].nav" or "summary.annualReturn". */
31
+ path: string
32
+ /** The value as cited in the document. */
33
+ value: number | string
34
+ /** Optional relative tolerance for numeric comparison, e.g. 0.01 = 1%. */
35
+ tolerance?: number
36
+ }>
37
+ }
38
+ export interface CitationCheckResult {
39
+ results: Array<{
40
+ id: string
41
+ status: 'verified' | 'mismatch' | 'not-found' | 'unverifiable'
42
+ /** Actual value found at path, when found. */
43
+ actual?: number | string
44
+ /** Human-readable evidence note. */
45
+ note?: string
46
+ }>
47
+ }
48
+
49
+ // ── The optional bridge surface ─────────────────────────────────────────────
50
+
51
+ /**
52
+ * The structural surface of the optional `ctx.dataQuality` service. Declared
53
+ * locally (never imported from the sibling package) and reached via
54
+ * `ctx.get('dataQuality')` + an `as unknown as` assertion.
55
+ */
56
+ export interface DataQualityBridge {
57
+ /**
58
+ * Verify citations against a structured dataset snapshot.
59
+ * @param request - the dataset and its citations.
60
+ * @returns per-citation outcomes.
61
+ */
62
+ verifyCitations(request: CitationCheckRequest): Promise<CitationCheckResult>
63
+ }
64
+
65
+ // ── Byte-level citation extraction ──────────────────────────────────────────
66
+
67
+ /** One literal citation extracted from a claim text. */
68
+ export interface Citation {
69
+ /** What kind of literal this is. */
70
+ kind: 'number' | 'quote'
71
+ /** The literal text that must be locatable verbatim in bound evidence. */
72
+ text: string
73
+ /** Left-context label of a number citation (drives the contradiction check). */
74
+ context?: string
75
+ }
76
+
77
+ /** Number literal: optional sign, grouped digits, decimals, trailing %. */
78
+ const NUMBER_PATTERN = /-?\d[\d,]*(?:\.\d+)?%?/gu
79
+
80
+ /** Quoted spans: ASCII double quotes, CJK corner brackets, full-width quotes. */
81
+ const QUOTE_PATTERNS = [
82
+ /"([^"\n]{4,200})"/gu,
83
+ /「([^」\n]{2,200})」/gu,
84
+ /“([^“”\n]{4,200})”/gu,
85
+ ]
86
+
87
+ /** The tail run of label characters (letters / CJK) ending a context window. */
88
+ const LABEL_PATTERN = /[\p{L}\p{N}_()()%$-]{2,24}$/u
89
+
90
+ /**
91
+ * Extract the trailing context label of a number citation: up to 24 characters
92
+ * before the number, trimmed to its trailing label run. Too-short labels are
93
+ * dropped (a weak label would false-positive the contradiction check).
94
+ * @param text - the full claim text.
95
+ * @param index - offset of the number in the text.
96
+ * @returns the label, or undefined when there is no usable one.
97
+ */
98
+ export function contextLabelOf(text: string, index: number): string | undefined {
99
+ const window = text.slice(Math.max(0, index - 24), index).trimEnd()
100
+ const match = LABEL_PATTERN.exec(window)
101
+ const label = match?.[0].trim()
102
+ if (label === undefined || label.length < 2) return undefined
103
+ return label
104
+ }
105
+
106
+ /**
107
+ * Extract every checkable citation from one claim text: number literals (with
108
+ * their left-context labels) and quoted spans.
109
+ * @param claimText - the claim to analyze.
110
+ * @returns citations in first-seen order (duplicates kept once).
111
+ */
112
+ export function extractCitations(claimText: string): Citation[] {
113
+ const citations: Citation[] = []
114
+ const seen = new Set<string>()
115
+ for (const match of claimText.matchAll(NUMBER_PATTERN)) {
116
+ const text = match[0]
117
+ if (seen.has(`number:${text}`)) continue
118
+ seen.add(`number:${text}`)
119
+ const context = contextLabelOf(claimText, match.index)
120
+ citations.push(context === undefined ? { kind: 'number', text } : { kind: 'number', text, context })
121
+ }
122
+ for (const pattern of QUOTE_PATTERNS) {
123
+ for (const match of claimText.matchAll(pattern)) {
124
+ const text = match[1]!
125
+ if (seen.has(`quote:${text}`)) continue
126
+ seen.add(`quote:${text}`)
127
+ citations.push({ kind: 'quote', text })
128
+ }
129
+ }
130
+ return citations
131
+ }
132
+
133
+ // ── Byte-level verification ─────────────────────────────────────────────────
134
+
135
+ /** The number token scan window after a context label (bytes of text). */
136
+ const CONTEXT_SCAN_WINDOW = 24
137
+
138
+ /** Normalize a number literal for comparison (drop grouping commas and %). */
139
+ export function normalizeNumber(text: string): string {
140
+ return text.replace(/[,%]/gu, '')
141
+ }
142
+
143
+ /** The first number literal found in `text` from `from` (fresh regex — no shared lastIndex). */
144
+ function numberAfter(text: string, from: number): string | undefined {
145
+ const window = text.slice(from, from + CONTEXT_SCAN_WINDOW)
146
+ const match = /-?\d[\d,]*(?:\.\d+)?%?/u.exec(window)
147
+ return match?.[0]
148
+ }
149
+
150
+ /** The outcome of the byte-level check of one claim. */
151
+ export interface ByteCheckOutcome {
152
+ /** The three-state verdict. */
153
+ status: 'verified' | 'unverified' | 'contradicted'
154
+ /** Human-readable evidence note. */
155
+ note: string
156
+ /** Citations that could not be located verbatim. */
157
+ missing: string[]
158
+ /** Contradiction details (`label: claimed X, snapshot says Y`). */
159
+ contradictions: string[]
160
+ }
161
+
162
+ /** Cap how many problem citations one note enumerates. */
163
+ const NOTE_LIST_CAP = 5
164
+
165
+ /**
166
+ * Run the byte-level check of one claim against its bound evidence snapshots.
167
+ * @param claimText - the claim text.
168
+ * @param evidenceContents - the verbatim snapshot contents of the bound evidence.
169
+ * @returns the outcome (never throws).
170
+ */
171
+ export function verifyClaimText(claimText: string, evidenceContents: readonly string[]): ByteCheckOutcome {
172
+ if (evidenceContents.length === 0) {
173
+ return { status: 'unverified', note: 'claim binds no evidence snapshot', missing: [], contradictions: [] }
174
+ }
175
+ const haystack = evidenceContents.join('\n')
176
+ const citations = extractCitations(claimText)
177
+ if (citations.length === 0) {
178
+ return {
179
+ status: 'unverified',
180
+ note: 'claim carries no checkable citation (number or quoted span); byte-level verification needs a literal to locate',
181
+ missing: [],
182
+ contradictions: [],
183
+ }
184
+ }
185
+ const missing: string[] = []
186
+ const contradictions: string[] = []
187
+ for (const citation of citations) {
188
+ if (citation.kind === 'quote') {
189
+ if (!haystack.includes(citation.text)) missing.push(`"${citation.text}"`)
190
+ continue
191
+ }
192
+ // Number citation, presence-first: a number locatable verbatim supports
193
+ // the claim. The contradiction check runs only when the claimed number is
194
+ // ABSENT — then a label occurrence carrying a different number means the
195
+ // snapshot explicitly contradicts the claim (the audit-critical signal).
196
+ if (haystack.includes(citation.text)) continue
197
+ if (citation.context !== undefined) {
198
+ let searchFrom = 0
199
+ let different: string | undefined
200
+ for (;;) {
201
+ const at = haystack.indexOf(citation.context, searchFrom)
202
+ if (at === -1) break
203
+ const found = numberAfter(haystack, at + citation.context.length)
204
+ if (found !== undefined && normalizeNumber(found) !== normalizeNumber(citation.text)) {
205
+ different = found
206
+ break
207
+ }
208
+ searchFrom = at + citation.context.length
209
+ }
210
+ if (different !== undefined) {
211
+ contradictions.push(`${citation.context}: claim says ${citation.text}, snapshot says ${different}`)
212
+ continue
213
+ }
214
+ }
215
+ missing.push(citation.text)
216
+ }
217
+ if (contradictions.length > 0) {
218
+ return {
219
+ status: 'contradicted',
220
+ note: `contradicts the snapshot: ${contradictions.slice(0, NOTE_LIST_CAP).join('; ')}`,
221
+ missing,
222
+ contradictions,
223
+ }
224
+ }
225
+ if (missing.length > 0) {
226
+ return {
227
+ status: 'unverified',
228
+ note: `citation(s) not found in bound evidence: ${missing.slice(0, NOTE_LIST_CAP).join(', ')}`,
229
+ missing,
230
+ contradictions,
231
+ }
232
+ }
233
+ return {
234
+ status: 'verified',
235
+ note: `${citations.length} citation(s) located verbatim in the bound snapshot(s)`,
236
+ missing,
237
+ contradictions,
238
+ }
239
+ }
240
+
241
+ // ── The numeric bridge mapping ──────────────────────────────────────────────
242
+
243
+ /**
244
+ * Map one bridge result set onto the three-state verdict vocabulary.
245
+ * `mismatch` maps to `contradicted`; `not-found`/`unverifiable` map to
246
+ * `unverified`.
247
+ * @param result - the dataQuality outcome.
248
+ * @returns the mapped status plus a human-readable note.
249
+ */
250
+ export function mapBridgeResults(result: CitationCheckResult): { status: 'verified' | 'unverified' | 'contradicted'; note: string } {
251
+ const mismatch = result.results.filter(entry => entry.status === 'mismatch')
252
+ if (mismatch.length > 0) {
253
+ const detail = mismatch
254
+ .slice(0, NOTE_LIST_CAP)
255
+ .map(entry => `${entry.id}: dataset has ${String(entry.actual ?? '?')}${entry.note === undefined ? '' : ` (${entry.note})`}`)
256
+ .join('; ')
257
+ return { status: 'contradicted', note: `dataset cross-check mismatch: ${detail}` }
258
+ }
259
+ const unresolved = result.results.filter(entry => entry.status === 'not-found' || entry.status === 'unverifiable')
260
+ if (unresolved.length > 0) {
261
+ const detail = unresolved
262
+ .slice(0, NOTE_LIST_CAP)
263
+ .map(entry => `${entry.id}: ${entry.status}${entry.note === undefined ? '' : ` (${entry.note})`}`)
264
+ .join('; ')
265
+ return { status: 'unverified', note: `dataset cross-check unresolved: ${detail}` }
266
+ }
267
+ return { status: 'verified', note: `${result.results.length} dataset citation(s) verified via ctx.dataQuality` }
268
+ }
269
+
270
+ /**
271
+ * Combine the byte-level and bridge outcomes: `contradicted` wins, then
272
+ * `unverified`, then `verified`.
273
+ * @param byte - the byte-level outcome.
274
+ * @param bridge - the bridge outcome, when the numeric bridge ran.
275
+ * @returns the combined status and a merged note.
276
+ */
277
+ export function combineOutcomes(
278
+ byte: ByteCheckOutcome,
279
+ bridge: { status: 'verified' | 'unverified' | 'contradicted'; note: string } | undefined,
280
+ ): { status: 'verified' | 'unverified' | 'contradicted'; note: string } {
281
+ if (bridge === undefined) return { status: byte.status, note: byte.note }
282
+ const rank = { verified: 0, unverified: 1, contradicted: 2 } as const
283
+ const status = rank[bridge.status] > rank[byte.status] ? bridge.status : byte.status
284
+ return { status, note: `${byte.note} | ${bridge.note}` }
285
+ }
package/src/version.ts ADDED
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Single source of truth for the plugin version (stamped by scripts/release.mjs).
3
+ * @module dsh-research-report/version
4
+ */
5
+
6
+ /** The published package version. */
7
+ export const VERSION = '0.1.0'