@seed-ship/mcp-ui-solid 3.0.5 → 4.0.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 (126) hide show
  1. package/CHANGELOG.md +115 -0
  2. package/README.md +253 -280
  3. package/dist/components/ChartJSRenderer.cjs +37 -15
  4. package/dist/components/ChartJSRenderer.cjs.map +1 -1
  5. package/dist/components/ChartJSRenderer.d.ts.map +1 -1
  6. package/dist/components/ChartJSRenderer.js +37 -15
  7. package/dist/components/ChartJSRenderer.js.map +1 -1
  8. package/dist/components/DataPreviewSection.cjs +172 -0
  9. package/dist/components/DataPreviewSection.cjs.map +1 -0
  10. package/dist/components/DataPreviewSection.d.ts +19 -0
  11. package/dist/components/DataPreviewSection.d.ts.map +1 -0
  12. package/dist/components/DataPreviewSection.js +172 -0
  13. package/dist/components/DataPreviewSection.js.map +1 -0
  14. package/dist/components/MapRenderer.cjs +168 -26
  15. package/dist/components/MapRenderer.cjs.map +1 -1
  16. package/dist/components/MapRenderer.d.ts +2 -2
  17. package/dist/components/MapRenderer.d.ts.map +1 -1
  18. package/dist/components/MapRenderer.js +169 -27
  19. package/dist/components/MapRenderer.js.map +1 -1
  20. package/dist/components/ScratchpadPanel.cjs +74 -0
  21. package/dist/components/ScratchpadPanel.cjs.map +1 -1
  22. package/dist/components/ScratchpadPanel.d.ts.map +1 -1
  23. package/dist/components/ScratchpadPanel.js +75 -1
  24. package/dist/components/ScratchpadPanel.js.map +1 -1
  25. package/dist/components/VerifiedText.cjs +166 -0
  26. package/dist/components/VerifiedText.cjs.map +1 -0
  27. package/dist/components/VerifiedText.d.ts +22 -0
  28. package/dist/components/VerifiedText.d.ts.map +1 -0
  29. package/dist/components/VerifiedText.js +166 -0
  30. package/dist/components/VerifiedText.js.map +1 -0
  31. package/dist/components/index.d.ts +4 -0
  32. package/dist/components/index.d.ts.map +1 -1
  33. package/dist/components.cjs +4 -0
  34. package/dist/components.cjs.map +1 -1
  35. package/dist/components.d.cts +4 -0
  36. package/dist/components.d.ts +4 -0
  37. package/dist/components.js +4 -0
  38. package/dist/components.js.map +1 -1
  39. package/dist/hooks/index.d.ts +2 -0
  40. package/dist/hooks/index.d.ts.map +1 -1
  41. package/dist/hooks/useDataValidator.cjs +31 -0
  42. package/dist/hooks/useDataValidator.cjs.map +1 -0
  43. package/dist/hooks/useDataValidator.d.ts +42 -0
  44. package/dist/hooks/useDataValidator.d.ts.map +1 -0
  45. package/dist/hooks/useDataValidator.js +31 -0
  46. package/dist/hooks/useDataValidator.js.map +1 -0
  47. package/dist/hooks.cjs +2 -0
  48. package/dist/hooks.cjs.map +1 -1
  49. package/dist/hooks.d.cts +2 -0
  50. package/dist/hooks.d.ts +2 -0
  51. package/dist/hooks.js +2 -0
  52. package/dist/hooks.js.map +1 -1
  53. package/dist/index.cjs +8 -0
  54. package/dist/index.cjs.map +1 -1
  55. package/dist/index.d.cts +9 -5
  56. package/dist/index.d.ts +9 -5
  57. package/dist/index.d.ts.map +1 -1
  58. package/dist/index.js +8 -0
  59. package/dist/index.js.map +1 -1
  60. package/dist/node_modules/.pnpm/@mapbox_point-geometry@1.1.0/node_modules/@mapbox/point-geometry/index.cjs +290 -0
  61. package/dist/node_modules/.pnpm/@mapbox_point-geometry@1.1.0/node_modules/@mapbox/point-geometry/index.cjs.map +1 -0
  62. package/dist/node_modules/.pnpm/@mapbox_point-geometry@1.1.0/node_modules/@mapbox/point-geometry/index.js +291 -0
  63. package/dist/node_modules/.pnpm/@mapbox_point-geometry@1.1.0/node_modules/@mapbox/point-geometry/index.js.map +1 -0
  64. package/dist/node_modules/.pnpm/@mapbox_vector-tile@2.0.4/node_modules/@mapbox/vector-tile/index.cjs +243 -0
  65. package/dist/node_modules/.pnpm/@mapbox_vector-tile@2.0.4/node_modules/@mapbox/vector-tile/index.cjs.map +1 -0
  66. package/dist/node_modules/.pnpm/@mapbox_vector-tile@2.0.4/node_modules/@mapbox/vector-tile/index.js +243 -0
  67. package/dist/node_modules/.pnpm/@mapbox_vector-tile@2.0.4/node_modules/@mapbox/vector-tile/index.js.map +1 -0
  68. package/dist/node_modules/.pnpm/color2k@2.0.3/node_modules/color2k/dist/index.exports.import.es.cjs +137 -0
  69. package/dist/node_modules/.pnpm/color2k@2.0.3/node_modules/color2k/dist/index.exports.import.es.cjs.map +1 -0
  70. package/dist/node_modules/.pnpm/color2k@2.0.3/node_modules/color2k/dist/index.exports.import.es.js +137 -0
  71. package/dist/node_modules/.pnpm/color2k@2.0.3/node_modules/color2k/dist/index.exports.import.es.js.map +1 -0
  72. package/dist/node_modules/.pnpm/pbf@4.0.1/node_modules/pbf/index.cjs +686 -0
  73. package/dist/node_modules/.pnpm/pbf@4.0.1/node_modules/pbf/index.cjs.map +1 -0
  74. package/dist/node_modules/.pnpm/pbf@4.0.1/node_modules/pbf/index.js +687 -0
  75. package/dist/node_modules/.pnpm/pbf@4.0.1/node_modules/pbf/index.js.map +1 -0
  76. package/dist/node_modules/.pnpm/pmtiles@3.2.1/node_modules/pmtiles/dist/index.cjs +1366 -0
  77. package/dist/node_modules/.pnpm/pmtiles@3.2.1/node_modules/pmtiles/dist/index.cjs.map +1 -0
  78. package/dist/node_modules/.pnpm/pmtiles@3.2.1/node_modules/pmtiles/dist/index.js +1366 -0
  79. package/dist/node_modules/.pnpm/pmtiles@3.2.1/node_modules/pmtiles/dist/index.js.map +1 -0
  80. package/dist/node_modules/.pnpm/potpack@1.0.2/node_modules/potpack/index.cjs +54 -0
  81. package/dist/node_modules/.pnpm/potpack@1.0.2/node_modules/potpack/index.cjs.map +1 -0
  82. package/dist/node_modules/.pnpm/potpack@1.0.2/node_modules/potpack/index.js +55 -0
  83. package/dist/node_modules/.pnpm/potpack@1.0.2/node_modules/potpack/index.js.map +1 -0
  84. package/dist/node_modules/.pnpm/protomaps-leaflet@4.1.1/node_modules/protomaps-leaflet/dist/esm/index.cjs +1256 -0
  85. package/dist/node_modules/.pnpm/protomaps-leaflet@4.1.1/node_modules/protomaps-leaflet/dist/esm/index.cjs.map +1 -0
  86. package/dist/node_modules/.pnpm/protomaps-leaflet@4.1.1/node_modules/protomaps-leaflet/dist/esm/index.js +1256 -0
  87. package/dist/node_modules/.pnpm/protomaps-leaflet@4.1.1/node_modules/protomaps-leaflet/dist/esm/index.js.map +1 -0
  88. package/dist/node_modules/.pnpm/quickselect@2.0.0/node_modules/quickselect/index.cjs +47 -0
  89. package/dist/node_modules/.pnpm/quickselect@2.0.0/node_modules/quickselect/index.cjs.map +1 -0
  90. package/dist/node_modules/.pnpm/quickselect@2.0.0/node_modules/quickselect/index.js +48 -0
  91. package/dist/node_modules/.pnpm/quickselect@2.0.0/node_modules/quickselect/index.js.map +1 -0
  92. package/dist/node_modules/.pnpm/rbush@3.0.1/node_modules/rbush/index.cjs +378 -0
  93. package/dist/node_modules/.pnpm/rbush@3.0.1/node_modules/rbush/index.cjs.map +1 -0
  94. package/dist/node_modules/.pnpm/rbush@3.0.1/node_modules/rbush/index.js +379 -0
  95. package/dist/node_modules/.pnpm/rbush@3.0.1/node_modules/rbush/index.js.map +1 -0
  96. package/dist/services/data-validator.cjs +85 -0
  97. package/dist/services/data-validator.cjs.map +1 -0
  98. package/dist/services/data-validator.d.ts +28 -0
  99. package/dist/services/data-validator.d.ts.map +1 -0
  100. package/dist/services/data-validator.js +85 -0
  101. package/dist/services/data-validator.js.map +1 -0
  102. package/dist/services/index.d.ts +1 -0
  103. package/dist/services/index.d.ts.map +1 -1
  104. package/dist/types/chat-bus.d.ts +88 -1
  105. package/dist/types/chat-bus.d.ts.map +1 -1
  106. package/dist/types/index.d.ts +135 -6
  107. package/dist/types/index.d.ts.map +1 -1
  108. package/dist/types.d.cts +135 -6
  109. package/dist/types.d.ts +135 -6
  110. package/package.json +5 -1
  111. package/src/components/ChartJSRenderer.tsx +35 -13
  112. package/src/components/DataPreviewSection.tsx +206 -0
  113. package/src/components/MapRenderer.test.tsx +94 -5
  114. package/src/components/MapRenderer.tsx +246 -45
  115. package/src/components/ScratchpadPanel.tsx +10 -2
  116. package/src/components/VerifiedText.tsx +187 -0
  117. package/src/components/index.ts +7 -0
  118. package/src/hooks/index.ts +7 -0
  119. package/src/hooks/useDataValidator.ts +68 -0
  120. package/src/index.ts +26 -1
  121. package/src/services/data-validator.test.ts +151 -0
  122. package/src/services/data-validator.ts +149 -0
  123. package/src/services/index.ts +2 -0
  124. package/src/types/chat-bus.ts +98 -1
  125. package/src/types/index.ts +145 -6
  126. package/tsconfig.tsbuildinfo +1 -1
