feat: establish reproducible FreeCAD web compatibility baseline
This commit is contained in:
140
src/chromeProductionDocumentHarness.ts
Normal file
140
src/chromeProductionDocumentHarness.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
import { createMockFacade } from './facade/mockFacade'
|
||||
import { createDraftDocument } from './facade/draft'
|
||||
import { createPlot } from './facade/plot'
|
||||
import { createProductionDocument } from './facade/productionDocument'
|
||||
import { createSpreadsheet } from './facade/spreadsheet'
|
||||
import { createTechDrawPage } from './facade/techDraw'
|
||||
import type { TopoRefValue } from './facade/types'
|
||||
|
||||
type ProductionReport = {
|
||||
schemaVersion: 1
|
||||
status: 'pass' | 'failed'
|
||||
browserId: 'chrome'
|
||||
crossOriginIsolated: boolean
|
||||
closure?: { complete: boolean; artifacts: number; missingKinds: number; missingDependencies: number; cycles: number; documentVersion: number; reopenedComplete: boolean; artifactVersions: number[] }
|
||||
workflow?: {
|
||||
created: { spreadsheetCells: number; draftObjects: number; techdrawViews: number; plotSeries: number }
|
||||
edited: { spreadsheetValue: number; draftStartX: number; techdrawDimension: number; plotValue: number }
|
||||
exports: { spreadsheetCsvBytes: number; draftJsonBytes: number; techdrawSvgBytes: number; techdrawPdfBytes: number; techdrawPdfValid: boolean; plotSvgBytes: number; plotCsvBytes: number }
|
||||
}
|
||||
persistence?: { mode: string; byteLength: number; hash: string; roundTrip: boolean; released: boolean }
|
||||
opfs?: { markerSuite: string; artifacts: number; markerRemoved: boolean }
|
||||
afterRelease?: { shapeCount: number; kernelReferenceCount: number }
|
||||
error?: string
|
||||
}
|
||||
|
||||
const sha256 = async (bytes: Uint8Array) => [...new Uint8Array(await crypto.subtle.digest('SHA-256', Uint8Array.from(bytes).buffer))].map((value) => value.toString(16).padStart(2, '0')).join('')
|
||||
const ref = (id: string, version: number): TopoRefValue => ({ schemaVersion: 1, objectId: 'draft-line', kind: 'vertex', persistentId: id, topologyVersion: version, generation: version, status: 'stable', signature: id })
|
||||
|
||||
const run = async (): Promise<ProductionReport> => {
|
||||
const facade = createMockFacade()
|
||||
const report: ProductionReport = { schemaVersion: 1, status: 'failed', browserId: 'chrome', crossOriginIsolated: globalThis.crossOriginIsolated === true }
|
||||
try {
|
||||
if (!report.crossOriginIsolated || !navigator.storage?.getDirectory) throw new Error('Chrome production document evidence requires isolated OPFS.')
|
||||
await facade.project.list()
|
||||
const document = createProductionDocument('doc', 'Production workflow')
|
||||
|
||||
const sheet = createSpreadsheet('sheet', 'Parameters')
|
||||
sheet.setCell('A1', 2, { alias: 'Input' })
|
||||
sheet.setCell('A2', '=Input * 2', { alias: 'Output' })
|
||||
const initialSheet = sheet.evaluate()
|
||||
document.add({ id: 'sheet', kind: 'Spreadsheet', dependencies: [], payload: { snapshot: sheet.snapshot(), evaluation: initialSheet } })
|
||||
|
||||
const draft = createDraftDocument('draft', 'Profile')
|
||||
draft.createLine('profile', { x: 0, y: 0 }, { x: 4, y: 0 })
|
||||
document.add({ id: 'draft', kind: 'Draft', dependencies: [], payload: { snapshot: draft.snapshot() } })
|
||||
|
||||
const page = createTechDrawPage('page', 'Profile drawing')
|
||||
page.addSource({ id: 'draft-line', revision: 1, points: [{ id: 'start', ref: ref('Vertex1', 1), point: { x: 0, y: 0, z: 0 } }, { id: 'end', ref: ref('Vertex2', 1), point: { x: 4, y: 0, z: 0 } }], edges: [{ id: 'edge', start: 'start', end: 'end' }] })
|
||||
page.addView({ id: 'front', sourceId: 'draft-line', direction: { x: 0, y: 0, z: 1 }, position: { x: 80, y: 80 }, scale: 8 })
|
||||
page.addDimension({ id: 'length', sourceId: 'draft-line', first: ref('Vertex1', 1), second: ref('Vertex2', 1), label: 'Profile length' })
|
||||
page.addAnnotation({ id: 'note', text: 'Production profile', position: { x: 20, y: 20 } })
|
||||
page.addGeometricTolerance({ id: 'straightness', sourceId: 'draft-line', reference: ref('Vertex1', 1), characteristic: 'flatness', value: 0.05 })
|
||||
document.add({ id: 'page', kind: 'TechDraw', dependencies: ['draft'], payload: { snapshot: page.snapshot() } })
|
||||
|
||||
const plot = createPlot('plot', 'Parameter history')
|
||||
plot.setAxes({ x: { label: 'Revision', scale: 'linear', minimum: 2, maximum: 3 }, y: { label: 'Output', scale: 'linear', minimum: 0, maximum: 8 } })
|
||||
plot.setSeries({ id: 'output', label: 'Output', points: [{ x: 2, y: 4 }, { x: 3, y: 4 }], style: { color: '#007f86', lineWidth: 2 } })
|
||||
plot.bindSeries('output', { sourceId: 'sheet', xRange: 'A1:A2', yRange: 'A1:A2' })
|
||||
document.add({ id: 'plot', kind: 'Plot', dependencies: ['sheet'], payload: { snapshot: plot.snapshot() } })
|
||||
|
||||
sheet.setCell('A1', 3, { alias: 'Input' })
|
||||
const editedSheet = sheet.evaluate()
|
||||
const spreadsheetValue = editedSheet.cells.A2.value?.value ?? Number.NaN
|
||||
const movedLine = draft.move('profile', { x: 1, y: 2 })
|
||||
page.updateSource({ id: 'draft-line', revision: 2, points: [{ id: 'start', ref: ref('Vertex1', 2), point: { x: 1, y: 2, z: 0 } }, { id: 'end', ref: ref('Vertex2', 2), point: { x: spreadsheetValue, y: 2, z: 0 } }], edges: [{ id: 'edge', start: 'start', end: 'end' }] })
|
||||
plot.updateBoundSeries('output', [{ x: 2, y: 4 }, { x: 3, y: spreadsheetValue }])
|
||||
|
||||
const spreadsheetCsv = sheet.exportCsv()
|
||||
const draftJson = JSON.stringify(draft.snapshot())
|
||||
const techdrawSvg = page.exportSvg()
|
||||
const techdrawPdf = page.exportPdf()
|
||||
const plotSvg = plot.exportSvg()
|
||||
const plotCsv = plot.exportCsv()
|
||||
const techdrawPdfText = new TextDecoder().decode(techdrawPdf)
|
||||
|
||||
document.update('sheet', { payload: { snapshot: sheet.snapshot(), evaluation: editedSheet, exportBytes: new TextEncoder().encode(spreadsheetCsv).byteLength } })
|
||||
document.update('draft', { payload: { snapshot: draft.snapshot(), exportBytes: new TextEncoder().encode(draftJson).byteLength } })
|
||||
document.update('page', { payload: { snapshot: page.snapshot(), svgBytes: new TextEncoder().encode(techdrawSvg).byteLength, pdfBytes: techdrawPdf.byteLength } })
|
||||
document.update('plot', { payload: { snapshot: plot.snapshot(), svgBytes: new TextEncoder().encode(plotSvg).byteLength, csvBytes: new TextEncoder().encode(plotCsv).byteLength } })
|
||||
|
||||
const closure = document.closure()
|
||||
const serialized = document.save()
|
||||
const reopened = createProductionDocument('reopened', 'Reopened production workflow')
|
||||
const reopenedSnapshot = reopened.load(serialized)
|
||||
const reopenedClosure = reopened.closure()
|
||||
const payload = new TextEncoder().encode(serialized)
|
||||
const stored = await facade.project.resource.put(payload, 'application/vnd.bitbybit.production-document+json')
|
||||
const loaded = await facade.project.resource.get(stored.hash)
|
||||
const roundTrip = loaded !== null && await sha256(loaded) === await sha256(payload)
|
||||
await facade.project.resource.release(stored.hash)
|
||||
const released = await facade.project.resource.get(stored.hash) === null
|
||||
|
||||
const directory = await navigator.storage.getDirectory()
|
||||
const markerName = 'production-document-chrome-marker.json'
|
||||
const marker = await directory.getFileHandle(markerName, { create: true })
|
||||
const writable = await marker.createWritable()
|
||||
await writable.write(JSON.stringify({ suite: 'DOC-CLOSURE', artifacts: closure.artifacts }))
|
||||
await writable.close()
|
||||
const markerPayload = JSON.parse(await (await marker.getFile()).text()) as { suite: string; artifacts: number }
|
||||
await directory.removeEntry(markerName)
|
||||
let markerRemoved = false
|
||||
try { await directory.getFileHandle(markerName) } catch (error) { markerRemoved = error instanceof DOMException && error.name === 'NotFoundError' }
|
||||
|
||||
const pageSnapshot = page.snapshot()
|
||||
const plotSnapshot = plot.snapshot()
|
||||
report.closure = { complete: closure.complete, artifacts: closure.artifacts, missingKinds: closure.missingKinds.length, missingDependencies: closure.missingDependencies.length, cycles: closure.cycles.length, documentVersion: document.snapshot().version, reopenedComplete: reopenedClosure.complete && reopenedSnapshot.artifacts.length === 4, artifactVersions: document.snapshot().artifacts.map((artifact) => artifact.version).sort() }
|
||||
report.workflow = {
|
||||
created: { spreadsheetCells: 2, draftObjects: 1, techdrawViews: 1, plotSeries: 1 },
|
||||
edited: { spreadsheetValue, draftStartX: movedLine.kind === 'line' ? movedLine.start.x : Number.NaN, techdrawDimension: pageSnapshot.dimensions[0]?.value ?? Number.NaN, plotValue: plotSnapshot.series[0]?.points.at(-1)?.y ?? Number.NaN },
|
||||
exports: { spreadsheetCsvBytes: new TextEncoder().encode(spreadsheetCsv).byteLength, draftJsonBytes: new TextEncoder().encode(draftJson).byteLength, techdrawSvgBytes: new TextEncoder().encode(techdrawSvg).byteLength, techdrawPdfBytes: techdrawPdf.byteLength, techdrawPdfValid: techdrawPdfText.startsWith('%PDF-1.4') && techdrawPdfText.endsWith('%%EOF\n'), plotSvgBytes: new TextEncoder().encode(plotSvg).byteLength, plotCsvBytes: new TextEncoder().encode(plotCsv).byteLength },
|
||||
}
|
||||
report.persistence = { mode: facade.project.capabilities().mode, byteLength: stored.byteLength, hash: stored.hash, roundTrip, released }
|
||||
report.opfs = { markerSuite: markerPayload.suite, artifacts: markerPayload.artifacts, markerRemoved }
|
||||
report.status = report.closure.complete && report.closure.artifacts === 4 && report.closure.missingKinds === 0 && report.closure.missingDependencies === 0 && report.closure.cycles === 0 && report.closure.documentVersion === 8 && report.closure.reopenedComplete && report.closure.artifactVersions.every((version) => version === 2)
|
||||
&& report.workflow.created.spreadsheetCells === 2 && report.workflow.created.draftObjects === 1 && report.workflow.created.techdrawViews === 1 && report.workflow.created.plotSeries === 1
|
||||
&& report.workflow.edited.spreadsheetValue === 6 && report.workflow.edited.draftStartX === 1 && report.workflow.edited.techdrawDimension === 5 && report.workflow.edited.plotValue === 6
|
||||
&& Object.entries(report.workflow.exports).every(([name, value]) => name === 'techdrawPdfValid' ? value === true : typeof value === 'number' && value > 0)
|
||||
&& report.persistence.mode === 'sqlite-opfs' && report.persistence.roundTrip && report.persistence.released && report.opfs.markerSuite === 'DOC-CLOSURE' && report.opfs.artifacts === 4 && report.opfs.markerRemoved ? 'pass' : 'failed'
|
||||
} catch (error) {
|
||||
report.error = error instanceof Error ? error.stack || error.message : String(error)
|
||||
} finally {
|
||||
facade.geometry.dispose()
|
||||
await new Promise((resolve) => setTimeout(resolve, 0))
|
||||
const capabilities = facade.geometry.capabilities()
|
||||
report.afterRelease = { shapeCount: capabilities.shapeCount, kernelReferenceCount: capabilities.kernelReferenceCount }
|
||||
if (capabilities.shapeCount !== 0 || capabilities.kernelReferenceCount !== 0) { report.status = 'failed'; report.error = `${report.error ? `${report.error} ` : ''}Shape ownership gate failed.` }
|
||||
}
|
||||
return report
|
||||
}
|
||||
|
||||
run().then((report) => {
|
||||
;(window as Window & { __bitbybitProductionReport?: ProductionReport }).__bitbybitProductionReport = report
|
||||
document.documentElement.dataset.status = report.status
|
||||
document.querySelector('#result')!.textContent = JSON.stringify(report, null, 2)
|
||||
}).catch((error) => {
|
||||
document.documentElement.dataset.status = 'failed'
|
||||
document.querySelector('#result')!.textContent = error instanceof Error ? error.stack || error.message : String(error)
|
||||
})
|
||||
|
||||
export {}
|
||||
Reference in New Issue
Block a user