@geotechcli/core 0.4.21 → 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.
Files changed (154) hide show
  1. package/dist/agents/brain.d.ts +1 -5
  2. package/dist/agents/brain.d.ts.map +1 -1
  3. package/dist/agents/brain.js +4 -120
  4. package/dist/agents/brain.js.map +1 -1
  5. package/dist/agents/data-tools.js +759 -0
  6. package/dist/agents/data-tools.js.map +1 -1
  7. package/dist/agents/runtime-bootstrap.d.ts +6 -0
  8. package/dist/agents/runtime-bootstrap.d.ts.map +1 -0
  9. package/dist/agents/runtime-bootstrap.js +8 -0
  10. package/dist/agents/runtime-bootstrap.js.map +1 -0
  11. package/dist/agents/runtime-fallbacks.d.ts +7 -0
  12. package/dist/agents/runtime-fallbacks.d.ts.map +1 -0
  13. package/dist/agents/runtime-fallbacks.js +87 -0
  14. package/dist/agents/runtime-fallbacks.js.map +1 -0
  15. package/dist/agents/swarm.d.ts +1 -4
  16. package/dist/agents/swarm.d.ts.map +1 -1
  17. package/dist/agents/swarm.js +74 -8
  18. package/dist/agents/swarm.js.map +1 -1
  19. package/dist/agents/tool-runtime.d.ts +7 -0
  20. package/dist/agents/tool-runtime.d.ts.map +1 -0
  21. package/dist/agents/tool-runtime.js +9 -0
  22. package/dist/agents/tool-runtime.js.map +1 -0
  23. package/dist/config/index.d.ts +4 -4
  24. package/dist/config/index.js +1 -1
  25. package/dist/config/index.js.map +1 -1
  26. package/dist/geo/coordinates.d.ts +40 -0
  27. package/dist/geo/coordinates.d.ts.map +1 -0
  28. package/dist/geo/coordinates.js +461 -0
  29. package/dist/geo/coordinates.js.map +1 -0
  30. package/dist/geo/index.d.ts +1 -0
  31. package/dist/geo/index.d.ts.map +1 -1
  32. package/dist/geo/index.js +1 -0
  33. package/dist/geo/index.js.map +1 -1
  34. package/dist/index.d.ts +3 -2
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +3 -2
  37. package/dist/index.js.map +1 -1
  38. package/dist/ingest/ags.d.ts +3 -0
  39. package/dist/ingest/ags.d.ts.map +1 -1
  40. package/dist/ingest/ags.js +98 -9
  41. package/dist/ingest/ags.js.map +1 -1
  42. package/dist/ingest/cpt.d.ts +4 -0
  43. package/dist/ingest/cpt.d.ts.map +1 -1
  44. package/dist/ingest/cpt.js +87 -25
  45. package/dist/ingest/cpt.js.map +1 -1
  46. package/dist/ingest/document-inputs.d.ts +37 -0
  47. package/dist/ingest/document-inputs.d.ts.map +1 -0
  48. package/dist/ingest/document-inputs.js +197 -0
  49. package/dist/ingest/document-inputs.js.map +1 -0
  50. package/dist/ingest/geotech-document.d.ts +118 -0
  51. package/dist/ingest/geotech-document.d.ts.map +1 -0
  52. package/dist/ingest/geotech-document.js +1006 -0
  53. package/dist/ingest/geotech-document.js.map +1 -0
  54. package/dist/ingest/geotech-extract.d.ts +86 -0
  55. package/dist/ingest/geotech-extract.d.ts.map +1 -0
  56. package/dist/ingest/geotech-extract.js +652 -0
  57. package/dist/ingest/geotech-extract.js.map +1 -0
  58. package/dist/ingest/geotech-schemas.d.ts +248 -0
  59. package/dist/ingest/geotech-schemas.d.ts.map +1 -0
  60. package/dist/ingest/geotech-schemas.js +150 -0
  61. package/dist/ingest/geotech-schemas.js.map +1 -0
  62. package/dist/ingest/index.d.ts +8 -0
  63. package/dist/ingest/index.d.ts.map +1 -1
  64. package/dist/ingest/index.js +8 -0
  65. package/dist/ingest/index.js.map +1 -1
  66. package/dist/ingest/ingest-job-child.d.ts +2 -0
  67. package/dist/ingest/ingest-job-child.d.ts.map +1 -0
  68. package/dist/ingest/ingest-job-child.js +45 -0
  69. package/dist/ingest/ingest-job-child.js.map +1 -0
  70. package/dist/ingest/job-store.d.ts +117 -0
  71. package/dist/ingest/job-store.d.ts.map +1 -0
  72. package/dist/ingest/job-store.js +541 -0
  73. package/dist/ingest/job-store.js.map +1 -0
  74. package/dist/ingest/job-worker.d.ts +24 -0
  75. package/dist/ingest/job-worker.d.ts.map +1 -0
  76. package/dist/ingest/job-worker.js +1129 -0
  77. package/dist/ingest/job-worker.js.map +1 -0
  78. package/dist/ingest/pdf.d.ts +102 -0
  79. package/dist/ingest/pdf.d.ts.map +1 -0
  80. package/dist/ingest/pdf.js +1544 -0
  81. package/dist/ingest/pdf.js.map +1 -0
  82. package/dist/ingest/review-store.d.ts +215 -0
  83. package/dist/ingest/review-store.d.ts.map +1 -0
  84. package/dist/ingest/review-store.js +1995 -0
  85. package/dist/ingest/review-store.js.map +1 -0
  86. package/dist/llm/capabilities.d.ts +8 -0
  87. package/dist/llm/capabilities.d.ts.map +1 -0
  88. package/dist/llm/capabilities.js +73 -0
  89. package/dist/llm/capabilities.js.map +1 -0
  90. package/dist/llm/index.d.ts +3 -2
  91. package/dist/llm/index.d.ts.map +1 -1
  92. package/dist/llm/index.js +2 -1
  93. package/dist/llm/index.js.map +1 -1
  94. package/dist/llm/providers/anthropic.d.ts +6 -0
  95. package/dist/llm/providers/anthropic.d.ts.map +1 -1
  96. package/dist/llm/providers/anthropic.js +10 -1
  97. package/dist/llm/providers/anthropic.js.map +1 -1
  98. package/dist/llm/providers/hosted-beta.d.ts +6 -0
  99. package/dist/llm/providers/hosted-beta.d.ts.map +1 -1
  100. package/dist/llm/providers/hosted-beta.js +40 -10
  101. package/dist/llm/providers/hosted-beta.js.map +1 -1
  102. package/dist/llm/providers/huggingface.d.ts +6 -0
  103. package/dist/llm/providers/huggingface.d.ts.map +1 -1
  104. package/dist/llm/providers/huggingface.js +21 -1
  105. package/dist/llm/providers/huggingface.js.map +1 -1
  106. package/dist/llm/providers/openai-compatible.d.ts +6 -0
  107. package/dist/llm/providers/openai-compatible.d.ts.map +1 -1
  108. package/dist/llm/providers/openai-compatible.js +21 -1
  109. package/dist/llm/providers/openai-compatible.js.map +1 -1
  110. package/dist/llm/providers/zhipu.d.ts +6 -0
  111. package/dist/llm/providers/zhipu.d.ts.map +1 -1
  112. package/dist/llm/providers/zhipu.js +15 -1
  113. package/dist/llm/providers/zhipu.js.map +1 -1
  114. package/dist/llm/router.d.ts +7 -0
  115. package/dist/llm/router.d.ts.map +1 -1
  116. package/dist/llm/router.js +33 -13
  117. package/dist/llm/router.js.map +1 -1
  118. package/dist/llm/types.d.ts +22 -4
  119. package/dist/llm/types.d.ts.map +1 -1
  120. package/dist/llm/types.js.map +1 -1
  121. package/dist/meta/metadata.json +1 -1
  122. package/dist/report/html.d.ts +3 -0
  123. package/dist/report/html.d.ts.map +1 -0
  124. package/dist/report/html.js +626 -0
  125. package/dist/report/html.js.map +1 -0
  126. package/dist/report/index.d.ts +2 -0
  127. package/dist/report/index.d.ts.map +1 -1
  128. package/dist/report/index.js +2 -0
  129. package/dist/report/index.js.map +1 -1
  130. package/dist/report/ingest-dossier.d.ts +81 -0
  131. package/dist/report/ingest-dossier.d.ts.map +1 -0
  132. package/dist/report/ingest-dossier.js +324 -0
  133. package/dist/report/ingest-dossier.js.map +1 -0
  134. package/dist/storage/index.d.ts +5 -0
  135. package/dist/storage/index.d.ts.map +1 -1
  136. package/dist/storage/index.js +12 -6
  137. package/dist/storage/index.js.map +1 -1
  138. package/dist/vision/geotech-document.d.ts +46 -0
  139. package/dist/vision/geotech-document.d.ts.map +1 -0
  140. package/dist/vision/geotech-document.js +576 -0
  141. package/dist/vision/geotech-document.js.map +1 -0
  142. package/dist/vision/index.d.ts +31 -0
  143. package/dist/vision/index.d.ts.map +1 -1
  144. package/dist/vision/index.js +659 -27
  145. package/dist/vision/index.js.map +1 -1
  146. package/dist/vision/ocr.d.ts +29 -0
  147. package/dist/vision/ocr.d.ts.map +1 -0
  148. package/dist/vision/ocr.js +287 -0
  149. package/dist/vision/ocr.js.map +1 -0
  150. package/dist/vision/preprocess.d.ts +26 -0
  151. package/dist/vision/preprocess.d.ts.map +1 -0
  152. package/dist/vision/preprocess.js +194 -0
  153. package/dist/vision/preprocess.js.map +1 -0
  154. 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