package/src/index.ts CHANGED
@@ -44,6 +44,10 @@ export { ChatPrompt } from './components/ChatPrompt'
44
44
  export { ScratchpadPanel } from './components/ScratchpadPanel'
45
45
  export { dispatchScratchpad, useScratchpadState } from './stores/scratchpad-store'
46
46
 
47
+ // Data Verification Components (v3.1.0)
48
+ export { VerifiedText } from './components/VerifiedText'
49
+ export { DataPreviewSection } from './components/DataPreviewSection'
50
+
47
51
  // Autocomplete Components
48
52
  export { GhostText, GhostTextInput } from './components/GhostText'
49
53
  export { AutocompleteDropdown } from './components/AutocompleteDropdown'
@@ -62,6 +66,8 @@ export type { ExpandableWrapperProps } from './components/ExpandableWrapper'
62
66
  export type { ComponentToolbarProps, ToolbarAction, ToolbarIcon } from './components/ComponentToolbar'
63
67
  export type { ChatPromptProps } from './components/ChatPrompt'
64
68
  export type { ScratchpadPanelProps } from './components/ScratchpadPanel'
69
+ export type { VerifiedTextProps } from './components/VerifiedText'
70
+ export type { DataPreviewSectionProps } from './components/DataPreviewSection'
65
71
  export type { GhostTextProps, GhostTextInputProps } from './components/GhostText'
