feat: extend FreeCAD parity queue and pair diagnostics
This commit is contained in:
File diff suppressed because it is too large
Load Diff
12
config/freecad-follow-up-task-progress.json
Normal file
12
config/freecad-follow-up-task-progress.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"schemaVersion": 1,
|
||||||
|
"baseline": {
|
||||||
|
"freecadVersion": "1.1.1",
|
||||||
|
"commit": "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d"
|
||||||
|
},
|
||||||
|
"policy": {
|
||||||
|
"ordering": "completed task IDs must form a contiguous prefix of the generated follow-up queue",
|
||||||
|
"evidence": "every completed task must name at least one machine-checkable artifact or focused check"
|
||||||
|
},
|
||||||
|
"completedTasks": []
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -34,7 +34,7 @@
|
|||||||
{ "id": "EX-TSN-01", "title": "Capture native stage Shape and Generated/Modified/Deleted for every builder", "priority": "P0", "status": "completed", "dependencies": ["EX-KER-01", "EX-TSN-00"], "deliverables": ["All builder-stage captures", "Multi-input and one-to-many lineage", "Local feature recompute history"], "acceptance": ["Every feature result relation has a producing native stage", "No final-result index is applied to an intermediate Shape"], "evidence": ["check:freecad-composite-history-elementmap", "check:freecad-tsn-stage-evidence"], "exactBlockedBy": [] },
|
{ "id": "EX-TSN-01", "title": "Capture native stage Shape and Generated/Modified/Deleted for every builder", "priority": "P0", "status": "completed", "dependencies": ["EX-KER-01", "EX-TSN-00"], "deliverables": ["All builder-stage captures", "Multi-input and one-to-many lineage", "Local feature recompute history"], "acceptance": ["Every feature result relation has a producing native stage", "No final-result index is applied to an intermediate Shape"], "evidence": ["check:freecad-composite-history-elementmap", "check:freecad-tsn-stage-evidence"], "exactBlockedBy": [] },
|
||||||
{ "id": "EX-TSN-02", "title": "Expose native MappedNameRef and StringHasher decisions for every feature", "priority": "P0", "status": "completed", "dependencies": ["EX-TSN-01"], "deliverables": ["Native naming callback ABI", "Per-stage StringHasher evidence", "Lossless ElementMap2 token generation"], "acceptance": ["All new tokens originate in native evidence", "FreeCAD round-trip names remain byte and semantic stable"], "evidence": ["check:freecad-native-naming-evidence", "check:freecad-sketcher-partdesign-abi", "check:freecad-naming-sdk", "check:freecad-naming-production", "check:chrome-freecad-naming-production", "check:freecad-private-naming-boundary", "check:occt-history-artifact"], "exactBlockedBy": [] },
|
{ "id": "EX-TSN-02", "title": "Expose native MappedNameRef and StringHasher decisions for every feature", "priority": "P0", "status": "completed", "dependencies": ["EX-TSN-01"], "deliverables": ["Native naming callback ABI", "Per-stage StringHasher evidence", "Lossless ElementMap2 token generation"], "acceptance": ["All new tokens originate in native evidence", "FreeCAD round-trip names remain byte and semantic stable"], "evidence": ["check:freecad-native-naming-evidence", "check:freecad-sketcher-partdesign-abi", "check:freecad-naming-sdk", "check:freecad-naming-production", "check:chrome-freecad-naming-production", "check:freecad-private-naming-boundary", "check:occt-history-artifact"], "exactBlockedBy": [] },
|
||||||
{ "id": "EX-TSN-03", "title": "Resolve isomorphic topology only from unique native provenance", "priority": "P0", "status": "completed", "dependencies": ["EX-TSN-01", "EX-TSN-02"], "deliverables": ["Symmetric Boolean corpus", "Candidate provenance graph", "Ambiguity repair lifecycle"], "acceptance": ["Unique native sources resolve deterministically", "Non-unique sources remain ambiguous across save, undo and recompute"], "evidence": ["test:topology-replay", "test:facade", "check:freecad-isomorphic-provenance", "check:freecad-naming-production", "check:chrome-freecad-naming-production", "check:freecad-exact-history-elementmap-gate"], "exactBlockedBy": [] },
|
{ "id": "EX-TSN-03", "title": "Resolve isomorphic topology only from unique native provenance", "priority": "P0", "status": "completed", "dependencies": ["EX-TSN-01", "EX-TSN-02"], "deliverables": ["Symmetric Boolean corpus", "Candidate provenance graph", "Ambiguity repair lifecycle"], "acceptance": ["Unique native sources resolve deterministically", "Non-unique sources remain ambiguous across save, undo and recompute"], "evidence": ["test:topology-replay", "test:facade", "check:freecad-isomorphic-provenance", "check:freecad-naming-production", "check:chrome-freecad-naming-production", "check:freecad-exact-history-elementmap-gate"], "exactBlockedBy": [] },
|
||||||
{ "id": "EX-TSN-04", "title": "Close topology naming across all feature edits and FCStd round-trips", "priority": "P0", "status": "in_progress", "dependencies": ["EX-TSN-02", "EX-TSN-03"], "deliverables": ["Cross-feature mutation matrix", "ElementMap2 history migration", "Long-chain LinkSub stability report"], "acceptance": ["Wrong bindings, unexplained relations and name drift are zero for the exhaustive corpus"], "evidence": ["check:freecad-isomorphic-provenance", "check:freecad-composite-history-elementmap", "check:freecad-tsn-stage-correlation", "check:freecad-fcstd-roundtrip", "check:freecad-exact-history-elementmap-gate", "check:freecad-recovered-naming-classification", "check:freecad-production-drift-classification", "check:freecad-ordered-operation-pairs"], "exactBlockedBy": ["The locked 30-case corpus passes 30/30 native property mutations, final-shape propagation and 219/219 geometry-stage restores; all 11 PartDesign recovered-naming cases/28 stages and both production restore-topology cases/5 stages are classified as allowed native evolution from independent deterministic replays with stable resave; the five-stage chain still has 3/5 generic Shape proxies and no registered rotate mutation family, while only 8/361 ordered operation pairs are classified and replayed"] }
|
{ "id": "EX-TSN-04", "title": "Close topology naming across all feature edits and FCStd round-trips", "priority": "P0", "status": "in_progress", "dependencies": ["EX-TSN-02", "EX-TSN-03"], "deliverables": ["Cross-feature mutation matrix", "ElementMap2 history migration", "Long-chain LinkSub stability report"], "acceptance": ["Wrong bindings, unexplained relations and name drift are zero for the exhaustive corpus"], "evidence": ["check:freecad-isomorphic-provenance", "check:freecad-composite-history-elementmap", "check:freecad-tsn-stage-correlation", "check:freecad-fcstd-roundtrip", "check:freecad-exact-history-elementmap-gate", "check:freecad-recovered-naming-classification", "check:freecad-production-drift-classification", "check:freecad-ordered-operation-pairs"], "exactBlockedBy": ["The locked mutation and recovery corpora have zero unclassified drift, but the five-stage chain still contains generic Shape proxies, the registered rotate mutation family is open, and the ordered operation-pair matrix remains incomplete; current counts and the unique next task are generated from config/freecad-active-work-queue.json"] }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -113,14 +113,9 @@ M3 不要求把 FreeCAD 本身拒绝的组合强行实现为顺序特征链。
|
|||||||
|
|
||||||
### 6.1 微任务队列
|
### 6.1 微任务队列
|
||||||
|
|
||||||
机器真值为 `config/freecad-active-work-queue.json`,由 `generate:freecad-active-work-queue` 从原生报告生成,并由 `check:freecad-active-work-queue` fail-closed 校验。当前共 487 个微任务:134 `completed`、1 `in_progress`、352 `pending`;`maxInProgress=1`,唯一下一任务是 `TSN-PAIR-fuse-pad`。
|
机器真值为 `config/freecad-active-work-queue.json`,由 `generate:freecad-active-work-queue` 从原生报告、Property 语义报告、exact 计划和后续进度账本生成,并由 `check:freecad-active-work-queue` fail-closed 校验。实时任务数、里程碑状态和唯一下一任务只发布在 `docs/freecad-active-work-status.generated.zh-CN.md`,本手写文档不再复制动态数字。
|
||||||
|
|
||||||
| 微里程碑 | 任务粒度 | 数量 | 状态 | 下一退出条件 |
|
队列现在覆盖 GUI oracle、TSN 漂移、全部有序操作对、逐 Property codec、Document 语义、核心建模、FCStd/格式、GUI 全量、文档/工程工作台、平台/脚本/代理和 QA/发布。每个后续能力拆为 A-I 九个串行任务;完成证据记录在 `config/freecad-follow-up-task-progress.json`,跳号、未知任务、空证据和第二个活动任务都会使生成器失败。
|
||||||
| --- | --- | ---: | --- | --- |
|
|
||||||
| `ORA-GUI-BASELINE` | 1 个配置边界或 1 个 GUI 命令分片 | 106 | 106 completed | 已关闭:100/100 分片、1111/1111 命令状态、2 个上下文延迟命令均有证据 |
|
|
||||||
| `ORA-GUI-WORKFLOWS` | 1 个命令族的 1 种工作流状态 | 7 | 7 completed | `PartDesign_Pad` 五态原生工作流、聚焦断言和 exact blocker 更新已关闭 |
|
|
||||||
| `TSN-RECOVERY-DRIFT` | 1 个原生 case 或 1 个生产 mutation | 13 | 13 completed | 11 个 PartDesign case 与生产 `cut`/`rotate` 漂移均已分类为允许演进 |
|
|
||||||
| `TSN-ORDERED-PAIRS` | 1 个有序操作对 | 361 | 8 completed / 1 in_progress / 352 pending | `fuse->rotate` 已关闭;当前只分类 `fuse->pad` |
|
|
||||||
|
|
||||||
`ORA-GUI-WORKFLOWS` 已按七步关闭:选定命令族、成功、禁用、失败、取消、恢复、更新 blocker/门禁。一步只新增一种原生状态证据,前一步未通过时不启动后一步。
|
`ORA-GUI-WORKFLOWS` 已按七步关闭:选定命令族、成功、禁用、失败、取消、恢复、更新 blocker/门禁。一步只新增一种原生状态证据,前一步未通过时不启动后一步。
|
||||||
|
|
||||||
|
|||||||
28
docs/freecad-active-work-status.generated.zh-CN.md
Normal file
28
docs/freecad-active-work-status.generated.zh-CN.md
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<!-- Generated by scripts/generate-freecad-active-work-queue.mjs. Do not edit. -->
|
||||||
|
# FreeCAD 后续工作机器状态
|
||||||
|
|
||||||
|
生成日期:2026-08-15。基线:FreeCAD 1.1.1 / `0108fd4b4850cc46e625b60e53cea7a7bbe69f8d`。
|
||||||
|
|
||||||
|
- 唯一活动任务:`TSN-PAIR-fuse-revolution`
|
||||||
|
- 微任务:1756 total / 138 completed / 1 in_progress / 1617 pending
|
||||||
|
- 有序操作对:12/361 classified / 349 unknown
|
||||||
|
- Property:27/85 native types / 58 opaque types
|
||||||
|
- exact 任务:5/52 completed
|
||||||
|
- 模块能力:0 exact / 26 compatible / 5 proxy / 3 development
|
||||||
|
|
||||||
|
| 里程碑 | 任务数 | completed | in_progress | pending | 当前任务 |
|
||||||
|
| --- | ---: | ---: | ---: | ---: | --- |
|
||||||
|
| `ORA-GUI-BASELINE` | 106 | 106 | 0 | 0 | `-` |
|
||||||
|
| `ORA-GUI-WORKFLOWS` | 7 | 7 | 0 | 0 | `-` |
|
||||||
|
| `TSN-RECOVERY-DRIFT` | 13 | 13 | 0 | 0 | `-` |
|
||||||
|
| `TSN-ORDERED-PAIRS` | 361 | 12 | 1 | 348 | `TSN-PAIR-fuse-revolution` |
|
||||||
|
| `PROPERTY-CODECS` | 522 | 0 | 0 | 522 | `-` |
|
||||||
|
| `DOCUMENT-SEMANTICS` | 90 | 0 | 0 | 90 | `-` |
|
||||||
|
| `CORE-MODELING` | 171 | 0 | 0 | 171 | `-` |
|
||||||
|
| `FCSTD-AND-FORMATS` | 72 | 0 | 0 | 72 | `-` |
|
||||||
|
| `GUI-CLOSURE` | 135 | 0 | 0 | 135 | `-` |
|
||||||
|
| `DOCUMENT-ENGINEERING-WORKBENCHES` | 90 | 0 | 0 | 90 | `-` |
|
||||||
|
| `PLATFORM-SCRIPT-PROXY` | 117 | 0 | 0 | 117 | `-` |
|
||||||
|
| `QA-RELEASE` | 72 | 0 | 0 | 72 | `-` |
|
||||||
|
|
||||||
|
详细任务、依赖、阶段、证据要求和退出条件见 `config/freecad-active-work-queue.json`。完成证据只能追加到 `config/freecad-follow-up-task-progress.json`,生成器会拒绝跳号、未知任务和无证据完成项。
|
||||||
55
docs/freecad-follow-up-execution-plan.zh-CN.md
Normal file
55
docs/freecad-follow-up-execution-plan.zh-CN.md
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
# FreeCAD 后续工作细粒度执行计划
|
||||||
|
|
||||||
|
## 1. 机器真值
|
||||||
|
|
||||||
|
- `config/freecad-active-work-queue.json`:全部微任务、依赖、阶段、证据要求、退出条件和唯一活动任务。
|
||||||
|
- `docs/freecad-active-work-status.generated.zh-CN.md`:由同一生成器产生的实时摘要;禁止手工编辑或在其他计划中复制动态数字。
|
||||||
|
- `config/freecad-follow-up-task-progress.json`:A-I 后续任务的完成证据账本;只能追加连续前缀。
|
||||||
|
- `config/freecad-ordered-operation-pair-classification.json`:19×19 操作对的原生接受/拒绝事实。
|
||||||
|
|
||||||
|
`./npmw run generate:freecad-active-work-queue` 同时生成队列和状态页;`./npmw run check:freecad-active-work-queue` 对两个产物 fail-closed。任何时刻必须恰好有一个 `in_progress` 任务。
|
||||||
|
|
||||||
|
## 2. 里程碑顺序
|
||||||
|
|
||||||
|
1. `ORA-GUI-BASELINE`:原生 GUI 命令清册和隔离探针。
|
||||||
|
2. `ORA-GUI-WORKFLOWS`:命令族成功、禁用、失败、取消和恢复。
|
||||||
|
3. `TSN-RECOVERY-DRIFT`:恢复命名与生产拓扑漂移分类。
|
||||||
|
4. `TSN-ORDERED-PAIRS`:19×19 操作对逐对接受/拒绝、变异、命名和往返。
|
||||||
|
5. `PROPERTY-CODECS`:从原生语义报告自动枚举所有 opaque Property 类型。
|
||||||
|
6. `DOCUMENT-SEMANTICS`:observer、事务、DAG、失败恢复、Extension、FeaturePython 和多文档。
|
||||||
|
7. `CORE-MODELING`:19 个生产建模操作的纵向闭环。
|
||||||
|
8. `FCSTD-AND-FORMATS`:FCStd 资源、对抗输入和 STEP/IGES/BREP 双向往返。
|
||||||
|
9. `GUI-CLOSURE`:应用壳、MDI、树、Task、属性编辑器、对话框、焦点、选择和响应式无障碍。
|
||||||
|
10. `DOCUMENT-ENGINEERING-WORKBENCHES`:Draft、Spreadsheet、TechDraw、Plot、Assembly、BIM、Surface、Mesh 和 Material。
|
||||||
|
11. `PLATFORM-SCRIPT-PROXY`:FEM、CAM、Robot、数据模块、Python/Macro、Addon 及正式代理边界。
|
||||||
|
12. `QA-RELEASE`:浏览器/OS/GPU、无障碍/locale、fuzz/长稳、资源、签名发布和 exact promotion。
|
||||||
|
|
||||||
|
前一里程碑未完成时,后一里程碑全部保持 `pending`。只读 oracle 可以准备,但不能写入第二个活动任务或提前提升能力。
|
||||||
|
|
||||||
|
## 3. 每个能力的九个最小任务
|
||||||
|
|
||||||
|
| 阶段 | 单一交付物 | 退出条件 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| A | 原生清册与前置 | TypeId、输入、默认值、依赖和适用性机器可读 |
|
||||||
|
| B | 原生成功证据 | 名义值和边界值的值、Shape、拓扑、状态与诊断齐全 |
|
||||||
|
| C | 原生失败/取消证据 | 无效输入、禁用、取消和文档未污染可复现 |
|
||||||
|
| D | 参数变异/恢复 | 编辑、重算、恢复后的几何与语义完成分类 |
|
||||||
|
| E | Facade 实现 | 生产路径无隐藏 fallback,UI 不直连内核 |
|
||||||
|
| F | 事务恢复 | abort/undo/redo/stale/失败和资源所有权闭合 |
|
||||||
|
| G | FCStd 往返 | FreeCAD-Web-FreeCAD 无未知语义漂移 |
|
||||||
|
| H | 真实浏览器 | Chrome Worker、OPFS、UI 状态和资源释放通过 |
|
||||||
|
| I | 能力提升 | A-H 全过,聚焦门禁与 exact blocker 由机器报告同步 |
|
||||||
|
|
||||||
|
每个阶段只允许一个主要产物和一个聚焦检查。缺少任一阶段时,能力最多保持 `compatible` 或既有的显式 proxy 等级。
|
||||||
|
|
||||||
|
## 4. 单轮执行协议
|
||||||
|
|
||||||
|
1. 读取队列的 `nextTask`,确认依赖和输入 artifact 哈希。
|
||||||
|
2. 只修改该任务直接需要的 oracle、实现、检查器和报告。
|
||||||
|
3. 先运行原生 probe 或真实浏览器 test,再运行对应只读 check。
|
||||||
|
4. 验证 Shape/Worker/OPFS 等资源回到基线,失败不污染文档。
|
||||||
|
5. 运行相关 Facade、FCStd、TSN/执行计划门禁。
|
||||||
|
6. 仅在退出条件全部满足后记录完成证据并重新生成队列。
|
||||||
|
7. 运行 `git diff --check`、全部 JSON 解析和与风险相称的回归;发布点运行完整 `./npmw run verify`。
|
||||||
|
|
||||||
|
原生拒绝是有效分类,不应被强行实现为接受;但必须固定失败阶段、诊断、事务回滚和干净往返。原生接受则必须使用真实 builder,不能用通用 Shape 代理冒充。
|
||||||
@@ -50,29 +50,14 @@
|
|||||||
- Property status:15 类已观察状态均已进入类型化 Facade 行为;`LockDynamic` 和 `PartialTrigger` 已有原生事务证据,但完整 observer 与局部重算序列未关闭。
|
- Property status:15 类已观察状态均已进入类型化 Facade 行为;`LockDynamic` 和 `PartialTrigger` 已有原生事务证据,但完整 observer 与局部重算序列未关闭。
|
||||||
- TSN:30/30 原生 mutation、219/219 几何阶段恢复通过;11 个 PartDesign case/28 个恢复命名阶段均已通过第二次独立原生复跑分类为允许演进。
|
- TSN:30/30 原生 mutation、219/219 几何阶段恢复通过;11 个 PartDesign case/28 个恢复命名阶段均已通过第二次独立原生复跑分类为允许演进。
|
||||||
- 生产命名链:五阶段 5/5 几何恢复,2 个恢复拓扑 case/5 个下游阶段已分类为允许演进;仍有 3/5 通用 Shape 代理并缺注册 rotate mutation family。
|
- 生产命名链:五阶段 5/5 几何恢复,2 个恢复拓扑 case/5 个下游阶段已分类为允许演进;仍有 3/5 通用 Shape 代理并缺注册 rotate mutation family。
|
||||||
- 有序操作:19×19 共 361 对,8 对已有分类回放,353 对待原生接受/拒绝判定。
|
- 有序操作:19×19 的逐对分类持续执行;实时已完成数、未知数和唯一活动项只从生成状态读取。
|
||||||
- FCStd:支持的原生 codec 与未知资源保留已有证据,但所有对象/属性/Extension/Shape/ElementMap2/StringHasher 的 FreeCAD-Web-FreeCAD 双向变异未穷尽。
|
- FCStd:支持的原生 codec 与未知资源保留已有证据,但所有对象/属性/Extension/Shape/ElementMap2/StringHasher 的 FreeCAD-Web-FreeCAD 双向变异未穷尽。
|
||||||
- GUI:应用壳和代表性工作流已有浏览器证据,但动态命令、Task、对话框、焦点、上下文和逐工作台流程没有完整原生对照。
|
- GUI:应用壳和代表性工作流已有浏览器证据,但动态命令、Task、对话框、焦点、上下文和逐工作台流程没有完整原生对照。
|
||||||
- 发布:系统级 exact 为 false,`EX-REL-01` 为 pending,跨浏览器、长期运行和全模块 exact promotion 尚未开始。
|
- 发布:系统级 exact 为 false,`EX-REL-01` 为 pending,跨浏览器、长期运行和全模块 exact promotion 尚未开始。
|
||||||
|
|
||||||
### 2.4 可机器核对的完成度仪表盘
|
### 2.4 可机器核对的完成度仪表盘
|
||||||
|
|
||||||
| 范围 | 当前值 | 已完成代表什么 | 下一个可关闭单元 |
|
实时仪表盘为 `docs/freecad-active-work-status.generated.zh-CN.md`,由 `generate:freecad-active-work-queue` 同活动队列一起生成,并由 `check:freecad-active-work-queue` 逐字校验。此处不再复制任务数、完成数、操作对覆盖数、Property 覆盖数或下一任务;任何手工状态摘要均不具备治理效力。
|
||||||
| --- | --- | --- | --- |
|
|
||||||
| 兼容实现计划 | 83/83 completed | 已有可运行 Web 实现、门禁和能力边界 | 不再用该数字表示 exact 进度 |
|
|
||||||
| exact 计划 | 5/52 completed,36 in_progress,11 pending | 仅 `EX-TSN-00..03` 与 `EX-PD-02` 达到自身验收范围 | `EX-TSN-04` 的恢复漂移与有序组合 |
|
|
||||||
| 模块能力 | 0 exact / 26 compatible / 5 proxy / 3 development | 34 模块均有明确对外等级,无 unexplained | 任一模块提升前必须关闭其 exact task |
|
|
||||||
| 原生 GUI 清册 | 1111 runtime / 19 source-only / 2 context-only | 命令 ID 和三种激活上下文已分类 | 选择下一命令族并逐个关闭五态 |
|
|
||||||
| Pad GUI 工作流 | 7/7 completed | success/disabled/failure/cancel/recovery 和 blocker 已对齐 | 不因单命令族完成而关闭 `EX-ORA-01` |
|
|
||||||
| TSN 30-case corpus | 30/30 mutation,219/219 几何恢复 | 几何传播与恢复已闭环 | 逐 case 解释 11 个命名漂移 |
|
|
||||||
| TSN 恢复命名 | 11/11 case,28/28 stage 已分类 | 全部具备第二次原生复跑与稳定 resave | 已关闭该队列,保持分类门禁 |
|
|
||||||
| 生产五阶段链 | 5/5 几何恢复,2/2 漂移 case 已分类,2/5 原生 builder | 漂移已解释且不错绑 | 逐 stage 去代理并补 rotate mutation family |
|
|
||||||
| 19x19 有序组合 | 8/361 已分类回放 | 4 条五阶段转换与 4 条 `fuse->*` 已覆盖 | `TSN-PAIR-fuse-pad` |
|
|
||||||
| Property codec | 27/85 类原生 codec,58 类 opaque | 4987/5510 记录可结构化处理 | 按 M04 顺序一次关闭 1 类 |
|
|
||||||
| FCStd | 23 个往返场景,未知差异 0 | 已支持范围内的当前样本稳定 | 对每种对象/Property/资源做双向 mutation |
|
|
||||||
| 活动微任务 | 487 项:134 completed / 1 in_progress / 352 pending | 队列满足 `maxInProgress=1` | 只允许执行 `TSN-PAIR-fuse-pad` |
|
|
||||||
|
|
||||||
仪表盘的任何数字变化都必须由配置/报告生成器证明;文档不能单独改变状态。
|
|
||||||
|
|
||||||
## 3. 目标架构和不可破坏边界
|
## 3. 目标架构和不可破坏边界
|
||||||
|
|
||||||
@@ -161,7 +146,7 @@ FCStd / STEP / IGES / mesh / CAM / engineering resources
|
|||||||
|
|
||||||
每项必须得到且只能得到一种结论:FreeCAD 稳定语义、允许的命名演进、实现缺陷。实现缺陷必须增加回归测试;未知不能作为结论。
|
每项必须得到且只能得到一种结论:FreeCAD 稳定语义、允许的命名演进、实现缺陷。实现缺陷必须增加回归测试;未知不能作为结论。
|
||||||
|
|
||||||
每个 case 再拆成六个不计入 487 项队列总数、但必须依次完成的最小检查点:
|
每个 case 再拆成六个必须依次完成的最小检查点;后续能力的 A-I 检查点已经全部进入生成队列:
|
||||||
|
|
||||||
| 检查点 | 唯一产物 | fail-closed 验收 |
|
| 检查点 | 唯一产物 | fail-closed 验收 |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
@@ -174,7 +159,7 @@ FCStd / STEP / IGES / mesh / CAM / engineering resources
|
|||||||
|
|
||||||
任一检查点失败时,当前任务保持 `in_progress`,不得领取下一 case。如果分类为 `implementation_defect`,还必须在同一队列项内完成最小修复、回归测试、原生/Web 对照和资源释放,不另行跳过。
|
任一检查点失败时,当前任务保持 `in_progress`,不得领取下一 case。如果分类为 `implementation_defect`,还必须在同一队列项内完成最小修复、回归测试、原生/Web 对照和资源释放,不另行跳过。
|
||||||
|
|
||||||
### 5.4 TSN 有序操作对,361 项,8 完成/1 进行中/352 待办
|
### 5.4 TSN 有序操作对
|
||||||
|
|
||||||
操作全集为 Fuse、Cut、Common、Rotate、Pad、Pocket、Loft、Pipe、Revolution、Groove、Fillet、Chamfer、Hole、Draft、Thickness、LinearPattern、PolarPattern、Mirrored、MultiTransform。
|
操作全集为 Fuse、Cut、Common、Rotate、Pad、Pocket、Loft、Pipe、Revolution、Groove、Fillet、Chamfer、Hole、Draft、Thickness、LinearPattern、PolarPattern、Mirrored、MultiTransform。
|
||||||
|
|
||||||
@@ -189,7 +174,7 @@ FCStd / STEP / IGES / mesh / CAM / engineering resources
|
|||||||
7. 在生产 Worker 回放并检查释放。
|
7. 在生产 Worker 回放并检查释放。
|
||||||
8. 拒绝时固定原生错误类型和文档未污染证据。
|
8. 拒绝时固定原生错误类型和文档未污染证据。
|
||||||
|
|
||||||
已覆盖的八对为 `cut->rotate`、`rotate->fillet`、`fillet->mirrored`、`mirrored->linear-pattern`、`fuse->fuse`、`fuse->cut`、`fuse->common`、`fuse->rotate`。当前只处理 `fuse->pad`,其余严格按机器队列逐对领取。
|
初始五阶段转换和 `fuse->*` 连续前缀均已进入机器分类账本;实时覆盖集合与当前操作对严格从生成状态和 `config/freecad-ordered-operation-pair-classification.json` 读取,其余逐对领取。
|
||||||
|
|
||||||
## 6. 后续关键路径微里程碑
|
## 6. 后续关键路径微里程碑
|
||||||
|
|
||||||
@@ -227,7 +212,7 @@ FCStd / STEP / IGES / mesh / CAM / engineering resources
|
|||||||
|
|
||||||
### M03 `EX-TSN-04B` builder 与 19×19 转换矩阵
|
### M03 `EX-TSN-04B` builder 与 19×19 转换矩阵
|
||||||
|
|
||||||
状态:`in_progress`;已覆盖 8/361 对,当前执行 `TSN-PAIR-fuse-pad`。
|
状态:`in_progress`;实时覆盖数和当前操作对见生成状态。
|
||||||
|
|
||||||
- M03.1:用真实 builder 替换五阶段链中 Cut 之后的通用 Shape 代理,一次替换一个 stage。
|
- M03.1:用真实 builder 替换五阶段链中 Cut 之后的通用 Shape 代理,一次替换一个 stage。
|
||||||
- M03.2:增加 Rotate 原生 mutation family。
|
- M03.2:增加 Rotate 原生 mutation family。
|
||||||
@@ -422,6 +407,6 @@ M00
|
|||||||
|
|
||||||
`TSN-DRIFT-003` 已完成:`partdesign-taper` 的 `Length 6 -> 6.75 -> 6` 在 Body13 和 Pad13 两个阶段恢复相同几何,关系与语义命名摘要演进为新的稳定历史。锁定报告与第二次独立 FreeCAD 1.1.1 复跑的逐阶段摘要一致,演进值在 save/reopen/resave 中保持,因此同样分类为 `allowed_evolution`,不是 Web 实现缺陷。
|
`TSN-DRIFT-003` 已完成:`partdesign-taper` 的 `Length 6 -> 6.75 -> 6` 在 Body13 和 Pad13 两个阶段恢复相同几何,关系与语义命名摘要演进为新的稳定历史。锁定报告与第二次独立 FreeCAD 1.1.1 复跑的逐阶段摘要一致,演进值在 save/reopen/resave 中保持,因此同样分类为 `allowed_evolution`,不是 Web 实现缺陷。
|
||||||
|
|
||||||
`TSN-DRIFT-004..010` 与生产 `cut`/`rotate` 漂移均已完成独立复跑,并唯一分类为 `allowed_evolution`;恢复漂移队列因此以 13/13 关闭。`fuse->fuse`、`fuse->cut`、`fuse->common`、`fuse->rotate` 的新增分类与既有五阶段四条转换合计覆盖 8/361 对。
|
`TSN-DRIFT-004..010` 与生产 `cut`/`rotate` 漂移均已完成独立复跑,并唯一分类为 `allowed_evolution`;恢复漂移队列因此关闭。
|
||||||
|
|
||||||
当前唯一下一任务为 `TSN-PAIR-fuse-pad`。退出条件是记录原生接受或拒绝;接受时必须具备真实 builder、mutation/restore、history/naming 与 FreeCAD save/reopen/resave 证据,拒绝时必须固定原生错误与无文档污染证据。
|
`fuse->pad` 已由原生 OCCT builder 和 FreeCAD `PartDesign::Pad` 文档路径稳定拒绝,事务 abort 后对象集与源 Shape 恢复,干净 FCStd save/reopen/resave 不受污染。`fuse->pocket` 已由真实 `BRepPrimAPI_MakePrism+BRepAlgoAPI_Cut` 接受,长度变异与恢复、history/naming 和 FCStd 三阶段均通过。唯一下一任务不在此手工记录,只读取生成状态。
|
||||||
|
|||||||
@@ -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.`)
|
if (bytes !== artifact.bytes || sha256(content) !== artifact.sha256) fail(`${artifact.name} provenance is stale.`)
|
||||||
}
|
}
|
||||||
const expectedPairs = [
|
const expectedPairs = [
|
||||||
{ pair: 'fuse->fuse', secondBuilder: 'BRepAlgoAPI_Fuse', secondInputCount: 2, mutationParameter: 'toolOffsetX', mutationTrajectory: [12, 11, 12] },
|
{ 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', secondBuilder: 'BRepAlgoAPI_Cut', secondInputCount: 2, mutationParameter: 'toolSize', mutationTrajectory: [4, 3, 4] },
|
{ 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', secondBuilder: 'BRepAlgoAPI_Common', secondInputCount: 2, mutationParameter: 'toolOffsetX', mutationTrajectory: [10, 9, 10] },
|
{ 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', secondBuilder: 'BRepBuilderAPI_Transform', secondInputCount: 1, mutationParameter: 'angle', mutationTrajectory: [15, 22.5, 15] },
|
{ 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.')
|
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()) {
|
for (const [index, entry] of report.classifications.entries()) {
|
||||||
const expected = expectedPairs[index]
|
const expected = expectedPairs[index]
|
||||||
const secondOperation = expected.pair.split('->')[1]
|
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.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 (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 (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.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.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.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.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
|
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.`)
|
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.')
|
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))
|
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))
|
||||||
|
|||||||
@@ -14,12 +14,11 @@ def version_text():
|
|||||||
return ".".join(str(value) for value in App.Version()[:3])
|
return ".".join(str(value) for value in App.Version()[:3])
|
||||||
|
|
||||||
|
|
||||||
def shape_snapshot(feature):
|
def shape_snapshot_from_shape(shape, include_brep=False):
|
||||||
shape = feature.Shape
|
|
||||||
if shape.isNull():
|
if shape.isNull():
|
||||||
raise RuntimeError("ordered pair produced a null Shape")
|
raise RuntimeError("ordered pair produced a null Shape")
|
||||||
bounds = shape.BoundBox
|
bounds = shape.BoundBox
|
||||||
return {
|
snapshot = {
|
||||||
"valid": bool(shape.isValid()),
|
"valid": bool(shape.isValid()),
|
||||||
"solids": len(shape.Solids),
|
"solids": len(shape.Solids),
|
||||||
"faces": len(shape.Faces),
|
"faces": len(shape.Faces),
|
||||||
@@ -29,6 +28,15 @@ def shape_snapshot(feature):
|
|||||||
"area": round(float(shape.Area), 7),
|
"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)],
|
"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):
|
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"]
|
step_path = os.environ["FREECAD_PAIR_STEP_PATH"]
|
||||||
evidence_path = os.environ["FREECAD_PAIR_EVIDENCE_PATH"]
|
evidence_path = os.environ["FREECAD_PAIR_EVIDENCE_PATH"]
|
||||||
pair = os.environ["FREECAD_ORDERED_PAIR"]
|
pair = os.environ["FREECAD_ORDERED_PAIR"]
|
||||||
@@ -85,6 +121,7 @@ def collect():
|
|||||||
"freecadVersion": version_text(),
|
"freecadVersion": version_text(),
|
||||||
"gitCommit": FREECAD_COMMIT,
|
"gitCommit": FREECAD_COMMIT,
|
||||||
"pair": pair,
|
"pair": pair,
|
||||||
|
"decision": "accepted",
|
||||||
"phases": {"initial": initial, "reopened": reopened_snapshot, "resaved": resaved_snapshot},
|
"phases": {"initial": initial, "reopened": reopened_snapshot, "resaved": resaved_snapshot},
|
||||||
"checks": {
|
"checks": {
|
||||||
"shapeStable": initial["shape"] == reopened_snapshot["shape"] and initial["shape"] == resaved_snapshot["shape"],
|
"shapeStable": initial["shape"] == reopened_snapshot["shape"] and initial["shape"] == resaved_snapshot["shape"],
|
||||||
@@ -97,8 +134,162 @@ def collect():
|
|||||||
App.closeDocument(name)
|
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:
|
try:
|
||||||
report = collect()
|
report = collect_rejected() if os.environ.get("FREECAD_PAIR_DECISION") == "rejected" else collect_accepted()
|
||||||
except Exception as error:
|
except Exception as error:
|
||||||
report = {
|
report = {
|
||||||
"schemaVersion": 1,
|
"schemaVersion": 1,
|
||||||
|
|||||||
@@ -3,8 +3,9 @@ import { resolve } from 'node:path'
|
|||||||
|
|
||||||
const root = resolve(new URL('..', import.meta.url).pathname)
|
const root = resolve(new URL('..', import.meta.url).pathname)
|
||||||
const outputPath = resolve(root, 'config/freecad-active-work-queue.json')
|
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 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('.cache/freecad/reference-desktop-gui-commands.json'),
|
||||||
load('config/freecad-gui-workflow-plan.json'),
|
load('config/freecad-gui-workflow-plan.json'),
|
||||||
load('config/freecad-composite-history-elementmap-oracle.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-recovered-naming-classification.json'),
|
||||||
load('config/freecad-production-drift-classification.json'),
|
load('config/freecad-production-drift-classification.json'),
|
||||||
load('config/freecad-ordered-operation-pair-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) => ({
|
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 = [
|
const setupTasks = [
|
||||||
{ id: 'ORA-GUI-SETUP-000', title: 'Split GUI state probing from object/property probing', status: 'completed' },
|
{ 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'] },
|
{ 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-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: '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-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 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 count = (status) => allTasks.filter((task) => task.status === status).length
|
||||||
const nextTask = allTasks.find((task) => task.status === 'in_progress')?.id
|
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 (!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 = {
|
const queue = {
|
||||||
schemaVersion: 1,
|
schemaVersion: 1,
|
||||||
baseline: { freecadVersion: '1.1.1', commit: '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d' },
|
baseline: { freecadVersion: '1.1.1', commit: '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d' },
|
||||||
@@ -138,17 +270,46 @@ const queue = {
|
|||||||
unit: 'one artifact, one focused check, one explicit exit condition',
|
unit: 'one artifact, one focused check, one explicit exit condition',
|
||||||
transitionUnit: 'one ordered operation pair',
|
transitionUnit: 'one ordered operation pair',
|
||||||
driftUnit: 'one native case',
|
driftUnit: 'one native case',
|
||||||
|
capabilityDefinition: completionSteps.map(({ id, title }) => `${id}: ${title}`),
|
||||||
|
followUpProgress: 'config/freecad-follow-up-task-progress.json',
|
||||||
},
|
},
|
||||||
nextTask,
|
nextTask,
|
||||||
summary: { tasks: allTasks.length, completed: count('completed'), inProgress: count('in_progress'), pending: count('pending') },
|
summary: { tasks: allTasks.length, completed: count('completed'), inProgress: count('in_progress'), pending: count('pending') },
|
||||||
milestones,
|
milestones,
|
||||||
}
|
}
|
||||||
const content = `${JSON.stringify(queue, null, 2)}\n`
|
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')) {
|
if (process.argv.includes('--check')) {
|
||||||
const current = await readFile(outputPath, 'utf8').catch(() => '')
|
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.')
|
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))
|
console.log(JSON.stringify({ status: 'freecad-active-work-queue-pass', nextTask: queue.nextTask, ...queue.summary }, null, 2))
|
||||||
} else {
|
} else {
|
||||||
await writeFile(outputPath, content)
|
await Promise.all([writeFile(outputPath, content), writeFile(statusOutputPath, statusContent)])
|
||||||
console.log(JSON.stringify({ status: 'freecad-active-work-queue-generated', output: outputPath, nextTask: queue.nextTask, ...queue.summary }, null, 2))
|
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))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 },
|
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 },
|
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 },
|
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 baseStep = objectStep ?? shapeStep(module, 'makeBox', 10, 10, 10)
|
||||||
const objectInput = input('object', 'object', stage.objectId, stage.objectTag, baseStep, stage.namingEvidence)
|
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 === '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 toolStep = shapeStep(module, 'makeBoxPlaced', toolSize, toolSize, toolSize, toolOffset, 0, 0)
|
||||||
const inputs = [
|
const inputs = [
|
||||||
objectInput,
|
objectInput,
|
||||||
@@ -64,6 +90,61 @@ const createOperation = (module, { operation, objectStep, toolSize, toolOffset,
|
|||||||
return { response: module.booleanHistoryFromStep(baseStep, toolStep, operation), inputs }
|
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 captureStage = (module, definition) => {
|
||||||
const { response, inputs } = createOperation(module, definition)
|
const { response, inputs } = createOperation(module, definition)
|
||||||
try {
|
try {
|
||||||
@@ -114,12 +195,12 @@ const captureStage = (module, definition) => {
|
|||||||
|
|
||||||
const createModule = (await import(pathToFileURL(modulePath).href)).default
|
const createModule = (await import(pathToFileURL(modulePath).href)).default
|
||||||
const module = await createModule({ locateFile: (path) => resolve(dist, path) })
|
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-'))
|
const temporaryDirectory = await mkdtemp(join(tmpdir(), 'freecad-ordered-pair-'))
|
||||||
try {
|
try {
|
||||||
const stepPath = join(temporaryDirectory, 'pair-result.step')
|
const stepPath = join(temporaryDirectory, 'pair-result.step')
|
||||||
const evidencePath = join(temporaryDirectory, 'pair-naming.json')
|
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], {
|
const execution = spawnSync(freecad, ['--python-path', resolve(sysroot, 'usr/lib/python3/dist-packages'), resaveHarnessPath], {
|
||||||
cwd: root,
|
cwd: root,
|
||||||
encoding: 'utf8',
|
encoding: 'utf8',
|
||||||
@@ -128,6 +209,7 @@ const persistAcceptedPair = async (pair, second) => {
|
|||||||
env: {
|
env: {
|
||||||
...process.env,
|
...process.env,
|
||||||
FREECAD_ORDERED_PAIR: pair,
|
FREECAD_ORDERED_PAIR: pair,
|
||||||
|
FREECAD_PAIR_DECISION: decision,
|
||||||
FREECAD_PAIR_STEP_PATH: stepPath,
|
FREECAD_PAIR_STEP_PATH: stepPath,
|
||||||
FREECAD_PAIR_EVIDENCE_PATH: evidencePath,
|
FREECAD_PAIR_EVIDENCE_PATH: evidencePath,
|
||||||
FREECAD_PAIR_OUTPUT_DIRECTORY: temporaryDirectory,
|
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
|
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()}`)
|
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)
|
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
|
return persistence
|
||||||
} finally {
|
} finally {
|
||||||
await rm(temporaryDirectory, { recursive: true, force: true })
|
await rm(temporaryDirectory, { recursive: true, force: true })
|
||||||
@@ -164,6 +246,41 @@ const classifyFusePair = async (toOperation, pairIndex) => {
|
|||||||
...spec.nominal,
|
...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 },
|
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 second = captureStage(module, secondDefinition)
|
||||||
const mutated = captureStage(module, { ...secondDefinition, ...spec.edited })
|
const mutated = captureStage(module, { ...secondDefinition, ...spec.edited })
|
||||||
const restored = captureStage(module, secondDefinition)
|
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 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
|
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.`)
|
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 {
|
return {
|
||||||
taskId: `TSN-PAIR-fuse-${toOperation}`,
|
taskId: `TSN-PAIR-fuse-${toOperation}`,
|
||||||
pair,
|
pair,
|
||||||
@@ -190,7 +307,7 @@ const classifyFusePair = async (toOperation, pairIndex) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const classifications = []
|
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([
|
const [artifacts, executorHarness, matrixHarness, resaveHarness] = await Promise.all([
|
||||||
Promise.all(artifactNames.map(async (name) => {
|
Promise.all(artifactNames.map(async (name) => {
|
||||||
|
|||||||
132
后续工作.txt
Normal file
132
后续工作.txt
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
• 本项目应继续以锁定的 FreeCAD 1.1.1 为基线。当前已有可运行的 Web CAD 兼
|
||||||
|
容基线,但距离“全部功能对标”仍较远:exact 任务为 5/52 completed,34 个
|
||||||
|
模块中 0 exact / 26 compatible / 5 proxy / 3 development。
|
||||||
|
|
||||||
|
详细机器账本见 config/freecad-web-exact-parity-plan.json:1,当前执行总
|
||||||
|
览见 docs/freecad-web-execution-master-plan.zh-CN.md:194。
|
||||||
|
|
||||||
|
需要完善的工作
|
||||||
|
|
||||||
|
工作域 当前主要缺口 完成标准
|
||||||
|
━━━━━━━━━━━━━━━━━ ━━━━━━━━━━━━━━━━━━━━━━━━ ━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
原生 Oracle 1111 个命令已完成三态 每个 TypeId、Property、
|
||||||
|
清册,但只有 Pad 命令 Command、Task 和对话框
|
||||||
|
族完成成功、禁用、失 都有原生成功与负向证据
|
||||||
|
败、取消、恢复全流程
|
||||||
|
───────────────── ──────────────────────── ─────────────────────────
|
||||||
|
OCCT 几何内核 部分 builder 参数、容 每个操作的 Shape、质量
|
||||||
|
差、校验和失败诊断未穷 属性、拓扑、容差、错误
|
||||||
|
尽;原生 history 和资源释放与 FreeCAD 一
|
||||||
|
provider 尚未成为完整 致
|
||||||
|
默认链路
|
||||||
|
───────────────── ──────────────────────── ─────────────────────────
|
||||||
|
拓扑命名 仍有 7 个 PartDesign 零错绑、零未知漂移、
|
||||||
|
case/20 个恢复阶段未分 361/361 组合分类、允许
|
||||||
|
类;五阶段链存在 3 个 组合全部使用真实
|
||||||
|
通用 Shape 代理;361 builder
|
||||||
|
个有序操作对只完成 4
|
||||||
|
个
|
||||||
|
───────────────── ──────────────────────── ─────────────────────────
|
||||||
|
Property 系统 85 种 Property 中仍有 默认值、边界值、状态、
|
||||||
|
58 种/523 条记录只能 编辑器、Undo/Redo、
|
||||||
|
opaque 保存 FCStd、Chrome 回放全部
|
||||||
|
闭合
|
||||||
|
───────────────── ──────────────────────── ─────────────────────────
|
||||||
|
Document/App observer 顺序、嵌套事 事务、DAG、失败恢复、链
|
||||||
|
务、局部重算、 接和对象生命周期与原生
|
||||||
|
Extension、 事件序列一致
|
||||||
|
FeaturePython、多文档
|
||||||
|
和合并恢复不完整
|
||||||
|
───────────────── ──────────────────────── ─────────────────────────
|
||||||
|
Sketcher 几何与约束已有较强基 每种几何/约束/工具均通
|
||||||
|
线,但全部 overload、 过求解、编辑、取消、撤
|
||||||
|
失败消息、编辑工具、自 销、FCStd 和浏览器回放
|
||||||
|
动约束、虚拟空间和焦点
|
||||||
|
矩阵未闭合
|
||||||
|
───────────────── ──────────────────────── ─────────────────────────
|
||||||
|
Part 基本体和代表性 builder 所有注册 Part 命令、选
|
||||||
|
已实现,但全部参数、 项和 TypeId 均有原生差
|
||||||
|
Refine、Section/ 分证据
|
||||||
|
Slice、Healing、检查和
|
||||||
|
失败分支未穷尽
|
||||||
|
───────────────── ──────────────────────── ─────────────────────────
|
||||||
|
PartDesign Attachment 等独立切片 Pad 到 MultiTransform
|
||||||
|
已 exact;其余特征的全 的每种模式、失败、Body
|
||||||
|
部模式组合仍未完成 Tip、Support、命名和
|
||||||
|
FCStd 均一致
|
||||||
|
───────────────── ──────────────────────── ─────────────────────────
|
||||||
|
FCStd 当前支持 23 个往返场景 FreeCAD→Web 修改
|
||||||
|
和未知资源保留,但未完 →FreeCAD 重开/再保存后
|
||||||
|
成所有对象、Property、 零未知语义差异
|
||||||
|
Extension、Shape 的可
|
||||||
|
编辑往返
|
||||||
|
───────────────── ──────────────────────── ─────────────────────────
|
||||||
|
数据格式 STEP/IGES 高级元数据、 15 种格式分别具备单位、
|
||||||
|
装配、颜色层级不完整; 元数据、失败、取消、大
|
||||||
|
部分 STL/OBJ/PLY/DXF/ 文件和往返报告
|
||||||
|
SVG/PDF 只读;JT/IDF/
|
||||||
|
OpenSCAD 为代理
|
||||||
|
───────────────── ──────────────────────── ─────────────────────────
|
||||||
|
GUI 应用壳可用,但完整 每个工作台端到端流程与
|
||||||
|
MDI、动态命令状态、所 原生 GUI oracle 一致
|
||||||
|
有 Task/属性编辑器/对
|
||||||
|
话框、快捷键和焦点行为
|
||||||
|
未闭合
|
||||||
|
───────────────── ──────────────────────── ─────────────────────────
|
||||||
|
文档工作台 Draft、TechDraw、 对象、命令、表达式、
|
||||||
|
Spreadsheet、Plot 目前 TopoRef、导出和跨工作台
|
||||||
|
是兼容子集 引用全部往返
|
||||||
|
───────────────── ──────────────────────── ─────────────────────────
|
||||||
|
工程工作台 Assembly、BIM、 装配求解、IFC、曲面修
|
||||||
|
Surface、Mesh、 复、网格分析和材料继承
|
||||||
|
Material 均缺高级算法 全部闭合
|
||||||
|
和完整对象语义
|
||||||
|
───────────────── ──────────────────────── ─────────────────────────
|
||||||
|
分析与制造 FEM、CAM、Robot、 完整求解器/刀路/运动学/
|
||||||
|
Inspection、Points/RE 点云算法、结果对象、失
|
||||||
|
只有代表性实现 败与导出闭合
|
||||||
|
───────────────── ──────────────────────── ─────────────────────────
|
||||||
|
Python 与 Addon 当前沙箱不能运行完整 受控 Python API、宏、权
|
||||||
|
FreeCAD Python API,桌 限、依赖隔离、签名、回
|
||||||
|
面插件无法原样运行 滚和资源配额完成
|
||||||
|
───────────────── ──────────────────────── ─────────────────────────
|
||||||
|
QA 与发布 Chrome 基线较完整,但 52/52 exact、34/34
|
||||||
|
跨 OS/GPU、长期运行、 exact、零未知指标、签名
|
||||||
|
全 locale 和完整无障碍 发布和回滚演练通过
|
||||||
|
认证未完成
|
||||||
|
|
||||||
|
各工作台的细分任务范围已列在 docs/freecad-complete-parity-backlog.zh-
|
||||||
|
CN.md:88 和 docs/freecad-full-parity-execution-wbs.zh-CN.md:55。
|
||||||
|
|
||||||
|
建议执行顺序
|
||||||
|
|
||||||
|
1. 先关闭 EX-ORA-01 和 EX-TSN-04,不要继续横向增加工作台。
|
||||||
|
2. 完成 7 个剩余命名漂移 case、生产 Cut/Rotate 漂移和 361 个有序操作
|
||||||
|
对。
|
||||||
|
|
||||||
|
3. 按类型逐项完成 58 种 opaque Property。
|
||||||
|
4. 关闭事务、observer、DAG、Extension、FeaturePython 和多文档语义。
|
||||||
|
5. 按单特征纵向完成 Kernel、Sketcher、Part、PartDesign。
|
||||||
|
6. 完成 FCStd 和 STEP/IGES/BREP 双向往返。
|
||||||
|
7. 完成 GUI 全量工作流。
|
||||||
|
8. 依次关闭文档、工程、分析制造和数据工作台。
|
||||||
|
9. 最后处理 Python、Addon、代理模块和跨平台发布。
|
||||||
|
|
||||||
|
立即执行入口
|
||||||
|
|
||||||
|
当前唯一机器任务是 config/freecad-active-work-queue.json:1563。完成后
|
||||||
|
依次处理剩余 Pocket 系列、生产 Cut、生产 Rotate,再进入 357 个未分类操
|
||||||
|
作对。
|
||||||
|
|
||||||
|
此外需要修复规划治理本身:
|
||||||
|
|
||||||
|
- EX-TSN-04 的 blocker 文本仍写着旧的 1/11,实际已经是 4/11。
|
||||||
|
- 早期 Backlog/WBS 仍包含过期完成度描述。
|
||||||
|
- 当前 487 项队列只细化了 GUI Oracle 和 TSN,尚未为 Property、
|
||||||
|
Document、核心建模及 M10-M14 生成同等粒度的机器任务。
|
||||||
|
|
||||||
|
- 必须让状态摘要由机器报告自动生成,禁止手工维护重复数字。
|
||||||
|
|
||||||
|
每个能力应统一采用“原生清册、成功、失败/取消、参数变异、Facade 实现、
|
||||||
|
事务恢复、FCStd 往返、真实浏览器、能力提升”九步完成定义;缺任何一步只
|
||||||
|
能保持 compatible。
|
||||||
Reference in New Issue
Block a user