feat: extend FreeCAD parity queue and pair diagnostics
Some checks failed
real-verification / chrome (push) Has been cancelled
real-verification / freecad-oracle (push) Has been cancelled
real-verification / wasm (push) Has been cancelled

This commit is contained in:
2026-08-15 06:29:20 -04:00
parent 83cbc80971
commit 378a6e7c0a
13 changed files with 21979 additions and 84 deletions

View File

@@ -18,25 +18,54 @@ for (const artifact of report.nativeProbe?.artifacts ?? []) {
if (bytes !== artifact.bytes || sha256(content) !== artifact.sha256) fail(`${artifact.name} provenance is stale.`)
}
const expectedPairs = [
{ pair: 'fuse->fuse', secondBuilder: 'BRepAlgoAPI_Fuse', secondInputCount: 2, mutationParameter: 'toolOffsetX', mutationTrajectory: [12, 11, 12] },
{ pair: 'fuse->cut', secondBuilder: 'BRepAlgoAPI_Cut', secondInputCount: 2, mutationParameter: 'toolSize', mutationTrajectory: [4, 3, 4] },
{ pair: 'fuse->common', secondBuilder: 'BRepAlgoAPI_Common', secondInputCount: 2, mutationParameter: 'toolOffsetX', mutationTrajectory: [10, 9, 10] },
{ pair: 'fuse->rotate', secondBuilder: 'BRepBuilderAPI_Transform', secondInputCount: 1, mutationParameter: 'angle', mutationTrajectory: [15, 22.5, 15] },
{ pair: 'fuse->fuse', decision: 'accepted', reasonCode: 'native-builders-mutation-naming-and-fcstd-resave-pass', secondBuilder: 'BRepAlgoAPI_Fuse', secondInputCount: 2, mutationParameter: 'toolOffsetX', mutationTrajectory: [12, 11, 12] },
{ pair: 'fuse->cut', decision: 'accepted', reasonCode: 'native-builders-mutation-naming-and-fcstd-resave-pass', secondBuilder: 'BRepAlgoAPI_Cut', secondInputCount: 2, mutationParameter: 'toolSize', mutationTrajectory: [4, 3, 4] },
{ pair: 'fuse->common', decision: 'accepted', reasonCode: 'native-builders-mutation-naming-and-fcstd-resave-pass', secondBuilder: 'BRepAlgoAPI_Common', secondInputCount: 2, mutationParameter: 'toolOffsetX', mutationTrajectory: [10, 9, 10] },
{ pair: 'fuse->rotate', decision: 'accepted', reasonCode: 'native-builders-mutation-naming-and-fcstd-resave-pass', secondBuilder: 'BRepBuilderAPI_Transform', secondInputCount: 1, mutationParameter: 'angle', mutationTrajectory: [15, 22.5, 15] },
{ pair: 'fuse->pad', decision: 'rejected', reasonCode: 'native-builder-and-freecad-profile-reject-with-clean-resave', rejectionAuthority: 'occt-builder', secondBuilder: 'BRepPrimAPI_MakePrism', secondInputCount: 1, mutationParameter: 'length', mutationTrajectory: [5, 7.5, 5], nativeTypeId: 'PartDesign::Pad' },
{ pair: 'fuse->pocket', decision: 'accepted', reasonCode: 'native-builders-mutation-naming-and-fcstd-resave-pass', secondBuilder: 'BRepPrimAPI_MakePrism+BRepAlgoAPI_Cut', secondInputCount: 2, mutationParameter: 'length', mutationTrajectory: [5, 4, 5] },
{ pair: 'fuse->loft', decision: 'rejected', reasonCode: 'freecad-input-precondition-reject-with-kernel-diagnostic-and-clean-resave', rejectionAuthority: 'freecad-document', kernelOutcome: 'accepted', secondBuilder: 'BRepOffsetAPI_ThruSections', secondInputCount: 2, mutationParameter: 'ruled', mutationTrajectory: [false, true, false], nativeTypeId: 'Part::Loft', freeCadDiagnostic: 'Profile shape is not a single vertex, edge, wire nor face.' },
{ pair: 'fuse->pipe', decision: 'rejected', reasonCode: 'freecad-input-precondition-reject-with-kernel-diagnostic-and-clean-resave', rejectionAuthority: 'freecad-document', kernelOutcome: 'invalid-result', secondBuilder: 'BRepOffsetAPI_MakePipe', secondInputCount: 2, mutationParameter: 'spineLength', mutationTrajectory: [15, 12, 15], nativeTypeId: 'Part::Sweep', freeCadDiagnostic: 'A fatal error occurred when making the sweep' },
]
if (report.nativeProbe?.artifacts?.length !== 3 || report.classifications?.length !== expectedPairs.length) fail('native artifacts or classification prefix is incomplete.')
for (const [index, entry] of report.classifications.entries()) {
const expected = expectedPairs[index]
const secondOperation = expected.pair.split('->')[1]
if (entry.taskId !== `TSN-PAIR-${expected.pair.replace('->', '-')}` || entry.pair !== expected.pair || entry.classification !== 'accepted' || entry.nativeDecision !== 'accepted' || entry.first?.operation !== 'fuse' || entry.first.builder !== 'BRepAlgoAPI_Fuse' || entry.first.inputCount !== 2 || entry.second?.operation !== secondOperation || entry.second.builder !== expected.secondBuilder || entry.second.inputCount !== expected.secondInputCount || entry.first.historyProvider !== 'occt-native' || entry.second.historyProvider !== 'occt-native') fail(`${expected.pair} decision contract is invalid.`)
if (entry.first.summary?.isValid !== true || entry.second.summary?.isValid !== true || entry.first.historyRecords < 1 || entry.second.historyRecords < 1 || entry.mutation?.changed !== true || entry.mutation?.restoredExactly !== true) fail(`${expected.pair} builder or mutation evidence is incomplete.`)
if (entry.taskId !== `TSN-PAIR-${expected.pair.replace('->', '-')}` || entry.pair !== expected.pair || entry.classification !== expected.decision || entry.reasonCode !== expected.reasonCode || entry.nativeDecision !== expected.decision || entry.first?.operation !== 'fuse' || entry.first.builder !== 'BRepAlgoAPI_Fuse' || entry.first.inputCount !== 2 || entry.first.historyProvider !== 'occt-native' || entry.first.summary?.isValid !== true || entry.first.historyRecords < 1) fail(`${expected.pair} decision contract is invalid.`)
if (expected.decision === 'rejected') {
if ('second' in entry || 'mutation' in entry || 'naming' in entry) fail(`${expected.pair} fabricates evidence for a result that FreeCAD rejected.`)
const rejection = entry.rejection
if (rejection?.scope !== 'second-operation-only' || rejection.authority !== expected.rejectionAuthority || rejection.parameter !== expected.mutationParameter || JSON.stringify(rejection.trajectory) !== JSON.stringify(expected.mutationTrajectory)) fail(`${expected.pair} rejection trajectory is invalid.`)
if (expected.rejectionAuthority === 'occt-builder') {
if (rejection.stable !== true || rejection.attempts?.length !== 3) fail(`${expected.pair} OCCT rejection is unstable.`)
for (const [attemptIndex, attempt] of rejection.attempts.entries()) {
if (attempt.operation !== secondOperation || attempt.builder !== expected.secondBuilder || attempt.inputCount !== expected.secondInputCount || attempt.parameter !== expected.mutationParameter || attempt.parameterValue !== expected.mutationTrajectory[attemptIndex] || attempt.runtime !== 'occt-native' || attempt.outcome !== 'rejected' || attempt.resultProduced !== false || attempt.shapeProduced !== false || attempt.historyProduced !== false || attempt.rejectionStage !== 'builder' || attempt.exceptionTransport !== 'emscripten-cpp-exception' || attempt.thrownType !== 'number') fail(`${expected.pair} OCCT attempt ${attemptIndex} is invalid.`)
for (const absent of ['summary', 'historyProvider', 'historyRecords', 'historySha256', 'namingStatus', 'namingEvidenceSha256', 'namingSemanticSha256', 'resultStep', 'resultBrep', 'shape', 'history']) if (absent in attempt) fail(`${expected.pair} OCCT attempt ${attemptIndex} fabricates ${absent}.`)
}
} else {
if (rejection.kernelRestoredExactly !== true || rejection.kernelAttempts?.length !== 3) fail(`${expected.pair} OCCT diagnostic trajectory is incomplete.`)
for (const [attemptIndex, attempt] of rejection.kernelAttempts.entries()) if (attempt.operation !== secondOperation || attempt.builder !== expected.secondBuilder || attempt.inputCount !== expected.secondInputCount || attempt.parameter !== expected.mutationParameter || attempt.parameterValue !== expected.mutationTrajectory[attemptIndex] || attempt.runtime !== 'occt-native' || attempt.outcome !== expected.kernelOutcome || attempt.resultProduced !== true || attempt.shapeProduced !== true || attempt.validShapeProduced !== (expected.kernelOutcome === 'accepted') || attempt.historyProduced !== true || attempt.historyProvider !== 'occt-native' || attempt.summary?.isValid !== (expected.kernelOutcome === 'accepted') || attempt.historyRecords < 1 || !attempt.historySha256 || attempt.authority !== (expected.kernelOutcome === 'accepted' ? 'kernel-superset-only' : 'kernel-invalid-result')) fail(`${expected.pair} OCCT compatibility attempt ${attemptIndex} is invalid.`)
}
const persistence = rejection.persistence
const diagnostic = persistence?.diagnostic
if (persistence?.status !== 'pass' || persistence.decision !== 'rejected' || persistence.pair !== entry.pair || persistence.operation !== secondOperation || persistence.nativeTypeId !== expected.nativeTypeId || persistence.freecadVersion !== '1.1.1' || !Object.values(persistence.checks ?? {}).every(Boolean)) fail(`${expected.pair} FCStd rejection evidence is incomplete.`)
if (diagnostic?.nativeTypeId !== expected.nativeTypeId || diagnostic.parameter?.name !== expected.mutationParameter || diagnostic.parameter.value !== expected.mutationTrajectory[0] || diagnostic.inputShape?.solids !== 1 || diagnostic.recomputeResult !== true || diagnostic.shapeNull !== true || diagnostic.shapeValid !== false || !diagnostic.state?.includes('Invalid') || diagnostic.statusString === 'Valid' || !diagnostic.objects?.some(({ name, typeId }) => name === 'PairResult' && typeId === expected.nativeTypeId)) fail(`${expected.pair} did not capture a real rejected FreeCAD recompute.`)
if (expected.rejectionAuthority === 'freecad-document' && diagnostic.statusString !== expected.freeCadDiagnostic) fail(`${expected.pair} FreeCAD profile diagnostic changed.`)
if (persistence.source?.shape?.solids !== 1 || !persistence.source.shape.brepSha256 || JSON.stringify(persistence.rollback?.before) !== JSON.stringify(persistence.rollback?.after)) fail(`${expected.pair} abort did not restore the source object set and Shape.`)
const phases = persistence.phases
if (JSON.stringify(phases?.initial) !== JSON.stringify(phases?.reopened) || JSON.stringify(phases?.initial) !== JSON.stringify(phases?.resaved) || phases.initial.objects?.length !== persistence.rollback.before.objects.length || phases.initial.objects?.some(({ typeId }) => typeId === 'PartDesign::Body' || typeId === 'PartDesign::Pad' || typeId === 'Part::Loft')) fail(`${expected.pair} polluted the clean FCStd save/reopen/resave chain.`)
continue
}
if (entry.second?.operation !== secondOperation || entry.second.builder !== expected.secondBuilder || entry.second.inputCount !== expected.secondInputCount || entry.second.historyProvider !== 'occt-native') fail(`${expected.pair} accepted second operation is invalid.`)
if (entry.second.summary?.isValid !== true || entry.second.historyRecords < 1 || entry.mutation?.changed !== true || entry.mutation?.restoredExactly !== true) fail(`${expected.pair} builder or mutation evidence is incomplete.`)
if (entry.mutation.scope !== 'second-operation-only' || entry.mutation.parameter !== expected.mutationParameter || JSON.stringify(entry.mutation.trajectory) !== JSON.stringify(expected.mutationTrajectory) || entry.mutation.beforeValue !== expected.mutationTrajectory[0] || entry.mutation.editedValue !== expected.mutationTrajectory[1] || entry.mutation.restoredValue !== expected.mutationTrajectory[2]) fail(`${expected.pair} mutation trajectory is invalid.`)
if (entry.mutation.editedEvidence?.operation !== secondOperation || entry.mutation.restoredEvidence?.operation !== secondOperation || entry.mutation.editedEvidence?.builder !== expected.secondBuilder || entry.mutation.restoredEvidence?.builder !== expected.secondBuilder || entry.mutation.editedEvidence?.inputCount !== expected.secondInputCount || entry.mutation.restoredEvidence?.inputCount !== expected.secondInputCount || entry.mutation.editedEvidence?.historyProvider !== 'occt-native' || entry.mutation.restoredEvidence?.historyProvider !== 'occt-native') fail(`${expected.pair} mutation history provider is invalid.`)
if (entry.second.namingSemanticSha256 !== entry.mutation.restoredEvidence?.namingSemanticSha256) fail(`${expected.pair} semantic naming evidence did not restore to nominal.`)
if (entry.naming?.upstreamEvidenceRestored !== true || entry.naming?.downstreamEvidence !== true || entry.naming?.jsonRoundtripStable !== true) fail(`${expected.pair} naming evidence is incomplete.`)
if (entry.persistence?.status !== 'pass' || entry.persistence.pair !== entry.pair || entry.persistence.freecadVersion !== '1.1.1' || !Object.values(entry.persistence.checks ?? {}).every(Boolean)) fail(`${expected.pair} FCStd persistence evidence is incomplete.`)
if (entry.persistence?.status !== 'pass' || entry.persistence.decision !== 'accepted' || entry.persistence.pair !== entry.pair || entry.persistence.freecadVersion !== '1.1.1' || !Object.values(entry.persistence.checks ?? {}).every(Boolean)) fail(`${expected.pair} FCStd persistence evidence is incomplete.`)
const phases = entry.persistence.phases
if (JSON.stringify(phases?.initial) !== JSON.stringify(phases?.reopened) || JSON.stringify(phases?.initial) !== JSON.stringify(phases?.resaved) || phases.initial.namingEvidenceSha256 !== entry.second.namingEvidenceSha256) fail(`${expected.pair} FCStd phases changed Shape or naming evidence.`)
}
const expectedSummary = { registeredOperations: 19, orderedPairs: 361, classifiedPairs: 4, accepted: 4, rejected: 0, unknown: 357 }
const expectedSummary = { registeredOperations: 19, orderedPairs: 361, classifiedPairs: 8, accepted: 5, rejected: 3, unknown: 353 }
if (JSON.stringify(report.summary) !== JSON.stringify(expectedSummary)) fail('summary is inconsistent.')
console.log(JSON.stringify({ status: 'freecad-ordered-operation-pair-classification-pass', completedTasks: report.classifications.map(({ taskId }) => taskId), pairs: report.classifications.map(({ pair, classification }) => ({ pair, classification })), nativeBuilderRuns: report.classifications.length * 4, fcstdPhases: report.classifications.length * 3, remainingPairs: report.summary.unknown }, null, 2))

