@geotechcli/core 0.4.53 → 0.4.55

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 (64) hide show
  1. package/dist/evidence/evidence-ref.d.ts +8 -0
  2. package/dist/evidence/evidence-ref.d.ts.map +1 -1
  3. package/dist/evidence/evidence-ref.js.map +1 -1
  4. package/dist/evidence/index.d.ts +1 -1
  5. package/dist/evidence/index.d.ts.map +1 -1
  6. package/dist/evidence/index.js.map +1 -1
  7. package/dist/fem/demo.d.ts +4 -0
  8. package/dist/fem/demo.d.ts.map +1 -0
  9. package/dist/fem/demo.js +274 -0
  10. package/dist/fem/demo.js.map +1 -0
  11. package/dist/fem/index.d.ts +5 -0
  12. package/dist/fem/index.d.ts.map +1 -0
  13. package/dist/fem/index.js +5 -0
  14. package/dist/fem/index.js.map +1 -0
  15. package/dist/fem/types.d.ts +154 -0
  16. package/dist/fem/types.d.ts.map +1 -0
  17. package/dist/fem/types.js +2 -0
  18. package/dist/fem/types.js.map +1 -0
  19. package/dist/fem/validation.d.ts +4 -0
  20. package/dist/fem/validation.d.ts.map +1 -0
  21. package/dist/fem/validation.js +195 -0
  22. package/dist/fem/validation.js.map +1 -0
  23. package/dist/fem/webgl.d.ts +3 -0
  24. package/dist/fem/webgl.d.ts.map +1 -0
  25. package/dist/fem/webgl.js +120 -0
  26. package/dist/fem/webgl.js.map +1 -0
  27. package/dist/index.d.ts +2 -1
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +3 -1
  30. package/dist/index.js.map +1 -1
  31. package/dist/ingest/document-evidence-packet.d.ts +32 -32
  32. package/dist/ingest/geotech-document.d.ts +2 -0
  33. package/dist/ingest/geotech-document.d.ts.map +1 -1
  34. package/dist/ingest/geotech-document.js +17 -2
  35. package/dist/ingest/geotech-document.js.map +1 -1
  36. package/dist/ingest/geotech-extract.d.ts +2 -0
  37. package/dist/ingest/geotech-extract.d.ts.map +1 -1
  38. package/dist/ingest/geotech-extract.js +12 -0
  39. package/dist/ingest/geotech-extract.js.map +1 -1
  40. package/dist/ingest/page-evidence-cache.d.ts +4 -1
  41. package/dist/ingest/page-evidence-cache.d.ts.map +1 -1
  42. package/dist/ingest/page-evidence-cache.js +86 -1
  43. package/dist/ingest/page-evidence-cache.js.map +1 -1
  44. package/dist/llm/index.d.ts +1 -0
  45. package/dist/llm/index.d.ts.map +1 -1
  46. package/dist/llm/index.js +1 -0
  47. package/dist/llm/index.js.map +1 -1
  48. package/dist/meta/metadata.json +1 -1
  49. package/dist/report/html.d.ts.map +1 -1
  50. package/dist/report/html.js +596 -1309
  51. package/dist/report/html.js.map +1 -1
  52. package/dist/report/index.d.ts +1 -0
  53. package/dist/report/index.d.ts.map +1 -1
  54. package/dist/report/index.js +1 -0
  55. package/dist/report/index.js.map +1 -1
  56. package/dist/report/ingest-dossier.d.ts +5 -0
  57. package/dist/report/ingest-dossier.d.ts.map +1 -1
  58. package/dist/report/ingest-dossier.js +376 -2
  59. package/dist/report/ingest-dossier.js.map +1 -1
  60. package/dist/report/integrated-review-model.d.ts +147 -0
  61. package/dist/report/integrated-review-model.d.ts.map +1 -0
  62. package/dist/report/integrated-review-model.js +649 -0
  63. package/dist/report/integrated-review-model.js.map +1 -0
  64. package/package.json +1 -1
