@geotechcli/core 0.4.22 → 0.4.23
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/agents/brain.d.ts.map +1 -1
- package/dist/agents/brain.js +2 -1
- package/dist/agents/brain.js.map +1 -1
- package/dist/agents/data-tools.js +759 -0
- package/dist/agents/data-tools.js.map +1 -1
- package/dist/agents/swarm.d.ts.map +1 -1
- package/dist/agents/swarm.js +22 -2
- package/dist/agents/swarm.js.map +1 -1
- package/dist/agents/tool-runtime.d.ts +7 -0
- package/dist/agents/tool-runtime.d.ts.map +1 -0
- package/dist/agents/tool-runtime.js +9 -0
- package/dist/agents/tool-runtime.js.map +1 -0
- package/dist/config/index.d.ts +4 -4
- package/dist/config/index.js +1 -1
- package/dist/config/index.js.map +1 -1
- package/dist/geo/coordinates.d.ts +40 -0
- package/dist/geo/coordinates.d.ts.map +1 -0
- package/dist/geo/coordinates.js +461 -0
- package/dist/geo/coordinates.js.map +1 -0
- package/dist/geo/index.d.ts +1 -0
- package/dist/geo/index.d.ts.map +1 -1
- package/dist/geo/index.js +1 -0
- package/dist/geo/index.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/ingest/ags.d.ts +3 -0
- package/dist/ingest/ags.d.ts.map +1 -1
- package/dist/ingest/ags.js +98 -9
- package/dist/ingest/ags.js.map +1 -1
- package/dist/ingest/cpt.d.ts +4 -0
- package/dist/ingest/cpt.d.ts.map +1 -1
- package/dist/ingest/cpt.js +87 -25
- package/dist/ingest/cpt.js.map +1 -1
- package/dist/ingest/document-inputs.d.ts +37 -0
- package/dist/ingest/document-inputs.d.ts.map +1 -0
- package/dist/ingest/document-inputs.js +197 -0
- package/dist/ingest/document-inputs.js.map +1 -0
- package/dist/ingest/geotech-document.d.ts +118 -0
- package/dist/ingest/geotech-document.d.ts.map +1 -0
- package/dist/ingest/geotech-document.js +1006 -0
- package/dist/ingest/geotech-document.js.map +1 -0
- package/dist/ingest/geotech-extract.d.ts +86 -0
- package/dist/ingest/geotech-extract.d.ts.map +1 -0
- package/dist/ingest/geotech-extract.js +652 -0
- package/dist/ingest/geotech-extract.js.map +1 -0
- package/dist/ingest/geotech-schemas.d.ts +248 -0
- package/dist/ingest/geotech-schemas.d.ts.map +1 -0
- package/dist/ingest/geotech-schemas.js +150 -0
- package/dist/ingest/geotech-schemas.js.map +1 -0
- package/dist/ingest/index.d.ts +8 -0
- package/dist/ingest/index.d.ts.map +1 -1
- package/dist/ingest/index.js +8 -0
- package/dist/ingest/index.js.map +1 -1
- package/dist/ingest/ingest-job-child.d.ts +2 -0
- package/dist/ingest/ingest-job-child.d.ts.map +1 -0
- package/dist/ingest/ingest-job-child.js +45 -0
- package/dist/ingest/ingest-job-child.js.map +1 -0
- package/dist/ingest/job-store.d.ts +117 -0
- package/dist/ingest/job-store.d.ts.map +1 -0
- package/dist/ingest/job-store.js +541 -0
- package/dist/ingest/job-store.js.map +1 -0
- package/dist/ingest/job-worker.d.ts +24 -0
- package/dist/ingest/job-worker.d.ts.map +1 -0
- package/dist/ingest/job-worker.js +1129 -0
- package/dist/ingest/job-worker.js.map +1 -0
- package/dist/ingest/pdf.d.ts +102 -0
- package/dist/ingest/pdf.d.ts.map +1 -0
- package/dist/ingest/pdf.js +1544 -0
- package/dist/ingest/pdf.js.map +1 -0
- package/dist/ingest/review-store.d.ts +215 -0
- package/dist/ingest/review-store.d.ts.map +1 -0
- package/dist/ingest/review-store.js +1995 -0
- package/dist/ingest/review-store.js.map +1 -0
- package/dist/llm/capabilities.d.ts +8 -0
- package/dist/llm/capabilities.d.ts.map +1 -0
- package/dist/llm/capabilities.js +73 -0
- package/dist/llm/capabilities.js.map +1 -0
- package/dist/llm/index.d.ts +3 -2
- package/dist/llm/index.d.ts.map +1 -1
- package/dist/llm/index.js +2 -1
- package/dist/llm/index.js.map +1 -1
- package/dist/llm/providers/anthropic.d.ts +6 -0
- package/dist/llm/providers/anthropic.d.ts.map +1 -1
- package/dist/llm/providers/anthropic.js +10 -1
- package/dist/llm/providers/anthropic.js.map +1 -1
- package/dist/llm/providers/hosted-beta.d.ts +6 -0
- package/dist/llm/providers/hosted-beta.d.ts.map +1 -1
- package/dist/llm/providers/hosted-beta.js +40 -10
- package/dist/llm/providers/hosted-beta.js.map +1 -1
- package/dist/llm/providers/huggingface.d.ts +6 -0
- package/dist/llm/providers/huggingface.d.ts.map +1 -1
- package/dist/llm/providers/huggingface.js +21 -1
- package/dist/llm/providers/huggingface.js.map +1 -1
- package/dist/llm/providers/openai-compatible.d.ts +6 -0
- package/dist/llm/providers/openai-compatible.d.ts.map +1 -1
- package/dist/llm/providers/openai-compatible.js +21 -1
- package/dist/llm/providers/openai-compatible.js.map +1 -1
- package/dist/llm/providers/zhipu.d.ts +6 -0
- package/dist/llm/providers/zhipu.d.ts.map +1 -1
- package/dist/llm/providers/zhipu.js +15 -1
- package/dist/llm/providers/zhipu.js.map +1 -1
- package/dist/llm/router.d.ts +7 -0
- package/dist/llm/router.d.ts.map +1 -1
- package/dist/llm/router.js +33 -13
- package/dist/llm/router.js.map +1 -1
- package/dist/llm/types.d.ts +22 -4
- package/dist/llm/types.d.ts.map +1 -1
- package/dist/llm/types.js.map +1 -1
- package/dist/meta/metadata.json +1 -1
- package/dist/report/html.d.ts +3 -0
- package/dist/report/html.d.ts.map +1 -0
- package/dist/report/html.js +626 -0
- package/dist/report/html.js.map +1 -0
- package/dist/report/index.d.ts +2 -0
- package/dist/report/index.d.ts.map +1 -1
- package/dist/report/index.js +2 -0
- package/dist/report/index.js.map +1 -1
- package/dist/report/ingest-dossier.d.ts +81 -0
- package/dist/report/ingest-dossier.d.ts.map +1 -0
- package/dist/report/ingest-dossier.js +324 -0
- package/dist/report/ingest-dossier.js.map +1 -0
- package/dist/storage/index.d.ts +5 -0
- package/dist/storage/index.d.ts.map +1 -1
- package/dist/storage/index.js +12 -6
- package/dist/storage/index.js.map +1 -1
- package/dist/vision/geotech-document.d.ts +46 -0
- package/dist/vision/geotech-document.d.ts.map +1 -0
- package/dist/vision/geotech-document.js +576 -0
- package/dist/vision/geotech-document.js.map +1 -0
- package/dist/vision/index.d.ts +31 -0
- package/dist/vision/index.d.ts.map +1 -1
- package/dist/vision/index.js +659 -27
- package/dist/vision/index.js.map +1 -1
- package/dist/vision/ocr.d.ts +29 -0
- package/dist/vision/ocr.d.ts.map +1 -0
- package/dist/vision/ocr.js +287 -0
- package/dist/vision/ocr.js.map +1 -0
- package/dist/vision/preprocess.d.ts +26 -0
- package/dist/vision/preprocess.d.ts.map +1 -0
- package/dist/vision/preprocess.js +194 -0
- package/dist/vision/preprocess.js.map +1 -0
- package/package.json +5 -1
|
@@ -0,0 +1,1995 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { basename } from 'node:path';
|
|
4
|
+
import { addArtifact, addNote, addSoilProfile, loadProject, saveNamedDataset, setActiveAnalysisContext, } from '../storage/index.js';
|
|
5
|
+
import { readDocumentPdfPageInputs, readDocumentVisionInput, } from './document-inputs.js';
|
|
6
|
+
import { ingestBoreholeLogDocument, } from './geotech-extract.js';
|
|
7
|
+
import { ingestGeotechDocument, } from './geotech-document.js';
|
|
8
|
+
import { inspectPdfDocument } from './pdf.js';
|
|
9
|
+
function isRecord(value) {
|
|
10
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
11
|
+
}
|
|
12
|
+
function asOptionalString(value) {
|
|
13
|
+
return typeof value === 'string' && value.trim() ? value.trim() : undefined;
|
|
14
|
+
}
|
|
15
|
+
function sanitizeToken(value) {
|
|
16
|
+
return value
|
|
17
|
+
.toLowerCase()
|
|
18
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
19
|
+
.replace(/^-|-$/g, '')
|
|
20
|
+
.slice(0, 48);
|
|
21
|
+
}
|
|
22
|
+
function buildTimestampToken(value) {
|
|
23
|
+
return value
|
|
24
|
+
.toLowerCase()
|
|
25
|
+
.replace(/[^0-9tz]+/g, '-')
|
|
26
|
+
.replace(/^-|-$/g, '');
|
|
27
|
+
}
|
|
28
|
+
function hashString(value) {
|
|
29
|
+
return createHash('sha256').update(value).digest('hex');
|
|
30
|
+
}
|
|
31
|
+
function normalizeForStableHash(value) {
|
|
32
|
+
if (Array.isArray(value)) {
|
|
33
|
+
return value.map((item) => normalizeForStableHash(item));
|
|
34
|
+
}
|
|
35
|
+
if (isRecord(value)) {
|
|
36
|
+
return Object.fromEntries(Object.keys(value)
|
|
37
|
+
.sort()
|
|
38
|
+
.map((key) => [key, normalizeForStableHash(value[key])]));
|
|
39
|
+
}
|
|
40
|
+
return value;
|
|
41
|
+
}
|
|
42
|
+
function stableHash(value) {
|
|
43
|
+
return hashString(JSON.stringify(normalizeForStableHash(value)));
|
|
44
|
+
}
|
|
45
|
+
function isBoreholeIngestResult(result) {
|
|
46
|
+
return result.documentType === 'borehole-log';
|
|
47
|
+
}
|
|
48
|
+
function isGeotechDocumentIngestResult(result) {
|
|
49
|
+
return result.documentType === 'geotech-document';
|
|
50
|
+
}
|
|
51
|
+
function reviewSourceLabel(result) {
|
|
52
|
+
return result.source.fileName ?? result.source.filePath ?? result.documentType;
|
|
53
|
+
}
|
|
54
|
+
function buildReviewId(result) {
|
|
55
|
+
const sourceToken = sanitizeToken(reviewSourceLabel(result)) || result.documentType;
|
|
56
|
+
const timestampToken = buildTimestampToken(result.generatedAt);
|
|
57
|
+
return `${timestampToken}-${sourceToken}`;
|
|
58
|
+
}
|
|
59
|
+
function buildApprovalId(reviewId, approvedAt) {
|
|
60
|
+
const reviewToken = sanitizeToken(reviewId) || 'review';
|
|
61
|
+
const timestampToken = buildTimestampToken(approvedAt);
|
|
62
|
+
return `${timestampToken}-${reviewToken}`;
|
|
63
|
+
}
|
|
64
|
+
function buildApprovalDatasetName(reviewId, approvedAt) {
|
|
65
|
+
return `ingest-review-approval:${reviewId}:${buildTimestampToken(approvedAt)}`;
|
|
66
|
+
}
|
|
67
|
+
function buildApprovalPointerDatasetName(reviewId) {
|
|
68
|
+
return `ingest-review-approval:latest:${reviewId}`;
|
|
69
|
+
}
|
|
70
|
+
function buildJobId(documentType, path, createdAt) {
|
|
71
|
+
const sourceToken = sanitizeToken(basename(path) || path) || documentType;
|
|
72
|
+
return `${buildTimestampToken(createdAt)}-${sanitizeToken(documentType)}-${sourceToken}`;
|
|
73
|
+
}
|
|
74
|
+
function buildJobDatasetName(jobId) {
|
|
75
|
+
return `ingest-job:${jobId}`;
|
|
76
|
+
}
|
|
77
|
+
function buildPromotedBoreholeDatasetName(reviewId, boreholeId) {
|
|
78
|
+
return `promoted-borehole:${reviewId}:${sanitizeToken(boreholeId) || 'unknown-borehole'}`;
|
|
79
|
+
}
|
|
80
|
+
function buildPromotedDocumentDatasetName(reviewId, role) {
|
|
81
|
+
return `${role}:${reviewId}`;
|
|
82
|
+
}
|
|
83
|
+
function buildPromotionDatasetName(reviewId) {
|
|
84
|
+
return `ingest-promotion:${reviewId}`;
|
|
85
|
+
}
|
|
86
|
+
function buildPromotionSnapshotDatasetName(reviewId, targetDatasetName, promotedAt) {
|
|
87
|
+
const datasetToken = sanitizeToken(targetDatasetName) || 'dataset';
|
|
88
|
+
const timestampToken = buildTimestampToken(promotedAt);
|
|
89
|
+
return `ingest-promotion-snapshot:${reviewId}:${datasetToken}:${timestampToken}`;
|
|
90
|
+
}
|
|
91
|
+
function summarizeInspectionForStamps(inspection) {
|
|
92
|
+
if (!inspection) {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
kind: inspection.kind,
|
|
97
|
+
totalPages: inspection.totalPages,
|
|
98
|
+
capabilities: inspection.capabilities,
|
|
99
|
+
degradation: inspection.degradation,
|
|
100
|
+
gracefulDegradationNotes: inspection.gracefulDegradationNotes,
|
|
101
|
+
metadata: inspection.metadata,
|
|
102
|
+
warnings: inspection.warnings,
|
|
103
|
+
pages: inspection.pages.map((page) => ({
|
|
104
|
+
pageNumber: page.pageNumber,
|
|
105
|
+
totalPages: page.totalPages,
|
|
106
|
+
classification: page.classification,
|
|
107
|
+
degradation: page.degradation,
|
|
108
|
+
capabilities: page.capabilities,
|
|
109
|
+
metadata: page.metadata,
|
|
110
|
+
warnings: page.warnings,
|
|
111
|
+
})),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
function summarizeInspectionForJob(inspection) {
|
|
115
|
+
if (!inspection) {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
return {
|
|
119
|
+
totalPages: inspection.totalPages,
|
|
120
|
+
warnings: inspection.warnings,
|
|
121
|
+
parserVersion: inspection.metadata.parser,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
function buildParserVersionForInput(documentType, inspection) {
|
|
125
|
+
const ingestFamily = documentType === 'borehole-log' ? 'geotech-extract@1' : 'geotech-document@1';
|
|
126
|
+
return `${ingestFamily}|result-schema@1|pdf-parser:${inspection?.metadata.parser ?? 'none'}`;
|
|
127
|
+
}
|
|
128
|
+
function normalizeBoreholeForHash(result) {
|
|
129
|
+
return {
|
|
130
|
+
kind: result.kind,
|
|
131
|
+
schemaVersion: result.schemaVersion,
|
|
132
|
+
documentType: result.documentType,
|
|
133
|
+
source: result.source,
|
|
134
|
+
inspectionSummary: result.inspectionSummary,
|
|
135
|
+
inspection: summarizeInspectionForStamps(result.inspection),
|
|
136
|
+
boreholes: result.boreholes.map((borehole) => ({
|
|
137
|
+
boreholeId: borehole.boreholeId,
|
|
138
|
+
projectName: borehole.projectName,
|
|
139
|
+
location: borehole.location,
|
|
140
|
+
groundElevation: borehole.groundElevation,
|
|
141
|
+
dateDrilled: borehole.dateDrilled,
|
|
142
|
+
drillingMethod: borehole.drillingMethod,
|
|
143
|
+
totalDepth: borehole.totalDepth,
|
|
144
|
+
waterTableDepth: borehole.waterTableDepth,
|
|
145
|
+
layers: borehole.layers,
|
|
146
|
+
summary: borehole.summary,
|
|
147
|
+
pageNumber: borehole.pageNumber,
|
|
148
|
+
totalPages: borehole.totalPages,
|
|
149
|
+
continuationDepth: borehole.continuationDepth,
|
|
150
|
+
parseStatus: borehole.parseStatus,
|
|
151
|
+
confidence: borehole.confidence,
|
|
152
|
+
warnings: borehole.warnings,
|
|
153
|
+
canAutoProceed: borehole.canAutoProceed,
|
|
154
|
+
})),
|
|
155
|
+
pageAudits: result.pageAudits,
|
|
156
|
+
pageFailures: result.pageFailures,
|
|
157
|
+
warnings: result.warnings,
|
|
158
|
+
reviewFindings: result.reviewFindings,
|
|
159
|
+
reviewReasons: result.reviewReasons,
|
|
160
|
+
reviewRequired: result.reviewRequired,
|
|
161
|
+
confidence: result.confidence,
|
|
162
|
+
canAutoProceed: result.canAutoProceed,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
function normalizeGeotechDocumentForHash(result) {
|
|
166
|
+
return {
|
|
167
|
+
kind: result.kind,
|
|
168
|
+
schemaVersion: result.schemaVersion,
|
|
169
|
+
documentType: result.documentType,
|
|
170
|
+
source: result.source,
|
|
171
|
+
inspectionSummary: result.inspectionSummary,
|
|
172
|
+
inspection: summarizeInspectionForStamps(result.inspection),
|
|
173
|
+
documentClass: result.documentClass,
|
|
174
|
+
title: result.title,
|
|
175
|
+
summary: result.summary,
|
|
176
|
+
materials: result.materials,
|
|
177
|
+
classifications: result.classifications,
|
|
178
|
+
parameters: result.parameters,
|
|
179
|
+
risks: result.risks,
|
|
180
|
+
recommendations: result.recommendations,
|
|
181
|
+
pageAudits: result.pageAudits,
|
|
182
|
+
pageFailures: result.pageFailures,
|
|
183
|
+
warnings: result.warnings,
|
|
184
|
+
reviewFindings: result.reviewFindings,
|
|
185
|
+
reviewReasons: result.reviewReasons,
|
|
186
|
+
parseStatus: result.parseStatus,
|
|
187
|
+
confidence: result.confidence,
|
|
188
|
+
reviewRequired: result.reviewRequired,
|
|
189
|
+
canAutoProceed: result.canAutoProceed,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
function readSourceFileDigest(filePath) {
|
|
193
|
+
if (!filePath || !existsSync(filePath)) {
|
|
194
|
+
return undefined;
|
|
195
|
+
}
|
|
196
|
+
try {
|
|
197
|
+
return createHash('sha256').update(readFileSync(filePath)).digest('hex');
|
|
198
|
+
}
|
|
199
|
+
catch {
|
|
200
|
+
return undefined;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
function buildSourceFingerprintForInput(input) {
|
|
204
|
+
return stableHash({
|
|
205
|
+
documentType: input.documentType,
|
|
206
|
+
fileDigest: readSourceFileDigest(input.filePath),
|
|
207
|
+
filePath: input.filePath,
|
|
208
|
+
fileName: input.fileName,
|
|
209
|
+
inputKind: input.inputKind,
|
|
210
|
+
fileBytes: input.fileBytes,
|
|
211
|
+
totalPages: input.totalPages,
|
|
212
|
+
inspection: summarizeInspectionForStamps(input.inspection),
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
function buildSourceStampsForResult(result, overrides) {
|
|
216
|
+
return {
|
|
217
|
+
sourceFingerprint: overrides?.sourceFingerprint ?? buildSourceFingerprintForInput({
|
|
218
|
+
documentType: result.documentType,
|
|
219
|
+
filePath: result.source.filePath,
|
|
220
|
+
fileName: result.source.fileName,
|
|
221
|
+
inputKind: result.source.inputKind,
|
|
222
|
+
totalPages: result.source.totalPages,
|
|
223
|
+
inspection: result.inspection,
|
|
224
|
+
}),
|
|
225
|
+
parserVersion: overrides?.parserVersion ?? buildParserVersionForInput(result.documentType, result.inspection),
|
|
226
|
+
normalizedResultHash: overrides?.normalizedResultHash ?? stableHash(isBoreholeIngestResult(result)
|
|
227
|
+
? normalizeBoreholeForHash(result)
|
|
228
|
+
: normalizeGeotechDocumentForHash(result)),
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
function buildJobSourceStamps(documentType, input, normalizedResultHash) {
|
|
232
|
+
return {
|
|
233
|
+
sourceFingerprint: buildSourceFingerprintForInput({
|
|
234
|
+
documentType,
|
|
235
|
+
filePath: input.filePath,
|
|
236
|
+
fileName: input.fileName,
|
|
237
|
+
inputKind: input.inputKind,
|
|
238
|
+
fileBytes: input.fileBytes,
|
|
239
|
+
totalPages: input.totalPages,
|
|
240
|
+
inspection: input.inspection,
|
|
241
|
+
}),
|
|
242
|
+
parserVersion: buildParserVersionForInput(documentType, input.inspection),
|
|
243
|
+
normalizedResultHash,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
function buildSummary(result) {
|
|
247
|
+
const counts = result.reviewFindings.reduce((acc, finding) => {
|
|
248
|
+
if (finding.severity === 'blocking')
|
|
249
|
+
acc.blockingFindings += 1;
|
|
250
|
+
else if (finding.severity === 'review')
|
|
251
|
+
acc.reviewFindings += 1;
|
|
252
|
+
else
|
|
253
|
+
acc.advisoryFindings += 1;
|
|
254
|
+
return acc;
|
|
255
|
+
}, { blockingFindings: 0, reviewFindings: 0, advisoryFindings: 0 });
|
|
256
|
+
const boreholeIds = isBoreholeIngestResult(result)
|
|
257
|
+
? result.boreholes.map((borehole) => borehole.boreholeId)
|
|
258
|
+
: [];
|
|
259
|
+
return {
|
|
260
|
+
reviewRequired: result.reviewRequired,
|
|
261
|
+
canAutoProceed: result.canAutoProceed,
|
|
262
|
+
confidence: result.confidence,
|
|
263
|
+
totalPages: result.source.totalPages,
|
|
264
|
+
successfulPages: result.source.successfulPages,
|
|
265
|
+
failedPages: result.source.failedPages,
|
|
266
|
+
boreholeCount: boreholeIds.length,
|
|
267
|
+
boreholeIds,
|
|
268
|
+
...counts,
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
function buildTitle(result, explicitTitle) {
|
|
272
|
+
if (explicitTitle?.trim()) {
|
|
273
|
+
return explicitTitle.trim();
|
|
274
|
+
}
|
|
275
|
+
return `Ingest review: ${reviewSourceLabel(result)}`;
|
|
276
|
+
}
|
|
277
|
+
function buildJobTitle(documentType, fileName) {
|
|
278
|
+
return `Ingest job: ${fileName || documentType}`;
|
|
279
|
+
}
|
|
280
|
+
function buildArtifactPreview(record) {
|
|
281
|
+
const summary = record.summary;
|
|
282
|
+
const findings = [];
|
|
283
|
+
if (summary.blockingFindings > 0)
|
|
284
|
+
findings.push(`${summary.blockingFindings} blocking`);
|
|
285
|
+
if (summary.reviewFindings > 0)
|
|
286
|
+
findings.push(`${summary.reviewFindings} review`);
|
|
287
|
+
if (summary.advisoryFindings > 0)
|
|
288
|
+
findings.push(`${summary.advisoryFindings} advisory`);
|
|
289
|
+
const extractedSummary = isBoreholeIngestResult(record.result)
|
|
290
|
+
? [`Boreholes: ${summary.boreholeCount}`]
|
|
291
|
+
: [
|
|
292
|
+
`Materials: ${record.result.materials.length}`,
|
|
293
|
+
`Classifications: ${record.result.classifications.length}`,
|
|
294
|
+
`Parameters: ${record.result.parameters.length}`,
|
|
295
|
+
];
|
|
296
|
+
return [
|
|
297
|
+
`Document type: ${record.result.documentType}`,
|
|
298
|
+
`Source: ${reviewSourceLabel(record.result)}`,
|
|
299
|
+
...extractedSummary,
|
|
300
|
+
`Pages: ${summary.successfulPages}/${summary.totalPages}`,
|
|
301
|
+
`Confidence: ${summary.confidence}%`,
|
|
302
|
+
`Review required: ${summary.reviewRequired ? 'Yes' : 'No'}`,
|
|
303
|
+
`Auto proceed: ${summary.canAutoProceed ? 'Yes' : 'No'}`,
|
|
304
|
+
`Findings: ${findings.length > 0 ? findings.join(', ') : 'none'}`,
|
|
305
|
+
`Source fingerprint: ${record.sourceStamps.sourceFingerprint.slice(0, 12)}`,
|
|
306
|
+
`Parser version: ${record.sourceStamps.parserVersion}`,
|
|
307
|
+
].join('\n');
|
|
308
|
+
}
|
|
309
|
+
function buildApprovalArtifactPreview(record, approval) {
|
|
310
|
+
const extractedSummary = isBoreholeIngestResult(record.result)
|
|
311
|
+
? [`Boreholes at ingest: ${record.summary.boreholeCount}`]
|
|
312
|
+
: [
|
|
313
|
+
`Materials at ingest: ${record.result.materials.length}`,
|
|
314
|
+
`Parameters at ingest: ${record.result.parameters.length}`,
|
|
315
|
+
];
|
|
316
|
+
return [
|
|
317
|
+
`Source review: ${record.datasetName}`,
|
|
318
|
+
`Review title: ${record.title}`,
|
|
319
|
+
`Document type: ${record.result.documentType}`,
|
|
320
|
+
`Approved at: ${approval.approvedAt}`,
|
|
321
|
+
`Approved by: ${approval.approvedBy ?? 'Unspecified'}`,
|
|
322
|
+
`Rationale: ${approval.rationale}`,
|
|
323
|
+
`Confidence: ${record.summary.confidence}%`,
|
|
324
|
+
`Auto proceed at ingest: ${record.summary.canAutoProceed ? 'Yes' : 'No'}`,
|
|
325
|
+
...extractedSummary,
|
|
326
|
+
`Blocking findings: ${record.summary.blockingFindings}`,
|
|
327
|
+
`Review findings: ${record.summary.reviewFindings}`,
|
|
328
|
+
`Source fingerprint: ${record.sourceStamps.sourceFingerprint.slice(0, 12)}`,
|
|
329
|
+
`Result hash: ${record.sourceStamps.normalizedResultHash.slice(0, 12)}`,
|
|
330
|
+
].join('\n');
|
|
331
|
+
}
|
|
332
|
+
function buildJobArtifactPreview(record) {
|
|
333
|
+
const lines = [
|
|
334
|
+
`Job dataset: ${record.datasetName}`,
|
|
335
|
+
`Status: ${record.status}`,
|
|
336
|
+
`Document type: ${record.documentType}`,
|
|
337
|
+
`Source: ${record.source.fileName}`,
|
|
338
|
+
`Parser version: ${record.sourceStamps.parserVersion}`,
|
|
339
|
+
`Source fingerprint: ${record.sourceStamps.sourceFingerprint.slice(0, 12)}`,
|
|
340
|
+
];
|
|
341
|
+
if (record.resultSummary) {
|
|
342
|
+
lines.push(`Confidence: ${record.resultSummary.confidence}%`);
|
|
343
|
+
lines.push(`Blocking findings: ${record.resultSummary.blockingFindings}`);
|
|
344
|
+
lines.push(`Review findings: ${record.resultSummary.reviewFindings}`);
|
|
345
|
+
}
|
|
346
|
+
if (record.persistedReview) {
|
|
347
|
+
lines.push(`Persisted review: ${record.persistedReview.datasetName}`);
|
|
348
|
+
}
|
|
349
|
+
if (record.error) {
|
|
350
|
+
lines.push(`Error: ${record.error}`);
|
|
351
|
+
}
|
|
352
|
+
return lines.join('\n');
|
|
353
|
+
}
|
|
354
|
+
function normalizeBoreholeReviewFindings(value) {
|
|
355
|
+
if (!Array.isArray(value)) {
|
|
356
|
+
return [];
|
|
357
|
+
}
|
|
358
|
+
return value.flatMap((item) => {
|
|
359
|
+
if (!isRecord(item)) {
|
|
360
|
+
return [];
|
|
361
|
+
}
|
|
362
|
+
const code = asOptionalString(item.code);
|
|
363
|
+
const severity = asOptionalString(item.severity);
|
|
364
|
+
const scope = asOptionalString(item.scope);
|
|
365
|
+
const message = asOptionalString(item.message);
|
|
366
|
+
if (!code
|
|
367
|
+
|| !message
|
|
368
|
+
|| (severity !== 'advisory' && severity !== 'review' && severity !== 'blocking')
|
|
369
|
+
|| (scope !== 'document' && scope !== 'page' && scope !== 'borehole')) {
|
|
370
|
+
return [];
|
|
371
|
+
}
|
|
372
|
+
return [{
|
|
373
|
+
code,
|
|
374
|
+
severity,
|
|
375
|
+
scope,
|
|
376
|
+
message,
|
|
377
|
+
pageNumber: typeof item.pageNumber === 'number' && Number.isFinite(item.pageNumber) ? item.pageNumber : undefined,
|
|
378
|
+
boreholeId: asOptionalString(item.boreholeId),
|
|
379
|
+
}];
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
function normalizeGeotechDocumentReviewFindings(value) {
|
|
383
|
+
if (!Array.isArray(value)) {
|
|
384
|
+
return [];
|
|
385
|
+
}
|
|
386
|
+
return value.flatMap((item) => {
|
|
387
|
+
if (!isRecord(item)) {
|
|
388
|
+
return [];
|
|
389
|
+
}
|
|
390
|
+
const code = asOptionalString(item.code);
|
|
391
|
+
const severity = asOptionalString(item.severity);
|
|
392
|
+
const scope = asOptionalString(item.scope);
|
|
393
|
+
const message = asOptionalString(item.message);
|
|
394
|
+
if (!code
|
|
395
|
+
|| !message
|
|
396
|
+
|| (severity !== 'advisory' && severity !== 'review' && severity !== 'blocking')
|
|
397
|
+
|| (scope !== 'document' && scope !== 'page' && scope !== 'material')) {
|
|
398
|
+
return [];
|
|
399
|
+
}
|
|
400
|
+
return [{
|
|
401
|
+
code,
|
|
402
|
+
severity,
|
|
403
|
+
scope,
|
|
404
|
+
message,
|
|
405
|
+
pageNumber: typeof item.pageNumber === 'number' && Number.isFinite(item.pageNumber) ? item.pageNumber : undefined,
|
|
406
|
+
materialDescription: asOptionalString(item.materialDescription),
|
|
407
|
+
}];
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
function normalizeReviewSummary(value) {
|
|
411
|
+
if (!isRecord(value)) {
|
|
412
|
+
return null;
|
|
413
|
+
}
|
|
414
|
+
const reviewRequired = typeof value.reviewRequired === 'boolean' ? value.reviewRequired : null;
|
|
415
|
+
const canAutoProceed = typeof value.canAutoProceed === 'boolean' ? value.canAutoProceed : null;
|
|
416
|
+
const confidence = typeof value.confidence === 'number' && Number.isFinite(value.confidence) ? value.confidence : null;
|
|
417
|
+
const totalPages = typeof value.totalPages === 'number' && Number.isFinite(value.totalPages) ? value.totalPages : null;
|
|
418
|
+
const successfulPages = typeof value.successfulPages === 'number' && Number.isFinite(value.successfulPages) ? value.successfulPages : null;
|
|
419
|
+
const failedPages = typeof value.failedPages === 'number' && Number.isFinite(value.failedPages) ? value.failedPages : null;
|
|
420
|
+
const boreholeCount = typeof value.boreholeCount === 'number' && Number.isFinite(value.boreholeCount) ? value.boreholeCount : null;
|
|
421
|
+
const blockingFindings = typeof value.blockingFindings === 'number' && Number.isFinite(value.blockingFindings) ? value.blockingFindings : null;
|
|
422
|
+
const reviewFindings = typeof value.reviewFindings === 'number' && Number.isFinite(value.reviewFindings) ? value.reviewFindings : null;
|
|
423
|
+
const advisoryFindings = typeof value.advisoryFindings === 'number' && Number.isFinite(value.advisoryFindings) ? value.advisoryFindings : null;
|
|
424
|
+
const boreholeIds = Array.isArray(value.boreholeIds)
|
|
425
|
+
? value.boreholeIds.flatMap((item) => {
|
|
426
|
+
const normalized = asOptionalString(item);
|
|
427
|
+
return normalized ? [normalized] : [];
|
|
428
|
+
})
|
|
429
|
+
: null;
|
|
430
|
+
if (reviewRequired == null
|
|
431
|
+
|| canAutoProceed == null
|
|
432
|
+
|| confidence == null
|
|
433
|
+
|| totalPages == null
|
|
434
|
+
|| successfulPages == null
|
|
435
|
+
|| failedPages == null
|
|
436
|
+
|| boreholeCount == null
|
|
437
|
+
|| blockingFindings == null
|
|
438
|
+
|| reviewFindings == null
|
|
439
|
+
|| advisoryFindings == null
|
|
440
|
+
|| boreholeIds == null) {
|
|
441
|
+
return null;
|
|
442
|
+
}
|
|
443
|
+
return {
|
|
444
|
+
reviewRequired,
|
|
445
|
+
canAutoProceed,
|
|
446
|
+
confidence,
|
|
447
|
+
totalPages,
|
|
448
|
+
successfulPages,
|
|
449
|
+
failedPages,
|
|
450
|
+
boreholeCount,
|
|
451
|
+
boreholeIds,
|
|
452
|
+
blockingFindings,
|
|
453
|
+
reviewFindings,
|
|
454
|
+
advisoryFindings,
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
function normalizeSourceStamps(value) {
|
|
458
|
+
if (!isRecord(value)) {
|
|
459
|
+
return null;
|
|
460
|
+
}
|
|
461
|
+
const sourceFingerprint = asOptionalString(value.sourceFingerprint);
|
|
462
|
+
const parserVersion = asOptionalString(value.parserVersion);
|
|
463
|
+
const normalizedResultHash = asOptionalString(value.normalizedResultHash);
|
|
464
|
+
if (!sourceFingerprint || !parserVersion || !normalizedResultHash) {
|
|
465
|
+
return null;
|
|
466
|
+
}
|
|
467
|
+
return {
|
|
468
|
+
sourceFingerprint,
|
|
469
|
+
parserVersion,
|
|
470
|
+
normalizedResultHash,
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
function normalizeJobSourceStamps(value) {
|
|
474
|
+
if (!isRecord(value)) {
|
|
475
|
+
return null;
|
|
476
|
+
}
|
|
477
|
+
const sourceFingerprint = asOptionalString(value.sourceFingerprint);
|
|
478
|
+
const parserVersion = asOptionalString(value.parserVersion);
|
|
479
|
+
const normalizedResultHash = asOptionalString(value.normalizedResultHash);
|
|
480
|
+
if (!sourceFingerprint || !parserVersion) {
|
|
481
|
+
return null;
|
|
482
|
+
}
|
|
483
|
+
return {
|
|
484
|
+
sourceFingerprint,
|
|
485
|
+
parserVersion,
|
|
486
|
+
normalizedResultHash,
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
function normalizeBoreholeIngestResult(value) {
|
|
490
|
+
if (!isRecord(value) || value.kind !== 'geotech-ingest-result') {
|
|
491
|
+
return null;
|
|
492
|
+
}
|
|
493
|
+
if (value.schemaVersion !== 1
|
|
494
|
+
|| value.documentType !== 'borehole-log'
|
|
495
|
+
|| !isRecord(value.source)
|
|
496
|
+
|| !Array.isArray(value.boreholes)
|
|
497
|
+
|| !Array.isArray(value.pageAudits)
|
|
498
|
+
|| !Array.isArray(value.pageFailures)
|
|
499
|
+
|| !Array.isArray(value.warnings)
|
|
500
|
+
|| !Array.isArray(value.reviewReasons)) {
|
|
501
|
+
return null;
|
|
502
|
+
}
|
|
503
|
+
return {
|
|
504
|
+
...value,
|
|
505
|
+
reviewFindings: normalizeBoreholeReviewFindings(value.reviewFindings),
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
function normalizeGeotechDocumentIngestResult(value) {
|
|
509
|
+
if (!isRecord(value) || value.kind !== 'geotech-ingest-result') {
|
|
510
|
+
return null;
|
|
511
|
+
}
|
|
512
|
+
if (value.schemaVersion !== 1
|
|
513
|
+
|| value.documentType !== 'geotech-document'
|
|
514
|
+
|| !isRecord(value.source)
|
|
515
|
+
|| !Array.isArray(value.materials)
|
|
516
|
+
|| !Array.isArray(value.classifications)
|
|
517
|
+
|| !Array.isArray(value.parameters)
|
|
518
|
+
|| !Array.isArray(value.risks)
|
|
519
|
+
|| !Array.isArray(value.recommendations)
|
|
520
|
+
|| !Array.isArray(value.pageAudits)
|
|
521
|
+
|| !Array.isArray(value.pageFailures)
|
|
522
|
+
|| !Array.isArray(value.warnings)
|
|
523
|
+
|| !Array.isArray(value.reviewReasons)) {
|
|
524
|
+
return null;
|
|
525
|
+
}
|
|
526
|
+
return {
|
|
527
|
+
...value,
|
|
528
|
+
reviewFindings: normalizeGeotechDocumentReviewFindings(value.reviewFindings),
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
function normalizeIngestResult(value) {
|
|
532
|
+
if (!isRecord(value) || value.kind !== 'geotech-ingest-result') {
|
|
533
|
+
return null;
|
|
534
|
+
}
|
|
535
|
+
if (value.documentType === 'borehole-log') {
|
|
536
|
+
return normalizeBoreholeIngestResult(value);
|
|
537
|
+
}
|
|
538
|
+
if (value.documentType === 'geotech-document') {
|
|
539
|
+
return normalizeGeotechDocumentIngestResult(value);
|
|
540
|
+
}
|
|
541
|
+
return null;
|
|
542
|
+
}
|
|
543
|
+
function normalizeReviewRecord(value) {
|
|
544
|
+
if (!isRecord(value) || value.kind !== 'geotech-ingest-review-record') {
|
|
545
|
+
return null;
|
|
546
|
+
}
|
|
547
|
+
const result = normalizeIngestResult(value.result);
|
|
548
|
+
const reviewId = asOptionalString(value.reviewId);
|
|
549
|
+
const datasetName = asOptionalString(value.datasetName);
|
|
550
|
+
const projectId = asOptionalString(value.projectId);
|
|
551
|
+
const createdAt = asOptionalString(value.createdAt);
|
|
552
|
+
const title = asOptionalString(value.title);
|
|
553
|
+
if (!result || !reviewId || !datasetName || !projectId || !createdAt || !title) {
|
|
554
|
+
return null;
|
|
555
|
+
}
|
|
556
|
+
return {
|
|
557
|
+
kind: 'geotech-ingest-review-record',
|
|
558
|
+
schemaVersion: 1,
|
|
559
|
+
reviewId,
|
|
560
|
+
datasetName,
|
|
561
|
+
projectId,
|
|
562
|
+
createdAt,
|
|
563
|
+
title,
|
|
564
|
+
result,
|
|
565
|
+
summary: buildSummary(result),
|
|
566
|
+
sourceStamps: normalizeSourceStamps(value.sourceStamps) ?? buildSourceStampsForResult(result),
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
function normalizeApprovalRecord(value) {
|
|
570
|
+
if (!isRecord(value) || value.kind !== 'geotech-ingest-review-approval-record') {
|
|
571
|
+
return null;
|
|
572
|
+
}
|
|
573
|
+
const approvalId = asOptionalString(value.approvalId);
|
|
574
|
+
const datasetName = asOptionalString(value.datasetName);
|
|
575
|
+
const projectId = asOptionalString(value.projectId);
|
|
576
|
+
const reviewId = asOptionalString(value.reviewId);
|
|
577
|
+
const reviewDatasetName = asOptionalString(value.reviewDatasetName);
|
|
578
|
+
const approvedAt = asOptionalString(value.approvedAt);
|
|
579
|
+
const rationale = asOptionalString(value.rationale);
|
|
580
|
+
const approvedBy = asOptionalString(value.approvedBy);
|
|
581
|
+
const sourceSummary = normalizeReviewSummary(value.sourceSummary);
|
|
582
|
+
if (value.schemaVersion !== 1
|
|
583
|
+
|| !approvalId
|
|
584
|
+
|| !datasetName
|
|
585
|
+
|| !projectId
|
|
586
|
+
|| !reviewId
|
|
587
|
+
|| !reviewDatasetName
|
|
588
|
+
|| !approvedAt
|
|
589
|
+
|| !rationale
|
|
590
|
+
|| !sourceSummary) {
|
|
591
|
+
return null;
|
|
592
|
+
}
|
|
593
|
+
return {
|
|
594
|
+
kind: 'geotech-ingest-review-approval-record',
|
|
595
|
+
schemaVersion: 1,
|
|
596
|
+
approvalId,
|
|
597
|
+
datasetName,
|
|
598
|
+
projectId,
|
|
599
|
+
reviewId,
|
|
600
|
+
reviewDatasetName,
|
|
601
|
+
approvedAt,
|
|
602
|
+
approvedBy,
|
|
603
|
+
rationale,
|
|
604
|
+
sourceSummary,
|
|
605
|
+
sourceStamps: normalizeSourceStamps(value.sourceStamps) ?? undefined,
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
function normalizePointer(value) {
|
|
609
|
+
if (!isRecord(value) || value.kind !== 'geotech-ingest-review-pointer') {
|
|
610
|
+
return null;
|
|
611
|
+
}
|
|
612
|
+
const datasetName = asOptionalString(value.datasetName);
|
|
613
|
+
const reviewId = asOptionalString(value.reviewId);
|
|
614
|
+
const updatedAt = asOptionalString(value.updatedAt);
|
|
615
|
+
if (!datasetName || !reviewId || !updatedAt) {
|
|
616
|
+
return null;
|
|
617
|
+
}
|
|
618
|
+
return {
|
|
619
|
+
kind: 'geotech-ingest-review-pointer',
|
|
620
|
+
schemaVersion: 1,
|
|
621
|
+
datasetName,
|
|
622
|
+
reviewId,
|
|
623
|
+
updatedAt,
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
function normalizeApprovalPointer(value) {
|
|
627
|
+
if (!isRecord(value) || value.kind !== 'geotech-ingest-review-approval-pointer') {
|
|
628
|
+
return null;
|
|
629
|
+
}
|
|
630
|
+
const reviewId = asOptionalString(value.reviewId);
|
|
631
|
+
const reviewDatasetName = asOptionalString(value.reviewDatasetName);
|
|
632
|
+
const approvalDatasetName = asOptionalString(value.approvalDatasetName);
|
|
633
|
+
const approvalId = asOptionalString(value.approvalId);
|
|
634
|
+
const updatedAt = asOptionalString(value.updatedAt);
|
|
635
|
+
if (!reviewId || !reviewDatasetName || !approvalDatasetName || !approvalId || !updatedAt) {
|
|
636
|
+
return null;
|
|
637
|
+
}
|
|
638
|
+
return {
|
|
639
|
+
kind: 'geotech-ingest-review-approval-pointer',
|
|
640
|
+
schemaVersion: 1,
|
|
641
|
+
reviewId,
|
|
642
|
+
reviewDatasetName,
|
|
643
|
+
approvalDatasetName,
|
|
644
|
+
approvalId,
|
|
645
|
+
updatedAt,
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
function normalizeJobPointer(value) {
|
|
649
|
+
if (!isRecord(value) || value.kind !== 'geotech-ingest-job-pointer') {
|
|
650
|
+
return null;
|
|
651
|
+
}
|
|
652
|
+
const datasetName = asOptionalString(value.datasetName);
|
|
653
|
+
const jobId = asOptionalString(value.jobId);
|
|
654
|
+
const updatedAt = asOptionalString(value.updatedAt);
|
|
655
|
+
if (!datasetName || !jobId || !updatedAt) {
|
|
656
|
+
return null;
|
|
657
|
+
}
|
|
658
|
+
return {
|
|
659
|
+
kind: 'geotech-ingest-job-pointer',
|
|
660
|
+
schemaVersion: 1,
|
|
661
|
+
datasetName,
|
|
662
|
+
jobId,
|
|
663
|
+
updatedAt,
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
function normalizeJobRecord(value) {
|
|
667
|
+
if (!isRecord(value) || value.kind !== 'geotech-ingest-job-record') {
|
|
668
|
+
return null;
|
|
669
|
+
}
|
|
670
|
+
const jobId = asOptionalString(value.jobId);
|
|
671
|
+
const datasetName = asOptionalString(value.datasetName);
|
|
672
|
+
const projectId = asOptionalString(value.projectId);
|
|
673
|
+
const documentType = value.documentType === 'borehole-log' || value.documentType === 'geotech-document'
|
|
674
|
+
? value.documentType
|
|
675
|
+
: null;
|
|
676
|
+
const title = asOptionalString(value.title);
|
|
677
|
+
const status = value.status === 'queued'
|
|
678
|
+
|| value.status === 'running'
|
|
679
|
+
|| value.status === 'completed'
|
|
680
|
+
|| value.status === 'failed'
|
|
681
|
+
? value.status
|
|
682
|
+
: null;
|
|
683
|
+
const createdAt = asOptionalString(value.createdAt);
|
|
684
|
+
const updatedAt = asOptionalString(value.updatedAt);
|
|
685
|
+
const startedAt = asOptionalString(value.startedAt);
|
|
686
|
+
const completedAt = asOptionalString(value.completedAt);
|
|
687
|
+
const request = isRecord(value.request)
|
|
688
|
+
? {
|
|
689
|
+
path: asOptionalString(value.request.path),
|
|
690
|
+
boreholeId: asOptionalString(value.request.boreholeId),
|
|
691
|
+
persistReview: value.request.persistReview === true,
|
|
692
|
+
reviewTitle: asOptionalString(value.request.reviewTitle),
|
|
693
|
+
}
|
|
694
|
+
: null;
|
|
695
|
+
const source = isRecord(value.source)
|
|
696
|
+
? {
|
|
697
|
+
filePath: asOptionalString(value.source.filePath),
|
|
698
|
+
fileName: asOptionalString(value.source.fileName),
|
|
699
|
+
inputKind: value.source.inputKind === 'image' || value.source.inputKind === 'pdf'
|
|
700
|
+
? value.source.inputKind
|
|
701
|
+
: null,
|
|
702
|
+
fileBytes: typeof value.source.fileBytes === 'number' && Number.isFinite(value.source.fileBytes)
|
|
703
|
+
? value.source.fileBytes
|
|
704
|
+
: null,
|
|
705
|
+
totalPages: typeof value.source.totalPages === 'number' && Number.isFinite(value.source.totalPages)
|
|
706
|
+
? value.source.totalPages
|
|
707
|
+
: undefined,
|
|
708
|
+
}
|
|
709
|
+
: null;
|
|
710
|
+
const inspection = value.inspection === null
|
|
711
|
+
? null
|
|
712
|
+
: isRecord(value.inspection)
|
|
713
|
+
? {
|
|
714
|
+
totalPages: typeof value.inspection.totalPages === 'number' && Number.isFinite(value.inspection.totalPages)
|
|
715
|
+
? value.inspection.totalPages
|
|
716
|
+
: null,
|
|
717
|
+
warnings: Array.isArray(value.inspection.warnings)
|
|
718
|
+
? value.inspection.warnings.flatMap((item) => {
|
|
719
|
+
const normalized = asOptionalString(item);
|
|
720
|
+
return normalized ? [normalized] : [];
|
|
721
|
+
})
|
|
722
|
+
: [],
|
|
723
|
+
parserVersion: asOptionalString(value.inspection.parserVersion),
|
|
724
|
+
}
|
|
725
|
+
: null;
|
|
726
|
+
const result = value.result ? normalizeIngestResult(value.result) : undefined;
|
|
727
|
+
const resultSummary = value.resultSummary ? normalizeReviewSummary(value.resultSummary) : undefined;
|
|
728
|
+
const persistedReview = isRecord(value.persistedReview)
|
|
729
|
+
? {
|
|
730
|
+
reviewId: asOptionalString(value.persistedReview.reviewId),
|
|
731
|
+
datasetName: asOptionalString(value.persistedReview.datasetName),
|
|
732
|
+
title: asOptionalString(value.persistedReview.title),
|
|
733
|
+
summary: normalizeReviewSummary(value.persistedReview.summary),
|
|
734
|
+
sourceStamps: normalizeSourceStamps(value.persistedReview.sourceStamps),
|
|
735
|
+
}
|
|
736
|
+
: null;
|
|
737
|
+
const error = asOptionalString(value.error);
|
|
738
|
+
const sourceStamps = normalizeJobSourceStamps(value.sourceStamps)
|
|
739
|
+
?? (result
|
|
740
|
+
? buildJobSourceStamps(result.documentType, {
|
|
741
|
+
filePath: result.source.filePath ?? source?.filePath ?? '',
|
|
742
|
+
fileName: result.source.fileName ?? source?.fileName ?? '',
|
|
743
|
+
inputKind: result.source.inputKind,
|
|
744
|
+
fileBytes: source?.fileBytes ?? 0,
|
|
745
|
+
totalPages: result.source.totalPages,
|
|
746
|
+
inspection: result.inspection,
|
|
747
|
+
}, buildSourceStampsForResult(result).normalizedResultHash)
|
|
748
|
+
: null);
|
|
749
|
+
if (value.schemaVersion !== 1
|
|
750
|
+
|| !jobId
|
|
751
|
+
|| !datasetName
|
|
752
|
+
|| !projectId
|
|
753
|
+
|| !documentType
|
|
754
|
+
|| !title
|
|
755
|
+
|| !status
|
|
756
|
+
|| !createdAt
|
|
757
|
+
|| !updatedAt
|
|
758
|
+
|| !request?.path
|
|
759
|
+
|| !source?.filePath
|
|
760
|
+
|| !source.fileName
|
|
761
|
+
|| !source.inputKind
|
|
762
|
+
|| source.fileBytes == null
|
|
763
|
+
|| (inspection !== null && (!inspection?.parserVersion || inspection.totalPages == null))
|
|
764
|
+
|| !sourceStamps
|
|
765
|
+
|| (result && !resultSummary)
|
|
766
|
+
|| (persistedReview !== null && (!persistedReview?.reviewId
|
|
767
|
+
|| !persistedReview.datasetName
|
|
768
|
+
|| !persistedReview.title
|
|
769
|
+
|| !persistedReview.summary
|
|
770
|
+
|| !persistedReview.sourceStamps))) {
|
|
771
|
+
return null;
|
|
772
|
+
}
|
|
773
|
+
return {
|
|
774
|
+
kind: 'geotech-ingest-job-record',
|
|
775
|
+
schemaVersion: 1,
|
|
776
|
+
jobId,
|
|
777
|
+
datasetName,
|
|
778
|
+
projectId,
|
|
779
|
+
documentType,
|
|
780
|
+
title,
|
|
781
|
+
status,
|
|
782
|
+
createdAt,
|
|
783
|
+
updatedAt,
|
|
784
|
+
startedAt,
|
|
785
|
+
completedAt,
|
|
786
|
+
request: {
|
|
787
|
+
path: request.path,
|
|
788
|
+
boreholeId: request.boreholeId,
|
|
789
|
+
persistReview: request.persistReview,
|
|
790
|
+
reviewTitle: request.reviewTitle,
|
|
791
|
+
},
|
|
792
|
+
source: {
|
|
793
|
+
filePath: source.filePath,
|
|
794
|
+
fileName: source.fileName,
|
|
795
|
+
inputKind: source.inputKind,
|
|
796
|
+
fileBytes: source.fileBytes,
|
|
797
|
+
totalPages: source.totalPages,
|
|
798
|
+
},
|
|
799
|
+
inspection: inspection === null
|
|
800
|
+
? null
|
|
801
|
+
: {
|
|
802
|
+
totalPages: inspection.totalPages,
|
|
803
|
+
warnings: inspection.warnings,
|
|
804
|
+
parserVersion: inspection.parserVersion,
|
|
805
|
+
},
|
|
806
|
+
sourceStamps,
|
|
807
|
+
result: result ?? undefined,
|
|
808
|
+
resultSummary: resultSummary ?? undefined,
|
|
809
|
+
persistedReview: persistedReview === null
|
|
810
|
+
? undefined
|
|
811
|
+
: {
|
|
812
|
+
reviewId: persistedReview.reviewId,
|
|
813
|
+
datasetName: persistedReview.datasetName,
|
|
814
|
+
title: persistedReview.title,
|
|
815
|
+
summary: persistedReview.summary,
|
|
816
|
+
sourceStamps: persistedReview.sourceStamps,
|
|
817
|
+
},
|
|
818
|
+
error,
|
|
819
|
+
};
|
|
820
|
+
}
|
|
821
|
+
function reviewSummariesMatch(left, right) {
|
|
822
|
+
return stableHash(left) === stableHash(right);
|
|
823
|
+
}
|
|
824
|
+
function validateApprovalForReview(record, approval) {
|
|
825
|
+
const reasons = [];
|
|
826
|
+
if (approval.reviewId !== record.reviewId) {
|
|
827
|
+
reasons.push('review ID changed');
|
|
828
|
+
}
|
|
829
|
+
if (approval.reviewDatasetName !== record.datasetName) {
|
|
830
|
+
reasons.push('review dataset changed');
|
|
831
|
+
}
|
|
832
|
+
if (approval.sourceStamps) {
|
|
833
|
+
if (approval.sourceStamps.sourceFingerprint !== record.sourceStamps.sourceFingerprint) {
|
|
834
|
+
reasons.push('source fingerprint changed');
|
|
835
|
+
}
|
|
836
|
+
if (approval.sourceStamps.parserVersion !== record.sourceStamps.parserVersion) {
|
|
837
|
+
reasons.push('parser version changed');
|
|
838
|
+
}
|
|
839
|
+
if (approval.sourceStamps.normalizedResultHash !== record.sourceStamps.normalizedResultHash) {
|
|
840
|
+
reasons.push('normalized result hash changed');
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
else if (!reviewSummariesMatch(approval.sourceSummary, record.summary)) {
|
|
844
|
+
reasons.push('review summary changed');
|
|
845
|
+
}
|
|
846
|
+
return { valid: reasons.length === 0, reasons };
|
|
847
|
+
}
|
|
848
|
+
function decorateApprovalValidity(project, approval) {
|
|
849
|
+
const review = normalizeReviewRecord(project.namedDatasets[approval.reviewDatasetName]?.data);
|
|
850
|
+
if (!review) {
|
|
851
|
+
return {
|
|
852
|
+
...approval,
|
|
853
|
+
validForCurrentReview: false,
|
|
854
|
+
invalidationReasons: ['review dataset not found'],
|
|
855
|
+
};
|
|
856
|
+
}
|
|
857
|
+
const validation = validateApprovalForReview(review, approval);
|
|
858
|
+
return {
|
|
859
|
+
...approval,
|
|
860
|
+
validForCurrentReview: validation.valid,
|
|
861
|
+
invalidationReasons: validation.valid ? undefined : validation.reasons,
|
|
862
|
+
};
|
|
863
|
+
}
|
|
864
|
+
function getReviewDatasetEntries(project) {
|
|
865
|
+
return Object.entries(project.namedDatasets)
|
|
866
|
+
.filter(([, dataset]) => dataset.kind === 'geotech-ingest-review')
|
|
867
|
+
.map(([name, dataset]) => ({ name, data: dataset.data }));
|
|
868
|
+
}
|
|
869
|
+
function getReviewApprovalDatasetEntries(project) {
|
|
870
|
+
return Object.entries(project.namedDatasets)
|
|
871
|
+
.filter(([, dataset]) => dataset.kind === 'geotech-ingest-review-approval')
|
|
872
|
+
.map(([name, dataset]) => ({ name, data: dataset.data }));
|
|
873
|
+
}
|
|
874
|
+
function getJobDatasetEntries(project) {
|
|
875
|
+
return Object.entries(project.namedDatasets)
|
|
876
|
+
.filter(([, dataset]) => dataset.kind === 'geotech-ingest-job')
|
|
877
|
+
.map(([name, dataset]) => ({ name, data: dataset.data }));
|
|
878
|
+
}
|
|
879
|
+
function findLatestApprovalForReview(project, review) {
|
|
880
|
+
const pointerDatasetName = buildApprovalPointerDatasetName(review.reviewId);
|
|
881
|
+
const pointer = normalizeApprovalPointer(project.namedDatasets[pointerDatasetName]?.data);
|
|
882
|
+
if (pointer) {
|
|
883
|
+
const fromPointer = normalizeApprovalRecord(project.namedDatasets[pointer.approvalDatasetName]?.data);
|
|
884
|
+
if (fromPointer?.reviewId === review.reviewId) {
|
|
885
|
+
const decorated = decorateApprovalValidity(project, fromPointer);
|
|
886
|
+
if (decorated.validForCurrentReview !== false) {
|
|
887
|
+
return decorated;
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
return getReviewApprovalDatasetEntries(project)
|
|
892
|
+
.map((entry) => normalizeApprovalRecord(entry.data))
|
|
893
|
+
.filter((entry) => entry !== null && entry.reviewId === review.reviewId)
|
|
894
|
+
.map((entry) => decorateApprovalValidity(project, entry))
|
|
895
|
+
.filter((entry) => entry.validForCurrentReview !== false)
|
|
896
|
+
.sort((left, right) => right.approvedAt.localeCompare(left.approvedAt))[0];
|
|
897
|
+
}
|
|
898
|
+
function attachLatestApproval(project, record) {
|
|
899
|
+
const approval = findLatestApprovalForReview(project, record);
|
|
900
|
+
return approval ? { ...record, approval } : record;
|
|
901
|
+
}
|
|
902
|
+
function getSelectedReview(projectId, datasetName) {
|
|
903
|
+
return datasetName
|
|
904
|
+
? loadPersistedBoreholeIngestReview(projectId, datasetName)
|
|
905
|
+
: loadLatestPersistedBoreholeIngestReview(projectId);
|
|
906
|
+
}
|
|
907
|
+
function getSelectedJob(projectId, datasetName) {
|
|
908
|
+
return datasetName
|
|
909
|
+
? getGeotechIngestJob(projectId, datasetName)
|
|
910
|
+
: getLatestGeotechIngestJob(projectId);
|
|
911
|
+
}
|
|
912
|
+
function saveJobRecord(projectId, record) {
|
|
913
|
+
saveNamedDataset(projectId, {
|
|
914
|
+
name: record.datasetName,
|
|
915
|
+
kind: 'geotech-ingest-job',
|
|
916
|
+
data: record,
|
|
917
|
+
source: record.request.path,
|
|
918
|
+
metadata: {
|
|
919
|
+
workflow: 'geotech-ingest-job',
|
|
920
|
+
jobId: record.jobId,
|
|
921
|
+
documentType: record.documentType,
|
|
922
|
+
status: record.status,
|
|
923
|
+
sourceFingerprint: record.sourceStamps.sourceFingerprint,
|
|
924
|
+
parserVersion: record.sourceStamps.parserVersion,
|
|
925
|
+
normalizedResultHash: record.sourceStamps.normalizedResultHash,
|
|
926
|
+
persistedReviewDatasetName: record.persistedReview?.datasetName,
|
|
927
|
+
createdAt: record.createdAt,
|
|
928
|
+
updatedAt: record.updatedAt,
|
|
929
|
+
completedAt: record.completedAt,
|
|
930
|
+
},
|
|
931
|
+
});
|
|
932
|
+
saveNamedDataset(projectId, {
|
|
933
|
+
name: 'ingest-job:latest',
|
|
934
|
+
kind: 'geotech-ingest-job-pointer',
|
|
935
|
+
data: {
|
|
936
|
+
kind: 'geotech-ingest-job-pointer',
|
|
937
|
+
schemaVersion: 1,
|
|
938
|
+
datasetName: record.datasetName,
|
|
939
|
+
jobId: record.jobId,
|
|
940
|
+
updatedAt: record.updatedAt,
|
|
941
|
+
},
|
|
942
|
+
source: record.datasetName,
|
|
943
|
+
metadata: {
|
|
944
|
+
workflow: 'geotech-ingest-job',
|
|
945
|
+
jobId: record.jobId,
|
|
946
|
+
datasetName: record.datasetName,
|
|
947
|
+
status: record.status,
|
|
948
|
+
updatedAt: record.updatedAt,
|
|
949
|
+
},
|
|
950
|
+
});
|
|
951
|
+
return record;
|
|
952
|
+
}
|
|
953
|
+
function createPromotionDatasetMetadata(role, record, promotedAt, promotionDatasetName, options) {
|
|
954
|
+
return {
|
|
955
|
+
workflow: 'geotech-ingest-review-promotion',
|
|
956
|
+
documentType: record.result.documentType,
|
|
957
|
+
promotedDatasetRole: role,
|
|
958
|
+
promotedAt,
|
|
959
|
+
promotedFromReviewId: record.reviewId,
|
|
960
|
+
promotedFromReviewDatasetName: record.datasetName,
|
|
961
|
+
promotionDatasetName,
|
|
962
|
+
boreholeId: options?.boreholeId,
|
|
963
|
+
targetDatasetName: options?.targetDatasetName,
|
|
964
|
+
supersededDatasetName: options?.supersededDatasetName,
|
|
965
|
+
snapshotDatasetName: options?.snapshotDatasetName,
|
|
966
|
+
sourceFingerprint: record.sourceStamps.sourceFingerprint,
|
|
967
|
+
parserVersion: record.sourceStamps.parserVersion,
|
|
968
|
+
normalizedResultHash: record.sourceStamps.normalizedResultHash,
|
|
969
|
+
approvalDatasetName: options?.approval?.datasetName,
|
|
970
|
+
approvalId: options?.approval?.approvalId,
|
|
971
|
+
approvedAt: options?.approval?.approvedAt,
|
|
972
|
+
approvedBy: options?.approval?.approvedBy,
|
|
973
|
+
approvalRationale: options?.approval?.rationale,
|
|
974
|
+
};
|
|
975
|
+
}
|
|
976
|
+
function snapshotNamedDatasetForPromotion(projectId, existingDataset, targetDatasetName, record, promotedAt, promotionDatasetName, approval) {
|
|
977
|
+
if (!existingDataset) {
|
|
978
|
+
return {};
|
|
979
|
+
}
|
|
980
|
+
const snapshotDatasetName = buildPromotionSnapshotDatasetName(record.reviewId, targetDatasetName, promotedAt);
|
|
981
|
+
const snapshotRecord = {
|
|
982
|
+
kind: 'geotech-ingest-promotion-snapshot-record',
|
|
983
|
+
schemaVersion: 1,
|
|
984
|
+
snapshotDatasetName,
|
|
985
|
+
targetDatasetName,
|
|
986
|
+
sourceReviewDatasetName: record.datasetName,
|
|
987
|
+
sourceReviewId: record.reviewId,
|
|
988
|
+
promotionDatasetName,
|
|
989
|
+
promotedAt,
|
|
990
|
+
previousDataset: existingDataset,
|
|
991
|
+
};
|
|
992
|
+
saveNamedDataset(projectId, {
|
|
993
|
+
name: snapshotDatasetName,
|
|
994
|
+
kind: 'geotech-ingest-promotion-snapshot',
|
|
995
|
+
data: snapshotRecord,
|
|
996
|
+
source: record.datasetName,
|
|
997
|
+
metadata: createPromotionDatasetMetadata('snapshot', record, promotedAt, promotionDatasetName, {
|
|
998
|
+
targetDatasetName,
|
|
999
|
+
supersededDatasetName: targetDatasetName,
|
|
1000
|
+
snapshotDatasetName,
|
|
1001
|
+
approval,
|
|
1002
|
+
}),
|
|
1003
|
+
});
|
|
1004
|
+
return {
|
|
1005
|
+
supersededDatasetName: targetDatasetName,
|
|
1006
|
+
snapshotDatasetName,
|
|
1007
|
+
};
|
|
1008
|
+
}
|
|
1009
|
+
function createPromotionArtifactPreview(result) {
|
|
1010
|
+
const lines = [
|
|
1011
|
+
`Source review: ${result.sourceReviewDatasetName}`,
|
|
1012
|
+
`Document type: ${result.documentType}`,
|
|
1013
|
+
`Promoted datasets: ${result.promotedDatasetNames.length}`,
|
|
1014
|
+
];
|
|
1015
|
+
if (result.documentType === 'borehole-log') {
|
|
1016
|
+
lines.push(`Promoted boreholes: ${result.promotedBoreholes.length}`);
|
|
1017
|
+
}
|
|
1018
|
+
else {
|
|
1019
|
+
lines.push(`Promoted document views: ${result.promotedDocuments.length}`);
|
|
1020
|
+
}
|
|
1021
|
+
if (result.approvalDatasetName) {
|
|
1022
|
+
lines.push(`Recorded approval: ${result.approvalDatasetName}`);
|
|
1023
|
+
}
|
|
1024
|
+
if (result.supersededDatasetNames.length > 0) {
|
|
1025
|
+
lines.push(`Superseded datasets: ${result.supersededDatasetNames.length}`);
|
|
1026
|
+
}
|
|
1027
|
+
if (result.snapshotDatasetNames.length > 0) {
|
|
1028
|
+
lines.push(`Rollback snapshots: ${result.snapshotDatasetNames.length}`);
|
|
1029
|
+
}
|
|
1030
|
+
for (const item of result.promotedBoreholes) {
|
|
1031
|
+
lines.push(`- ${item.boreholeId}: raw dataset ${item.rawDatasetName}${item.promotedSoilProfile ? `, soil profile ${item.soilProfileDatasetName}` : ', soil profile skipped'}`);
|
|
1032
|
+
}
|
|
1033
|
+
for (const item of result.promotedDocuments) {
|
|
1034
|
+
lines.push(`- ${item.role}: ${item.datasetName}`);
|
|
1035
|
+
}
|
|
1036
|
+
if (result.warnings.length > 0) {
|
|
1037
|
+
lines.push(`Warnings: ${result.warnings.join(' | ')}`);
|
|
1038
|
+
}
|
|
1039
|
+
return lines.join('\n');
|
|
1040
|
+
}
|
|
1041
|
+
function toPromotableSoilProfile(borehole) {
|
|
1042
|
+
const warnings = [];
|
|
1043
|
+
if (borehole.layers.length === 0) {
|
|
1044
|
+
warnings.push(`Borehole ${borehole.boreholeId} has no parsed layers; skipped soil-profile promotion.`);
|
|
1045
|
+
return { profile: null, warnings };
|
|
1046
|
+
}
|
|
1047
|
+
const promotedLayers = borehole.layers.flatMap((layer, index) => {
|
|
1048
|
+
if (layer.depthFrom == null
|
|
1049
|
+
|| layer.depthTo == null
|
|
1050
|
+
|| !Number.isFinite(layer.depthFrom)
|
|
1051
|
+
|| !Number.isFinite(layer.depthTo)) {
|
|
1052
|
+
warnings.push(`Borehole ${borehole.boreholeId} layer ${index + 1} is missing complete depth geometry; skipped soil-profile promotion.`);
|
|
1053
|
+
return [];
|
|
1054
|
+
}
|
|
1055
|
+
return [{
|
|
1056
|
+
depthFrom: layer.depthFrom,
|
|
1057
|
+
depthTo: layer.depthTo,
|
|
1058
|
+
description: layer.description ?? layer.notes ?? layer.uscsSymbol ?? 'Unknown layer',
|
|
1059
|
+
uscs: layer.uscsSymbol ?? undefined,
|
|
1060
|
+
sptN: layer.sptN ?? undefined,
|
|
1061
|
+
}];
|
|
1062
|
+
});
|
|
1063
|
+
if (promotedLayers.length !== borehole.layers.length) {
|
|
1064
|
+
return { profile: null, warnings };
|
|
1065
|
+
}
|
|
1066
|
+
return {
|
|
1067
|
+
profile: {
|
|
1068
|
+
boreholeId: borehole.boreholeId,
|
|
1069
|
+
location: borehole.location ?? undefined,
|
|
1070
|
+
layers: promotedLayers,
|
|
1071
|
+
waterTableDepth: borehole.waterTableDepth ?? undefined,
|
|
1072
|
+
},
|
|
1073
|
+
warnings,
|
|
1074
|
+
};
|
|
1075
|
+
}
|
|
1076
|
+
export function persistBoreholeIngestReview(projectId, result, options) {
|
|
1077
|
+
const project = loadProject(projectId);
|
|
1078
|
+
const reviewId = buildReviewId(result);
|
|
1079
|
+
const datasetName = `ingest-review:${reviewId}`;
|
|
1080
|
+
const title = buildTitle(result, options?.title);
|
|
1081
|
+
const sourceStamps = buildSourceStampsForResult(result, options?.sourceStamps);
|
|
1082
|
+
const record = {
|
|
1083
|
+
kind: 'geotech-ingest-review-record',
|
|
1084
|
+
schemaVersion: 1,
|
|
1085
|
+
reviewId,
|
|
1086
|
+
datasetName,
|
|
1087
|
+
projectId,
|
|
1088
|
+
createdAt: result.generatedAt,
|
|
1089
|
+
title,
|
|
1090
|
+
result,
|
|
1091
|
+
summary: buildSummary(result),
|
|
1092
|
+
sourceStamps,
|
|
1093
|
+
};
|
|
1094
|
+
saveNamedDataset(projectId, {
|
|
1095
|
+
name: datasetName,
|
|
1096
|
+
kind: 'geotech-ingest-review',
|
|
1097
|
+
data: record,
|
|
1098
|
+
source: 'geotech-ingest',
|
|
1099
|
+
metadata: {
|
|
1100
|
+
workflow: 'geotech-ingest-review',
|
|
1101
|
+
reviewId,
|
|
1102
|
+
documentType: result.documentType,
|
|
1103
|
+
sourceFingerprint: sourceStamps.sourceFingerprint,
|
|
1104
|
+
parserVersion: sourceStamps.parserVersion,
|
|
1105
|
+
normalizedResultHash: sourceStamps.normalizedResultHash,
|
|
1106
|
+
},
|
|
1107
|
+
});
|
|
1108
|
+
const pointer = {
|
|
1109
|
+
kind: 'geotech-ingest-review-pointer',
|
|
1110
|
+
schemaVersion: 1,
|
|
1111
|
+
datasetName,
|
|
1112
|
+
reviewId,
|
|
1113
|
+
updatedAt: result.generatedAt,
|
|
1114
|
+
};
|
|
1115
|
+
saveNamedDataset(projectId, {
|
|
1116
|
+
name: 'ingest-review:latest',
|
|
1117
|
+
kind: 'geotech-ingest-review-pointer',
|
|
1118
|
+
data: pointer,
|
|
1119
|
+
source: 'geotech-ingest',
|
|
1120
|
+
});
|
|
1121
|
+
addArtifact(projectId, {
|
|
1122
|
+
kind: 'ingest-review',
|
|
1123
|
+
title,
|
|
1124
|
+
content: buildArtifactPreview(record),
|
|
1125
|
+
mimeType: 'text/plain',
|
|
1126
|
+
metadata: {
|
|
1127
|
+
datasetName,
|
|
1128
|
+
reviewId,
|
|
1129
|
+
source: reviewSourceLabel(result),
|
|
1130
|
+
documentType: result.documentType,
|
|
1131
|
+
reviewRequired: result.reviewRequired,
|
|
1132
|
+
canAutoProceed: result.canAutoProceed,
|
|
1133
|
+
confidence: result.confidence,
|
|
1134
|
+
sourceFingerprint: sourceStamps.sourceFingerprint,
|
|
1135
|
+
parserVersion: sourceStamps.parserVersion,
|
|
1136
|
+
normalizedResultHash: sourceStamps.normalizedResultHash,
|
|
1137
|
+
boreholeCount: isBoreholeIngestResult(result) ? result.boreholes.length : undefined,
|
|
1138
|
+
materialCount: isGeotechDocumentIngestResult(result) ? result.materials.length : undefined,
|
|
1139
|
+
classificationCount: isGeotechDocumentIngestResult(result) ? result.classifications.length : undefined,
|
|
1140
|
+
parameterCount: isGeotechDocumentIngestResult(result) ? result.parameters.length : undefined,
|
|
1141
|
+
},
|
|
1142
|
+
});
|
|
1143
|
+
addNote(projectId, `Persisted ingest review "${reviewId}" from ${reviewSourceLabel(result)} (${result.reviewRequired ? 'review required' : 'auto-proceed ready'}).`);
|
|
1144
|
+
const relatedDatasets = [...new Set([...project.activeAnalysisContext.relatedDatasets, datasetName])];
|
|
1145
|
+
setActiveAnalysisContext(projectId, {
|
|
1146
|
+
currentTask: `Review ingest result for ${reviewSourceLabel(result)}`,
|
|
1147
|
+
context: {
|
|
1148
|
+
...project.activeAnalysisContext.context,
|
|
1149
|
+
latestIngestReview: {
|
|
1150
|
+
datasetName,
|
|
1151
|
+
reviewId,
|
|
1152
|
+
documentType: result.documentType,
|
|
1153
|
+
reviewRequired: result.reviewRequired,
|
|
1154
|
+
canAutoProceed: result.canAutoProceed,
|
|
1155
|
+
sourceStamps,
|
|
1156
|
+
},
|
|
1157
|
+
},
|
|
1158
|
+
relatedDatasets,
|
|
1159
|
+
});
|
|
1160
|
+
return record;
|
|
1161
|
+
}
|
|
1162
|
+
export function loadPersistedBoreholeIngestReview(projectId, datasetName) {
|
|
1163
|
+
const project = loadProject(projectId);
|
|
1164
|
+
const record = normalizeReviewRecord(project.namedDatasets[datasetName]?.data);
|
|
1165
|
+
return record ? attachLatestApproval(project, record) : null;
|
|
1166
|
+
}
|
|
1167
|
+
export function listPersistedBoreholeIngestReviews(projectId) {
|
|
1168
|
+
const project = loadProject(projectId);
|
|
1169
|
+
return getReviewDatasetEntries(project)
|
|
1170
|
+
.map((entry) => normalizeReviewRecord(entry.data))
|
|
1171
|
+
.filter((entry) => entry !== null)
|
|
1172
|
+
.map((entry) => attachLatestApproval(project, entry))
|
|
1173
|
+
.sort((left, right) => right.createdAt.localeCompare(left.createdAt));
|
|
1174
|
+
}
|
|
1175
|
+
export function loadLatestPersistedBoreholeIngestReview(projectId) {
|
|
1176
|
+
const project = loadProject(projectId);
|
|
1177
|
+
const pointer = normalizePointer(project.namedDatasets['ingest-review:latest']?.data);
|
|
1178
|
+
if (pointer) {
|
|
1179
|
+
const fromPointer = normalizeReviewRecord(project.namedDatasets[pointer.datasetName]?.data);
|
|
1180
|
+
if (fromPointer) {
|
|
1181
|
+
return attachLatestApproval(project, fromPointer);
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
const reviews = getReviewDatasetEntries(project)
|
|
1185
|
+
.map((entry) => normalizeReviewRecord(entry.data))
|
|
1186
|
+
.filter((entry) => entry !== null)
|
|
1187
|
+
.sort((left, right) => right.createdAt.localeCompare(left.createdAt));
|
|
1188
|
+
return reviews[0] ? attachLatestApproval(project, reviews[0]) : null;
|
|
1189
|
+
}
|
|
1190
|
+
export function loadPersistedBoreholeIngestReviewApproval(projectId, approvalDatasetName) {
|
|
1191
|
+
const project = loadProject(projectId);
|
|
1192
|
+
const approval = normalizeApprovalRecord(project.namedDatasets[approvalDatasetName]?.data);
|
|
1193
|
+
return approval ? decorateApprovalValidity(project, approval) : null;
|
|
1194
|
+
}
|
|
1195
|
+
export function loadLatestPersistedBoreholeIngestReviewApproval(projectId, reviewDatasetName) {
|
|
1196
|
+
const project = loadProject(projectId);
|
|
1197
|
+
const review = normalizeReviewRecord(project.namedDatasets[reviewDatasetName]?.data);
|
|
1198
|
+
if (!review) {
|
|
1199
|
+
return null;
|
|
1200
|
+
}
|
|
1201
|
+
return findLatestApprovalForReview(project, review) ?? null;
|
|
1202
|
+
}
|
|
1203
|
+
export function listPersistedBoreholeIngestReviewApprovals(projectId, reviewDatasetName) {
|
|
1204
|
+
const project = loadProject(projectId);
|
|
1205
|
+
const targetReview = reviewDatasetName
|
|
1206
|
+
? normalizeReviewRecord(project.namedDatasets[reviewDatasetName]?.data)
|
|
1207
|
+
: null;
|
|
1208
|
+
const targetReviewId = targetReview?.reviewId;
|
|
1209
|
+
return getReviewApprovalDatasetEntries(project)
|
|
1210
|
+
.map((entry) => normalizeApprovalRecord(entry.data))
|
|
1211
|
+
.filter((entry) => entry !== null && (!targetReviewId || entry.reviewId === targetReviewId))
|
|
1212
|
+
.map((entry) => decorateApprovalValidity(project, entry))
|
|
1213
|
+
.sort((left, right) => right.approvedAt.localeCompare(left.approvedAt));
|
|
1214
|
+
}
|
|
1215
|
+
export function summarizePersistedBoreholeIngestReviewApproval(approval, options) {
|
|
1216
|
+
return {
|
|
1217
|
+
approvalId: approval.approvalId,
|
|
1218
|
+
datasetName: approval.datasetName,
|
|
1219
|
+
projectId: approval.projectId,
|
|
1220
|
+
reviewId: approval.reviewId,
|
|
1221
|
+
reviewDatasetName: approval.reviewDatasetName,
|
|
1222
|
+
approvedAt: approval.approvedAt,
|
|
1223
|
+
approvedBy: approval.approvedBy,
|
|
1224
|
+
rationale: approval.rationale,
|
|
1225
|
+
isLatestForReview: options?.latestApprovalDatasetName === approval.datasetName,
|
|
1226
|
+
isValidForCurrentReview: approval.validForCurrentReview !== false,
|
|
1227
|
+
invalidationReasons: approval.invalidationReasons,
|
|
1228
|
+
};
|
|
1229
|
+
}
|
|
1230
|
+
export function approvePersistedBoreholeIngestReview(projectId, datasetName, options) {
|
|
1231
|
+
const rationale = options.rationale?.trim();
|
|
1232
|
+
if (!rationale) {
|
|
1233
|
+
throw new Error('Approving a persisted ingest review requires a non-empty rationale.');
|
|
1234
|
+
}
|
|
1235
|
+
const project = loadProject(projectId);
|
|
1236
|
+
const record = getSelectedReview(projectId, datasetName);
|
|
1237
|
+
if (!record) {
|
|
1238
|
+
throw new Error(datasetName
|
|
1239
|
+
? `No persisted ingest review named "${datasetName}" was found in project "${projectId}".`
|
|
1240
|
+
: `No persisted ingest reviews were found in project "${projectId}".`);
|
|
1241
|
+
}
|
|
1242
|
+
const approvedAt = options.approvedAt?.trim() || new Date().toISOString();
|
|
1243
|
+
const approvedBy = asOptionalString(options.approvedBy);
|
|
1244
|
+
const approvalId = buildApprovalId(record.reviewId, approvedAt);
|
|
1245
|
+
const approvalDatasetName = buildApprovalDatasetName(record.reviewId, approvedAt);
|
|
1246
|
+
const approvalRecord = {
|
|
1247
|
+
kind: 'geotech-ingest-review-approval-record',
|
|
1248
|
+
schemaVersion: 1,
|
|
1249
|
+
approvalId,
|
|
1250
|
+
datasetName: approvalDatasetName,
|
|
1251
|
+
projectId,
|
|
1252
|
+
reviewId: record.reviewId,
|
|
1253
|
+
reviewDatasetName: record.datasetName,
|
|
1254
|
+
approvedAt,
|
|
1255
|
+
approvedBy,
|
|
1256
|
+
rationale,
|
|
1257
|
+
sourceSummary: record.summary,
|
|
1258
|
+
sourceStamps: record.sourceStamps,
|
|
1259
|
+
};
|
|
1260
|
+
saveNamedDataset(projectId, {
|
|
1261
|
+
name: approvalDatasetName,
|
|
1262
|
+
kind: 'geotech-ingest-review-approval',
|
|
1263
|
+
data: approvalRecord,
|
|
1264
|
+
source: record.datasetName,
|
|
1265
|
+
metadata: {
|
|
1266
|
+
workflow: 'geotech-ingest-review-approval',
|
|
1267
|
+
reviewId: record.reviewId,
|
|
1268
|
+
reviewDatasetName: record.datasetName,
|
|
1269
|
+
approvalId,
|
|
1270
|
+
approvedAt,
|
|
1271
|
+
approvedBy,
|
|
1272
|
+
documentType: record.result.documentType,
|
|
1273
|
+
rationale,
|
|
1274
|
+
sourceFingerprint: record.sourceStamps.sourceFingerprint,
|
|
1275
|
+
parserVersion: record.sourceStamps.parserVersion,
|
|
1276
|
+
normalizedResultHash: record.sourceStamps.normalizedResultHash,
|
|
1277
|
+
},
|
|
1278
|
+
});
|
|
1279
|
+
saveNamedDataset(projectId, {
|
|
1280
|
+
name: buildApprovalPointerDatasetName(record.reviewId),
|
|
1281
|
+
kind: 'geotech-ingest-review-approval-pointer',
|
|
1282
|
+
data: {
|
|
1283
|
+
kind: 'geotech-ingest-review-approval-pointer',
|
|
1284
|
+
schemaVersion: 1,
|
|
1285
|
+
reviewId: record.reviewId,
|
|
1286
|
+
reviewDatasetName: record.datasetName,
|
|
1287
|
+
approvalDatasetName,
|
|
1288
|
+
approvalId,
|
|
1289
|
+
updatedAt: approvedAt,
|
|
1290
|
+
},
|
|
1291
|
+
source: record.datasetName,
|
|
1292
|
+
metadata: {
|
|
1293
|
+
workflow: 'geotech-ingest-review-approval',
|
|
1294
|
+
reviewId: record.reviewId,
|
|
1295
|
+
reviewDatasetName: record.datasetName,
|
|
1296
|
+
approvalDatasetName,
|
|
1297
|
+
approvalId,
|
|
1298
|
+
approvedAt,
|
|
1299
|
+
documentType: record.result.documentType,
|
|
1300
|
+
sourceFingerprint: record.sourceStamps.sourceFingerprint,
|
|
1301
|
+
parserVersion: record.sourceStamps.parserVersion,
|
|
1302
|
+
normalizedResultHash: record.sourceStamps.normalizedResultHash,
|
|
1303
|
+
},
|
|
1304
|
+
});
|
|
1305
|
+
addArtifact(projectId, {
|
|
1306
|
+
kind: 'ingest-review-approval',
|
|
1307
|
+
title: `Approved ingest review: ${record.title}`,
|
|
1308
|
+
content: buildApprovalArtifactPreview(record, approvalRecord),
|
|
1309
|
+
mimeType: 'text/plain',
|
|
1310
|
+
metadata: {
|
|
1311
|
+
reviewId: record.reviewId,
|
|
1312
|
+
reviewDatasetName: record.datasetName,
|
|
1313
|
+
approvalDatasetName,
|
|
1314
|
+
approvalId,
|
|
1315
|
+
approvedAt,
|
|
1316
|
+
approvedBy,
|
|
1317
|
+
documentType: record.result.documentType,
|
|
1318
|
+
rationale,
|
|
1319
|
+
sourceFingerprint: record.sourceStamps.sourceFingerprint,
|
|
1320
|
+
parserVersion: record.sourceStamps.parserVersion,
|
|
1321
|
+
normalizedResultHash: record.sourceStamps.normalizedResultHash,
|
|
1322
|
+
},
|
|
1323
|
+
});
|
|
1324
|
+
addNote(projectId, `Approved ingest review "${record.reviewId}"${approvedBy ? ` by ${approvedBy}` : ''}.`);
|
|
1325
|
+
const relatedDatasets = [
|
|
1326
|
+
...new Set([
|
|
1327
|
+
...project.activeAnalysisContext.relatedDatasets,
|
|
1328
|
+
record.datasetName,
|
|
1329
|
+
approvalDatasetName,
|
|
1330
|
+
]),
|
|
1331
|
+
];
|
|
1332
|
+
setActiveAnalysisContext(projectId, {
|
|
1333
|
+
currentTask: `Approved ingest review for ${reviewSourceLabel(record.result)}`,
|
|
1334
|
+
context: {
|
|
1335
|
+
...project.activeAnalysisContext.context,
|
|
1336
|
+
latestApprovedIngestReview: {
|
|
1337
|
+
reviewDatasetName: record.datasetName,
|
|
1338
|
+
reviewId: record.reviewId,
|
|
1339
|
+
approvalDatasetName,
|
|
1340
|
+
approvalId,
|
|
1341
|
+
approvedAt,
|
|
1342
|
+
approvedBy,
|
|
1343
|
+
documentType: record.result.documentType,
|
|
1344
|
+
rationale,
|
|
1345
|
+
sourceStamps: record.sourceStamps,
|
|
1346
|
+
},
|
|
1347
|
+
},
|
|
1348
|
+
relatedDatasets,
|
|
1349
|
+
});
|
|
1350
|
+
return {
|
|
1351
|
+
...approvalRecord,
|
|
1352
|
+
validForCurrentReview: true,
|
|
1353
|
+
};
|
|
1354
|
+
}
|
|
1355
|
+
function getLatestGeotechIngestJob(projectId) {
|
|
1356
|
+
const project = loadProject(projectId);
|
|
1357
|
+
const pointer = normalizeJobPointer(project.namedDatasets['ingest-job:latest']?.data);
|
|
1358
|
+
if (pointer) {
|
|
1359
|
+
const fromPointer = normalizeJobRecord(project.namedDatasets[pointer.datasetName]?.data);
|
|
1360
|
+
if (fromPointer) {
|
|
1361
|
+
return fromPointer;
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
const jobs = getJobDatasetEntries(project)
|
|
1365
|
+
.map((entry) => normalizeJobRecord(entry.data))
|
|
1366
|
+
.filter((entry) => entry !== null)
|
|
1367
|
+
.sort((left, right) => right.updatedAt.localeCompare(left.updatedAt));
|
|
1368
|
+
return jobs[0] ?? null;
|
|
1369
|
+
}
|
|
1370
|
+
function markJobState(record, updates) {
|
|
1371
|
+
return {
|
|
1372
|
+
...record,
|
|
1373
|
+
...updates,
|
|
1374
|
+
request: {
|
|
1375
|
+
...record.request,
|
|
1376
|
+
...(updates.request ?? {}),
|
|
1377
|
+
},
|
|
1378
|
+
source: {
|
|
1379
|
+
...record.source,
|
|
1380
|
+
...(updates.source ?? {}),
|
|
1381
|
+
},
|
|
1382
|
+
sourceStamps: {
|
|
1383
|
+
...record.sourceStamps,
|
|
1384
|
+
...(updates.sourceStamps ?? {}),
|
|
1385
|
+
},
|
|
1386
|
+
};
|
|
1387
|
+
}
|
|
1388
|
+
function runIngestForJob(record, options) {
|
|
1389
|
+
return (async () => {
|
|
1390
|
+
const file = readDocumentVisionInput(record.source.filePath);
|
|
1391
|
+
if (file.kind === 'unknown') {
|
|
1392
|
+
throw new Error(`Unsupported document type for ingest: ${record.source.filePath}`);
|
|
1393
|
+
}
|
|
1394
|
+
const inspection = file.kind === 'pdf' ? inspectPdfDocument(record.source.filePath) : null;
|
|
1395
|
+
if (record.documentType === 'borehole-log') {
|
|
1396
|
+
const result = await ingestBoreholeLogDocument({
|
|
1397
|
+
config: options.config,
|
|
1398
|
+
source: {
|
|
1399
|
+
filePath: record.source.filePath,
|
|
1400
|
+
fileName: record.source.fileName,
|
|
1401
|
+
inputKind: file.kind === 'pdf' ? 'pdf' : 'image',
|
|
1402
|
+
},
|
|
1403
|
+
overrideBoreholeId: record.request.boreholeId,
|
|
1404
|
+
inspection,
|
|
1405
|
+
image: file.kind === 'pdf' ? undefined : file,
|
|
1406
|
+
pages: file.kind === 'pdf'
|
|
1407
|
+
? await readDocumentPdfPageInputs(record.source.filePath, { inspection })
|
|
1408
|
+
: undefined,
|
|
1409
|
+
});
|
|
1410
|
+
const persistedReview = record.request.persistReview
|
|
1411
|
+
? persistBoreholeIngestReview(record.projectId, result, {
|
|
1412
|
+
title: record.request.reviewTitle,
|
|
1413
|
+
})
|
|
1414
|
+
: undefined;
|
|
1415
|
+
return { result, inspection, persistedReview };
|
|
1416
|
+
}
|
|
1417
|
+
const result = await ingestGeotechDocument({
|
|
1418
|
+
config: options.config,
|
|
1419
|
+
source: {
|
|
1420
|
+
filePath: record.source.filePath,
|
|
1421
|
+
fileName: record.source.fileName,
|
|
1422
|
+
inputKind: file.kind === 'pdf' ? 'pdf' : 'image',
|
|
1423
|
+
},
|
|
1424
|
+
inspection,
|
|
1425
|
+
image: file.kind === 'pdf' ? undefined : file,
|
|
1426
|
+
pages: file.kind === 'pdf'
|
|
1427
|
+
? await readDocumentPdfPageInputs(record.source.filePath, { inspection })
|
|
1428
|
+
: undefined,
|
|
1429
|
+
});
|
|
1430
|
+
const persistedReview = record.request.persistReview
|
|
1431
|
+
? persistBoreholeIngestReview(record.projectId, result, {
|
|
1432
|
+
title: record.request.reviewTitle,
|
|
1433
|
+
})
|
|
1434
|
+
: undefined;
|
|
1435
|
+
return { result, inspection, persistedReview };
|
|
1436
|
+
})();
|
|
1437
|
+
}
|
|
1438
|
+
export function startGeotechIngestJob(projectId, options) {
|
|
1439
|
+
const filePath = options.path?.trim();
|
|
1440
|
+
if (!filePath) {
|
|
1441
|
+
throw new Error('Starting an ingest job requires a file path.');
|
|
1442
|
+
}
|
|
1443
|
+
if (!existsSync(filePath)) {
|
|
1444
|
+
throw new Error(`File not found: ${filePath}`);
|
|
1445
|
+
}
|
|
1446
|
+
const document = readDocumentVisionInput(filePath);
|
|
1447
|
+
if (document.kind === 'unknown') {
|
|
1448
|
+
throw new Error(`Unsupported document type for ingest: ${filePath}`);
|
|
1449
|
+
}
|
|
1450
|
+
const documentType = options.type === 'borehole-log' ? 'borehole-log' : 'geotech-document';
|
|
1451
|
+
const createdAt = new Date().toISOString();
|
|
1452
|
+
const inspection = document.kind === 'pdf' ? inspectPdfDocument(filePath) : null;
|
|
1453
|
+
const jobId = buildJobId(documentType, filePath, createdAt);
|
|
1454
|
+
const datasetName = buildJobDatasetName(jobId);
|
|
1455
|
+
const title = buildJobTitle(documentType, basename(filePath));
|
|
1456
|
+
const record = {
|
|
1457
|
+
kind: 'geotech-ingest-job-record',
|
|
1458
|
+
schemaVersion: 1,
|
|
1459
|
+
jobId,
|
|
1460
|
+
datasetName,
|
|
1461
|
+
projectId,
|
|
1462
|
+
documentType,
|
|
1463
|
+
title,
|
|
1464
|
+
status: 'queued',
|
|
1465
|
+
createdAt,
|
|
1466
|
+
updatedAt: createdAt,
|
|
1467
|
+
request: {
|
|
1468
|
+
path: filePath,
|
|
1469
|
+
boreholeId: asOptionalString(options.boreholeId),
|
|
1470
|
+
persistReview: options.persistReview === true,
|
|
1471
|
+
reviewTitle: asOptionalString(options.reviewTitle),
|
|
1472
|
+
},
|
|
1473
|
+
source: {
|
|
1474
|
+
filePath,
|
|
1475
|
+
fileName: basename(filePath),
|
|
1476
|
+
inputKind: document.kind,
|
|
1477
|
+
fileBytes: document.fileBytes,
|
|
1478
|
+
totalPages: inspection?.totalPages,
|
|
1479
|
+
},
|
|
1480
|
+
inspection: summarizeInspectionForJob(inspection),
|
|
1481
|
+
sourceStamps: buildJobSourceStamps(documentType, {
|
|
1482
|
+
filePath,
|
|
1483
|
+
fileName: basename(filePath),
|
|
1484
|
+
inputKind: document.kind,
|
|
1485
|
+
fileBytes: document.fileBytes,
|
|
1486
|
+
totalPages: inspection?.totalPages,
|
|
1487
|
+
inspection,
|
|
1488
|
+
}),
|
|
1489
|
+
};
|
|
1490
|
+
saveJobRecord(projectId, record);
|
|
1491
|
+
const project = loadProject(projectId);
|
|
1492
|
+
const relatedDatasets = [...new Set([...project.activeAnalysisContext.relatedDatasets, datasetName])];
|
|
1493
|
+
setActiveAnalysisContext(projectId, {
|
|
1494
|
+
currentTask: `Queued ingest job for ${record.source.fileName}`,
|
|
1495
|
+
context: {
|
|
1496
|
+
...project.activeAnalysisContext.context,
|
|
1497
|
+
latestIngestJob: {
|
|
1498
|
+
jobId,
|
|
1499
|
+
datasetName,
|
|
1500
|
+
documentType,
|
|
1501
|
+
status: 'queued',
|
|
1502
|
+
sourceFingerprint: record.sourceStamps.sourceFingerprint,
|
|
1503
|
+
parserVersion: record.sourceStamps.parserVersion,
|
|
1504
|
+
},
|
|
1505
|
+
},
|
|
1506
|
+
relatedDatasets,
|
|
1507
|
+
});
|
|
1508
|
+
addNote(projectId, `Queued ingest job "${jobId}" for ${record.source.fileName}.`);
|
|
1509
|
+
return record;
|
|
1510
|
+
}
|
|
1511
|
+
export function getGeotechIngestJob(projectId, datasetName) {
|
|
1512
|
+
const project = loadProject(projectId);
|
|
1513
|
+
if (datasetName) {
|
|
1514
|
+
return normalizeJobRecord(project.namedDatasets[datasetName]?.data);
|
|
1515
|
+
}
|
|
1516
|
+
return getLatestGeotechIngestJob(projectId);
|
|
1517
|
+
}
|
|
1518
|
+
export async function waitGeotechIngestJob(projectId, datasetName, options) {
|
|
1519
|
+
const project = loadProject(projectId);
|
|
1520
|
+
const record = getSelectedJob(projectId, datasetName);
|
|
1521
|
+
if (!record) {
|
|
1522
|
+
throw new Error(datasetName
|
|
1523
|
+
? `No persisted ingest job named "${datasetName}" was found in project "${projectId}".`
|
|
1524
|
+
: `No persisted ingest jobs were found in project "${projectId}".`);
|
|
1525
|
+
}
|
|
1526
|
+
if (record.status === 'completed' || record.status === 'failed') {
|
|
1527
|
+
return record;
|
|
1528
|
+
}
|
|
1529
|
+
const startedAt = record.startedAt ?? new Date().toISOString();
|
|
1530
|
+
const runningRecord = markJobState(record, {
|
|
1531
|
+
status: 'running',
|
|
1532
|
+
startedAt,
|
|
1533
|
+
updatedAt: startedAt,
|
|
1534
|
+
});
|
|
1535
|
+
saveJobRecord(projectId, runningRecord);
|
|
1536
|
+
try {
|
|
1537
|
+
const executed = await runIngestForJob(runningRecord, options);
|
|
1538
|
+
const sourceStamps = buildSourceStampsForResult(executed.result);
|
|
1539
|
+
const completedAt = executed.result.generatedAt || new Date().toISOString();
|
|
1540
|
+
const completedRecord = markJobState(runningRecord, {
|
|
1541
|
+
status: 'completed',
|
|
1542
|
+
completedAt,
|
|
1543
|
+
updatedAt: completedAt,
|
|
1544
|
+
source: {
|
|
1545
|
+
...runningRecord.source,
|
|
1546
|
+
totalPages: executed.result.source.totalPages,
|
|
1547
|
+
},
|
|
1548
|
+
inspection: summarizeInspectionForJob(executed.inspection),
|
|
1549
|
+
sourceStamps: {
|
|
1550
|
+
sourceFingerprint: sourceStamps.sourceFingerprint,
|
|
1551
|
+
parserVersion: sourceStamps.parserVersion,
|
|
1552
|
+
normalizedResultHash: sourceStamps.normalizedResultHash,
|
|
1553
|
+
},
|
|
1554
|
+
result: executed.result,
|
|
1555
|
+
resultSummary: buildSummary(executed.result),
|
|
1556
|
+
persistedReview: executed.persistedReview
|
|
1557
|
+
? {
|
|
1558
|
+
reviewId: executed.persistedReview.reviewId,
|
|
1559
|
+
datasetName: executed.persistedReview.datasetName,
|
|
1560
|
+
title: executed.persistedReview.title,
|
|
1561
|
+
summary: executed.persistedReview.summary,
|
|
1562
|
+
sourceStamps: executed.persistedReview.sourceStamps,
|
|
1563
|
+
}
|
|
1564
|
+
: undefined,
|
|
1565
|
+
error: undefined,
|
|
1566
|
+
});
|
|
1567
|
+
saveJobRecord(projectId, completedRecord);
|
|
1568
|
+
addArtifact(projectId, {
|
|
1569
|
+
kind: 'ingest-job',
|
|
1570
|
+
title: `Completed ingest job: ${completedRecord.title}`,
|
|
1571
|
+
content: buildJobArtifactPreview(completedRecord),
|
|
1572
|
+
mimeType: 'text/plain',
|
|
1573
|
+
metadata: {
|
|
1574
|
+
jobId: completedRecord.jobId,
|
|
1575
|
+
datasetName: completedRecord.datasetName,
|
|
1576
|
+
status: completedRecord.status,
|
|
1577
|
+
documentType: completedRecord.documentType,
|
|
1578
|
+
sourceFingerprint: sourceStamps.sourceFingerprint,
|
|
1579
|
+
parserVersion: sourceStamps.parserVersion,
|
|
1580
|
+
normalizedResultHash: sourceStamps.normalizedResultHash,
|
|
1581
|
+
persistedReviewDatasetName: completedRecord.persistedReview?.datasetName,
|
|
1582
|
+
},
|
|
1583
|
+
});
|
|
1584
|
+
addNote(projectId, `Completed ingest job "${completedRecord.jobId}"${completedRecord.persistedReview ? ` and persisted review "${completedRecord.persistedReview.datasetName}"` : ''}.`);
|
|
1585
|
+
const relatedDatasets = [
|
|
1586
|
+
...new Set([
|
|
1587
|
+
...project.activeAnalysisContext.relatedDatasets,
|
|
1588
|
+
completedRecord.datasetName,
|
|
1589
|
+
...(completedRecord.persistedReview ? [completedRecord.persistedReview.datasetName] : []),
|
|
1590
|
+
]),
|
|
1591
|
+
];
|
|
1592
|
+
setActiveAnalysisContext(projectId, {
|
|
1593
|
+
currentTask: `Completed ingest job for ${completedRecord.source.fileName}`,
|
|
1594
|
+
context: {
|
|
1595
|
+
...project.activeAnalysisContext.context,
|
|
1596
|
+
latestIngestJob: {
|
|
1597
|
+
jobId: completedRecord.jobId,
|
|
1598
|
+
datasetName: completedRecord.datasetName,
|
|
1599
|
+
documentType: completedRecord.documentType,
|
|
1600
|
+
status: completedRecord.status,
|
|
1601
|
+
persistedReviewDatasetName: completedRecord.persistedReview?.datasetName,
|
|
1602
|
+
sourceFingerprint: sourceStamps.sourceFingerprint,
|
|
1603
|
+
parserVersion: sourceStamps.parserVersion,
|
|
1604
|
+
normalizedResultHash: sourceStamps.normalizedResultHash,
|
|
1605
|
+
},
|
|
1606
|
+
},
|
|
1607
|
+
relatedDatasets,
|
|
1608
|
+
});
|
|
1609
|
+
return completedRecord;
|
|
1610
|
+
}
|
|
1611
|
+
catch (error) {
|
|
1612
|
+
const completedAt = new Date().toISOString();
|
|
1613
|
+
const failedRecord = markJobState(runningRecord, {
|
|
1614
|
+
status: 'failed',
|
|
1615
|
+
completedAt,
|
|
1616
|
+
updatedAt: completedAt,
|
|
1617
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1618
|
+
});
|
|
1619
|
+
saveJobRecord(projectId, failedRecord);
|
|
1620
|
+
addArtifact(projectId, {
|
|
1621
|
+
kind: 'ingest-job',
|
|
1622
|
+
title: `Failed ingest job: ${failedRecord.title}`,
|
|
1623
|
+
content: buildJobArtifactPreview(failedRecord),
|
|
1624
|
+
mimeType: 'text/plain',
|
|
1625
|
+
metadata: {
|
|
1626
|
+
jobId: failedRecord.jobId,
|
|
1627
|
+
datasetName: failedRecord.datasetName,
|
|
1628
|
+
status: failedRecord.status,
|
|
1629
|
+
documentType: failedRecord.documentType,
|
|
1630
|
+
sourceFingerprint: failedRecord.sourceStamps.sourceFingerprint,
|
|
1631
|
+
parserVersion: failedRecord.sourceStamps.parserVersion,
|
|
1632
|
+
error: failedRecord.error,
|
|
1633
|
+
},
|
|
1634
|
+
});
|
|
1635
|
+
addNote(projectId, `Ingest job "${failedRecord.jobId}" failed: ${failedRecord.error}.`);
|
|
1636
|
+
setActiveAnalysisContext(projectId, {
|
|
1637
|
+
currentTask: `Ingest job failed for ${failedRecord.source.fileName}`,
|
|
1638
|
+
context: {
|
|
1639
|
+
...project.activeAnalysisContext.context,
|
|
1640
|
+
latestIngestJob: {
|
|
1641
|
+
jobId: failedRecord.jobId,
|
|
1642
|
+
datasetName: failedRecord.datasetName,
|
|
1643
|
+
documentType: failedRecord.documentType,
|
|
1644
|
+
status: failedRecord.status,
|
|
1645
|
+
error: failedRecord.error,
|
|
1646
|
+
sourceFingerprint: failedRecord.sourceStamps.sourceFingerprint,
|
|
1647
|
+
parserVersion: failedRecord.sourceStamps.parserVersion,
|
|
1648
|
+
},
|
|
1649
|
+
},
|
|
1650
|
+
relatedDatasets: [...new Set([...project.activeAnalysisContext.relatedDatasets, failedRecord.datasetName])],
|
|
1651
|
+
});
|
|
1652
|
+
return failedRecord;
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
export function loadGeotechIngestJobResult(projectId, datasetName) {
|
|
1656
|
+
const record = getSelectedJob(projectId, datasetName);
|
|
1657
|
+
if (!record) {
|
|
1658
|
+
throw new Error(datasetName
|
|
1659
|
+
? `No persisted ingest job named "${datasetName}" was found in project "${projectId}".`
|
|
1660
|
+
: `No persisted ingest jobs were found in project "${projectId}".`);
|
|
1661
|
+
}
|
|
1662
|
+
if (record.status !== 'completed' || !record.result || !record.resultSummary || !record.completedAt || !record.sourceStamps.normalizedResultHash) {
|
|
1663
|
+
throw new Error(`Persisted ingest job "${record.datasetName}" is ${record.status} and does not have a completed result to load yet.`);
|
|
1664
|
+
}
|
|
1665
|
+
return {
|
|
1666
|
+
jobId: record.jobId,
|
|
1667
|
+
datasetName: record.datasetName,
|
|
1668
|
+
projectId: record.projectId,
|
|
1669
|
+
documentType: record.documentType,
|
|
1670
|
+
completedAt: record.completedAt,
|
|
1671
|
+
sourceStamps: {
|
|
1672
|
+
sourceFingerprint: record.sourceStamps.sourceFingerprint,
|
|
1673
|
+
parserVersion: record.sourceStamps.parserVersion,
|
|
1674
|
+
normalizedResultHash: record.sourceStamps.normalizedResultHash,
|
|
1675
|
+
},
|
|
1676
|
+
result: record.result,
|
|
1677
|
+
resultSummary: record.resultSummary,
|
|
1678
|
+
persistedReview: record.persistedReview,
|
|
1679
|
+
};
|
|
1680
|
+
}
|
|
1681
|
+
export function listGeotechIngestJobs(projectId) {
|
|
1682
|
+
const project = loadProject(projectId);
|
|
1683
|
+
return getJobDatasetEntries(project)
|
|
1684
|
+
.map((entry) => normalizeJobRecord(entry.data))
|
|
1685
|
+
.filter((entry) => entry !== null)
|
|
1686
|
+
.sort((left, right) => right.updatedAt.localeCompare(left.updatedAt));
|
|
1687
|
+
}
|
|
1688
|
+
export function promotePersistedBoreholeIngestReview(projectId, datasetName) {
|
|
1689
|
+
const project = loadProject(projectId);
|
|
1690
|
+
const record = getSelectedReview(projectId, datasetName);
|
|
1691
|
+
if (!record) {
|
|
1692
|
+
throw new Error(datasetName
|
|
1693
|
+
? `No persisted ingest review named "${datasetName}" was found in project "${projectId}".`
|
|
1694
|
+
: `No persisted ingest reviews were found in project "${projectId}".`);
|
|
1695
|
+
}
|
|
1696
|
+
const approval = record.approval;
|
|
1697
|
+
const promotionAllowedWithoutApproval = record.summary.canAutoProceed
|
|
1698
|
+
&& !record.summary.reviewRequired
|
|
1699
|
+
&& record.summary.blockingFindings === 0;
|
|
1700
|
+
if (!promotionAllowedWithoutApproval && !approval) {
|
|
1701
|
+
throw new Error(`Persisted ingest review "${record.datasetName}" requires recorded approval before promotion (auto-proceed: ${record.summary.canAutoProceed ? 'yes' : 'no'}, blocking findings: ${record.summary.blockingFindings}, review findings: ${record.summary.reviewFindings}).`);
|
|
1702
|
+
}
|
|
1703
|
+
const promotedAt = new Date().toISOString();
|
|
1704
|
+
const promotionDatasetName = buildPromotionDatasetName(record.reviewId);
|
|
1705
|
+
const warnings = [];
|
|
1706
|
+
const promotedBoreholes = [];
|
|
1707
|
+
const promotedDocuments = [];
|
|
1708
|
+
const promotedDatasetNames = [];
|
|
1709
|
+
const promotedDatasetKinds = [];
|
|
1710
|
+
const promotedBoreholeIds = [];
|
|
1711
|
+
const supersededDatasetNames = [];
|
|
1712
|
+
const snapshotDatasetNames = [];
|
|
1713
|
+
if (isBoreholeIngestResult(record.result)) {
|
|
1714
|
+
for (const borehole of record.result.boreholes) {
|
|
1715
|
+
const rawDatasetName = buildPromotedBoreholeDatasetName(record.reviewId, borehole.boreholeId);
|
|
1716
|
+
const itemSupersededDatasetNames = [];
|
|
1717
|
+
const itemSnapshotDatasetNames = [];
|
|
1718
|
+
const rawSnapshot = snapshotNamedDatasetForPromotion(projectId, project.namedDatasets[rawDatasetName], rawDatasetName, record, promotedAt, promotionDatasetName, approval);
|
|
1719
|
+
if (rawSnapshot.supersededDatasetName) {
|
|
1720
|
+
itemSupersededDatasetNames.push(rawSnapshot.supersededDatasetName);
|
|
1721
|
+
supersededDatasetNames.push(rawSnapshot.supersededDatasetName);
|
|
1722
|
+
}
|
|
1723
|
+
if (rawSnapshot.snapshotDatasetName) {
|
|
1724
|
+
itemSnapshotDatasetNames.push(rawSnapshot.snapshotDatasetName);
|
|
1725
|
+
snapshotDatasetNames.push(rawSnapshot.snapshotDatasetName);
|
|
1726
|
+
warnings.push(`Raw promoted dataset "${rawDatasetName}" already existed and was snapshotted to "${rawSnapshot.snapshotDatasetName}" before update.`);
|
|
1727
|
+
}
|
|
1728
|
+
saveNamedDataset(projectId, {
|
|
1729
|
+
name: rawDatasetName,
|
|
1730
|
+
kind: 'borehole-log',
|
|
1731
|
+
data: borehole,
|
|
1732
|
+
source: record.datasetName,
|
|
1733
|
+
metadata: createPromotionDatasetMetadata('raw-borehole', record, promotedAt, promotionDatasetName, {
|
|
1734
|
+
boreholeId: borehole.boreholeId,
|
|
1735
|
+
targetDatasetName: rawDatasetName,
|
|
1736
|
+
supersededDatasetName: rawSnapshot.supersededDatasetName,
|
|
1737
|
+
snapshotDatasetName: rawSnapshot.snapshotDatasetName,
|
|
1738
|
+
approval,
|
|
1739
|
+
}),
|
|
1740
|
+
});
|
|
1741
|
+
promotedDatasetNames.push(rawDatasetName);
|
|
1742
|
+
promotedDatasetKinds.push('borehole-log');
|
|
1743
|
+
promotedBoreholeIds.push(borehole.boreholeId);
|
|
1744
|
+
const soilProfileProjection = toPromotableSoilProfile(borehole);
|
|
1745
|
+
warnings.push(...soilProfileProjection.warnings);
|
|
1746
|
+
let promotedSoilProfile = false;
|
|
1747
|
+
let soilProfileDatasetName;
|
|
1748
|
+
if (soilProfileProjection.profile) {
|
|
1749
|
+
soilProfileDatasetName = borehole.boreholeId;
|
|
1750
|
+
const soilProfileSnapshot = snapshotNamedDatasetForPromotion(projectId, project.namedDatasets[soilProfileDatasetName], soilProfileDatasetName, record, promotedAt, promotionDatasetName, approval);
|
|
1751
|
+
if (soilProfileSnapshot.supersededDatasetName) {
|
|
1752
|
+
itemSupersededDatasetNames.push(soilProfileSnapshot.supersededDatasetName);
|
|
1753
|
+
supersededDatasetNames.push(soilProfileSnapshot.supersededDatasetName);
|
|
1754
|
+
}
|
|
1755
|
+
if (soilProfileSnapshot.snapshotDatasetName) {
|
|
1756
|
+
itemSnapshotDatasetNames.push(soilProfileSnapshot.snapshotDatasetName);
|
|
1757
|
+
snapshotDatasetNames.push(soilProfileSnapshot.snapshotDatasetName);
|
|
1758
|
+
warnings.push(`Soil profile dataset "${soilProfileDatasetName}" already existed and was snapshotted to "${soilProfileSnapshot.snapshotDatasetName}" before update.`);
|
|
1759
|
+
}
|
|
1760
|
+
else if (project.soilProfiles.some((profile) => profile.boreholeId === borehole.boreholeId)) {
|
|
1761
|
+
warnings.push(`Soil profile "${borehole.boreholeId}" already existed and was updated from the promoted review.`);
|
|
1762
|
+
}
|
|
1763
|
+
addSoilProfile(projectId, soilProfileProjection.profile);
|
|
1764
|
+
saveNamedDataset(projectId, {
|
|
1765
|
+
name: soilProfileDatasetName,
|
|
1766
|
+
kind: 'soil-profile',
|
|
1767
|
+
data: soilProfileProjection.profile,
|
|
1768
|
+
source: record.datasetName,
|
|
1769
|
+
metadata: createPromotionDatasetMetadata('soil-profile', record, promotedAt, promotionDatasetName, {
|
|
1770
|
+
boreholeId: borehole.boreholeId,
|
|
1771
|
+
targetDatasetName: soilProfileDatasetName,
|
|
1772
|
+
supersededDatasetName: soilProfileSnapshot.supersededDatasetName,
|
|
1773
|
+
snapshotDatasetName: soilProfileSnapshot.snapshotDatasetName,
|
|
1774
|
+
approval,
|
|
1775
|
+
}),
|
|
1776
|
+
});
|
|
1777
|
+
promotedSoilProfile = true;
|
|
1778
|
+
promotedDatasetNames.push(soilProfileDatasetName);
|
|
1779
|
+
promotedDatasetKinds.push('soil-profile');
|
|
1780
|
+
}
|
|
1781
|
+
promotedBoreholes.push({
|
|
1782
|
+
boreholeId: borehole.boreholeId,
|
|
1783
|
+
rawDatasetName,
|
|
1784
|
+
soilProfileDatasetName,
|
|
1785
|
+
promotedSoilProfile,
|
|
1786
|
+
supersededDatasetNames: [...new Set(itemSupersededDatasetNames)],
|
|
1787
|
+
snapshotDatasetNames: [...new Set(itemSnapshotDatasetNames)],
|
|
1788
|
+
warnings: soilProfileProjection.warnings,
|
|
1789
|
+
});
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
else {
|
|
1793
|
+
const documentDatasets = [
|
|
1794
|
+
{
|
|
1795
|
+
role: 'document-insight',
|
|
1796
|
+
kind: 'document-insight',
|
|
1797
|
+
data: {
|
|
1798
|
+
kind: 'document-insight-record',
|
|
1799
|
+
schemaVersion: 1,
|
|
1800
|
+
reviewId: record.reviewId,
|
|
1801
|
+
reviewDatasetName: record.datasetName,
|
|
1802
|
+
sourceStamps: record.sourceStamps,
|
|
1803
|
+
documentClass: record.result.documentClass,
|
|
1804
|
+
title: record.result.title,
|
|
1805
|
+
summary: record.result.summary,
|
|
1806
|
+
risks: record.result.risks,
|
|
1807
|
+
recommendations: record.result.recommendations,
|
|
1808
|
+
materialCount: record.result.materials.length,
|
|
1809
|
+
classificationCount: record.result.classifications.length,
|
|
1810
|
+
parameterCount: record.result.parameters.length,
|
|
1811
|
+
source: record.result.source,
|
|
1812
|
+
},
|
|
1813
|
+
},
|
|
1814
|
+
{
|
|
1815
|
+
role: 'material-observations',
|
|
1816
|
+
kind: 'material-observations',
|
|
1817
|
+
data: {
|
|
1818
|
+
kind: 'material-observations-record',
|
|
1819
|
+
schemaVersion: 1,
|
|
1820
|
+
reviewId: record.reviewId,
|
|
1821
|
+
reviewDatasetName: record.datasetName,
|
|
1822
|
+
sourceStamps: record.sourceStamps,
|
|
1823
|
+
materials: record.result.materials,
|
|
1824
|
+
},
|
|
1825
|
+
},
|
|
1826
|
+
{
|
|
1827
|
+
role: 'parameter-catalog',
|
|
1828
|
+
kind: 'parameter-catalog',
|
|
1829
|
+
data: {
|
|
1830
|
+
kind: 'parameter-catalog-record',
|
|
1831
|
+
schemaVersion: 1,
|
|
1832
|
+
reviewId: record.reviewId,
|
|
1833
|
+
reviewDatasetName: record.datasetName,
|
|
1834
|
+
sourceStamps: record.sourceStamps,
|
|
1835
|
+
parameters: record.result.parameters,
|
|
1836
|
+
},
|
|
1837
|
+
},
|
|
1838
|
+
{
|
|
1839
|
+
role: 'classification-summary',
|
|
1840
|
+
kind: 'classification-summary',
|
|
1841
|
+
data: {
|
|
1842
|
+
kind: 'classification-summary-record',
|
|
1843
|
+
schemaVersion: 1,
|
|
1844
|
+
reviewId: record.reviewId,
|
|
1845
|
+
reviewDatasetName: record.datasetName,
|
|
1846
|
+
sourceStamps: record.sourceStamps,
|
|
1847
|
+
classifications: record.result.classifications,
|
|
1848
|
+
},
|
|
1849
|
+
},
|
|
1850
|
+
];
|
|
1851
|
+
for (const item of documentDatasets) {
|
|
1852
|
+
const datasetNameForRole = buildPromotedDocumentDatasetName(record.reviewId, item.role);
|
|
1853
|
+
const roleWarnings = [];
|
|
1854
|
+
const itemSupersededDatasetNames = [];
|
|
1855
|
+
const itemSnapshotDatasetNames = [];
|
|
1856
|
+
const snapshot = snapshotNamedDatasetForPromotion(projectId, project.namedDatasets[datasetNameForRole], datasetNameForRole, record, promotedAt, promotionDatasetName, approval);
|
|
1857
|
+
if (snapshot.supersededDatasetName) {
|
|
1858
|
+
itemSupersededDatasetNames.push(snapshot.supersededDatasetName);
|
|
1859
|
+
supersededDatasetNames.push(snapshot.supersededDatasetName);
|
|
1860
|
+
}
|
|
1861
|
+
if (snapshot.snapshotDatasetName) {
|
|
1862
|
+
itemSnapshotDatasetNames.push(snapshot.snapshotDatasetName);
|
|
1863
|
+
snapshotDatasetNames.push(snapshot.snapshotDatasetName);
|
|
1864
|
+
roleWarnings.push(`Dataset "${datasetNameForRole}" already existed and was snapshotted to "${snapshot.snapshotDatasetName}" before update.`);
|
|
1865
|
+
}
|
|
1866
|
+
saveNamedDataset(projectId, {
|
|
1867
|
+
name: datasetNameForRole,
|
|
1868
|
+
kind: item.kind,
|
|
1869
|
+
data: item.data,
|
|
1870
|
+
source: record.datasetName,
|
|
1871
|
+
metadata: createPromotionDatasetMetadata(item.role, record, promotedAt, promotionDatasetName, {
|
|
1872
|
+
targetDatasetName: datasetNameForRole,
|
|
1873
|
+
supersededDatasetName: snapshot.supersededDatasetName,
|
|
1874
|
+
snapshotDatasetName: snapshot.snapshotDatasetName,
|
|
1875
|
+
approval,
|
|
1876
|
+
}),
|
|
1877
|
+
});
|
|
1878
|
+
promotedDatasetNames.push(datasetNameForRole);
|
|
1879
|
+
promotedDatasetKinds.push(item.kind);
|
|
1880
|
+
promotedDocuments.push({
|
|
1881
|
+
role: item.role,
|
|
1882
|
+
datasetName: datasetNameForRole,
|
|
1883
|
+
datasetKind: item.kind,
|
|
1884
|
+
supersededDatasetNames: [...new Set(itemSupersededDatasetNames)],
|
|
1885
|
+
snapshotDatasetNames: [...new Set(itemSnapshotDatasetNames)],
|
|
1886
|
+
warnings: roleWarnings,
|
|
1887
|
+
});
|
|
1888
|
+
warnings.push(...roleWarnings);
|
|
1889
|
+
}
|
|
1890
|
+
}
|
|
1891
|
+
const promotionSnapshot = snapshotNamedDatasetForPromotion(projectId, project.namedDatasets[promotionDatasetName], promotionDatasetName, record, promotedAt, promotionDatasetName, approval);
|
|
1892
|
+
if (promotionSnapshot.supersededDatasetName) {
|
|
1893
|
+
supersededDatasetNames.push(promotionSnapshot.supersededDatasetName);
|
|
1894
|
+
}
|
|
1895
|
+
if (promotionSnapshot.snapshotDatasetName) {
|
|
1896
|
+
snapshotDatasetNames.push(promotionSnapshot.snapshotDatasetName);
|
|
1897
|
+
warnings.push(`Promotion dataset "${promotionDatasetName}" already existed and was snapshotted to "${promotionSnapshot.snapshotDatasetName}" before update.`);
|
|
1898
|
+
}
|
|
1899
|
+
const result = {
|
|
1900
|
+
kind: 'geotech-ingest-promotion-result',
|
|
1901
|
+
schemaVersion: 1,
|
|
1902
|
+
projectId,
|
|
1903
|
+
documentType: record.result.documentType,
|
|
1904
|
+
sourceDatasetName: record.datasetName,
|
|
1905
|
+
sourceReviewDatasetName: record.datasetName,
|
|
1906
|
+
sourceReviewId: record.reviewId,
|
|
1907
|
+
sourceStamps: record.sourceStamps,
|
|
1908
|
+
approvalDatasetName: approval?.datasetName,
|
|
1909
|
+
approvalId: approval?.approvalId,
|
|
1910
|
+
approvedAt: approval?.approvedAt,
|
|
1911
|
+
approvedBy: approval?.approvedBy,
|
|
1912
|
+
approvalRationale: approval?.rationale,
|
|
1913
|
+
promotedAt,
|
|
1914
|
+
promotionDatasetName,
|
|
1915
|
+
promotedDatasetNames: [...new Set(promotedDatasetNames)],
|
|
1916
|
+
promotedDatasetKinds: [...new Set(promotedDatasetKinds)],
|
|
1917
|
+
promotedBoreholeIds: [...new Set(promotedBoreholeIds)],
|
|
1918
|
+
supersededDatasetNames: [...new Set(supersededDatasetNames)],
|
|
1919
|
+
snapshotDatasetNames: [...new Set(snapshotDatasetNames)],
|
|
1920
|
+
promotedBoreholes,
|
|
1921
|
+
promotedDocuments,
|
|
1922
|
+
warnings: [...new Set(warnings)],
|
|
1923
|
+
};
|
|
1924
|
+
saveNamedDataset(projectId, {
|
|
1925
|
+
name: promotionDatasetName,
|
|
1926
|
+
kind: 'geotech-ingest-promotion',
|
|
1927
|
+
data: result,
|
|
1928
|
+
source: record.datasetName,
|
|
1929
|
+
metadata: createPromotionDatasetMetadata('promotion-result', record, promotedAt, promotionDatasetName, {
|
|
1930
|
+
targetDatasetName: promotionDatasetName,
|
|
1931
|
+
supersededDatasetName: promotionSnapshot.supersededDatasetName,
|
|
1932
|
+
snapshotDatasetName: promotionSnapshot.snapshotDatasetName,
|
|
1933
|
+
approval,
|
|
1934
|
+
}),
|
|
1935
|
+
});
|
|
1936
|
+
addArtifact(projectId, {
|
|
1937
|
+
kind: 'ingest-promotion',
|
|
1938
|
+
title: `Promoted ingest review: ${record.title}`,
|
|
1939
|
+
content: createPromotionArtifactPreview(result),
|
|
1940
|
+
mimeType: 'text/plain',
|
|
1941
|
+
metadata: {
|
|
1942
|
+
sourceReviewDatasetName: record.datasetName,
|
|
1943
|
+
sourceReviewId: record.reviewId,
|
|
1944
|
+
documentType: record.result.documentType,
|
|
1945
|
+
sourceFingerprint: record.sourceStamps.sourceFingerprint,
|
|
1946
|
+
parserVersion: record.sourceStamps.parserVersion,
|
|
1947
|
+
normalizedResultHash: record.sourceStamps.normalizedResultHash,
|
|
1948
|
+
promotedBoreholeIds: result.promotedBoreholeIds,
|
|
1949
|
+
promotedDatasetNames: result.promotedDatasetNames,
|
|
1950
|
+
promotedDatasetKinds: result.promotedDatasetKinds,
|
|
1951
|
+
supersededDatasetNames: result.supersededDatasetNames,
|
|
1952
|
+
snapshotDatasetNames: result.snapshotDatasetNames,
|
|
1953
|
+
approvalDatasetName: result.approvalDatasetName,
|
|
1954
|
+
approvalId: result.approvalId,
|
|
1955
|
+
approvedAt: result.approvedAt,
|
|
1956
|
+
approvedBy: result.approvedBy,
|
|
1957
|
+
approvalRationale: result.approvalRationale,
|
|
1958
|
+
},
|
|
1959
|
+
});
|
|
1960
|
+
addNote(projectId, `Promoted ingest review "${record.reviewId}" into ${result.promotedDatasetNames.length} dataset(s)${record.result.documentType === 'borehole-log' && promotedBoreholes.some((item) => item.promotedSoilProfile) ? ' with soil-profile updates' : ''}${approval ? ` using recorded approval "${approval.datasetName}"` : ''}${result.snapshotDatasetNames.length > 0 ? ` and ${result.snapshotDatasetNames.length} rollback snapshot(s)` : ''}.`);
|
|
1961
|
+
const relatedDatasets = [
|
|
1962
|
+
...new Set([
|
|
1963
|
+
...project.activeAnalysisContext.relatedDatasets,
|
|
1964
|
+
record.datasetName,
|
|
1965
|
+
promotionDatasetName,
|
|
1966
|
+
...result.promotedDatasetNames,
|
|
1967
|
+
...result.snapshotDatasetNames,
|
|
1968
|
+
]),
|
|
1969
|
+
];
|
|
1970
|
+
setActiveAnalysisContext(projectId, {
|
|
1971
|
+
currentTask: `Promoted ingest review for ${reviewSourceLabel(record.result)}`,
|
|
1972
|
+
context: {
|
|
1973
|
+
...project.activeAnalysisContext.context,
|
|
1974
|
+
latestPromotedIngestReview: {
|
|
1975
|
+
reviewDatasetName: record.datasetName,
|
|
1976
|
+
promotionDatasetName,
|
|
1977
|
+
approvalDatasetName: result.approvalDatasetName,
|
|
1978
|
+
approvalId: result.approvalId,
|
|
1979
|
+
approvedAt: result.approvedAt,
|
|
1980
|
+
approvedBy: result.approvedBy,
|
|
1981
|
+
reviewId: record.reviewId,
|
|
1982
|
+
documentType: result.documentType,
|
|
1983
|
+
sourceStamps: result.sourceStamps,
|
|
1984
|
+
promotedBoreholeIds: result.promotedBoreholeIds,
|
|
1985
|
+
promotedDatasetNames: result.promotedDatasetNames,
|
|
1986
|
+
promotedDatasetKinds: result.promotedDatasetKinds,
|
|
1987
|
+
supersededDatasetNames: result.supersededDatasetNames,
|
|
1988
|
+
snapshotDatasetNames: result.snapshotDatasetNames,
|
|
1989
|
+
},
|
|
1990
|
+
},
|
|
1991
|
+
relatedDatasets,
|
|
1992
|
+
});
|
|
1993
|
+
return result;
|
|
1994
|
+
}
|
|
1995
|
+
//# sourceMappingURL=review-store.js.map
|