66
72
  export type { AutocompleteDropdownProps } from './components/AutocompleteDropdown'
67
73
  export type { AutocompleteFormFieldProps, AutocompleteFormFieldParams } from './components/AutocompleteFormField'
@@ -81,6 +87,8 @@ export {
81
87
  useResize,
82
88
  // Autocomplete hooks
83
89
  useAutocomplete,
90
+ // Data Validator hooks (v3.1.0)
91
+ useDataValidator,
84
92
  } from './hooks'
85
93
 
86
94
  export type {
@@ -107,6 +115,9 @@ export type {
107
115
  // Autocomplete types
108
116
  UseAutocompleteOptions,
109
117
  UseAutocompleteReturn,
118
+ // Data Validator types (v3.1.0)
119
+ UseDataValidatorOptions,
120
+ UseDataValidatorReturn,
110
121
  } from './hooks'
111
122
 
112
123
  // Context (Phase 5.0)
@@ -177,10 +188,14 @@ export type {
177
188
  GalleryImage,
178
189
  ImageGalleryParams,
179
190
  VideoComponentParams,
180
- // Code & Maps types (Sprint 6)
191
+ // Code & Maps types (Sprint 6 + v3.1.0)
181
192
  CodeComponentParams,
182
193
  MapMarker,
183
194
  MapComponentParams,
195
+ MapPopupConfig,
196
+ MapGeoJSONStyle,
197
+ MapLayer,
198
+ MapPMTilesConfig,
184
199
  // Validation options (v2.0.0)
185
200
  IframePolicy,
186
201
  ValidationOptions,
@@ -217,6 +232,7 @@ export {
217
232
  createCommandHandler,
218
233
  createChatBus,
219
234
  mergeScratchpadSections,
235
+ validateAgainstSource,
220
236
  } from './services'
221
237
 
222
238
  // Chat Bus Types (v2.4.0 — @experimental)
@@ -246,4 +262,13 @@ export type {
246
262
  Citation,
247
263
  ToolCallEvent,
248
264
  ClarificationEvent,
265
+ // Data Validation types (v3.1.0)
266
+ DataValidation,
267
+ LLMNumber,
268
+ HallucinatedNumber,
269
+ DataValidationOptions,
270
+ VerifiedTextContent,
271
+ DataPreviewColumn,
272
+ DataPreviewContent,
273
+ MapSectionContent,
249
274
  } from './types/chat-bus'
@@ -0,0 +1,151 @@
1
+ /**
2
+ * Tests for validateAgainstSource — anti-hallucination data validator
3
+ */
4
+
5
+ import { describe, it, expect } from 'vitest'
6
+ import { validateAgainstSource } from './data-validator'
7
+
8
+ const SAMPLE_ROWS = [
9
+ { type: 'Appartement', ventes: 22306, prix_m2: 3337 },
10
+ { type: 'Maison', ventes: 2492, prix_m2: 4230 },
11
+ ]
12
+
13
+ describe('validateAgainstSource', () => {
14
+ it('validates text with exact source numbers', () => {
15
+ const text = 'On observe 22 306 ventes appartements et 3 337 EUR/m2 moyen.'
16
+ const result = validateAgainstSource(text, SAMPLE_ROWS)
17
+
18
+ expect(result.valid).toBe(true)
19
+ expect(result.hallucinated).toHaveLength(0)
20
+ expect(result.confidence).toBe(1)
21
+ })
22
+
23
+ it('detects hallucinated numbers', () => {
24
+ const text = 'On observe 18 245 ventes en 2023, prix moyen 2 850 EUR.'
25
+ const result = validateAgainstSource(text, SAMPLE_ROWS)
26
+
27
+ expect(result.valid).toBe(false)
28
+ expect(result.hallucinated.length).toBeGreaterThan(0)
29
+ expect(result.confidence).toBeLessThan(1)
30
+ })
31
+
32
+ it('returns hallucinated items with closest source number', () => {
33
+ const text = 'On observe 18 245 ventes.'
34
+ const result = validateAgainstSource(text, SAMPLE_ROWS)
35
+
36
+ expect(result.hallucinated).toHaveLength(1)
37
+ expect(result.hallucinated[0].value).toBe(18245)
38
+ expect(result.hallucinated[0].closest).toBeDefined()
39
+ expect(result.hallucinated[0].distance).toBeDefined()
40
+ })
41
+
42
+ it('accepts rounding within tolerance', () => {
43
+ // 3337 rounded to 3340 is 0.09% — within 1% tolerance
44
+ const text = 'Le prix moyen est de 3 340 EUR.'
45
+ const result = validateAgainstSource(text, SAMPLE_ROWS, { tolerance: 0.01 })
46
+
47
+ expect(result.valid).toBe(true)
48
+ })
49
+
50
+ it('rejects rounding beyond tolerance', () => {
51
+ // 3337 vs 3500 is ~4.9% — beyond 1% tolerance
52
+ const text = 'Le prix moyen est de 3 500 EUR.'
53
+ const result = validateAgainstSource(text, SAMPLE_ROWS, { tolerance: 0.01 })
54
+
55
+ expect(result.valid).toBe(false)
56
+ })
57
+
58
+ it('ignores years by default', () => {
59
+ const text = 'En 2023, on observe 22 306 ventes.'
60
+ const result = validateAgainstSource(text, SAMPLE_ROWS)
61
+
62
+ // 2023 should be ignored, 22306 should be verified
63
+ expect(result.valid).toBe(true)
64
+ expect(result.llmNumbers).toHaveLength(1) // only 22306
65
+ })
66
+
67
+ it('ignores postal/INSEE codes by default', () => {
68
+ const text = 'Code commune 34172, on observe 22 306 ventes.'
69
+ const result = validateAgainstSource(text, SAMPLE_ROWS)
70
+
71
+ // 34172 should be ignored (5-digit pattern)
72
+ expect(result.valid).toBe(true)
73
+ })
74
+
75
+ it('ignores specified columns', () => {
76
+ const rows = [
77
+ { code_geo: '34172', ventes: 22306 },
78
+ ]
79
+ const text = 'Le code est 34172 avec 22 306 ventes.'
80
+ const result = validateAgainstSource(text, rows, {
81
+ ignoreColumns: ['code_geo'],
82
+ })
83
+
84
+ expect(result.valid).toBe(true)
85
+ })
86
+
87
+ it('handles string-encoded numbers in source', () => {
88
+ const rows = [
89
+ { prix: '3 337', total: '22306' },
90
+ ]
91
+ const text = 'Prix 3 337, total 22 306.'
92
+ const result = validateAgainstSource(text, rows)
93
+
94
+ expect(result.valid).toBe(true)
95
+ })
96
+
97
+ it('returns confidence = 1 for text with no numbers', () => {
98
+ const text = 'Les données sont stables et cohérentes.'
99
+ const result = validateAgainstSource(text, SAMPLE_ROWS)
100
+
101
+ expect(result.valid).toBe(true)
102
+ expect(result.confidence).toBe(1)
103
+ expect(result.llmNumbers).toHaveLength(0)
104
+ })
105
+
106
+ it('handles empty source rows', () => {
107
+ const text = 'On observe 22 306 ventes.'
108
+ const result = validateAgainstSource(text, [])
109
+
110
+ // With empty source, any number in text is hallucinated
111
+ expect(result.valid).toBe(false)
112
+ })
113
+
114
+ it('mixes verified and hallucinated', () => {
115
+ const text = 'On observe 22 306 ventes reelles et 15 000 ventes estimees.'
116
+ const result = validateAgainstSource(text, SAMPLE_ROWS)
117
+
118
+ expect(result.valid).toBe(false)
119
+ expect(result.hallucinated).toHaveLength(1)
120
+ expect(result.hallucinated[0].value).toBe(15000)
121
+ // 1 hallucinated out of 2 = 50% confidence
122
+ expect(result.confidence).toBe(0.5)
123
+ })
124
+
125
+ it('provides context around numbers', () => {
126
+ const text = 'Le prix moyen est de 3 337 EUR/m2 a Montpellier.'
127
+ const result = validateAgainstSource(text, SAMPLE_ROWS)
128
+
129
+ expect(result.llmNumbers[0].context).toContain('3 337')
130
+ })
131
+
132
+ it('handles custom ignorePatterns', () => {
133
+ const text = 'Parcelle 1234 avec 22 306 ventes.'
134
+ const result = validateAgainstSource(text, SAMPLE_ROWS, {
135
+ ignorePatterns: [/^\d{4}$/], // ignore 4-digit numbers
136
+ })
137
+
138
+ expect(result.valid).toBe(true)
139
+ expect(result.llmNumbers).toHaveLength(1) // only 22306
140
+ })
141
+
142
+ it('returns sourceNumbers as a Set', () => {
143
+ const result = validateAgainstSource('test', SAMPLE_ROWS)
144
+
145
+ expect(result.sourceNumbers).toBeInstanceOf(Set)
146
+ expect(result.sourceNumbers.has(22306)).toBe(true)
147
+ expect(result.sourceNumbers.has(3337)).toBe(true)
148
+ expect(result.sourceNumbers.has(2492)).toBe(true)
149
+ expect(result.sourceNumbers.has(4230)).toBe(true)
150
+ })
151
+ })
@@ -0,0 +1,149 @@
1
+ /**
2
+ * Data Validator — anti-hallucination for LLM-generated text
3
+ * v3.1.0: Pure regex-based number verification against source data
4
+ *
5
+ * Compares numbers in LLM text to numbers in source data rows.
6
+ * Detects ~90% of numerical hallucinations with zero LLM cost, <1ms latency.
7
+ */
8
+
9
+ import type { DataValidation, DataValidationOptions, LLMNumber, HallucinatedNumber } from '../types/chat-bus'
10
+
11
+ const DEFAULT_IGNORE_COLUMNS = new Set(['id', 'code_geo', 'code_parent'])
12
+ const DEFAULT_IGNORE_PATTERNS: RegExp[] = [
13
+ /^20[012]\d$/, // years 2000-2029
14
+ /^\d{5}$/, // postal codes / INSEE codes
15
+ /^\d{1,2}$/, // indices, ranks (1-99)
16
+ ]
17
+
18
+ /**
19
+ * Extract all numeric values from source data rows.
20
+ * Handles both number types and string-encoded numbers (e.g. "22 306", "3,337").
21
+ */
22
+ function extractSourceNumbers(
23
+ rows: Record<string, unknown>[],
24
+ ignoreColumns: Set<string>
25
+ ): Set<number> {
26
+ const numbers = new Set<number>()
27
+ for (const row of rows) {
28
+ for (const [col, val] of Object.entries(row)) {
29
+ if (ignoreColumns.has(col)) continue
30
+ if (typeof val === 'number' && isFinite(val)) {
31
+ numbers.add(val)
32
+ } else if (typeof val === 'string') {
33
+ const parsed = Number(val.replace(/\s/g, '').replace(',', '.'))
34
+ if (!isNaN(parsed) && isFinite(parsed)) {
35
+ numbers.add(parsed)
36
+ }
37
+ }
38
+ }
39
+ }
40
+ return numbers
41
+ }
42
+
43
+ /**
44
+ * Extract all numbers from LLM text.
45
+ * Handles French/European formats: "22 306", "3 337", "3,337", "22306".
46
+ */
47
+ function extractLLMNumbers(
48
+ text: string,
49
+ ignorePatterns: RegExp[]
50
+ ): LLMNumber[] {
51
+ const numberRegex = /\d[\d\s,.]*\d|\d+/g
52
+ const results: LLMNumber[] = []
53
+ let match: RegExpExecArray | null
54
+
55
+ while ((match = numberRegex.exec(text)) !== null) {
56
+ const raw = match[0]
57
+ // Normalize: remove spaces/dots (thousand separators), comma → decimal point
58
+ const cleaned = raw.replace(/[\s.]/g, '').replace(',', '.')
59
+ const value = Number(cleaned)
60
+
61
+ if (isNaN(value) || !isFinite(value)) continue
62
+ if (ignorePatterns.some(p => p.test(raw.trim()))) continue
63
+
64
+ results.push({
65
+ value,
66
+ position: match.index,
67
+ context: text.slice(
68
+ Math.max(0, match.index - 10),
69
+ match.index + raw.length + 10
70
+ ),
71
+ })
72
+ }
73
+
74
+ return results
75
+ }
76
+
77
+ /**
78
+ * Validate LLM-generated text against source data rows.
79
+ *
80
+ * Pure function — no IO, no LLM calls, no side effects.
81
+ * Returns which numbers in the text are verified vs hallucinated.
82
+ *
83
+ * @example
84
+ * ```typescript
85
+ * const rows = [{ type: 'Appartement', ventes: 22306, prix_m2: 3337 }]
86
+ * const result = validateAgainstSource(
87
+ * "On observe 22 306 ventes à 3 337 EUR/m². En 2023, 18 245 ventes.",
88
+ * rows
89
+ * )
90
+ * // result.valid === false
91
+ * // result.hallucinated === [{ value: 18245, ... }]
92
+ * // result.confidence === 0.67
93
+ * ```
94
+ */
95
+ export function validateAgainstSource(
96
+ text: string,
97
+ sourceRows: Record<string, unknown>[],
98
+ options: DataValidationOptions = {}
99
+ ): DataValidation {
100
+ const tolerance = options.tolerance ?? 0.01
101
+ const ignoreColumns = new Set(options.ignoreColumns || [...DEFAULT_IGNORE_COLUMNS])
102
+ const ignorePatterns = options.ignorePatterns || DEFAULT_IGNORE_PATTERNS
103
+
104
+ // 1. Extract source numbers
105
+ const sourceNumbers = extractSourceNumbers(sourceRows, ignoreColumns)
106
+
107
+ // 2. Extract LLM numbers
108
+ const llmNumbers = extractLLMNumbers(text, ignorePatterns)
109
+
110
+ // 3. Check each LLM number against source
111
+ const hallucinated: HallucinatedNumber[] = []
112
+
113
+ for (const num of llmNumbers) {
114
+ // Exact match
115
+ if (sourceNumbers.has(num.value)) continue
116
+
117
+ // Tolerance match (rounding)
118
+ let closest: number | undefined
119
+ let minDistance = Infinity
120
+
121
+ for (const src of sourceNumbers) {
122
+ const dist = Math.abs(num.value - src) / Math.max(Math.abs(src), 1)
123
+ if (dist < minDistance) {
124
+ minDistance = dist
125
+ closest = src
126
+ }
127
+ }
128
+
129
+ if (minDistance <= tolerance) continue // acceptable rounding
130
+
131
+ hallucinated.push({
132
+ ...num,
133
+ closest,
134
+ distance: minDistance,
135
+ })
136
+ }
137
+
138
+ const confidence = llmNumbers.length > 0
139
+ ? 1 - (hallucinated.length / llmNumbers.length)
140
+ : 1
141
+
142
+ return {
143
+ valid: hallucinated.length === 0,
144
+ llmNumbers,
145
+ sourceNumbers,
146
+ hallucinated,
147
+ confidence,
148
+ }
149
+ }
@@ -17,3 +17,5 @@ export {
17
17
  export { ComponentRegistry } from './component-registry'
18
18
 
19
19
  export { createEventEmitter, createCommandHandler, createChatBus, mergeScratchpadSections } from './chat-bus'
20
+
21
+ export { validateAgainstSource } from './data-validator'
@@ -366,7 +366,7 @@ export interface ScratchpadState {
366
366
  export interface ScratchpadSection {
367
367
  id: string
368
368
  title: string
369
- type: 'data' | 'filter' | 'preview' | 'message' | 'action' | 'steps' | 'form' | 'understanding' | 'feedback' | 'prompt' | 'stepper' | 'error' | 'source_card' | 'diff'
369
+ type: 'data' | 'filter' | 'preview' | 'message' | 'action' | 'steps' | 'form' | 'understanding' | 'feedback' | 'prompt' | 'stepper' | 'error' | 'source_card' | 'diff' | 'verified_text' | 'data_preview' | 'map' | 'chart'
370
370
  content: unknown
371
371
  /** Can the human edit this section? */
372
372
  editable: boolean
@@ -438,3 +438,100 @@ export interface ClarificationEvent {
438
438
  }>
439
439
  original_message?: string
440
440
  }
441
+
442
+ // ─── Data Validation (v3.1.0 — anti-hallucination) ──────────
443
+
444
+ /**
445
+ * Result of validating LLM text against source data.
446
+ * Pure regex-based — zero LLM cost, <1ms latency.
447
+ */
448
+ export interface DataValidation {
449
+ /** Is the text free of hallucinated numbers? */
450
+ valid: boolean
451
+ /** Numbers found in the LLM text */
452
+ llmNumbers: LLMNumber[]
453
+ /** Numbers present in the source data */
454
+ sourceNumbers: Set<number>
455
+ /** Numbers from the LLM NOT found in the source */
456
+ hallucinated: HallucinatedNumber[]
457
+ /** Confidence score 0-1 (1 = all numbers verified) */
458
+ confidence: number
459
+ }
460
+
461
+ export interface LLMNumber {
462
+ value: number
463
+ /** Character index in the text */
464
+ position: number
465
+ /** ~20 chars surrounding context */
466
+ context: string
467
+ }
468
+
469
+ export interface HallucinatedNumber extends LLMNumber {
470
+ /** Closest number in source data */
471
+ closest?: number
472
+ /** Distance as ratio (0.18 = 18% off) */
473
+ distance?: number
474
+ }
475
+
476
+ /** Options for validateAgainstSource */
477
+ export interface DataValidationOptions {
478
+ /** Tolerance for rounding (default: 0.01 = 1%) */
479
+ tolerance?: number
480
+ /** Columns to ignore (e.g. 'id', 'code_geo') */
481
+ ignoreColumns?: string[]
482
+ /** Number patterns to ignore (e.g. years, postal codes) */
483
+ ignorePatterns?: RegExp[]
484
+ }
485
+
486
+ /** Content for verified_text scratchpad section */
487
+ export interface VerifiedTextContent {
488
+ /** Original LLM text */
489
+ text: string
490
+ /** Validation result from validateAgainstSource */
491
+ validation: DataValidation
492
+ /** Display mode */
493
+ mode?: 'highlight' | 'strip' | 'annotate'
494
+ }
495
+
496
+ /** Column definition for data_preview section */
497
+ export interface DataPreviewColumn {
498
+ key: string
499
+ label: string
500
+ type?: 'number' | 'string' | 'date'
501
+ format?: string
502
+ align?: 'left' | 'right' | 'center'
503
+ }
504
+
505
+ /** Content for data_preview scratchpad section */
506
+ export interface DataPreviewContent {
507
+ columns: DataPreviewColumn[]
508
+ rows: Record<string, unknown>[]
509
+ /** Total rows (if paginated — e.g. 22306 total, 30 displayed) */
510
+ totalRows?: number
511
+ /** Data source attribution */
512
+ source?: string
513
+ /** Data freshness label */
514
+ freshness?: string
515
+ /** Enable export buttons (CSV/JSON) */
516
+ exportable?: boolean
517
+ /** Rows per page (default: 25) */
518
+ pageSize?: number
519
+ }
520
+
521
+ /** Content for map scratchpad section (v3.1.0) */
522
+ export interface MapSectionContent {
523
+ /** GeoJSON FeatureCollection */
524
+ geojson: unknown
525
+ /** Map center [lat, lng] */
526
+ center?: [number, number]
527
+ /** Zoom level */
528
+ zoom?: number
529
+ /** GeoJSON style (including choropleth) */
530
+ style?: import('./index').MapGeoJSONStyle
531
+ /** Popup config for feature click */
532
+ popup?: import('./index').MapPopupConfig
533
+ /** Named layers */
534
+ layers?: import('./index').MapLayer[]
535
+ /** Map height (CSS, default: '300px') */
536
+ height?: string
537
+ }
@@ -94,10 +94,14 @@ export interface ChartComponentParams {
94
94
  labels: string[]
95
95
  datasets: Array<{
96
96
  label: string
97
- data: number[]
97
+ data: number[] | Array<{ x: string | number; y: number }>
98
98
  backgroundColor?: string | string[]
99
99
  borderColor?: string | string[]
100
100
  borderWidth?: number
101
+ /** Fill area under line (useful for time-series) */
102
+ fill?: boolean | string
103
+ /** Line tension (0 = straight, 0.4 = smooth) */
104
+ tension?: number
101
105
  }>
102
106
  }
103
107
  options?: {
@@ -118,6 +122,22 @@ export interface ChartComponentParams {
118
122
  * Enable PNG export button (v2.2.0)
119
123
  */
120
124
  exportable?: boolean
125
+ /**
126
+ * Time-series axis configuration (v3.1.0).
127
+ * When set, x-axis labels are parsed as dates.
128
+ */
129
+ timeAxis?: {
130
+ /** Date format for parsing labels (Chart.js adapter format, e.g. 'yyyy-MM-dd') */
131
+ parser?: string
132
+ /** Display unit for x-axis ticks */
133
+ unit?: 'day' | 'week' | 'month' | 'quarter' | 'year'
134
+ /** Date format for tooltip display */
135
+ tooltipFormat?: string
136
+ /** Min date (ISO string) */
137
+ min?: string
138
+ /** Max date (ISO string) */
139
+ max?: string
140
+ }
121
141
  /**
122
142
  * Chart container height as CSS value (v2.2.0, default '250px')
123
143
  */
@@ -750,9 +770,60 @@ export interface MapClusterOptions {
750
770
  animateAddingMarkers?: boolean
751
771
  }
752
772
 
773
+ /**
774
+ * GeoJSON feature popup configuration (v3.1.0)
775
+ */
776
+ export interface MapPopupConfig {
777
+ /** Property key used as popup title */
778
+ titleField?: string
779
+ /** Property keys to display in popup body */
780
+ fields?: string[]
781
+ /** Custom HTML template (use {{property}} placeholders) */
782
+ template?: string
783
+ }
784
+
785
+ /**
786
+ * GeoJSON style configuration (v3.1.0)
787
+ * Supports static styles and choropleth (data-driven) coloring.
788
+ */
789
+ export interface MapGeoJSONStyle {
790
+ /** Fill color (CSS color or choropleth config) */
791
+ fillColor?: string
792
+ /** Fill opacity (0-1, default: 0.6) */
793
+ fillOpacity?: number
794
+ /** Stroke color (default: '#333') */
795
+ strokeColor?: string
796
+ /** Stroke width (default: 1) */
797
+ strokeWeight?: number
798
+ /** Stroke opacity (0-1, default: 1) */
799
+ strokeOpacity?: number
800
+ /** Choropleth: property key for data-driven coloring */
801
+ choroplethField?: string
802
+ /** Choropleth: color scale stops [value, color][] sorted ascending */
803
+ choroplethScale?: Array<[number, string]>
804
+ /** Choropleth: color for features with missing/null values */
805
+ choroplethFallback?: string
806
+ }
807
+
808
+ /**
809
+ * Named GeoJSON layer for multi-layer maps (v3.1.0)
810
+ */
811
+ export interface MapLayer {
812
+ /** Layer name (shown in layer control) */
813
+ name: string
814
+ /** Is this layer visible by default? */
815
+ visible?: boolean
816
+ /** GeoJSON FeatureCollection (inline or from API) */
817
+ geojson: unknown // GeoJSON.FeatureCollection — kept as unknown for zero-dep types
818
+ /** Per-layer style override */
819
+ style?: MapGeoJSONStyle
820
+ /** Per-layer popup config */
821
+ popup?: MapPopupConfig
822
+ }
823
+
753
824
  /**
754
825
  * Map component parameters (Sprint 6)
755
- * Updated Sprint Ultimate U.2: Added clustering support
826
+ * Updated v3.1.0: GeoJSON, choropleth, popups, layers
756
827
  */
757
828
  export interface MapComponentParams {
758
829
  /**
@@ -776,7 +847,7 @@ export interface MapComponentParams {
776
847
  height?: string
777
848
 
778
849
  /**
779
- * Auto-fit bounds to show all markers (default: false)
850
+ * Auto-fit bounds to show all markers/features (default: false)
780
851
  */
781
852
  fitBounds?: boolean
782
853
 
@@ -802,9 +873,6 @@ export interface MapComponentParams {
802
873
 
803
874
  /**
804
875
  * Enable marker clustering (Sprint Ultimate U.2)
805
- * - true: Enable with default options
806
- * - false: Disable clustering
807
- * - MapClusterOptions: Enable with custom options
808
876
  */
809
877
  clustering?: boolean | MapClusterOptions
810
878
 
@@ -812,6 +880,77 @@ export interface MapComponentParams {
812
880
  * Custom CSS class (Sprint 7)
813
881
  */
814
882
  className?: string
883
+
884
+ // ─── GeoJSON (v3.1.0) ────────────────────
885
+
886
+ /**
887
+ * GeoJSON FeatureCollection to render on the map.
888
+ * Use this for polygons, lines, points from structured data.
889
+ */
890
+ geojson?: unknown // GeoJSON.FeatureCollection
891
+
892
+ /**
893
+ * Style for the GeoJSON layer.
894
+ * Supports static colors and choropleth (data-driven) coloring.
895
+ */
896
+ geojsonStyle?: MapGeoJSONStyle
897
+
898
+ /**
899
+ * Popup configuration for GeoJSON features.
900
+ * Shown on feature click.
901
+ */
902
+ popup?: MapPopupConfig
903
+
904
+ /**
905
+ * Named layers for multi-layer maps.
906
+ * Each layer has its own GeoJSON, style, and popup config.
907
+ * A Leaflet layer control is added when layers are present.
908
+ */
909
+ layers?: MapLayer[]
910
+
911
+ // ─── PMTiles (v3.1.0) ────────────────────
912
+
913
+ /**
914
+ * PMTiles vector tile source for large datasets (>5000 features).
915
+ * Requires protomaps-leaflet peer dependency.
916
+ * Pipeline: GeoParquet -> Tippecanoe -> PMTiles (static file on S3/CDN).
917
+ */
918
+ pmtiles?: MapPMTilesConfig
919
+ }
920
+
921
+ /**
922
+ * PMTiles configuration for large vector tile datasets (v3.1.0)
923
+ */
924
+ export interface MapPMTilesConfig {
925
+ /** URL to the .pmtiles file (S3, CDN, local) */
926
+ url: string
927
+ /** Attribution text for this tile source */
928
+ attribution?: string
929
+ /** Style rules for vector features */
930
+ paintRules?: Array<{
931
+ /** Layer name in the PMTiles source */
932
+ dataLayer: string
933
+ /** Symbol type */
934
+ symbolizer: 'polygon' | 'line' | 'circle'
935
+ /** Fill/stroke color (CSS color or function name) */
936
+ color?: string
937
+ /** Stroke width */
938
+ width?: number
939
+ /** Fill opacity */
940
+ opacity?: number
941
+ }>
942
+ /** Label rules for text labels */
943
+ labelRules?: Array<{
944
+ dataLayer: string
945
+ /** Property key for label text */
946
+ textField: string
947
+ /** Font size */
948
+ fontSize?: number
949
+ }>
950
+ /** Max zoom level */
951
+ maxZoom?: number
952
+ /** Min zoom level */
953
+ minZoom?: number
815
954
  }
816
955
 
817
956
  /**