View File

@@ -14,12 +14,11 @@ def version_text():
return ".".join(str(value) for value in App.Version()[:3])
def shape_snapshot(feature):
shape = feature.Shape
def shape_snapshot_from_shape(shape, include_brep=False):
if shape.isNull():
raise RuntimeError("ordered pair produced a null Shape")
bounds = shape.BoundBox
return {
snapshot = {
"valid": bool(shape.isValid()),
"solids": len(shape.Solids),
"faces": len(shape.Faces),
@@ -29,6 +28,15 @@ def shape_snapshot(feature):
"area": round(float(shape.Area), 7),
"bounds": [round(float(value), 7) for value in (bounds.XMin, bounds.YMin, bounds.ZMin, bounds.XMax, bounds.YMax, bounds.ZMax)],
}
if include_brep:
brep = shape.exportBrepToString()
brep_bytes = brep.encode("utf-8") if isinstance(brep, str) else bytes(brep)
snapshot["brepSha256"] = hashlib.sha256(brep_bytes).hexdigest()
return snapshot
def shape_snapshot(feature):
return shape_snapshot_from_shape(feature.Shape)
def phase_snapshot(document):
@@ -44,7 +52,35 @@ def phase_snapshot(document):
}
def collect():
def document_snapshot(document):
return {
"objects": [{
"name": obj.Name,
"typeId": obj.TypeId,
**({"shape": shape_snapshot(obj)} if hasattr(obj, "Shape") and not obj.Shape.isNull() else {}),
} for obj in document.Objects],
}
def object_set_snapshot(document):
return [{"name": obj.Name, "typeId": obj.TypeId} for obj in document.Objects]
def link_snapshot(value):
linked = value
subelements = []
if isinstance(value, tuple):
linked = value[0]
if len(value) > 1:
subelements = [str(item) for item in value[1]]
return {
"name": linked.Name if linked is not None else None,
"typeId": linked.TypeId if linked is not None else None,
"subelements": subelements,
}
def collect_accepted():
step_path = os.environ["FREECAD_PAIR_STEP_PATH"]
evidence_path = os.environ["FREECAD_PAIR_EVIDENCE_PATH"]
pair = os.environ["FREECAD_ORDERED_PAIR"]
@@ -85,6 +121,7 @@ def collect():
"freecadVersion": version_text(),
"gitCommit": FREECAD_COMMIT,
"pair": pair,
"decision": "accepted",
"phases": {"initial": initial, "reopened": reopened_snapshot, "resaved": resaved_snapshot},
"checks": {
"shapeStable": initial["shape"] == reopened_snapshot["shape"] and initial["shape"] == resaved_snapshot["shape"],
@@ -97,8 +134,162 @@ def collect():
App.closeDocument(name)
def collect_rejected():
step_path = os.environ["FREECAD_PAIR_STEP_PATH"]
pair = os.environ["FREECAD_ORDERED_PAIR"]
operation = pair.split("->", 1)[1]
output_directory = os.environ["FREECAD_PAIR_OUTPUT_DIRECTORY"]
document = App.newDocument("FreeCadOrderedPairRejection")
try:
Import.insert(step_path, document.Name)
document.recompute()
source_objects = [obj for obj in document.Objects if hasattr(obj, "Shape") and not obj.Shape.isNull()]
if len(source_objects) != 1:
raise RuntimeError("Fuse STEP import must produce exactly one Shape object")
source = source_objects[0]
if len(source.Shape.Solids) != 1:
raise RuntimeError("Fuse STEP import did not produce one Solid")
if source.Shape.isNull():
raise RuntimeError("STEP import produced no Shape")
source_name = source.Name
source_type_id = source.TypeId
document.UndoMode = 1
baseline = document_snapshot(document)
baseline_objects = object_set_snapshot(document)
baseline_source_shape = shape_snapshot_from_shape(source.Shape, include_brep=True)
diagnostic = {
"exceptionType": None,
"exception": None,
"recomputeResult": None,
"statusString": None,
"state": [],
"shapeNull": True,
"shapeValid": False,
}
document.openTransaction("ordered-pair-rejection")
try:
if operation == "pad":
body = document.addObject("PartDesign::Body", "Body")
feature = body.newObject("PartDesign::Pad", "PairResult")
feature.Profile = source
feature.Length = 5.0
diagnostic["parameter"] = {"name": "length", "value": round(float(feature.Length), 7)}
diagnostic["input"] = link_snapshot(feature.Profile)
elif operation == "loft":
section = document.addObject("Part::Feature", "Section")
section.Shape = Part.makePolygon([
App.Vector(0.0, 0.0, 15.0),
App.Vector(2.0, 0.0, 15.0),
App.Vector(2.0, 3.0, 15.0),
App.Vector(0.0, 3.0, 15.0),
App.Vector(0.0, 0.0, 15.0),
])
feature = document.addObject("Part::Loft", "PairResult")
feature.Sections = [source, section]
feature.Solid = True
feature.Ruled = False
diagnostic["parameter"] = {"name": "ruled", "value": bool(feature.Ruled)}
diagnostic["input"] = {
"sections": [{"name": item.Name, "typeId": item.TypeId} for item in feature.Sections],
}
elif operation == "pipe":
spine = document.addObject("Part::Feature", "Spine")
spine.Shape = Part.makePolygon([App.Vector(0.0, 0.0, 0.0), App.Vector(0.0, 0.0, 15.0)])
feature = document.addObject("Part::Sweep", "PairResult")
feature.Sections = [source]
feature.Spine = spine
feature.Solid = True
feature.Frenet = False
diagnostic["parameter"] = {"name": "spineLength", "value": 15.0}
diagnostic["input"] = {
"sections": [{"name": item.Name, "typeId": item.TypeId} for item in feature.Sections],
"spine": link_snapshot(feature.Spine),
}
else:
raise RuntimeError("Unsupported rejected ordered-pair operation: " + operation)
diagnostic["nativeTypeId"] = feature.TypeId
diagnostic["inputShape"] = shape_snapshot_from_shape(source.Shape)
diagnostic["objects"] = object_set_snapshot(document)
try:
diagnostic["recomputeResult"] = bool(document.recompute())
except Exception as error:
diagnostic["exceptionType"] = type(error).__name__
diagnostic["exception"] = str(error)
diagnostic["statusString"] = str(feature.getStatusString())
diagnostic["state"] = [str(value) for value in feature.State]
diagnostic["shapeNull"] = bool(feature.Shape.isNull())
diagnostic["shapeValid"] = False if feature.Shape.isNull() else bool(feature.Shape.isValid())
finally:
document.abortTransaction()
document.recompute()
initial = document_snapshot(document)
restored_objects = object_set_snapshot(document)
restored_source = document.getObject(source_name)
if restored_source is None or not hasattr(restored_source, "Shape") or restored_source.Shape.isNull():
raise RuntimeError("Fuse source Shape is absent after transaction abort")
restored_source_shape = shape_snapshot_from_shape(restored_source.Shape, include_brep=True)
initial_path = os.path.join(output_directory, "ordered-pair-rejection.FCStd")
resaved_path = os.path.join(output_directory, "ordered-pair-rejection-resaved.FCStd")
document.saveAs(initial_path)
App.closeDocument(document.Name)
reopened = App.openDocument(initial_path)
reopened.recompute()
reopened_snapshot = document_snapshot(reopened)
reopened.saveAs(resaved_path)
App.closeDocument(reopened.Name)
resaved = App.openDocument(resaved_path)
resaved.recompute()
resaved_snapshot = document_snapshot(resaved)
App.closeDocument(resaved.Name)
feature_rejected = diagnostic["shapeNull"] and (
diagnostic["exceptionType"] is not None
or diagnostic["recomputeResult"] is False
or diagnostic["statusString"] != "Valid"
or "Error" in diagnostic["state"]
or "Invalid" in diagnostic["state"]
)
expected_type_id = {"pad": "PartDesign::Pad", "loft": "Part::Loft", "pipe": "Part::Sweep"}[operation]
input_names = [diagnostic.get("input", {}).get("name")] if operation == "pad" else [item["name"] for item in diagnostic.get("input", {}).get("sections", [])]
checks = {
"nativeFeatureConstructed": diagnostic.get("nativeTypeId") == expected_type_id,
"fuseSolidUsedAsInput": source_name in input_names and diagnostic.get("inputShape", {}).get("solids") == 1,
"recomputeStatusCaptured": diagnostic["recomputeResult"] is not None or diagnostic["exceptionType"] is not None,
"nativeFeatureRejected": feature_rejected,
"objectSetRestored": baseline_objects == restored_objects,
"sourceShapeRestored": baseline_source_shape == restored_source_shape,
"documentUnpolluted": baseline == initial,
"documentStable": initial == reopened_snapshot and initial == resaved_snapshot,
}
return {
"schemaVersion": 1,
"baselineId": "freecad-1.1.1-ordered-operation-pair-resave",
"freecadVersion": version_text(),
"gitCommit": FREECAD_COMMIT,
"pair": pair,
"decision": "rejected",
"operation": operation,
"nativeTypeId": expected_type_id,
"source": {
"name": source_name,
"typeId": source_type_id,
"shape": baseline_source_shape,
},
"diagnostic": diagnostic,
"rollback": {
"before": {"objects": baseline_objects, "sourceShape": baseline_source_shape},
"after": {"objects": restored_objects, "sourceShape": restored_source_shape},
},
"phases": {"initial": initial, "reopened": reopened_snapshot, "resaved": resaved_snapshot},
"checks": checks,
"status": "pass" if all(checks.values()) else "failed",
}
finally:
for name in list(App.listDocuments().keys()):
App.closeDocument(name)
try:
report = collect()
report = collect_rejected() if os.environ.get("FREECAD_PAIR_DECISION") == "rejected" else collect_accepted()
except Exception as error:
report = {
"schemaVersion": 1,

View File

@@ -3,8 +3,9 @@ import { resolve } from 'node:path'
const root = resolve(new URL('..', import.meta.url).pathname)
const outputPath = resolve(root, 'config/freecad-active-work-queue.json')
const statusOutputPath = resolve(root, 'docs/freecad-active-work-status.generated.zh-CN.md')
const load = (path) => readFile(resolve(root, path), 'utf8').then(JSON.parse)
const [gui, workflowPlan, composite, correlation, production, recoveredNaming, productionDriftClassification, orderedPairClassification] = await Promise.all([
const [gui, workflowPlan, composite, correlation, production, recoveredNaming, productionDriftClassification, orderedPairClassification, propertySemantics, exactPlan, platformCoverage, followUpProgress] = await Promise.all([
load('.cache/freecad/reference-desktop-gui-commands.json'),
load('config/freecad-gui-workflow-plan.json'),
load('config/freecad-composite-history-elementmap-oracle.json'),
@@ -13,6 +14,10 @@ const [gui, workflowPlan, composite, correlation, production, recoveredNaming, p
load('config/freecad-recovered-naming-classification.json'),
load('config/freecad-production-drift-classification.json'),
load('config/freecad-ordered-operation-pair-classification.json'),
load('config/freecad-native-property-semantics.json'),
load('config/freecad-web-exact-parity-plan.json'),
load('config/platform-module-coverage.json'),
load('config/freecad-follow-up-task-progress.json'),
])
const guiShardTasks = gui.guiCommands.mergedShards.map((shard, index) => ({
@@ -108,6 +113,127 @@ const transitionTasks = production.operations.flatMap((from) => production.opera
}
}))
const completionSteps = [
{ id: 'A', title: 'Lock native inventory and preconditions', exit: 'the native TypeId, inputs, defaults, dependencies and applicability are machine recorded' },
{ id: 'B', title: 'Capture native success evidence', exit: 'nominal and boundary successes record values, Shape, topology, status and diagnostics' },
{ id: 'C', title: 'Capture native failure and cancel evidence', exit: 'invalid input, disabled state, cancellation and document non-pollution are reproducible' },
{ id: 'D', title: 'Capture parameter mutation and restore evidence', exit: 'edit, recompute and restore return geometry and semantics to the classified native state' },
{ id: 'E', title: 'Implement the Facade capability', exit: 'the production Facade uses the declared native or explicit proxy path without hidden fallback' },
{ id: 'F', title: 'Close transaction and recovery behavior', exit: 'abort, undo, redo, stale, failure and resource ownership return to the expected state' },
{ id: 'G', title: 'Close FCStd save, reopen and resave', exit: 'FreeCAD to Web to FreeCAD persistence has zero unknown semantic drift' },
{ id: 'H', title: 'Replay in a real browser', exit: 'Chrome Worker, OPFS, UI state and resource release evidence pass' },
{ id: 'I', title: 'Promote capability and synchronize blockers', exit: 'all eight evidence tasks pass and machine reports alone advance the capability level' },
]
const slug = (value) => value.replaceAll('::', '-').replace(/[^A-Za-z0-9]+/g, '-').replace(/^-|-$/g, '').toLowerCase()
const makeCapabilityTasks = (prefix, capabilities, firstDependency) => {
const tasks = []
for (const capability of capabilities) {
for (const step of completionSteps) {
const id = `${prefix}-${slug(capability.id)}-${step.id}`
const previous = tasks.at(-1)?.id
tasks.push({
id,
title: `${step.title}: ${capability.title}`,
status: 'pending',
dependencies: [previous ?? firstDependency],
capability: capability.id,
phase: step.id,
...(capability.recordCount === undefined ? {} : { recordCount: capability.recordCount }),
evidenceRequired: step.id === 'I' ? ['all-prior-phases-pass', 'focused-check', 'exact-blocker-sync'] : ['one-native-or-production-artifact', 'one-focused-check'],
exit: step.exit,
})
}
}
return tasks
}
const propertyCapabilities = propertySemantics.types
.filter(({ support }) => support === 'opaque-fcstd-proxy')
.map(({ typeId, recordCount }) => ({ id: typeId, title: typeId, recordCount }))
if (propertyCapabilities.length !== propertySemantics.supportSummary?.['opaque-fcstd-proxy']?.typeCount) throw new Error('Opaque Property capability inventory is inconsistent.')
const followUpMilestoneDefinitions = [
{
id: 'PROPERTY-CODECS', exactTasks: ['EX-DOC-01'], title: 'Close every opaque Property type', prefix: 'PROP', capabilities: propertyCapabilities,
},
{
id: 'DOCUMENT-SEMANTICS', exactTasks: ['EX-DOC-02', 'EX-DOC-03', 'EX-DOC-04'], title: 'Close document, transaction and lifecycle semantics', prefix: 'DOC', capabilities: [
['observer-object-order', 'Object add/remove/rename observer order'],
['observer-property-order', 'Property add/remove/rename/change observer order'],
['nested-transactions', 'Nested commit/abort/undo/redo transactions'],
['dag-partial-recompute', 'DAG dirty propagation and partial recompute'],
['recompute-failure-recovery', 'Last-valid Shape and diagnostic recovery'],
['partial-document-load', 'Partial document load and PartialTrigger'],
['object-lifecycle', 'Copy/clone/delete/relink/group lifecycle'],
['extension-lifecycle', 'Extension schema, migration and unknown preservation'],
['feature-python-boundary', 'FeaturePython proxy, signature and execution boundary'],
['multi-document-links', 'External links, close/reopen, merge and recovery'],
].map(([id, title]) => ({ id, title })),
},
{
id: 'CORE-MODELING', exactTasks: ['EX-KER-01', 'EX-SK-01', 'EX-SK-02', 'EX-PART-01', 'EX-PD-01'], title: 'Close core modeling operations', prefix: 'CORE', capabilities: production.operations.map((operation) => ({ id: operation, title: `core operation ${operation}` })),
},
{
id: 'FCSTD-AND-FORMATS', exactTasks: ['EX-FC-01', 'EX-FMT-01', 'EX-FMT-02', 'EX-FMT-03', 'EX-FMT-04'], title: 'Close FCStd and interchange formats', prefix: 'IO', capabilities: [
['fcstd-readonly-roundtrip', 'FCStd unchanged object and Property round-trip'],
['fcstd-edited-roundtrip', 'Web edit, FreeCAD reopen/resave and Web reopen'],
['fcstd-shape-resources', 'Shape, ElementMap2, StringHasher, Expression and GuiDocument resources'],
['fcstd-unknown-resources', 'Unknown XML, BRep, script and Extension preservation'],
['fcstd-adversarial-input', 'Corrupt ZIP, traversal, compression limit and cancellation'],
['step', 'STEP units, colors, layers, names, assemblies and metadata'],
['iges', 'IGES units, colors, layers, names, assemblies and metadata'],
['brep', 'BREP tolerance, topology and editable round-trip'],
].map(([id, title]) => ({ id, title })),
},
{
id: 'GUI-CLOSURE', exactTasks: ['EX-UI-01', 'EX-UI-02', 'EX-UI-03', 'EX-UI-04', 'EX-UI-05'], title: 'Close full GUI behavior', prefix: 'GUI', capabilities: [
'application-shell', 'menus', 'toolbars', 'workbench-switching', 'mdi', 'combo-view', 'model-tree', 'task-panels', 'data-view-editors', 'dialogs', 'shortcuts', 'context-menus', 'status-report-jobs', '3d-selection', 'responsive-accessibility',
].map((id) => ({ id, title: id.replaceAll('-', ' ') })),
},
{
id: 'DOCUMENT-ENGINEERING-WORKBENCHES', exactTasks: ['EX-DOCWB-01', 'EX-ASM-01', 'EX-BIM-01', 'EX-SURF-01', 'EX-MESH-01', 'EX-MAT-01'], title: 'Close document and engineering workbenches', prefix: 'WB', capabilities: [
'draft', 'spreadsheet', 'techdraw', 'plot', 'cross-workbench-document', 'assembly', 'bim-ifc', 'surface', 'mesh-meshpart', 'material',
].map((id) => ({ id, title: id.replaceAll('-', ' ') })),
},
{
id: 'PLATFORM-SCRIPT-PROXY', exactTasks: ['EX-FEM-01', 'EX-CAM-01', 'EX-ROBOT-01', 'EX-DATA-01', 'EX-INSPECT-01', 'EX-SCRIPT-01', 'EX-ADDON-01', 'EX-PROXY-01', 'EX-PLATFORM-01'], title: 'Close platform, scripting and proxy capabilities', prefix: 'PLAT', capabilities: [
'fem', 'cam', 'robot', 'points-reverseengineering', 'inspection-measure', 'python-macro', 'addon', 'cloud', 'help', 'idf', 'jt', 'openscad', 'platform-boundaries',
].map((id) => ({ id, title: id.replaceAll('-', ' ') })),
},
{
id: 'QA-RELEASE', exactTasks: ['EX-QA-01', 'EX-QA-02', 'EX-QA-03', 'EX-REL-01'], title: 'Close QA and exact release', prefix: 'REL', capabilities: [
['chrome-baseline', 'Chrome locked baseline'],
['browser-matrix', 'Firefox, Safari and explicit unsupported boundaries'],
['os-gpu-matrix', 'Windows, macOS, Linux and GPU/software rendering'],
['accessibility-locale', 'Keyboard, screen reader, locale, long text and narrow viewport'],
['fuzz-soak-recovery', 'Fuzz, 1000 recomputes, long session, crash, migration and rollback'],
['resource-performance', 'Performance and WASM/JS/GPU/OPFS release accounting'],
['signed-release', 'SBOM, licenses, signing, offline install and rollback drill'],
['exact-promotion', '52-task, 34-module and zero-unknown promotion report'],
].map(([id, title]) => ({ id, title })),
},
]
let followUpDependency = transitionTasks.at(-1).id
const followUpMilestones = followUpMilestoneDefinitions.map((definition) => {
const tasks = makeCapabilityTasks(definition.prefix, definition.capabilities, followUpDependency)
followUpDependency = tasks.at(-1).id
return { id: definition.id, title: definition.title, exactTasks: definition.exactTasks, status: 'pending', tasks }
})
const followUpTasks = followUpMilestones.flatMap(({ tasks }) => tasks)
if (followUpProgress.schemaVersion !== 1 || followUpProgress.baseline?.freecadVersion !== '1.1.1' || followUpProgress.baseline?.commit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d' || !Array.isArray(followUpProgress.completedTasks)) throw new Error('Follow-up task progress ledger is invalid.')
const followUpTaskIds = new Set(followUpTasks.map(({ id }) => id))
const completedFollowUpIds = new Set()
for (const entry of followUpProgress.completedTasks) {
if (!followUpTaskIds.has(entry.id) || completedFollowUpIds.has(entry.id) || !Array.isArray(entry.evidence) || entry.evidence.length === 0) throw new Error(`Follow-up progress entry ${entry.id} is invalid.`)
completedFollowUpIds.add(entry.id)
}
const completedFollowUpPrefix = followUpTasks.findIndex(({ id }) => !completedFollowUpIds.has(id))
const completedFollowUpCount = completedFollowUpPrefix < 0 ? followUpTasks.length : completedFollowUpPrefix
if (followUpTasks.slice(completedFollowUpCount).some(({ id }) => completedFollowUpIds.has(id))) throw new Error('Follow-up completed tasks must form one contiguous serial prefix.')
const orderedPairsClosed = transitionTasks.every(({ status }) => status === 'completed')
for (const [index, task] of followUpTasks.entries()) task.status = index < completedFollowUpCount ? 'completed' : orderedPairsClosed && index === completedFollowUpCount ? 'in_progress' : 'pending'
for (const milestone of followUpMilestones) milestone.status = milestone.tasks.every(({ status }) => status === 'completed') ? 'completed' : milestone.tasks.some(({ status }) => status === 'in_progress') ? 'in_progress' : 'pending'
const setupTasks = [
{ id: 'ORA-GUI-SETUP-000', title: 'Split GUI state probing from object/property probing', status: 'completed' },
{ id: 'ORA-GUI-SETUP-001', title: 'Diagnose the BIM first-run modal block', status: 'completed', dependencies: ['ORA-GUI-SETUP-000'] },
@@ -122,12 +248,18 @@ const milestones = [
{ id: 'ORA-GUI-BASELINE', exactTask: 'EX-ORA-01', status: 'completed', tasks: [...setupTasks, ...guiShardTasks, ...closureTasks] },
{ id: 'ORA-GUI-WORKFLOWS', exactTask: 'EX-ORA-01', status: workflowClosed ? 'completed' : 'in_progress', tasks: workflowTasks },
{ id: 'TSN-RECOVERY-DRIFT', exactTask: 'EX-TSN-04', status: productionDriftClosed ? 'completed' : workflowClosed ? 'in_progress' : 'pending', tasks: [...driftTasks, ...productionDriftTasks] },
{ id: 'TSN-ORDERED-PAIRS', exactTask: 'EX-TSN-04', status: productionDriftClosed ? 'in_progress' : 'pending', tasks: transitionTasks },
{ id: 'TSN-ORDERED-PAIRS', exactTask: 'EX-TSN-04', status: orderedPairsClosed ? 'completed' : productionDriftClosed ? 'in_progress' : 'pending', tasks: transitionTasks },
...followUpMilestones,
]
const allTasks = milestones.flatMap((milestone) => milestone.tasks)
const allTaskIds = new Set(allTasks.map(({ id }) => id))
if (allTaskIds.size !== allTasks.length) throw new Error('FreeCAD active work queue contains duplicate task IDs.')
for (const milestone of followUpMilestones) for (const exactTask of milestone.exactTasks) if (!exactPlan.programs.some(({ tasks }) => tasks.some(({ id }) => id === exactTask))) throw new Error(`${milestone.id} references unknown exact task ${exactTask}.`)
for (const task of allTasks) for (const dependency of task.dependencies ?? []) if (!allTaskIds.has(dependency)) throw new Error(`${task.id} depends on unknown task ${dependency}.`)
const count = (status) => allTasks.filter((task) => task.status === status).length
const nextTask = allTasks.find((task) => task.status === 'in_progress')?.id
if (!nextTask) throw new Error('FreeCAD active work queue requires one in-progress task.')
if (count('in_progress') !== 1) throw new Error('FreeCAD active work queue requires exactly one in-progress task.')
const queue = {
schemaVersion: 1,
baseline: { freecadVersion: '1.1.1', commit: '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d' },
@@ -138,17 +270,46 @@ const queue = {
unit: 'one artifact, one focused check, one explicit exit condition',
transitionUnit: 'one ordered operation pair',
driftUnit: 'one native case',
capabilityDefinition: completionSteps.map(({ id, title }) => `${id}: ${title}`),
followUpProgress: 'config/freecad-follow-up-task-progress.json',
},
nextTask,
summary: { tasks: allTasks.length, completed: count('completed'), inProgress: count('in_progress'), pending: count('pending') },
milestones,
}
const content = `${JSON.stringify(queue, null, 2)}\n`
const exactTasks = exactPlan.programs.flatMap(({ tasks }) => tasks)
const moduleCounts = Object.fromEntries(['exact', 'compatible', 'proxy', 'development'].map((level) => [level, platformCoverage.modules.filter((module) => module.level === level).length]))
const milestoneRows = queue.milestones.map((milestone) => {
const taskCount = (status) => milestone.tasks.filter((task) => task.status === status).length
const active = milestone.tasks.find((task) => task.status === 'in_progress')?.id ?? '-'
return `| \`${milestone.id}\` | ${milestone.tasks.length} | ${taskCount('completed')} | ${taskCount('in_progress')} | ${taskCount('pending')} | \`${active}\` |`
})
const statusContent = `<!-- Generated by scripts/generate-freecad-active-work-queue.mjs. Do not edit. -->
# FreeCAD 后续工作机器状态
生成日期:${queue.updatedAt}。基线FreeCAD ${queue.baseline.freecadVersion} / \`${queue.baseline.commit}\`
- 唯一活动任务:\`${queue.nextTask}\`
- 微任务:${queue.summary.tasks} total / ${queue.summary.completed} completed / ${queue.summary.inProgress} in_progress / ${queue.summary.pending} pending
- 有序操作对:${coveredTransitions.size}/${transitionTasks.length} classified / ${transitionTasks.length - coveredTransitions.size} unknown
- Property${propertySemantics.supportSummary['native-editable-codec'].typeCount + propertySemantics.supportSummary['native-specialized-codec'].typeCount}/${propertySemantics.types.length} native types / ${propertySemantics.supportSummary['opaque-fcstd-proxy'].typeCount} opaque types
- exact 任务:${exactTasks.filter(({ status }) => status === 'completed').length}/${exactTasks.length} completed
- 模块能力:${moduleCounts.exact} exact / ${moduleCounts.compatible} compatible / ${moduleCounts.proxy} proxy / ${moduleCounts.development} development
| 里程碑 | 任务数 | completed | in_progress | pending | 当前任务 |
| --- | ---: | ---: | ---: | ---: | --- |
${milestoneRows.join('\n')}
详细任务、依赖、阶段、证据要求和退出条件见 \`config/freecad-active-work-queue.json\`。完成证据只能追加到 \`config/freecad-follow-up-task-progress.json\`,生成器会拒绝跳号、未知任务和无证据完成项。
`
if (process.argv.includes('--check')) {
const current = await readFile(outputPath, 'utf8').catch(() => '')
if (current !== content) throw new Error('FreeCAD active work queue is stale; run generate:freecad-active-work-queue.')
const currentStatus = await readFile(statusOutputPath, 'utf8').catch(() => '')
if (currentStatus !== statusContent) throw new Error('FreeCAD active work status is stale; run generate:freecad-active-work-queue.')
console.log(JSON.stringify({ status: 'freecad-active-work-queue-pass', nextTask: queue.nextTask, ...queue.summary }, null, 2))
} else {
await writeFile(outputPath, content)
console.log(JSON.stringify({ status: 'freecad-active-work-queue-generated', output: outputPath, nextTask: queue.nextTask, ...queue.summary }, null, 2))
await Promise.all([writeFile(outputPath, content), writeFile(statusOutputPath, statusContent)])
console.log(JSON.stringify({ status: 'freecad-active-work-queue-generated', output: outputPath, statusOutput: 'docs/freecad-active-work-status.generated.zh-CN.md', nextTask: queue.nextTask, ...queue.summary }, null, 2))
}

View File

@@ -50,12 +50,38 @@ const operationSpecs = {
cut: { builder: 'BRepAlgoAPI_Cut', nominal: { toolSize: 4, toolOffset: 2 }, edited: { toolSize: 3, toolOffset: 2 }, parameter: 'toolSize', beforeValue: 4, editedValue: 3 },
common: { builder: 'BRepAlgoAPI_Common', nominal: { toolSize: 5, toolOffset: 10 }, edited: { toolSize: 5, toolOffset: 9 }, parameter: 'toolOffsetX', beforeValue: 10, editedValue: 9 },
rotate: { builder: 'BRepBuilderAPI_Transform', nominal: { angle: 15 }, edited: { angle: 22.5 }, parameter: 'angle', beforeValue: 15, editedValue: 22.5 },
pad: { builder: 'BRepPrimAPI_MakePrism', nominal: { length: 5 }, edited: { length: 7.5 }, parameter: 'length', beforeValue: 5, editedValue: 7.5, decision: 'rejected' },
pocket: { builder: 'BRepPrimAPI_MakePrism+BRepAlgoAPI_Cut', nominal: { length: 5 }, edited: { length: 4 }, parameter: 'length', beforeValue: 5, editedValue: 4 },
loft: { builder: 'BRepOffsetAPI_ThruSections', nominal: { ruled: false }, edited: { ruled: true }, parameter: 'ruled', beforeValue: false, editedValue: true, decision: 'rejected', rejectionAuthority: 'freecad-document', kernelOutcome: 'accepted' },
pipe: { builder: 'BRepOffsetAPI_MakePipe', nominal: { spineLength: 15 }, edited: { spineLength: 12 }, parameter: 'spineLength', beforeValue: 15, editedValue: 12, decision: 'rejected', rejectionAuthority: 'freecad-document', kernelOutcome: 'invalid-result' },
}
const createOperation = (module, { operation, objectStep, toolSize, toolOffset, angle, stage }) => {
const createOperation = (module, { operation, objectStep, toolSize, toolOffset, angle, length, ruled, spineLength, stage }) => {
const baseStep = objectStep ?? shapeStep(module, 'makeBox', 10, 10, 10)
const objectInput = input('object', 'object', stage.objectId, stage.objectTag, baseStep, stage.namingEvidence)
if (operation === 'rotate') return { response: module.rotateHistoryFromStep(baseStep, 0, 0, 0, 0, 0, 1, angle), inputs: [objectInput] }
if (operation === 'pad') return { response: module.prismHistoryFromStep(baseStep, 0, 0, length), inputs: [objectInput] }
if (operation === 'pocket') {
const profileStep = shapeStep(module, 'makeRectangleFace', 2, 3)
return {
response: module.pocketHistoryFromStep(baseStep, profileStep, 0, 0, length),
inputs: [objectInput, input('tool', 'tool', `${stage.resultObjectId}:profile`, stage.objectTag + 1, profileStep)],
}
}
if (operation === 'loft') {
const sectionStep = shapeStep(module, 'makeRectangleFacePlaced', 2, 3, 0, 0, 15)
return {
response: module.loftHistoryFromStep(baseStep, sectionStep, ruled),
inputs: [objectInput, input('tool', 'tool', `${stage.resultObjectId}:section`, stage.objectTag + 1, sectionStep)],
}
}
if (operation === 'pipe') {
const spineStep = shapeStep(module, 'makeLineWire', 0, 0, 0, 0, 0, spineLength)
return {
response: module.pipeHistoryFromStep(baseStep, spineStep),
inputs: [objectInput, input('tool', 'tool', `${stage.resultObjectId}:spine`, stage.objectTag + 1, spineStep)],
}
}
const toolStep = shapeStep(module, 'makeBoxPlaced', toolSize, toolSize, toolSize, toolOffset, 0, 0)
const inputs = [
objectInput,
@@ -64,6 +90,61 @@ const createOperation = (module, { operation, objectStep, toolSize, toolOffset,
return { response: module.booleanHistoryFromStep(baseStep, toolStep, operation), inputs }
}
const captureRejectedStage = (module, definition) => {
let capture
try {
capture = createOperation(module, definition)
} catch (error) {
return {
operation: definition.operation,
builder: operationSpecs[definition.operation].builder,
inputCount: 1,
parameter: operationSpecs[definition.operation].parameter,
parameterValue: definition.length,
runtime: 'occt-native',
outcome: 'rejected',
resultProduced: false,
shapeProduced: false,
historyProduced: false,
rejectionStage: 'builder',
exceptionTransport: typeof error === 'number' ? 'emscripten-cpp-exception' : 'javascript-exception',
thrownType: typeof error,
}
}
capture.response.result?.delete?.()
fail(`${definition.stage.stageId} unexpectedly produced a native result.`)
}
const rejectionSignature = ({ parameterValue: _parameterValue, ...attempt }) => JSON.stringify(attempt)
const captureKernelCompatibilityStage = (module, definition, parameterValue, expectedOutcome) => {
const { response, inputs } = createOperation(module, definition)
try {
const outcome = response.summary?.isValid === true ? 'accepted' : 'invalid-result'
if (response.provider !== 'occt-native' || outcome !== expectedOutcome || !response.resultStep?.startsWith('ISO-10303-21;') || response.records?.length < 1) fail(`${definition.stage.stageId} OCCT compatibility probe returned an unexpected result.`)
return {
operation: definition.operation,
builder: operationSpecs[definition.operation].builder,
inputCount: inputs.length,
parameter: operationSpecs[definition.operation].parameter,
parameterValue,
runtime: 'occt-native',
outcome,
resultProduced: true,
shapeProduced: true,
validShapeProduced: response.summary.isValid === true,
historyProduced: true,
historyProvider: response.provider,
summary: canonicalSummary(response.summary),
historyRecords: response.records.length,
historySha256: sha256(JSON.stringify(response.records)),
authority: outcome === 'accepted' ? 'kernel-superset-only' : 'kernel-invalid-result',
}
} finally {
response.result?.delete?.()
}
}
const captureStage = (module, definition) => {
const { response, inputs } = createOperation(module, definition)
try {
@@ -114,12 +195,12 @@ const captureStage = (module, definition) => {
const createModule = (await import(pathToFileURL(modulePath).href)).default
const module = await createModule({ locateFile: (path) => resolve(dist, path) })
const persistAcceptedPair = async (pair, second) => {
const persistPair = async (pair, step, namingEvidence, decision) => {
const temporaryDirectory = await mkdtemp(join(tmpdir(), 'freecad-ordered-pair-'))
try {
const stepPath = join(temporaryDirectory, 'pair-result.step')
const evidencePath = join(temporaryDirectory, 'pair-naming.json')
await Promise.all([writeFile(stepPath, second.response.resultStep), writeFile(evidencePath, JSON.stringify(second.namingEvidence))])
await Promise.all([writeFile(stepPath, step), writeFile(evidencePath, JSON.stringify(namingEvidence ?? null))])
const execution = spawnSync(freecad, ['--python-path', resolve(sysroot, 'usr/lib/python3/dist-packages'), resaveHarnessPath], {
cwd: root,
encoding: 'utf8',
@@ -128,6 +209,7 @@ const persistAcceptedPair = async (pair, second) => {
env: {
...process.env,
FREECAD_ORDERED_PAIR: pair,
FREECAD_PAIR_DECISION: decision,
FREECAD_PAIR_STEP_PATH: stepPath,
FREECAD_PAIR_EVIDENCE_PATH: evidencePath,
FREECAD_PAIR_OUTPUT_DIRECTORY: temporaryDirectory,
@@ -143,7 +225,7 @@ const persistAcceptedPair = async (pair, second) => {
const payload = markerIndex >= 0 ? output.slice(markerIndex + marker.length).match(/\{.*\}/s)?.[0] : undefined
if (execution.error || execution.status !== 0 || !payload) fail(`FreeCAD FCStd replay exited with ${execution.status}: ${execution.error?.message || output.trim()}`)
const persistence = JSON.parse(payload)
if (persistence.status !== 'pass' || persistence.freecadVersion !== '1.1.1' || persistence.gitCommit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d' || !Object.values(persistence.checks).every(Boolean)) fail(`FreeCAD FCStd replay failed: ${JSON.stringify(persistence)}`)
if (persistence.status !== 'pass' || persistence.decision !== decision || persistence.freecadVersion !== '1.1.1' || persistence.gitCommit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d' || !Object.values(persistence.checks).every(Boolean)) fail(`FreeCAD FCStd replay failed: ${JSON.stringify(persistence)}`)
return persistence
} finally {
await rm(temporaryDirectory, { recursive: true, force: true })
@@ -164,6 +246,41 @@ const classifyFusePair = async (toOperation, pairIndex) => {
...spec.nominal,
stage: { stageId: `pair:${pairId}:second`, ordinal: 1, objectId: `pair:${pairId}:first`, objectTag: 1101 + pairIndex * 100, resultObjectId: `pair:${pairId}:second`, resultObjectTag: 1201 + pairIndex * 100, namingEvidence: first.namingEvidence },
}
if (spec.decision === 'rejected') {
try {
const trajectory = [spec.beforeValue, spec.editedValue, spec.beforeValue]
if (spec.rejectionAuthority === 'freecad-document') {
const kernelAttempts = trajectory.map((value) => captureKernelCompatibilityStage(module, { ...secondDefinition, [spec.parameter]: value }, value, spec.kernelOutcome))
const kernelRestoredExactly = JSON.stringify(kernelAttempts[0]) === JSON.stringify(kernelAttempts[2])
if (!kernelRestoredExactly) fail(`${pair} OCCT diagnostic probe did not restore exactly.`)
const persistence = await persistPair(pair, first.response.resultStep, first.namingEvidence, 'rejected')
return {
taskId: `TSN-PAIR-fuse-${toOperation}`,
pair,
classification: 'rejected',
reasonCode: 'freecad-input-precondition-reject-with-kernel-diagnostic-and-clean-resave',
nativeDecision: 'rejected',
first: first.report,
rejection: { scope: 'second-operation-only', authority: 'freecad-document', parameter: spec.parameter, trajectory, kernelAttempts, kernelRestoredExactly, persistence },
}
}
const attempts = trajectory.map((length) => captureRejectedStage(module, { ...secondDefinition, length }))
const stableRejection = attempts.every((attempt) => rejectionSignature(attempt) === rejectionSignature(attempts[0]))
if (!stableRejection) fail(`${pair} rejection changed across its parameter trajectory.`)
const persistence = await persistPair(pair, first.response.resultStep, first.namingEvidence, 'rejected')
return {
taskId: `TSN-PAIR-fuse-${toOperation}`,
pair,
classification: 'rejected',
reasonCode: 'native-builder-and-freecad-profile-reject-with-clean-resave',
nativeDecision: 'rejected',
first: first.report,
rejection: { scope: 'second-operation-only', authority: 'occt-builder', parameter: spec.parameter, trajectory, stable: stableRejection, attempts, persistence },
}
} finally {
first.response.result?.delete?.()
}
}
const second = captureStage(module, secondDefinition)
const mutated = captureStage(module, { ...secondDefinition, ...spec.edited })
const restored = captureStage(module, secondDefinition)
@@ -171,7 +288,7 @@ const classifyFusePair = async (toOperation, pairIndex) => {
const mutationChanged = JSON.stringify(second.report.summary) !== JSON.stringify(mutated.report.summary)
const restoredExactly = JSON.stringify(second.report.summary) === JSON.stringify(restored.report.summary) && second.report.historySha256 === restored.report.historySha256 && second.report.namingSemanticSha256 === restored.report.namingSemanticSha256
if (!mutationChanged || !restoredExactly) fail(`${pair} mutation did not change and restore native geometry/history.`)
const persistence = await persistAcceptedPair(pair, second)
const persistence = await persistPair(pair, second.response.resultStep, second.namingEvidence, 'accepted')
return {
taskId: `TSN-PAIR-fuse-${toOperation}`,
pair,
@@ -190,7 +307,7 @@ const classifyFusePair = async (toOperation, pairIndex) => {
}
const classifications = []
for (const [index, operation] of ['fuse', 'cut', 'common', 'rotate'].entries()) classifications.push(await classifyFusePair(operation, index))
for (const [index, operation] of ['fuse', 'cut', 'common', 'rotate', 'pad', 'pocket', 'loft', 'pipe'].entries()) classifications.push(await classifyFusePair(operation, index))
const [artifacts, executorHarness, matrixHarness, resaveHarness] = await Promise.all([
Promise.all(artifactNames.map(async (name) => {