@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.
- package/dist/evidence/evidence-ref.d.ts +8 -0
- package/dist/evidence/evidence-ref.d.ts.map +1 -1
- package/dist/evidence/evidence-ref.js.map +1 -1
- package/dist/evidence/index.d.ts +1 -1
- package/dist/evidence/index.d.ts.map +1 -1
- package/dist/evidence/index.js.map +1 -1
- package/dist/fem/demo.d.ts +4 -0
- package/dist/fem/demo.d.ts.map +1 -0
- package/dist/fem/demo.js +274 -0
- package/dist/fem/demo.js.map +1 -0
- package/dist/fem/index.d.ts +5 -0
- package/dist/fem/index.d.ts.map +1 -0
- package/dist/fem/index.js +5 -0
- package/dist/fem/index.js.map +1 -0
- package/dist/fem/types.d.ts +154 -0
- package/dist/fem/types.d.ts.map +1 -0
- package/dist/fem/types.js +2 -0
- package/dist/fem/types.js.map +1 -0
- package/dist/fem/validation.d.ts +4 -0
- package/dist/fem/validation.d.ts.map +1 -0
- package/dist/fem/validation.js +195 -0
- package/dist/fem/validation.js.map +1 -0
- package/dist/fem/webgl.d.ts +3 -0
- package/dist/fem/webgl.d.ts.map +1 -0
- package/dist/fem/webgl.js +120 -0
- package/dist/fem/webgl.js.map +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/ingest/document-evidence-packet.d.ts +32 -32
- package/dist/ingest/geotech-document.d.ts +2 -0
- package/dist/ingest/geotech-document.d.ts.map +1 -1
- package/dist/ingest/geotech-document.js +17 -2
- package/dist/ingest/geotech-document.js.map +1 -1
- package/dist/ingest/geotech-extract.d.ts +2 -0
- package/dist/ingest/geotech-extract.d.ts.map +1 -1
- package/dist/ingest/geotech-extract.js +12 -0
- package/dist/ingest/geotech-extract.js.map +1 -1
- package/dist/ingest/page-evidence-cache.d.ts +4 -1
- package/dist/ingest/page-evidence-cache.d.ts.map +1 -1
- package/dist/ingest/page-evidence-cache.js +86 -1
- package/dist/ingest/page-evidence-cache.js.map +1 -1
- package/dist/llm/index.d.ts +1 -0
- package/dist/llm/index.d.ts.map +1 -1
- package/dist/llm/index.js +1 -0
- package/dist/llm/index.js.map +1 -1
- package/dist/meta/metadata.json +1 -1
- package/dist/report/html.d.ts.map +1 -1
- package/dist/report/html.js +596 -1309
- package/dist/report/html.js.map +1 -1
- package/dist/report/index.d.ts +1 -0
- package/dist/report/index.d.ts.map +1 -1
- package/dist/report/index.js +1 -0
- package/dist/report/index.js.map +1 -1
- package/dist/report/ingest-dossier.d.ts +5 -0
- package/dist/report/ingest-dossier.d.ts.map +1 -1
- package/dist/report/ingest-dossier.js +376 -2
- package/dist/report/ingest-dossier.js.map +1 -1
- package/dist/report/integrated-review-model.d.ts +147 -0
- package/dist/report/integrated-review-model.d.ts.map +1 -0
- package/dist/report/integrated-review-model.js +649 -0
- package/dist/report/integrated-review-model.js.map +1 -0
- 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
|