ReviewOS

also looking at this

stacks/drone

feat: import LAS surfaces and consume maintained source releases

#107
Merged chris wants to merge codex/las-stockpile-processing into main
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.

resources/components/ApplicationWorkspace.stxmodified+9-9
Changes to resources/components/ApplicationWorkspace.stx
@@ -10,7 +10,7 @@ const scenario = state<ApplicationScenario>('nominal')
1010const mode = state('test')
1111const authorization = state('none')
1212const run = state<ApplicationRun | null>(null)
13const report = state<ApplicationReport | null>(null)
13const applicationReport = state<ApplicationReport | null>(null)
1414const busy = state(false)
1515const error = state('')
1616const epoch = state(600)
@@ -22,9 +22,9 @@ const validEpoch = derived(() => !run() || Number.isSafeInteger(Number(epoch()))
2222const input = derived(() => validEpoch() ? run()?.inputs.find(row => row.atMs === Number(epoch())) ?? null : null)
2323const decision = derived(() => validEpoch() ? run()?.decisions.find(row => row.atMs === Number(epoch())) ?? null : null)
2424const probe = derived(() => validEpoch() ? run()?.probes.find(row => row.atMs === Number(epoch())) ?? null : null)
25const cell = derived(() => Number.isInteger(Number(cellIndex())) && Number(cellIndex()) >= 0 ? report()?.cells[Number(cellIndex())] ?? null : null)
25const cell = derived(() => Number.isInteger(Number(cellIndex())) && Number(cellIndex()) >= 0 ? applicationReport()?.cells[Number(cellIndex())] ?? null : null)
2626const doseMaximum = derived(() => Math.max(1, ...config().prescription.map(zone => zone.ratePerHa)))
27const preview = derived(() => report() ? applicationReportPreview(report()!, doseMaximum()) : '')
27const preview = derived(() => applicationReport() ? applicationReportPreview(applicationReport()!, doseMaximum()) : '')
2828const rateMaximum = derived(() => Math.max(config().calibration.maxRatePerSecond, ...(run()?.decisions.map(row => Math.max(row.targetPerSecond, row.measuredPerSecond ?? 0)) ?? [])))
2929const ratePlot = derived(() => applicationRatePlot(run()?.decisions ?? [], rateMaximum()))
3030function 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
3333 try {
3434 const result = simulateApplication({ config: exampleApplicationConfig(implement()), scenario: scenario(), testMode: mode() === 'test', authorizeImplement: authorization() === 'grant' })
3535 const audited = processApplication(result)
36 run.set(result); report.set(audited)
36 run.set(result); applicationReport.set(audited)
3737 void nextTick(() => { if (run() === result) epoch.set(result.final.atMs) })
3838 }
3939 catch (failure) { error.set(failure instanceof Error ? failure.message : 'Application simulation failed') }
4040 finally { busy.set(false) }
4141}, 32)
4242pending.stop()
43function clear() { pending.stop(); run.set(null); report.set(null); error.set(''); busy.set(false); epoch.set(600); cellIndex.set(0) }
43function clear() { pending.stop(); run.set(null); applicationReport.set(null); error.set(''); busy.set(false); epoch.set(600); cellIndex.set(0) }
4444effect(() => { implement(); scenario(); mode(); authorization(); clear() })
4545onDestroy(() => pending.stop())
4646function execute() { clear(); busy.set(true); pending.start() }
@@ -61,10 +61,10 @@ function execute() { clear(); busy.set(true); pending.start() }
6161 <p :if="busy()" role="status" class="mt-5 text-emerald-200 text-sm">Running sensor, estimator, mission, metering and exact coverage processing.</p>
6262 <p :if="error()" role="alert" class="mt-5 p-4 break-words text-red-200 border border-red-400 rounded">{{ error() }}</p>
6363 <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">
6565 <section aria-labelledby="application-result" class="py-5 border-neutral-700 border-t">
6666 <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>
6868 <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>
6969 </section>
7070 <section aria-labelledby="application-rate" class="min-w-0">
@@ -78,9 +78,9 @@ function execute() { clear(); busy.set(true); pending.start() }
7878 </section>
7979 <section aria-labelledby="application-dose" class="grid gap-6 md:grid-cols-2 pt-7 border-neutral-700 border-t">
8080 <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>
8282 </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>
8484 </div>
8585 <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>
8686 </section>
resources/components/DiagnosticsReplay.stxmodified+2-2
Changes to resources/components/DiagnosticsReplay.stx
@@ -10,7 +10,7 @@ const busy = state(false)
1010const error = state('')
1111const offset = state(0)
1212const replay = derived(() => current() ? createDiagnosticsReplay(current()!).advanceTo(offset()) : null)
13const report = derived(() => current() ? diagnoseDiagnostics(current()!) : null)
13const diagnosticReport = derived(() => current() ? diagnoseDiagnostics(current()!) : null)
1414const duration = derived(() => current()?.footer?.atMs ?? current()?.events.at(-1)?.atMs ?? 0)
1515const rows = derived(() => (current()?.events ?? []).filter(row => row.atMs <= offset()).slice(-24))
1616let revision = 0
@@ -63,7 +63,7 @@ onDestroy(() => { revision++ })
6363 <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>
6464 <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>
6565 </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>
6767 <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>
6868 </aside>
6969 </div>
resources/components/VolumesWorkspace.stxmodified+29-29
Changes to resources/components/VolumesWorkspace.stx
@@ -10,10 +10,10 @@ const result = state<VolumeReport | null>(null) renamed throughout
1010const busy = state(false)
1111const error = state('')
1212const page = state(0)
13const report = derived(() => resultScenario() === scenario() ? result() : null)
14const source = derived(() => report() ? request() : null)
15const pages = derived(() => Math.max(1, Math.ceil((report()?.integrationPieces.length ?? 0) / 12)))
16const pieces = derived(() => report()?.integrationPieces.slice(page() * 12, page() * 12 + 12) ?? [])
13const volumeReport = derived(() => resultScenario() === scenario() ? result() : null)
14const source = derived(() => volumeReport() ? request() : null)
15const pages = derived(() => Math.max(1, Math.ceil((volumeReport()?.integrationPieces.length ?? 0) / 12)))
16const pieces = derived(() => volumeReport()?.integrationPieces.slice(page() * 12, page() * 12 + 12) ?? [])
1717const extent = derived(() => {
1818 const points = source()?.boundary.outer ?? [[0, 0], [1, 1]]
1919 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() }
9494 </div>
9595 <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>
9696 <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">
9999 <div class="grid gap-8 lg:grid-cols-[1fr_1.3fr]">
100100 <aside class="min-w-0">
101101 <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>
103103 <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>
108108 </dl>
109109 <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>
110110 <h3 class="mt-7 font-semibold text-sm">Boundary and domain</h3>
111111 <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>
116116 <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>
118118 </dl>
119119 <h3 class="mt-7 font-semibold text-sm">Explicit base plane</h3>
120120 <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() }
136136 <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>
137137 <h3 class="mt-7 font-semibold text-sm">Uncertainty and reference</h3>
138138 <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>
140140 <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>
143143 </dl>
144144 <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>
145145 <div :if="source()?.reference" class="mt-4 pl-4 border-l border-neutral-600">
146146 <p class="break-words text-sm">{{ source()?.reference?.id }} / {{ source()?.reference?.independence }}</p>
147147 <p class="mt-2 break-words text-neutral-400 text-xs">{{ source()?.reference?.method }} / {{ source()?.reference?.surveyedAt }}</p>
148148 <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>
153153 </div>
154154 </div>
155155 </div>
@@ -158,9 +158,9 @@ function execute() { clear(); busy.set(true); computation.start() }
158158 <dl class="grid gap-5 sm:grid-cols-2 mt-5 text-sm">
159159 <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>
160160 <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>
164164 <div><dt class="text-neutral-400">Axes and units</dt><dd class="mt-1">Easting / northing / elevation up, metres</dd></div>
165165 <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>
166166 <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() }
175175 <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">
176176 <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>
177177 </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>
180180 </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>
182182 <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>
183183 <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>
184184 </div>
tests/tsconfig.runtime.jsonmodified+1-0
Changes to tests/tsconfig.runtime.json
@@ -1,6 +1,7 @@
11{
22 "extends": "../tsconfig.json",
33 "include": [
4 "./unit/workspace-server-render.test.ts",
45 "../storage/framework/types/env.d.ts",
56 "../bin/drone.ts",
67 "../resources/functions/drone/drivers/**/*.ts",
tests/unit/workspace-server-render.test.tsadded+51-0
Changes to tests/unit/workspace-server-render.test.ts
@@ -0,0 +1,51 @@
1import { expect, test } from 'bun:test'
2import { mkdtemp, rm } from 'node:fs/promises'
3import { tmpdir } from 'node:os'
4import { join, resolve } from 'node:path'
5import { log, registerTransport } from '@stacksjs/logging'
6import { report } from '@stacksjs/logging/runtime'
7import type { LogRecord } from '@stacksjs/logging'
8import { renderTemplate } from '@stacksjs/stx'
9
10for (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}