also looking at this
feat: import LAS surfaces and consume maintained source releases
#107
35 files
+1061
-416
Review threads live on the whole diff, not on one commit, so none are shown here - a thread's line means something in the branch's final form, and painting it into an intermediate step would put it on code it is not about.
| @@ -10,7 +10,7 @@ const scenario = state<ApplicationScenario>('nominal') | ||
| 10 | 10 | const mode = state('test') |
| 11 | 11 | const authorization = state('none') |
| 12 | 12 | const run = state<ApplicationRun | null>(null) |
| 13 | const report = state<ApplicationReport | null>(null) | |
| 13 | const applicationReport = state<ApplicationReport | null>(null) | |
| 14 | 14 | const busy = state(false) |
| 15 | 15 | const error = state('') |
| 16 | 16 | const epoch = state(600) |
| @@ -22,9 +22,9 @@ const validEpoch = derived(() => !run() || Number.isSafeInteger(Number(epoch())) | ||
| 22 | 22 | const input = derived(() => validEpoch() ? run()?.inputs.find(row => row.atMs === Number(epoch())) ?? null : null) |
| 23 | 23 | const decision = derived(() => validEpoch() ? run()?.decisions.find(row => row.atMs === Number(epoch())) ?? null : null) |
| 24 | 24 | const probe = derived(() => validEpoch() ? run()?.probes.find(row => row.atMs === Number(epoch())) ?? null : null) |
| 25 | const cell = derived(() => Number.isInteger(Number(cellIndex())) && Number(cellIndex()) >= 0 ? report()?.cells[Number(cellIndex())] ?? null : null) | |
| 25 | const cell = derived(() => Number.isInteger(Number(cellIndex())) && Number(cellIndex()) >= 0 ? applicationReport()?.cells[Number(cellIndex())] ?? null : null) | |
| 26 | 26 | const doseMaximum = derived(() => Math.max(1, ...config().prescription.map(zone => zone.ratePerHa))) |
| 27 | const preview = derived(() => report() ? applicationReportPreview(report()!, doseMaximum()) : '') | |
| 27 | const preview = derived(() => applicationReport() ? applicationReportPreview(applicationReport()!, doseMaximum()) : '') | |
| 28 | 28 | const rateMaximum = derived(() => Math.max(config().calibration.maxRatePerSecond, ...(run()?.decisions.map(row => Math.max(row.targetPerSecond, row.measuredPerSecond ?? 0)) ?? []))) |
| 29 | 29 | const ratePlot = derived(() => applicationRatePlot(run()?.decisions ?? [], rateMaximum())) |
| 30 | 30 | function quantity(value: number | null | undefined, digits = 6) { return value === null || value === undefined ? 'unavailable' : value.toFixed(digits).replace(/^-0(\.0+)$/, '0$1') } |
| @@ -33,14 +33,14 @@ const pending = useTimeout(() => { renamed throughout | ||
| 33 | 33 | try { |
| 34 | 34 | const result = simulateApplication({ config: exampleApplicationConfig(implement()), scenario: scenario(), testMode: mode() === 'test', authorizeImplement: authorization() === 'grant' }) |
| 35 | 35 | const audited = processApplication(result) |
| 36 | run.set(result); report.set(audited) | |
| 36 | run.set(result); applicationReport.set(audited) | |
| 37 | 37 | void nextTick(() => { if (run() === result) epoch.set(result.final.atMs) }) |
| 38 | 38 | } |
| 39 | 39 | catch (failure) { error.set(failure instanceof Error ? failure.message : 'Application simulation failed') } |
| 40 | 40 | finally { busy.set(false) } |
| 41 | 41 | }, 32) |
| 42 | 42 | pending.stop() |
| 43 | function clear() { pending.stop(); run.set(null); report.set(null); error.set(''); busy.set(false); epoch.set(600); cellIndex.set(0) } | |
| 43 | function clear() { pending.stop(); run.set(null); applicationReport.set(null); error.set(''); busy.set(false); epoch.set(600); cellIndex.set(0) } | |
| 44 | 44 | effect(() => { implement(); scenario(); mode(); authorization(); clear() }) |
| 45 | 45 | onDestroy(() => pending.stop()) |
| 46 | 46 | function execute() { clear(); busy.set(true); pending.start() } |
| @@ -61,10 +61,10 @@ function execute() { clear(); busy.set(true); pending.start() } | ||
| 61 | 61 | <p :if="busy()" role="status" class="mt-5 text-emerald-200 text-sm">Running sensor, estimator, mission, metering and exact coverage processing.</p> |
| 62 | 62 | <p :if="error()" role="alert" class="mt-5 p-4 break-words text-red-200 border border-red-400 rounded">{{ error() }}</p> |
| 63 | 63 | <p :if="!run() && !busy()" class="mt-7 py-5 text-neutral-400 border-neutral-700 border-t">No application recording. Dry test and no grant are the defaults. Metered discharge requires both the metered model and a separate grant.</p> |
| 64 | <div :if="run() && report()" class="mt-8 space-y-8"> | |
| 64 | <div :if="run() && applicationReport()" class="mt-8 space-y-8"> | |
| 65 | 65 | <section aria-labelledby="application-result" class="py-5 border-neutral-700 border-t"> |
| 66 | 66 | <h2 id="application-result" class="font-semibold text-lg">{{ run()?.status }} / {{ run()?.reason }}</h2> |
| 67 | <dl class="grid gap-5 grid-cols-2 sm:grid-cols-4 mt-5 text-sm"><div><dt class="text-neutral-400">Modeled emitted</dt><dd class="mt-1 font-mono">{{ quantity(report()?.emittedAmount) }} {{ unit() }}</dd></div><div><dt class="text-neutral-400">Treatable field</dt><dd class="mt-1 font-mono">{{ quantity(report()?.treatableAreaM2, 2) }} m2</dd></div><div><dt class="text-neutral-400">Covered / missed</dt><dd class="mt-1 font-mono">{{ quantity(report()?.coveredAreaM2, 2) }} / {{ quantity(report()?.missedAreaM2, 2) }} m2</dd></div><div><dt class="text-neutral-400">Distinct-pass overlap</dt><dd class="mt-1 font-mono">{{ quantity(report()?.overlapAreaM2, 3) }} m2</dd></div></dl> | |
| 67 | <dl class="grid gap-5 grid-cols-2 sm:grid-cols-4 mt-5 text-sm"><div><dt class="text-neutral-400">Modeled emitted</dt><dd class="mt-1 font-mono">{{ quantity(applicationReport()?.emittedAmount) }} {{ unit() }}</dd></div><div><dt class="text-neutral-400">Treatable field</dt><dd class="mt-1 font-mono">{{ quantity(applicationReport()?.treatableAreaM2, 2) }} m2</dd></div><div><dt class="text-neutral-400">Covered / missed</dt><dd class="mt-1 font-mono">{{ quantity(applicationReport()?.coveredAreaM2, 2) }} / {{ quantity(applicationReport()?.missedAreaM2, 2) }} m2</dd></div><div><dt class="text-neutral-400">Distinct-pass overlap</dt><dd class="mt-1 font-mono">{{ quantity(applicationReport()?.overlapAreaM2, 3) }} m2</dd></div></dl> | |
| 68 | 68 | <p class="mt-4 break-words text-neutral-400 text-sm">Material {{ config().material.id }}, batch {{ config().material.batch }}. Calibration {{ config().calibration.id }} is synthetic. Route completion does not establish complete field coverage.</p> |
| 69 | 69 | </section> |
| 70 | 70 | <section aria-labelledby="application-rate" class="min-w-0"> |
| @@ -78,9 +78,9 @@ function execute() { clear(); busy.set(true); pending.start() } | ||
| 78 | 78 | </section> |
| 79 | 79 | <section aria-labelledby="application-dose" class="grid gap-6 md:grid-cols-2 pt-7 border-neutral-700 border-t"> |
| 80 | 80 | <div class="min-w-0"><h2 id="application-dose" class="font-semibold text-lg">Retained dose cells</h2><img :src="preview()" width="320" height="320" alt="North-up exact modeled deposited dose cells; red is zero, green reaches the declared maximum, grey is unavailable treatable support" class="block object-contain mt-4 h-80 w-full bg-neutral-900 border border-neutral-700 rounded [image-rendering:pixelated]"><p class="mt-3 text-neutral-400 text-xs">North is up. Fixed colour scale: 0 to {{ doseMaximum() }} {{ unit() }}/ha and above. Grey is unavailable treatable support. Uniform rectangular deposition is a declared model, not measured field dose.</p></div> |
| 81 | <div class="min-w-0"><label for="application-cell" class="block mb-2 text-sm">Cell index, north to south, west to east</label><input id="application-cell" type="number" min="0" :max="(report()?.cells.length || 1) - 1" step="1" x-model.number="cellIndex" class="px-3 py-2 w-32 bg-neutral-900 border border-neutral-600 rounded"><p :if="!cell()" role="alert" class="mt-3 text-red-200 text-sm">Choose a retained cell index.</p><dl :if="cell()" class="mt-5 space-y-3 break-words text-sm"><div><dt class="text-neutral-400">Cell / north-east bounds</dt><dd class="font-mono">{{ cell()?.id }} / {{ vector(cell()?.boundsM) }} m</dd></div><div><dt class="text-neutral-400">Dose / prescribed amount</dt><dd class="font-mono">{{ quantity(cell()?.dosePerHa, 2) }} {{ unit() }}/ha / {{ quantity(cell()?.prescribedAmount) }} {{ unit() }}</dd></div><div><dt class="text-neutral-400">Field support / excluded support</dt><dd class="font-mono">{{ quantity(cell()?.areaM2, 3) }} / {{ quantity(cell()?.excludedAreaM2, 3) }} m2</dd></div><div><dt class="text-neutral-400">Covered / distinct-pass overlap</dt><dd class="font-mono">{{ quantity(cell()?.coveredAreaM2, 3) }} / {{ quantity(cell()?.overlapAreaM2, 3) }} m2</dd></div></dl></div> | |
| 81 | <div class="min-w-0"><label for="application-cell" class="block mb-2 text-sm">Cell index, north to south, west to east</label><input id="application-cell" type="number" min="0" :max="(applicationReport()?.cells.length || 1) - 1" step="1" x-model.number="cellIndex" class="px-3 py-2 w-32 bg-neutral-900 border border-neutral-600 rounded"><p :if="!cell()" role="alert" class="mt-3 text-red-200 text-sm">Choose a retained cell index.</p><dl :if="cell()" class="mt-5 space-y-3 break-words text-sm"><div><dt class="text-neutral-400">Cell / north-east bounds</dt><dd class="font-mono">{{ cell()?.id }} / {{ vector(cell()?.boundsM) }} m</dd></div><div><dt class="text-neutral-400">Dose / prescribed amount</dt><dd class="font-mono">{{ quantity(cell()?.dosePerHa, 2) }} {{ unit() }}/ha / {{ quantity(cell()?.prescribedAmount) }} {{ unit() }}</dd></div><div><dt class="text-neutral-400">Field support / excluded support</dt><dd class="font-mono">{{ quantity(cell()?.areaM2, 3) }} / {{ quantity(cell()?.excludedAreaM2, 3) }} m2</dd></div><div><dt class="text-neutral-400">Covered / distinct-pass overlap</dt><dd class="font-mono">{{ quantity(cell()?.coveredAreaM2, 3) }} / {{ quantity(cell()?.overlapAreaM2, 3) }} m2</dd></div></dl></div> | |
| 82 | 82 | </section> |
| 83 | <section aria-labelledby="application-closure" class="pt-7 border-neutral-700 border-t"><h2 id="application-closure" class="font-semibold text-lg">Shutdown and material balance</h2><p class="mt-3 text-sm">Modeled closure {{ run()?.shutdown.closureVerified ? 'verified in the independent tail' : 'unverified' }}. Request {{ run()?.shutdown.requestedAtMs }} ms, final probe {{ run()?.shutdown.finalAtMs }} ms. Emitted after request: {{ quantity(run()?.shutdown.emittedAfterRequest) }} {{ unit() }}.</p><dl class="grid gap-4 grid-cols-2 sm:grid-cols-4 mt-5 text-sm"><div><dt class="text-neutral-400">Treatable field</dt><dd class="font-mono">{{ quantity(report()?.inFieldAmount) }} {{ unit() }}</dd></div><div><dt class="text-neutral-400">Excluded support</dt><dd class="font-mono">{{ quantity(report()?.excludedAmount) }} {{ unit() }}</dd></div><div><dt class="text-neutral-400">Outside field</dt><dd class="font-mono">{{ quantity(report()?.outsideAmount) }} {{ unit() }}</dd></div><div><dt class="text-neutral-400">Still in flight</dt><dd class="font-mono">{{ quantity(report()?.inFlightAmount) }} {{ unit() }}</dd></div></dl><p class="mt-4 text-neutral-400 text-xs">Conservation residual {{ quantity(report()?.conservationError, 10) }} {{ unit() }}. Host measured totals and independent material truth are separate evidence.</p></section> | |
| 83 | <section aria-labelledby="application-closure" class="pt-7 border-neutral-700 border-t"><h2 id="application-closure" class="font-semibold text-lg">Shutdown and material balance</h2><p class="mt-3 text-sm">Modeled closure {{ run()?.shutdown.closureVerified ? 'verified in the independent tail' : 'unverified' }}. Request {{ run()?.shutdown.requestedAtMs }} ms, final probe {{ run()?.shutdown.finalAtMs }} ms. Emitted after request: {{ quantity(run()?.shutdown.emittedAfterRequest) }} {{ unit() }}.</p><dl class="grid gap-4 grid-cols-2 sm:grid-cols-4 mt-5 text-sm"><div><dt class="text-neutral-400">Treatable field</dt><dd class="font-mono">{{ quantity(applicationReport()?.inFieldAmount) }} {{ unit() }}</dd></div><div><dt class="text-neutral-400">Excluded support</dt><dd class="font-mono">{{ quantity(applicationReport()?.excludedAmount) }} {{ unit() }}</dd></div><div><dt class="text-neutral-400">Outside field</dt><dd class="font-mono">{{ quantity(applicationReport()?.outsideAmount) }} {{ unit() }}</dd></div><div><dt class="text-neutral-400">Still in flight</dt><dd class="font-mono">{{ quantity(applicationReport()?.inFlightAmount) }} {{ unit() }}</dd></div></dl><p class="mt-4 text-neutral-400 text-xs">Conservation residual {{ quantity(applicationReport()?.conservationError, 10) }} {{ unit() }}. Host measured totals and independent material truth are separate evidence.</p></section> | |
| 84 | 84 | </div> |
| 85 | 85 | <section aria-labelledby="application-requirements" class="mt-8 pt-7 border-neutral-700 border-t"><h2 id="application-requirements" class="font-semibold text-lg">Declared constraints</h2><p class="mt-3 break-words text-neutral-400 text-sm">Wind north/east {{ vector(config().weather.windMps) }} m/s, maximum {{ config().weatherLimits.maxWindMps }} m/s. Temperature {{ config().weather.temperatureC }} C within {{ config().weatherLimits.minTemperatureC }}..{{ config().weatherLimits.maxTemperatureC }} C; humidity {{ config().weather.humidityPercent }}%, minimum {{ config().weatherLimits.minHumidityPercent }}%. Inversion must be absent. Weather age limit {{ config().weatherLimits.maxAgeMs }} ms.</p><p class="mt-3 break-words text-neutral-400 text-sm">Operator {{ config().localRequirements.operatorId }}. Jurisdiction {{ config().localRequirements.jurisdiction ?? 'unspecified' }}. Local requirements remain {{ config().localRequirements.status }}; physical trial eligible: false. Material-specific distribution, independent catch/scale calibration, drift/weather and local operating requirements need physical qualification.</p></section> |
| 86 | 86 | </section> |
| @@ -10,7 +10,7 @@ const busy = state(false) | ||
| 10 | 10 | const error = state('') |
| 11 | 11 | const offset = state(0) |
| 12 | 12 | const replay = derived(() => current() ? createDiagnosticsReplay(current()!).advanceTo(offset()) : null) |
| 13 | const report = derived(() => current() ? diagnoseDiagnostics(current()!) : null) | |
| 13 | const diagnosticReport = derived(() => current() ? diagnoseDiagnostics(current()!) : null) | |
| 14 | 14 | const duration = derived(() => current()?.footer?.atMs ?? current()?.events.at(-1)?.atMs ?? 0) |
| 15 | 15 | const rows = derived(() => (current()?.events ?? []).filter(row => row.atMs <= offset()).slice(-24)) |
| 16 | 16 | let revision = 0 |
| @@ -63,7 +63,7 @@ onDestroy(() => { revision++ }) | ||
| 63 | 63 | <p class="mt-5 text-neutral-400 text-sm">Observed faults: <span class="font-mono text-neutral-100" data-replay-faults>{{ replay()?.health.join(', ') || 'none at this epoch' }}</span></p> |
| 64 | 64 | <h3 class="mt-8 text-neutral-400 text-sm">Last 24 records at this epoch</h3><div class="overflow-x-auto mt-3"><table class="w-full text-left text-xs"><thead><tr class="text-neutral-400 border-b border-neutral-700"><th scope="col" class="pr-4 py-3">Seq</th><th scope="col" class="pr-4 py-3">Time / ms</th><th scope="col" class="py-3">Event</th></tr></thead><tbody><tr :for="row in rows()" class="border-b border-neutral-800"><td class="pr-4 py-3 font-mono">{{ row.sequence }}</td><td class="pr-4 py-3 font-mono">{{ row.atMs }}</td><td class="py-3 font-mono">{{ row.event.kind === 'runtime' ? row.event.direction + ' / ' + row.event.frame.kind : row.event.kind === 'health' ? row.event.code : 'board tick' }}</td></tr></tbody></table></div> |
| 65 | 65 | </div> |
| 66 | <aside class="min-w-0"><h2 class="font-semibold text-lg">Recording checks</h2><p class="mt-2 text-neutral-400 text-sm">These checks diagnose the file. Hardware commissioning remains blocked.</p><dl class="mt-5 text-sm"><div :for="check in report()?.checks || []" class="py-3 border-b border-neutral-700"><dt class="flex gap-3 justify-between"><span>{{ check.id }}</span><span class="font-mono">{{ check.state }}</span></dt><dd class="mt-2 break-words text-neutral-400 text-xs">{{ check.detail }}</dd></div></dl> | |
| 66 | <aside class="min-w-0"><h2 class="font-semibold text-lg">Recording checks</h2><p class="mt-2 text-neutral-400 text-sm">These checks diagnose the file. Hardware commissioning remains blocked.</p><dl class="mt-5 text-sm"><div :for="check in diagnosticReport()?.checks || []" class="py-3 border-b border-neutral-700"><dt class="flex gap-3 justify-between"><span>{{ check.id }}</span><span class="font-mono">{{ check.state }}</span></dt><dd class="mt-2 break-words text-neutral-400 text-xs">{{ check.detail }}</dd></div></dl> | |
| 67 | 67 | <details class="mt-6"><summary class="text-sm cursor-pointer">Firmware and configuration provenance</summary><dl class="mt-4 space-y-4 text-xs"><div><dt class="text-neutral-400">Firmware SHA-256</dt><dd class="mt-1 break-all font-mono">{{ current()?.header.provenance.firmwareSha256 }}</dd></div><div><dt class="text-neutral-400">Configuration SHA-256</dt><dd class="mt-1 break-all font-mono">{{ current()?.header.provenance.configurationSha256 }}</dd></div><div><dt class="text-neutral-400">Model SHA-256</dt><dd class="mt-1 break-all font-mono">{{ current()?.header.provenance.modelSha256 }}</dd></div><div><dt class="text-neutral-400">Calibration SHA-256</dt><dd class="mt-1 break-all font-mono">{{ current()?.header.provenance.calibrationSha256 }}</dd></div></dl></details> |
| 68 | 68 | </aside> |
| 69 | 69 | </div> |
| @@ -10,10 +10,10 @@ const result = state<VolumeReport | null>(null) renamed throughout | ||
| 10 | 10 | const busy = state(false) |
| 11 | 11 | const error = state('') |
| 12 | 12 | const page = state(0) |
| 13 | const report = derived(() => resultScenario() === scenario() ? result() : null) | |
| 14 | const source = derived(() => report() ? request() : null) | |
| 15 | const pages = derived(() => Math.max(1, Math.ceil((report()?.integrationPieces.length ?? 0) / 12))) | |
| 16 | const pieces = derived(() => report()?.integrationPieces.slice(page() * 12, page() * 12 + 12) ?? []) | |
| 13 | const volumeReport = derived(() => resultScenario() === scenario() ? result() : null) | |
| 14 | const source = derived(() => volumeReport() ? request() : null) | |
| 15 | const pages = derived(() => Math.max(1, Math.ceil((volumeReport()?.integrationPieces.length ?? 0) / 12))) | |
| 16 | const pieces = derived(() => volumeReport()?.integrationPieces.slice(page() * 12, page() * 12 + 12) ?? []) | |
| 17 | 17 | const extent = derived(() => { |
| 18 | 18 | const points = source()?.boundary.outer ?? [[0, 0], [1, 1]] |
| 19 | 19 | const east = points.map(point => point[0]), north = points.map(point => point[1]) |
| @@ -94,27 +94,27 @@ function execute() { clear(); busy.set(true); computation.start() } | ||
| 94 | 94 | </div> |
| 95 | 95 | <p :if="busy()" role="status" class="mt-5 text-emerald-200 text-sm">Clipping the known surface and integrating above and below the declared base.</p> |
| 96 | 96 | <p :if="error()" role="alert" class="mt-5 p-4 break-words text-red-200 border border-red-400 rounded">{{ error() }}</p> |
| 97 | <p :if="!report() && !busy() && !error()" class="mt-7 py-5 text-neutral-400 border-neutral-700 border-t">No analysis yet. Choose an example to inspect exact surface integration and its evidence limits.</p> | |
| 98 | <div :if="report()" class="mt-9"> | |
| 97 | <p :if="!volumeReport() && !busy() && !error()" class="mt-7 py-5 text-neutral-400 border-neutral-700 border-t">No analysis yet. Choose an example to inspect exact surface integration and its evidence limits.</p> | |
| 98 | <div :if="volumeReport()" class="mt-9"> | |
| 99 | 99 | <div class="grid gap-8 lg:grid-cols-[1fr_1.3fr]"> |
| 100 | 100 | <aside class="min-w-0"> |
| 101 | 101 | <h2 class="font-semibold text-lg">Covered-domain result</h2> |
| 102 | <p class="mt-3 break-words font-mono text-emerald-200 text-sm" data-volume-status>{{ report()?.status }} / {{ report()?.evidence }}</p> | |
| 102 | <p class="mt-3 break-words font-mono text-emerald-200 text-sm" data-volume-status>{{ volumeReport()?.status }} / {{ volumeReport()?.evidence }}</p> | |
| 103 | 103 | <dl class="grid gap-5 grid-cols-2 mt-5 text-sm"> |
| 104 | <div><dt class="text-neutral-400">Above base</dt><dd class="mt-2 font-mono" data-volume-above>{{ quantity(report()?.aboveBaseM3) }}</dd></div> | |
| 105 | <div><dt class="text-neutral-400">Below base</dt><dd class="mt-2 font-mono" data-volume-below>{{ quantity(report()?.belowBaseM3) }}</dd></div> | |
| 106 | <div><dt class="text-neutral-400">Signed top minus base</dt><dd class="mt-2 font-mono" data-volume-signed>{{ quantity(report()?.signedTopMinusBaseM3) }}</dd></div> | |
| 107 | <div><dt class="text-neutral-400">Full boundary signed</dt><dd class="mt-2 font-mono" data-volume-full>{{ quantity(report()?.fullBoundarySignedM3) }}</dd></div> | |
| 104 | <div><dt class="text-neutral-400">Above base</dt><dd class="mt-2 font-mono" data-volume-above>{{ quantity(volumeReport()?.aboveBaseM3) }}</dd></div> | |
| 105 | <div><dt class="text-neutral-400">Below base</dt><dd class="mt-2 font-mono" data-volume-below>{{ quantity(volumeReport()?.belowBaseM3) }}</dd></div> | |
| 106 | <div><dt class="text-neutral-400">Signed top minus base</dt><dd class="mt-2 font-mono" data-volume-signed>{{ quantity(volumeReport()?.signedTopMinusBaseM3) }}</dd></div> | |
| 107 | <div><dt class="text-neutral-400">Full boundary signed</dt><dd class="mt-2 font-mono" data-volume-full>{{ quantity(volumeReport()?.fullBoundarySignedM3) }}</dd></div> | |
| 108 | 108 | </dl> |
| 109 | 109 | <p class="mt-4 leading-relaxed text-neutral-400 text-xs">Above and below are positive magnitudes. Signed volume equals above minus below. Partial coverage does not establish the volume of the whole boundary.</p> |
| 110 | 110 | <h3 class="mt-7 font-semibold text-sm">Boundary and domain</h3> |
| 111 | 111 | <dl class="grid gap-4 grid-cols-2 mt-4 text-sm"> |
| 112 | <div><dt class="text-neutral-400">Boundary area</dt><dd class="mt-1">{{ quantity(report()?.boundaryAreaM2, 'm²') }}</dd></div> | |
| 113 | <div><dt class="text-neutral-400">Covered area</dt><dd class="mt-1">{{ quantity(report()?.coveredAreaM2, 'm²') }}</dd></div> | |
| 114 | <div><dt class="text-neutral-400">Uncovered area</dt><dd class="mt-1" data-volume-gap>{{ quantity(report()?.uncoveredAreaM2, 'm²') }}</dd></div> | |
| 115 | <div><dt class="text-neutral-400">Coverage</dt><dd class="mt-1">{{ ((report()?.coverageFraction ?? 0) * 100).toFixed(2) }}%</dd></div> | |
| 112 | <div><dt class="text-neutral-400">Boundary area</dt><dd class="mt-1">{{ quantity(volumeReport()?.boundaryAreaM2, 'm²') }}</dd></div> | |
| 113 | <div><dt class="text-neutral-400">Covered area</dt><dd class="mt-1">{{ quantity(volumeReport()?.coveredAreaM2, 'm²') }}</dd></div> | |
| 114 | <div><dt class="text-neutral-400">Uncovered area</dt><dd class="mt-1" data-volume-gap>{{ quantity(volumeReport()?.uncoveredAreaM2, 'm²') }}</dd></div> | |
| 115 | <div><dt class="text-neutral-400">Coverage</dt><dd class="mt-1">{{ ((volumeReport()?.coverageFraction ?? 0) * 100).toFixed(2) }}%</dd></div> | |
| 116 | 116 | <div><dt class="text-neutral-400">Boundary vertices / holes</dt><dd class="mt-1">{{ source()?.boundary.outer.length }} / {{ source()?.boundary.holes.length }}</dd></div> |
| 117 | <div><dt class="text-neutral-400">Excluded nodata cells</dt><dd class="mt-1">{{ report()?.excludedGridCells.length }}</dd></div> | |
| 117 | <div><dt class="text-neutral-400">Excluded nodata cells</dt><dd class="mt-1">{{ volumeReport()?.excludedGridCells.length }}</dd></div> | |
| 118 | 118 | </dl> |
| 119 | 119 | <h3 class="mt-7 font-semibold text-sm">Explicit base plane</h3> |
| 120 | 120 | <p class="mt-3 leading-relaxed text-neutral-400 text-sm">{{ source()?.base.assumption }}</p> |
| @@ -136,20 +136,20 @@ function execute() { clear(); busy.set(true); computation.start() } | ||
| 136 | 136 | <p class="mt-3 text-neutral-400 text-xs">Colour intensity spans 0 to {{ colorScale().toFixed(3) }} m in absolute mean height difference. Exact volume integration splits every contribution at the base crossing. Holes are excluded from the domain.</p> |
| 137 | 137 | <h3 class="mt-7 font-semibold text-sm">Uncertainty and reference</h3> |
| 138 | 138 | <dl class="grid gap-5 grid-cols-2 mt-4 text-sm"> |
| 139 | <div><dt class="text-neutral-400">Conditional vertical bound</dt><dd class="mt-1 font-mono" data-volume-conditional>{{ quantity(report()?.uncertainty.conditionalVerticalBoundM3) }}</dd></div> | |
| 139 | <div><dt class="text-neutral-400">Conditional vertical bound</dt><dd class="mt-1 font-mono" data-volume-conditional>{{ quantity(volumeReport()?.uncertainty.conditionalVerticalBoundM3) }}</dd></div> | |
| 140 | 140 | <div><dt class="text-neutral-400">Overall accuracy bound</dt><dd class="mt-1 font-mono">unavailable</dd></div> |
| 141 | <div><dt class="text-neutral-400">Declared horizontal bound</dt><dd class="mt-1">{{ quantity(report()?.uncertainty.horizontalBoundM, 'm') }}</dd></div> | |
| 142 | <div><dt class="text-neutral-400">Reference comparison</dt><dd class="mt-1 break-words" data-volume-reference>{{ report()?.comparison.status }}</dd></div> | |
| 141 | <div><dt class="text-neutral-400">Declared horizontal bound</dt><dd class="mt-1">{{ quantity(volumeReport()?.uncertainty.horizontalBoundM, 'm') }}</dd></div> | |
| 142 | <div><dt class="text-neutral-400">Reference comparison</dt><dd class="mt-1 break-words" data-volume-reference>{{ volumeReport()?.comparison.status }}</dd></div> | |
| 143 | 143 | </dl> |
| 144 | 144 | <p class="mt-4 leading-relaxed text-neutral-400 text-xs">A conditional bound adds the declared top and base vertical errors over covered area. It excludes boundary, horizontal, classification and field calibration errors. Unknown inputs remain unavailable.</p> |
| 145 | 145 | <div :if="source()?.reference" class="mt-4 pl-4 border-l border-neutral-600"> |
| 146 | 146 | <p class="break-words text-sm">{{ source()?.reference?.id }} / {{ source()?.reference?.independence }}</p> |
| 147 | 147 | <p class="mt-2 break-words text-neutral-400 text-xs">{{ source()?.reference?.method }} / {{ source()?.reference?.surveyedAt }}</p> |
| 148 | 148 | <p class="mt-2 text-neutral-400 text-xs">Reference evidence: {{ source()?.reference?.evidence }}. Operator declarations are not authenticated survey validation.</p> |
| 149 | <p class="mt-3 text-sm">Computed minus reference: {{ quantity(report()?.comparison.signedDifferenceM3) }}</p> | |
| 150 | <p class="mt-2 text-neutral-400 text-xs">Relative signed difference: {{ report()?.comparison.relativeSignedDifference === null ? 'unavailable' : ((report()?.comparison.relativeSignedDifference ?? 0) * 100).toFixed(2) + '%' }}</p> | |
| 151 | <p class="mt-2 text-neutral-400 text-xs">Combined declared bound: {{ quantity(report()?.comparison.combinedDeclaredBoundM3) }}</p> | |
| 152 | <p class="mt-2 text-neutral-400 text-xs">Within combined declared bound: {{ report()?.comparison.withinCombinedDeclaredBound === null ? 'unavailable' : report()?.comparison.withinCombinedDeclaredBound ? 'yes' : 'no' }}. This comparison does not establish overall accuracy.</p> | |
| 149 | <p class="mt-3 text-sm">Computed minus reference: {{ quantity(volumeReport()?.comparison.signedDifferenceM3) }}</p> | |
| 150 | <p class="mt-2 text-neutral-400 text-xs">Relative signed difference: {{ volumeReport()?.comparison.relativeSignedDifference === null ? 'unavailable' : ((volumeReport()?.comparison.relativeSignedDifference ?? 0) * 100).toFixed(2) + '%' }}</p> | |
| 151 | <p class="mt-2 text-neutral-400 text-xs">Combined declared bound: {{ quantity(volumeReport()?.comparison.combinedDeclaredBoundM3) }}</p> | |
| 152 | <p class="mt-2 text-neutral-400 text-xs">Within combined declared bound: {{ volumeReport()?.comparison.withinCombinedDeclaredBound === null ? 'unavailable' : volumeReport()?.comparison.withinCombinedDeclaredBound ? 'yes' : 'no' }}. This comparison does not establish overall accuracy.</p> | |
| 153 | 153 | </div> |
| 154 | 154 | </div> |
| 155 | 155 | </div> |
| @@ -158,9 +158,9 @@ function execute() { clear(); busy.set(true); computation.start() } | ||
| 158 | 158 | <dl class="grid gap-5 sm:grid-cols-2 mt-5 text-sm"> |
| 159 | 159 | <div class="min-w-0"><dt class="text-neutral-400">Source identity / geometry</dt><dd class="mt-1 break-words">{{ source()?.surface.metadata.id }} / {{ source()?.surface.geometry.kind }}</dd></div> |
| 160 | 160 | <div class="min-w-0"><dt class="text-neutral-400">Evidence / source kind</dt><dd class="mt-1 break-words">{{ source()?.surface.metadata.provenance.evidence }} / {{ source()?.surface.metadata.provenance.sourceKind }}</dd></div> |
| 161 | <div class="min-w-0"><dt class="text-neutral-400">Horizontal reference</dt><dd class="mt-1 break-words">{{ report()?.reference.horizontalCrs.id }} / {{ report()?.reference.horizontalCrs.definition }}</dd></div> | |
| 162 | <div class="min-w-0"><dt class="text-neutral-400">Vertical datum</dt><dd class="mt-1 break-words">{{ report()?.reference.verticalDatum.id }} / {{ report()?.reference.verticalDatum.definition }}</dd></div> | |
| 163 | <div><dt class="text-neutral-400">Coordinate epoch</dt><dd class="mt-1">{{ report()?.reference.horizontalCrs.coordinateEpoch ?? 'not declared' }}</dd></div> | |
| 161 | <div class="min-w-0"><dt class="text-neutral-400">Horizontal reference</dt><dd class="mt-1 break-words">{{ volumeReport()?.reference.horizontalCrs.id }} / {{ volumeReport()?.reference.horizontalCrs.definition }}</dd></div> | |
| 162 | <div class="min-w-0"><dt class="text-neutral-400">Vertical datum</dt><dd class="mt-1 break-words">{{ volumeReport()?.reference.verticalDatum.id }} / {{ volumeReport()?.reference.verticalDatum.definition }}</dd></div> | |
| 163 | <div><dt class="text-neutral-400">Coordinate epoch</dt><dd class="mt-1">{{ volumeReport()?.reference.horizontalCrs.coordinateEpoch ?? 'not declared' }}</dd></div> | |
| 164 | 164 | <div><dt class="text-neutral-400">Axes and units</dt><dd class="mt-1">Easting / northing / elevation up, metres</dd></div> |
| 165 | 165 | <div><dt class="text-neutral-400">Acquisition / meshing status</dt><dd class="mt-1">{{ source()?.surface.metadata.provenance.acquisition.status }} / {{ source()?.surface.metadata.provenance.reconstructionOrMeshing.status }}</dd></div> |
| 166 | 166 | <div><dt class="text-neutral-400">Boresight / time calibration</dt><dd class="mt-1">{{ source()?.surface.metadata.provenance.boresightCalibration.status }} / {{ source()?.surface.metadata.provenance.timeCalibration.status }}</dd></div> |
| @@ -175,10 +175,10 @@ function execute() { clear(); busy.set(true); computation.start() } | ||
| 175 | 175 | <div class="overflow-x-auto mt-5 max-w-full border border-neutral-700 rounded" tabindex="0" role="region" aria-label="Scrollable contribution audit table"> |
| 176 | 176 | <table class="w-full text-left text-xs whitespace-nowrap"><caption class="sr-only">Clipped surface contributions, unweighted area and volumes, with signed boundary weights</caption><thead class="text-neutral-300 bg-neutral-900"><tr><th scope="col" class="p-3">Source / part</th><th scope="col" class="p-3">Weight</th><th scope="col" class="p-3">Area m²</th><th scope="col" class="p-3">Above m³</th><th scope="col" class="p-3">Below m³</th><th scope="col" class="p-3">Signed m³</th></tr></thead><tbody><tr :for="piece in pieces()" class="border-neutral-800 border-t"><th scope="row" class="p-3 font-normal"><span class="block">{{ piece.sourceId }}</span><span class="block mt-1 text-neutral-400">{{ piece.boundaryPart }}</span></th><td class="p-3 font-mono">{{ piece.weight }}</td><td class="p-3 font-mono">{{ piece.areaM2.toFixed(3) }}</td><td class="p-3 font-mono">{{ piece.aboveBaseM3.toFixed(3) }}</td><td class="p-3 font-mono">{{ piece.belowBaseM3.toFixed(3) }}</td><td class="p-3 font-mono">{{ piece.signedTopMinusBaseM3.toFixed(3) }}</td></tr></tbody></table> |
| 177 | 177 | </div> |
| 178 | <p :if="!report()?.integrationPieces.length" class="mt-4 text-neutral-400 text-sm">No covered surface contributions intersect this boundary.</p> | |
| 179 | <div class="flex flex-wrap gap-3 items-center mt-4 text-sm"><button type="button" @click="page.update(value => Math.max(0, value - 1))" :disabled="page() === 0" class="px-3 py-2 border border-neutral-600 rounded focus-visible:outline-emerald-200 disabled:opacity-40">Previous pieces</button><span role="status">Page {{ page() + 1 }} / {{ pages() }}. {{ report()?.integrationPieces.length }} pieces.</span><button type="button" @click="page.update(value => Math.min(pages() - 1, value + 1))" :disabled="page() >= pages() - 1" class="px-3 py-2 border border-neutral-600 rounded focus-visible:outline-emerald-200 disabled:opacity-40">Next pieces</button></div> | |
| 178 | <p :if="!volumeReport()?.integrationPieces.length" class="mt-4 text-neutral-400 text-sm">No covered surface contributions intersect this boundary.</p> | |
| 179 | <div class="flex flex-wrap gap-3 items-center mt-4 text-sm"><button type="button" @click="page.update(value => Math.max(0, value - 1))" :disabled="page() === 0" class="px-3 py-2 border border-neutral-600 rounded focus-visible:outline-emerald-200 disabled:opacity-40">Previous pieces</button><span role="status">Page {{ page() + 1 }} / {{ pages() }}. {{ volumeReport()?.integrationPieces.length }} pieces.</span><button type="button" @click="page.update(value => Math.min(pages() - 1, value + 1))" :disabled="page() >= pages() - 1" class="px-3 py-2 border border-neutral-600 rounded focus-visible:outline-emerald-200 disabled:opacity-40">Next pieces</button></div> | |
| 180 | 180 | </section> |
| 181 | <details class="mt-8 pt-5 border-neutral-700 border-t"><summary class="text-sm focus-visible:outline-emerald-200 cursor-pointer">Warnings, uncertainty assumptions and evidence limits</summary><ul class="mt-4 space-y-3 text-neutral-400 text-sm"><li :for="warning in report()?.warnings ?? []" class="break-words text-amber-200">{{ warning }}</li><li :for="assumption in report()?.uncertainty.assumptions ?? []" class="break-words">{{ assumption }}</li><li :for="limitation in report()?.limitations ?? []" class="break-words">{{ limitation }}</li></ul></details> | |
| 181 | <details class="mt-8 pt-5 border-neutral-700 border-t"><summary class="text-sm focus-visible:outline-emerald-200 cursor-pointer">Warnings, uncertainty assumptions and evidence limits</summary><ul class="mt-4 space-y-3 text-neutral-400 text-sm"><li :for="warning in volumeReport()?.warnings ?? []" class="break-words text-amber-200">{{ warning }}</li><li :for="assumption in volumeReport()?.uncertainty.assumptions ?? []" class="break-words">{{ assumption }}</li><li :for="limitation in volumeReport()?.limitations ?? []" class="break-words">{{ limitation }}</li></ul></details> | |
| 182 | 182 | <p class="mt-6 text-neutral-400 text-xs">For known-surface import and auditable dataset export, use the native Stacks volume commands. Binary LiDAR, raw-point meshing and surveyed field validation remain separate work.</p> |
| 183 | 183 | <a href="https://github.com/stacksjs/drone/issues/19" class="inline-block mt-4 text-emerald-200 text-sm underline underline-offset-4">Volume processing scope and remaining evidence</a> |
| 184 | 184 | </div> |
| @@ -0,0 +1,51 @@ | ||
| 1 | import { expect, test } from 'bun:test' | |
| 2 | import { mkdtemp, rm } from 'node:fs/promises' | |
| 3 | import { tmpdir } from 'node:os' | |
| 4 | import { join, resolve } from 'node:path' | |
| 5 | import { log, registerTransport } from '@stacksjs/logging' | |
| 6 | import { report } from '@stacksjs/logging/runtime' | |
| 7 | import type { LogRecord } from '@stacksjs/logging' | |
| 8 | import { renderTemplate } from '@stacksjs/stx' | |
| 9 | ||
| 10 | for (const [page, heading] of [ | |
| 11 | ['diagnostics', 'Inspect what happened.'], | |
| 12 | ['volumes', 'Stockpile volume analysis'], | |
| 13 | ['application', 'Prescribed application'], | |
| 14 | ] as const) { | |
| 15 | test(`${page} server render keeps client analysis out of the native error reporter`, async () => { | |
| 16 | const root = resolve(import.meta.dir, '../..') | |
| 17 | const directory = await mkdtemp(join(tmpdir(), 'drone-workspace-render-')) | |
| 18 | const previous = Object.getOwnPropertyDescriptor(globalThis, 'report') | |
| 19 | const errors: LogRecord[] = [] | |
| 20 | const detach = registerTransport({ name: 'workspace-render-observer', level: 'error', log: record => { errors.push(record) } }) | |
| 21 | try { | |
| 22 | // Use the actual production global, with an observing transport rather than a mock reporter. | |
| 23 | Object.defineProperty(globalThis, 'report', { configurable: true, writable: true, value: report }) | |
| 24 | report(new Error('workspace-render-observer-control')) | |
| 25 | await log.flush() | |
| 26 | expect(errors).toHaveLength(1) | |
| 27 | errors.length = 0 | |
| 28 | const html = await renderTemplate(join(root, 'resources/views', `${page}.stx`), { | |
| 29 | injectCSS: true, | |
| 30 | options: { | |
| 31 | root: join(root, 'resources'), | |
| 32 | componentsDir: join(root, 'resources/components'), | |
| 33 | composablesDir: join(directory, 'composables'), | |
| 34 | storesDir: join(directory, 'stores'), | |
| 35 | stateDir: join(directory, 'stx'), | |
| 36 | envFile: false, | |
| 37 | strict: { enabled: true, bundlerFallback: 'error' }, | |
| 38 | }, | |
| 39 | }) | |
| 40 | await log.flush() | |
| 41 | expect(html).toContain(heading) | |
| 42 | expect(errors).toEqual([]) | |
| 43 | } | |
| 44 | finally { | |
| 45 | detach() | |
| 46 | if (previous) Object.defineProperty(globalThis, 'report', previous) | |
| 47 | else Reflect.deleteProperty(globalThis, 'report') | |
| 48 | await rm(directory, { recursive: true, force: true }) | |
| 49 | } | |
| 50 | }, 30_000) | |
| 51 | } | |