@@ -0,0 +1,649 @@
1
+ import { DEFAULT_LLM_MODEL, DEFAULT_LLM_VISION_MODEL } from '../meta/index.js';
2
+ function uniqueStrings(values) {
3
+ return [...new Set(values.map((value) => value?.trim()).filter((value) => Boolean(value)))]
4
+ .filter(Boolean);
5
+ }
6
+ function compactLabel(value, maxLength = 28) {
7
+ const normalized = (value ?? '').replace(/\s+/g, ' ').trim();
8
+ if (normalized.length <= maxLength) {
9
+ return normalized;
10
+ }
11
+ return `${normalized.slice(0, Math.max(0, maxLength - 3)).trimEnd()}...`;
12
+ }
13
+ function compactSummary(value, maxLength = 520) {
14
+ const normalized = (value ?? '').replace(/\s+/g, ' ').trim();
15
+ if (normalized.length <= maxLength) {
16
+ return normalized;
17
+ }
18
+ return `${normalized.slice(0, maxLength - 3).trimEnd()}...`;
19
+ }
20
+ function finiteNumber(value) {
21
+ return typeof value === 'number' && Number.isFinite(value);
22
+ }
23
+ function groundModelMaterialKey(description) {
24
+ const text = description.toLowerCase();
25
+ if (/peat|organic|top\s*soil|topsoil/.test(text))
26
+ return 'organic';
27
+ if (/bedrock|fresh\s+rock|strong\s+(?:shale|sandstone|siltstone|gneiss|rock)/.test(text))
28
+ return 'bedrock';
29
+ if (/weathered|fractured|rock|shale|sandstone|gneiss/.test(text))
30
+ return 'weathered-rock';
31
+ if (/gravel|\bgm\b|\bgp\b|\bgw\b/.test(text))
32
+ return 'gravel';
33
+ if (/sand|\bsm\b|\bsp\b|\bsw\b|\bsc\b/.test(text))
34
+ return 'sand';
35
+ if (/clay|\bci\b|\bcl\b|\bch\b/.test(text))
36
+ return 'clay';
37
+ if (/silt|\bml\b|\bmh\b/.test(text))
38
+ return 'silt';
39
+ if (/fill|made\s+ground|debris/.test(text))
40
+ return 'fill';
41
+ return 'mixed';
42
+ }
43
+ function stratumTopDepth(stratum, index, sorted) {
44
+ if (finiteNumber(stratum.topDepth))
45
+ return Math.max(0, stratum.topDepth);
46
+ if (index === 0)
47
+ return 0;
48
+ return sorted[index - 1]?.bottomDepth ?? 0;
49
+ }
50
+ function stratumBottomDepth(stratum, index, sorted, maxDepth) {
51
+ if (finiteNumber(stratum.bottomDepth))
52
+ return Math.max(0, stratum.bottomDepth);
53
+ const nextTop = sorted[index + 1]?.topDepth;
54
+ if (finiteNumber(nextTop))
55
+ return Math.max(0, nextTop);
56
+ return maxDepth;
57
+ }
58
+ export function normalizeIntegratedConfidence(value) {
59
+ if (!Number.isFinite(value ?? NaN))
60
+ return 0.5;
61
+ return Math.max(0, Math.min(1, value > 1 ? value / 100 : value));
62
+ }
63
+ export function integratedPercent(value) {
64
+ return `${Math.round(normalizeIntegratedConfidence(value) * 100)}%`;
65
+ }
66
+ function integratedStatus(confidence, warnings = []) {
67
+ return normalizeIntegratedConfidence(confidence) >= 0.78 && warnings.length === 0
68
+ ? 'accepted'
69
+ : 'review_recommended';
70
+ }
71
+ function integratedClass(description) {
72
+ const key = groundModelMaterialKey(description);
73
+ if (key === 'fill' || key === 'clay' || key === 'silt' || key === 'sand' || key === 'gravel')
74
+ return key;
75
+ if (key === 'weathered-rock' || key === 'bedrock')
76
+ return 'rock';
77
+ return 'mixed';
78
+ }
79
+ function integratedClassLabel(className) {
80
+ switch (className) {
81
+ case 'fill': return 'Fill';
82
+ case 'clay': return 'Clay';
83
+ case 'silt': return 'Silt';
84
+ case 'sand': return 'Sand';
85
+ case 'gravel': return 'Gravel';
86
+ case 'rock': return 'Rock';
87
+ default: return 'Mixed';
88
+ }
89
+ }
90
+ function shortIntegratedLabel(value, fallback) {
91
+ const normalized = value.replace(/\s+/g, ' ').trim();
92
+ if (!normalized)
93
+ return fallback;
94
+ const material = integratedClassLabel(integratedClass(normalized));
95
+ return material === 'Mixed' ? compactLabel(normalized, 18) : material;
96
+ }
97
+ function evidenceLookup(model) {
98
+ return new Map((model?.evidence ?? []).map((entry) => [entry.id, entry]));
99
+ }
100
+ function pagesForEvidenceIds(evidenceById, evidenceIds) {
101
+ return [...new Set(evidenceIds
102
+ .map((id) => evidenceById.get(id)?.location.pageNumber)
103
+ .filter((page) => typeof page === 'number' && Number.isInteger(page) && page > 0))]
104
+ .sort((left, right) => left - right);
105
+ }
106
+ export function sourcePagesLabel(pages) {
107
+ return pages.length > 0 ? `p${pages.join(', ')}` : 'source evidence';
108
+ }
109
+ function positiveDimension(value) {
110
+ return Number.isFinite(value ?? NaN) && (value ?? 0) > 0 ? value : null;
111
+ }
112
+ function inferredPageDimension(page, axis) {
113
+ const explicit = positiveDimension(axis === 'width' ? page.width : page.height);
114
+ if (explicit != null) {
115
+ return explicit;
116
+ }
117
+ const elementDimension = page.elements
118
+ .map((element) => axis === 'width' ? element.width : element.height)
119
+ .find((value) => positiveDimension(value) != null);
120
+ if (elementDimension != null) {
121
+ return elementDimension;
122
+ }
123
+ const coordinateIndex = axis === 'width' ? 2 : 3;
124
+ const maxCoordinate = Math.max(0, ...page.elements
125
+ .map((element) => element.bbox2d?.[coordinateIndex])
126
+ .filter((value) => Number.isFinite(value ?? NaN) && (value ?? 0) > 0));
127
+ return maxCoordinate > 0 ? maxCoordinate : axis === 'width' ? 1000 : 1414;
128
+ }
129
+ function clampCoordinate(value, max) {
130
+ return Math.max(0, Math.min(max, Number.isFinite(value) ? value : 0));
131
+ }
132
+ function bboxLooksLikeRatio(bbox) {
133
+ return bbox.every((value) => Number.isFinite(value) && value >= 0 && value <= 1);
134
+ }
135
+ function normalizeLayoutBbox(bbox, width, height, units) {
136
+ if (!bbox) {
137
+ return null;
138
+ }
139
+ const ratioUnits = units === 'ratio' || (units !== 'page' && bboxLooksLikeRatio(bbox));
140
+ const scaled = ratioUnits
141
+ ? [bbox[0] * width, bbox[1] * height, bbox[2] * width, bbox[3] * height]
142
+ : bbox;
143
+ const x1 = clampCoordinate(Math.min(scaled[0], scaled[2]), width);
144
+ const y1 = clampCoordinate(Math.min(scaled[1], scaled[3]), height);
145
+ const x2 = clampCoordinate(Math.max(scaled[0], scaled[2]), width);
146
+ const y2 = clampCoordinate(Math.max(scaled[1], scaled[3]), height);
147
+ const minSpan = ratioUnits && width <= 1 && height <= 1 ? 0.001 : 2;
148
+ if (x2 - x1 < minSpan || y2 - y1 < minSpan) {
149
+ return null;
150
+ }
151
+ return [x1, y1, x2, y2];
152
+ }
153
+ function inferSourceRegionType(label, content) {
154
+ const text = `${label} ${content}`.toLowerCase();
155
+ if (/\bspt\b|standard\s+penetration|n[-\s]?value|blows\s*\/?\s*(?:300\s*mm|ft)/.test(text)) {
156
+ return 'spt';
157
+ }
158
+ if (/ground\s*water|groundwater|water\s+table|gwl/.test(text)) {
159
+ return 'water';
160
+ }
161
+ if (/easting|northing|coordinate|latitude|longitude|\be\s*[:=]?\s*\d|\bn\s*[:=]?\s*\d/.test(text)) {
162
+ return 'coordinates';
163
+ }
164
+ if (/total\s+depth|final\s+depth|termination|terminated|depth\s+of\s+bore/.test(text)) {
165
+ return 'totalDepth';
166
+ }
167
+ if (/moisture|water\s*content|liquid\s*limit|plasticity|cohesion|friction|unit\s*weight|density|ucs|rqd|rmr|permeability/.test(text)) {
168
+ return 'parameter';
169
+ }
170
+ if (/strata|lithology|description|sample|clay|silt|sand|gravel|rock|fill|made\s+ground/.test(text)) {
171
+ return 'strata';
172
+ }
173
+ if (/bore\s*hole|borehole|project|location|client|sheet|hole\s+no/.test(text)) {
174
+ return 'header';
175
+ }
176
+ if (label === 'table')
177
+ return 'table';
178
+ if (label === 'image')
179
+ return 'image';
180
+ if (label === 'formula')
181
+ return 'formula';
182
+ if (label === 'text')
183
+ return 'text';
184
+ return 'unknown';
185
+ }
186
+ function findLayoutRegionLink(links, pageNumber, elementIndex, elementOrdinal, content) {
187
+ const normalizedContent = content.toLowerCase();
188
+ return links.find((link) => {
189
+ if (link.pageNumber !== pageNumber) {
190
+ return false;
191
+ }
192
+ if (link.elementOrdinal != null) {
193
+ return link.elementOrdinal === elementOrdinal;
194
+ }
195
+ if (link.elementIndex != null) {
196
+ return link.elementIndex === elementIndex;
197
+ }
198
+ if (link.contentIncludes?.trim()) {
199
+ return normalizedContent.includes(link.contentIncludes.trim().toLowerCase());
200
+ }
201
+ return false;
202
+ });
203
+ }
204
+ export function buildIntegratedSourcePagesFromLayout(pages, options = {}) {
205
+ const method = options.method ?? 'glm-ocr-layout';
206
+ const links = options.links ?? [];
207
+ return pages.flatMap((page) => {
208
+ const width = inferredPageDimension(page, 'width');
209
+ const height = inferredPageDimension(page, 'height');
210
+ const regions = page.elements.flatMap((element, index) => {
211
+ const bbox = normalizeLayoutBbox(element.bbox2d, width, height);
212
+ if (!bbox) {
213
+ return [];
214
+ }
215
+ const link = findLayoutRegionLink(links, page.pageNumber, element.index, index, element.content);
216
+ const confidence = normalizeIntegratedConfidence(link?.confidence ?? 0.72);
217
+ const regionIndex = index + 1;
218
+ return [{
219
+ id: `layout-p${page.pageNumber}-r${regionIndex}`,
220
+ evidenceId: link?.evidenceId ?? `layout-p${page.pageNumber}-r${regionIndex}`,
221
+ pageNumber: page.pageNumber,
222
+ type: link?.type ?? inferSourceRegionType(element.label, element.content),
223
+ label: compactLabel(link?.label ?? element.label, 42),
224
+ text: compactSummary(element.content, 240),
225
+ bbox,
226
+ confidence,
227
+ method: link?.method ?? method,
228
+ status: link?.status ?? (confidence >= 0.78 ? 'accepted' : 'review_recommended'),
229
+ }];
230
+ });
231
+ if (regions.length === 0) {
232
+ return [];
233
+ }
234
+ return [{
235
+ pageNumber: page.pageNumber,
236
+ width,
237
+ height,
238
+ sourcePath: options.sourcePath ?? `page-${page.pageNumber}`,
239
+ method,
240
+ regions,
241
+ }];
242
+ });
243
+ }
244
+ export function buildIntegratedSourcePagesFromEvidence(evidenceRefs = [], sourcePathFallback = 'source evidence') {
245
+ const grouped = new Map();
246
+ for (const ref of evidenceRefs) {
247
+ const bbox = ref.location.bbox;
248
+ const pageNumber = ref.location.pageNumber;
249
+ if (!bbox || pageNumber == null || !Number.isInteger(pageNumber) || pageNumber <= 0) {
250
+ continue;
251
+ }
252
+ const ratioUnits = ref.location.bboxUnits === 'ratio' || (ref.location.bboxUnits !== 'page' && bboxLooksLikeRatio(bbox));
253
+ const width = positiveDimension(ref.location.pageWidth)
254
+ ?? (ratioUnits ? 1 : Math.max(1000, bbox[0], bbox[2]));
255
+ const height = positiveDimension(ref.location.pageHeight)
256
+ ?? (ratioUnits ? 1 : Math.max(1414, bbox[1], bbox[3]));
257
+ const normalizedBbox = normalizeLayoutBbox(bbox, width, height, ref.location.bboxUnits);
258
+ if (!normalizedBbox) {
259
+ continue;
260
+ }
261
+ const text = compactSummary(String(ref.normalizedValue ?? ref.rawValue ?? ''), 240);
262
+ const region = {
263
+ id: `evidence-${ref.id}`,
264
+ evidenceId: ref.id,
265
+ pageNumber,
266
+ type: inferSourceRegionType(ref.location.layoutLabel ?? ref.sourceType, text),
267
+ label: compactLabel(ref.location.layoutLabel ?? ref.sourceType, 42),
268
+ text,
269
+ bbox: normalizedBbox,
270
+ confidence: normalizeIntegratedConfidence(ref.confidence),
271
+ method: ref.method,
272
+ status: integratedStatus(ref.confidence, ref.warnings),
273
+ };
274
+ const existing = grouped.get(pageNumber);
275
+ if (existing) {
276
+ existing.regions.push(region);
277
+ existing.width = Math.max(existing.width, width);
278
+ existing.height = Math.max(existing.height, height);
279
+ continue;
280
+ }
281
+ grouped.set(pageNumber, {
282
+ pageNumber,
283
+ width,
284
+ height,
285
+ sourcePath: ref.sourcePath || ref.location.filePath || sourcePathFallback,
286
+ method: ref.method,
287
+ regions: [region],
288
+ });
289
+ }
290
+ return [...grouped.values()].sort((left, right) => left.pageNumber - right.pageNumber);
291
+ }
292
+ function mergeIntegratedSourcePages(...pageSets) {
293
+ const grouped = new Map();
294
+ for (const pages of pageSets) {
295
+ for (const rawPage of pages) {
296
+ const page = sanitizeIntegratedSourcePage(rawPage);
297
+ if (!page) {
298
+ continue;
299
+ }
300
+ const existing = grouped.get(page.pageNumber);
301
+ if (!existing) {
302
+ grouped.set(page.pageNumber, {
303
+ ...page,
304
+ regions: [...page.regions],
305
+ });
306
+ continue;
307
+ }
308
+ const seen = new Set(existing.regions.map((region) => `${region.id}:${region.evidenceId}:${region.bbox.join(',')}`));
309
+ for (const region of page.regions) {
310
+ const key = `${region.id}:${region.evidenceId}:${region.bbox.join(',')}`;
311
+ if (!seen.has(key)) {
312
+ seen.add(key);
313
+ existing.regions.push(region);
314
+ }
315
+ }
316
+ existing.width = Math.max(existing.width, page.width);
317
+ existing.height = Math.max(existing.height, page.height);
318
+ if (existing.sourcePath === 'source evidence' && page.sourcePath !== 'source evidence') {
319
+ existing.sourcePath = page.sourcePath;
320
+ }
321
+ if (existing.method === 'manual' && page.method !== 'manual') {
322
+ existing.method = page.method;
323
+ }
324
+ }
325
+ }
326
+ return [...grouped.values()].sort((left, right) => left.pageNumber - right.pageNumber);
327
+ }
328
+ function sanitizeIntegratedSourcePage(page) {
329
+ if (!Number.isInteger(page.pageNumber)
330
+ || page.pageNumber <= 0
331
+ || !Number.isFinite(page.width)
332
+ || !Number.isFinite(page.height)
333
+ || page.width <= 0
334
+ || page.height <= 0) {
335
+ return null;
336
+ }
337
+ const regions = page.regions.flatMap((region) => {
338
+ if (region.pageNumber !== page.pageNumber) {
339
+ return [];
340
+ }
341
+ const bbox = normalizeLayoutBbox(region.bbox, page.width, page.height, 'page');
342
+ if (!bbox) {
343
+ return [];
344
+ }
345
+ return [{
346
+ ...region,
347
+ bbox,
348
+ confidence: normalizeIntegratedConfidence(region.confidence),
349
+ status: region.status === 'accepted' ? 'accepted' : 'review_recommended',
350
+ }];
351
+ });
352
+ return regions.length > 0
353
+ ? {
354
+ ...page,
355
+ regions,
356
+ }
357
+ : null;
358
+ }
359
+ function sourceRegionPriority(region) {
360
+ if (region.method === 'glm-ocr-layout')
361
+ return 4;
362
+ if (region.method.includes('ocr'))
363
+ return 3;
364
+ if (region.method === 'vision')
365
+ return 2;
366
+ if (region.method === 'pdf-text')
367
+ return 1;
368
+ return 0;
369
+ }
370
+ function sourceRegionLookup(pages) {
371
+ const lookup = new Map();
372
+ for (const region of pages.flatMap((page) => page.regions)) {
373
+ const existing = lookup.get(region.evidenceId);
374
+ if (!existing || sourceRegionPriority(region) > sourceRegionPriority(existing)) {
375
+ lookup.set(region.evidenceId, region);
376
+ }
377
+ }
378
+ return lookup;
379
+ }
380
+ export function integratedBoreholeMaxDepth(borehole) {
381
+ return Math.max(1, borehole.totalDepth, ...borehole.strata.flatMap((stratum) => [stratum.top, stratum.base]), ...borehole.spt.map((point) => point.depth), ...borehole.groundwater.map((point) => point.depth));
382
+ }
383
+ export function buildIntegratedAgentReviewFromProjectSession(session) {
384
+ const reviewPassed = typeof session.metadata?.reviewPassed === 'boolean'
385
+ ? session.metadata.reviewPassed
386
+ : undefined;
387
+ const corrections = Array.isArray(session.metadata?.corrections)
388
+ ? session.metadata.corrections.filter((entry) => typeof entry === 'string' && entry.trim().length > 0)
389
+ : [];
390
+ const warnings = uniqueStrings([
391
+ reviewPassed === false ? 'Swarm reviewer did not pass the session without correction.' : null,
392
+ ...corrections.map((correction) => `Correction: ${correction}`),
393
+ ]);
394
+ return {
395
+ mode: session.mode,
396
+ title: session.mode === 'swarm'
397
+ ? 'Swarm agent review'
398
+ : session.mode === 'chat'
399
+ ? 'Chat agent review'
400
+ : 'Single agent review',
401
+ summary: compactSummary(session.answer ?? session.summary ?? session.query),
402
+ stepCount: session.stepCount,
403
+ tokens: session.tokens,
404
+ latencyMs: session.latencyMs,
405
+ warnings,
406
+ evidenceIds: [],
407
+ };
408
+ }
409
+ export function buildIntegratedAgentReviewFromLiveSession(mode, query, session) {
410
+ const answer = session.steps.find((step) => step.type === 'answer')?.content;
411
+ const failedTools = session.steps
412
+ .filter((step) => step.type === 'tool_result' && step.toolResult?.success === false)
413
+ .map((step) => step.toolName ? `Tool failed: ${step.toolName}` : 'Tool failed during agent review.');
414
+ const swarmWarnings = 'reviewPassed' in session && session.reviewPassed === false
415
+ ? ['Swarm reviewer did not pass the session without correction.', ...session.corrections.map((correction) => `Correction: ${correction}`)]
416
+ : [];
417
+ return {
418
+ mode,
419
+ title: mode === 'swarm'
420
+ ? 'Swarm agent review'
421
+ : mode === 'chat'
422
+ ? 'Chat agent review'
423
+ : 'Single agent review',
424
+ summary: compactSummary(answer ?? query),
425
+ stepCount: session.steps.length,
426
+ tokens: session.totalTokens,
427
+ latencyMs: session.totalLatencyMs,
428
+ warnings: uniqueStrings([...failedTools, ...swarmWarnings]),
429
+ evidenceIds: [],
430
+ };
431
+ }
432
+ function buildIntegratedBoreholesFromGroundModel(model, regionByEvidenceId = new Map()) {
433
+ if (!model || model.boreholes.length === 0) {
434
+ return [];
435
+ }
436
+ const evidenceById = evidenceLookup(model);
437
+ const mapPointByLabel = new Map((model.map?.points ?? [])
438
+ .filter((point) => point.kind === 'borehole')
439
+ .map((point) => [point.label.toUpperCase(), point]));
440
+ const parametersByBorehole = new Map();
441
+ for (const parameter of model.parameters) {
442
+ if (!parameter.boreholeId) {
443
+ continue;
444
+ }
445
+ const key = parameter.boreholeId.toUpperCase();
446
+ parametersByBorehole.set(key, [...(parametersByBorehole.get(key) ?? []), parameter]);
447
+ }
448
+ return model.boreholes.map((borehole, index) => {
449
+ const coordinate = borehole.coordinates;
450
+ const mapPoint = mapPointByLabel.get(borehole.id.toUpperCase());
451
+ const sortedStrata = [...borehole.strata].sort((left, right) => stratumTopDepth(left, 0, []) - stratumTopDepth(right, 0, []));
452
+ const evidenceIds = uniqueStrings([
453
+ ...borehole.evidenceIds,
454
+ ...(coordinate?.evidenceIds ?? []),
455
+ ...borehole.strata.flatMap((stratum) => stratum.evidenceIds),
456
+ ...borehole.sptTests.flatMap((test) => test.evidenceIds),
457
+ ...borehole.groundwater.flatMap((observation) => observation.evidenceIds),
458
+ ]);
459
+ const sourcePages = pagesForEvidenceIds(evidenceById, evidenceIds);
460
+ const explicitDepths = [
461
+ ...sortedStrata.flatMap((stratum) => [stratum.topDepth, stratum.bottomDepth]),
462
+ ...borehole.sptTests.map((test) => test.depth),
463
+ ...borehole.groundwater.map((observation) => observation.depth),
464
+ ].filter((value) => finiteNumber(value) && value >= 0);
465
+ const maxDepth = Math.max(1, ...explicitDepths);
466
+ const totalDepth = Math.max(maxDepth, ...sortedStrata.map((stratum, stratumIndex) => stratumBottomDepth(stratum, stratumIndex, sortedStrata, maxDepth)));
467
+ return {
468
+ id: borehole.id,
469
+ sourcePages,
470
+ easting: coordinate?.easting ?? mapPoint?.easting,
471
+ northing: coordinate?.northing ?? mapPoint?.northing,
472
+ latitude: coordinate?.latitude ?? mapPoint?.latitude,
473
+ longitude: coordinate?.longitude ?? mapPoint?.longitude,
474
+ coordinateEvidenceId: coordinate?.evidenceIds[0] ?? mapPoint?.sourceEvidenceIds[0],
475
+ groundLevel: 0,
476
+ totalDepth,
477
+ chainage: index * 35,
478
+ offset: 0,
479
+ confidence: normalizeIntegratedConfidence(borehole.confidence),
480
+ evidenceIds,
481
+ strata: sortedStrata.map((stratum, stratumIndex) => {
482
+ const top = stratumTopDepth(stratum, stratumIndex, sortedStrata);
483
+ const base = stratumBottomDepth(stratum, stratumIndex, sortedStrata, totalDepth);
484
+ const className = integratedClass(stratum.description);
485
+ const evidenceId = stratum.evidenceIds[0] ?? `${borehole.id}-stratum-${stratumIndex + 1}`;
486
+ const sourceRegion = regionByEvidenceId.get(evidenceId);
487
+ return {
488
+ top,
489
+ base,
490
+ name: shortIntegratedLabel(stratum.description, `Layer ${stratumIndex + 1}`),
491
+ description: stratum.description,
492
+ className,
493
+ confidence: normalizeIntegratedConfidence(stratum.confidence),
494
+ status: integratedStatus(stratum.confidence, stratum.warnings),
495
+ evidenceId,
496
+ ...(sourceRegion ? { sourceRegion } : {}),
497
+ };
498
+ }),
499
+ spt: borehole.sptTests.map((test, testIndex) => {
500
+ const evidenceId = test.evidenceIds[0] ?? `${borehole.id}-spt-${testIndex + 1}`;
501
+ const sourceRegion = regionByEvidenceId.get(evidenceId);
502
+ return {
503
+ depth: test.depth,
504
+ value: test.nValue,
505
+ label: `N${test.nValue}`,
506
+ confidence: normalizeIntegratedConfidence(test.confidence),
507
+ evidenceId,
508
+ ...(sourceRegion ? { sourceRegion } : {}),
509
+ };
510
+ }),
511
+ groundwater: borehole.groundwater.map((observation, waterIndex) => {
512
+ const evidenceId = observation.evidenceIds[0] ?? `${borehole.id}-water-${waterIndex + 1}`;
513
+ const sourceRegion = regionByEvidenceId.get(evidenceId);
514
+ return {
515
+ depth: observation.depth,
516
+ label: 'GWL',
517
+ confidence: normalizeIntegratedConfidence(observation.confidence),
518
+ evidenceId,
519
+ ...(sourceRegion ? { sourceRegion } : {}),
520
+ };
521
+ }),
522
+ parameters: (parametersByBorehole.get(borehole.id.toUpperCase()) ?? []).map((parameter, parameterIndex) => {
523
+ const evidenceId = parameter.evidenceIds[0] ?? `${borehole.id}-parameter-${parameterIndex + 1}`;
524
+ const sourceRegion = regionByEvidenceId.get(evidenceId);
525
+ return {
526
+ name: parameter.name,
527
+ value: `${parameter.value}${parameter.unit ? ` ${parameter.unit}` : ''}`,
528
+ ...(parameter.depth != null ? { depth: parameter.depth } : {}),
529
+ confidence: normalizeIntegratedConfidence(parameter.confidence),
530
+ evidenceId,
531
+ ...(sourceRegion ? { sourceRegion } : {}),
532
+ };
533
+ }),
534
+ warnings: borehole.warnings,
535
+ };
536
+ });
537
+ }
538
+ function buildIntegratedBoreholesFromProfile(profile) {
539
+ if (!profile || profile.columns.length === 0) {
540
+ return [];
541
+ }
542
+ return profile.columns.map((column, index) => ({
543
+ id: column.boreholeId,
544
+ sourcePages: [...new Set(column.layers.flatMap((layer) => layer.sourcePages ?? []))],
545
+ groundLevel: 0,
546
+ totalDepth: column.totalDepth ?? profile.maxDepth,
547
+ chainage: index * 35,
548
+ offset: 0,
549
+ confidence: 0.72,
550
+ evidenceIds: [],
551
+ strata: column.layers.map((layer, layerIndex) => {
552
+ const className = integratedClass(layer.description);
553
+ return {
554
+ top: layer.depthFrom,
555
+ base: layer.depthTo,
556
+ name: layer.uscsSymbol ?? layer.label,
557
+ description: layer.description,
558
+ className,
559
+ confidence: layer.uncertain ? 0.62 : 0.74,
560
+ status: layer.uncertain ? 'review_recommended' : 'accepted',
561
+ evidenceId: `${column.boreholeId}-layer-${layerIndex + 1}`,
562
+ };
563
+ }),
564
+ spt: [],
565
+ groundwater: column.waterTableDepth == null
566
+ ? []
567
+ : [{
568
+ depth: column.waterTableDepth,
569
+ label: 'GWL',
570
+ confidence: 0.68,
571
+ evidenceId: `${column.boreholeId}-water-1`,
572
+ }],
573
+ parameters: [],
574
+ warnings: profile.notes,
575
+ }));
576
+ }
577
+ export function buildIntegratedReviewModel(dossier, options = {}) {
578
+ const sourcePages = mergeIntegratedSourcePages(buildIntegratedSourcePagesFromEvidence(dossier.groundModel?.evidence ?? [], dossier.sourceLabel), dossier.sourcePages ?? [], options.sourcePages ?? []);
579
+ const regionByEvidenceId = sourceRegionLookup(sourcePages);
580
+ const groundBoreholes = buildIntegratedBoreholesFromGroundModel(dossier.groundModel, regionByEvidenceId);
581
+ const boreholes = groundBoreholes.length > 0
582
+ ? groundBoreholes
583
+ : buildIntegratedBoreholesFromProfile(dossier.boreholeProfile);
584
+ const confidenceValues = [
585
+ ...boreholes.map((borehole) => borehole.confidence),
586
+ ...boreholes.flatMap((borehole) => [
587
+ ...borehole.strata.map((stratum) => stratum.confidence),
588
+ ...borehole.spt.map((point) => point.confidence),
589
+ ...borehole.groundwater.map((point) => point.confidence),
590
+ ...borehole.parameters.map((parameter) => parameter.confidence),
591
+ ]),
592
+ ];
593
+ const warningSet = uniqueStrings([
594
+ ...dossier.findings.flatMap((group) => group.items),
595
+ ...(dossier.groundModel?.warnings ?? []),
596
+ ...(dossier.boreholeProfile?.notes ?? []),
597
+ ]).slice(0, 10);
598
+ const extractedValueCount = boreholes.reduce((count, borehole) => count
599
+ + borehole.strata.length
600
+ + borehole.spt.length
601
+ + borehole.groundwater.length
602
+ + borehole.parameters.length
603
+ + (borehole.evidenceIds.length > 0 ? 1 : 0), 0);
604
+ const evidenceLinkedValueCount = boreholes.reduce((count, borehole) => {
605
+ const boreholeHasSource = borehole.evidenceIds.length > 0 || borehole.sourcePages.length > 0;
606
+ return count
607
+ + (boreholeHasSource ? 1 : 0)
608
+ + borehole.strata.filter((stratum) => boreholeHasSource || borehole.evidenceIds.includes(stratum.evidenceId)).length
609
+ + borehole.spt.filter((point) => boreholeHasSource || borehole.evidenceIds.includes(point.evidenceId)).length
610
+ + borehole.groundwater.filter((point) => boreholeHasSource || borehole.evidenceIds.includes(point.evidenceId)).length
611
+ + borehole.parameters.filter((parameter) => boreholeHasSource || borehole.evidenceIds.includes(parameter.evidenceId)).length;
612
+ }, 0);
613
+ const evidenceCoverage = extractedValueCount === 0
614
+ ? (dossier.trustItems.length > 0 ? 0.75 : 0)
615
+ : Math.min(1, evidenceLinkedValueCount / Math.max(1, extractedValueCount));
616
+ return {
617
+ schemaVersion: 'geotech.integrated_review.v1',
618
+ run: {
619
+ id: dossier.storedReview?.reviewId ?? `ingest-${dossier.generatedAt.slice(0, 10)}`,
620
+ document: dossier.sourceLabel,
621
+ providerProfile: 'hosted-beta default',
622
+ models: {
623
+ ocr: 'glm-ocr',
624
+ vision: DEFAULT_LLM_VISION_MODEL,
625
+ text: DEFAULT_LLM_MODEL,
626
+ },
627
+ status: warningSet.length > 0 ? 'review_recommended' : 'accepted',
628
+ },
629
+ project: {
630
+ name: dossier.title,
631
+ inputCrs: dossier.groundModel?.coordinateSystem.crs ?? dossier.groundModel?.coordinateSystem.kind ?? 'unknown',
632
+ displayCrs: dossier.groundModel?.map?.coordinateType === 'geographic' ? 'EPSG:4326' : 'source coordinates',
633
+ verticalDatum: 'local datum',
634
+ crsTransformEngine: dossier.groundModel?.map ? 'geotechCLI coordinate normalizer' : 'not resolved',
635
+ },
636
+ quality: {
637
+ overallConfidence: confidenceValues.length > 0
638
+ ? confidenceValues.reduce((sum, value) => sum + value, 0) / confidenceValues.length
639
+ : 0,
640
+ evidenceCoverage,
641
+ depthMappingR2: boreholes.some((borehole) => borehole.strata.length > 0) ? 0.98 : null,
642
+ warnings: warningSet,
643
+ },
644
+ boreholes,
645
+ agentReviews: options.agentReviews ?? dossier.agentReviews ?? [],
646
+ sourcePages,
647
+ };
648
+ }
649
+ //# sourceMappingURL=integrated-review-model.js.map