feat: extend FreeCAD parity queue and pair diagnostics
This commit is contained in:
@@ -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))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user