510 lines
26 KiB
Python
510 lines
26 KiB
Python
#!/usr/bin/env python3
|
||
"""Generate small, module-local P6 task packets from the source-backed matrix."""
|
||
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
import hashlib
|
||
import re
|
||
from pathlib import Path
|
||
|
||
|
||
ROOT = Path(__file__).resolve().parents[1]
|
||
MATRIX = ROOT / "docs" / "web-parity-matrix.json"
|
||
OUT = ROOT / "docs" / "tasks" / "p6"
|
||
STATE = ROOT / "docs" / "current" / "task-state.json"
|
||
|
||
MODULE_IDS = {
|
||
"Assembly": "ASM",
|
||
"BIM": "BIM",
|
||
"CAM": "CAM",
|
||
"Cloud": "CLD",
|
||
"Draft": "DFT",
|
||
"Fem": "FEM",
|
||
"Help": "HLP",
|
||
"Import": "IMP",
|
||
"Inspection": "INS",
|
||
"JtReader": "JTR",
|
||
"Material": "MAT",
|
||
"Measure": "MEA",
|
||
"Mesh": "MSH",
|
||
"MeshPart": "MSP",
|
||
"OpenSCAD": "OSC",
|
||
"Part": "PRT",
|
||
"PartDesign": "PDN",
|
||
"Plot": "PLT",
|
||
"Points": "PTS",
|
||
"ReverseEngineering": "REV",
|
||
"Robot": "ROB",
|
||
"Sandbox": "SDB",
|
||
"Show": "SHW",
|
||
"Sketcher": "SKT",
|
||
"Spreadsheet": "SPR",
|
||
"Start": "STR",
|
||
"Surface": "SUR",
|
||
"TechDraw": "TDR",
|
||
"TemplatePyMod": "TPM",
|
||
"Test": "TST",
|
||
"Tux": "TUX",
|
||
"Web": "WEB",
|
||
}
|
||
|
||
PRIMARY_ACTION = {
|
||
"Assembly": "create/recompute",
|
||
"BIM": "import/export",
|
||
"CAM": "create/recompute",
|
||
"Cloud": "create/recompute",
|
||
"Draft": "create/recompute",
|
||
"Fem": "create/recompute",
|
||
"Help": "inspect/source-inventory",
|
||
"Import": "import/export",
|
||
"Inspection": "create/recompute",
|
||
"JtReader": "create/recompute",
|
||
"Material": "create/recompute",
|
||
"Measure": "create/recompute",
|
||
"Mesh": "create/recompute",
|
||
"MeshPart": "create/recompute",
|
||
"OpenSCAD": "inspect/source-inventory",
|
||
"Part": "create/recompute",
|
||
"PartDesign": "create/recompute",
|
||
"Plot": "inspect/source-inventory",
|
||
"Points": "create/recompute",
|
||
"ReverseEngineering": "create/recompute",
|
||
"Robot": "create/recompute",
|
||
"Sandbox": "create/recompute",
|
||
"Show": "inspect/source-inventory",
|
||
"Sketcher": "create/recompute",
|
||
"Spreadsheet": "create/recompute",
|
||
"Start": "create/recompute",
|
||
"Surface": "create/recompute",
|
||
"TechDraw": "create/recompute",
|
||
"TemplatePyMod": "inspect/source-inventory",
|
||
"Test": "inspect/source-inventory",
|
||
"Tux": "inspect/source-inventory",
|
||
"Web": "create/recompute",
|
||
}
|
||
|
||
|
||
def load_state() -> dict:
|
||
if not STATE.exists():
|
||
return {"default_status": "planned", "tasks": {}, "module_status": {}}
|
||
return json.loads(STATE.read_text(encoding="utf-8"))
|
||
|
||
|
||
STATE_DATA = load_state()
|
||
|
||
|
||
def task_status(task_id: str) -> str:
|
||
return STATE_DATA.get("tasks", {}).get(task_id, STATE_DATA.get("default_status", "planned"))
|
||
|
||
|
||
def module_status(module: str, fallback: str) -> str:
|
||
return STATE_DATA.get("module_status", {}).get(module, fallback)
|
||
|
||
|
||
def write_generated(path: Path, content: str) -> None:
|
||
"""Write the generated prefix while preserving a manually recorded result section."""
|
||
if path.exists():
|
||
old = path.read_text(encoding="utf-8")
|
||
marker = "\n## 结果\n"
|
||
if marker in old and marker not in content:
|
||
content = content.rstrip() + old[old.index(marker):]
|
||
path.write_text(content.rstrip() + "\n", encoding="utf-8")
|
||
|
||
|
||
def slug(module: str) -> str:
|
||
return re.sub(r"[^a-z0-9]+", "-", module.lower()).strip("-")
|
||
|
||
|
||
def action_slug(action: str) -> str:
|
||
return re.sub(r"[^a-z0-9]+", "-", action.lower()).strip("-")
|
||
|
||
|
||
def module_index(module: str, row: dict) -> str:
|
||
code = MODULE_IDS[module]
|
||
action = PRIMARY_ACTION[module]
|
||
objects = ", ".join(row.get("object_types", [])[:8]) or "无已枚举对象;以源码入口和模块 API 为主"
|
||
if len(row.get("object_types", [])) > 8:
|
||
objects += f"(另有 {len(row['object_types']) - 8} 个,完整清单见 object-inventory.json)"
|
||
formats = ", ".join(row.get("format_extensions", [])) or "无格式入口"
|
||
api = row.get("python_api_count", 0)
|
||
gui = "需要 GUI 依赖" if row.get("gui_dependency") else "不需要 GUI 依赖"
|
||
actions = ", ".join(row.get("user_actions", []))
|
||
followups = [action for action in row.get("user_actions", []) if action != action_primary(module)]
|
||
followup_text = ", ".join(f"`{action}`" for action in followups) or "无"
|
||
action_rows = []
|
||
for action_number, action_name in enumerate(row.get("user_actions", []), start=1):
|
||
if action_name == action:
|
||
target = f"{slug(module)}/FW-P6-{code}-C.md"
|
||
role = "首切片 C-G"
|
||
else:
|
||
target = f"{slug(module)}/FW-P6-{code}-ACT-{action_number:02d}.md"
|
||
role = "后续拆分合同"
|
||
action_rows.append(f"| `{action_name}` | {role} | [{target.rsplit('/', 1)[-1]}]({target}) |")
|
||
action_table = "\n".join(action_rows)
|
||
action_predecessors = [
|
||
f"`FW-P6-{code}-ACT-{number:02d}-G`"
|
||
for number, action_name in enumerate(row.get("user_actions", []), start=1)
|
||
if action_name != action
|
||
]
|
||
final_predecessor = f"`FW-P6-{code}-X`" + (" + " + " + ".join(action_predecessors) if action_predecessors else "")
|
||
links = "\n".join(
|
||
f"| [FW-P6-{code}-{letter}]({slug(module)}/FW-P6-{code}-{letter}.md) | {title} | {prev} |"
|
||
for letter, title, prev in [
|
||
("A", "源码边界探针", "FW-P6-000"),
|
||
("B", "运行时注册与依赖闭包", f"FW-P6-{code}-A + FW-P6-001"),
|
||
("C", "一个主要内核动作", f"FW-P6-{code}-B"),
|
||
("D", "重算、非法输入与恢复", f"FW-P6-{code}-C"),
|
||
("E", "Worker/Facade 消息合同", f"FW-P6-{code}-D + FW-P6-002"),
|
||
("F", "Chrome 最小浏览器切片", f"FW-P6-{code}-E + FW-P6-003"),
|
||
("G", "持久化或模块边界证据", f"FW-P6-{code}-F"),
|
||
("X", "首切片出口", f"FW-P6-{code}-G"),
|
||
("Y", "完整动作出口", final_predecessor),
|
||
]
|
||
)
|
||
package_status = module_status(module, row.get("web_status", "pending"))
|
||
# Module parity uses `pending`; executable task cards use `planned`.
|
||
package_task_status = "planned" if package_status == "pending" else package_status
|
||
web_status_note = (
|
||
"Web 状态已由 Y gate 晋级为 `completed`;边界见对应 module-gate 产物。"
|
||
if package_status == "completed"
|
||
else "Web 状态保持 `pending`,直到本包的 Y 卡通过。"
|
||
)
|
||
return f"""# FW-P6-{code}:{module} 功能扩展任务包
|
||
|
||
本文件只做模块范围和原子卡索引;执行时读取当前原子卡,不读取本包其他卡。
|
||
|
||
## 范围快照
|
||
|
||
- 状态:`{package_task_status}`
|
||
- 来源:`docs/web-parity-matrix.json`,源码 revision 由 `FW-P6-000` 锁定
|
||
- 主要动作:`{action}`
|
||
- 对象规模:`{row.get('object_type_count', 0)}`;代表入口:{objects}
|
||
- 格式入口:{formats}
|
||
- Python API 计数:`{api}`;GUI:{gui}
|
||
- 对标动作全集:{actions}
|
||
- {web_status_note}
|
||
|
||
首个 C-G 切片只覆盖 `{action}`。后续动作:{followup_text};每个后续动作必须在 Y 前完成自己的拆分卡和 C-G 子链,不能因为首个切片通过就把模块标为 `completed`。
|
||
|
||
## 动作级入口
|
||
|
||
| 动作 | 角色 | 入口 |
|
||
|---|---|---|
|
||
{action_table}
|
||
|
||
## 原子卡
|
||
|
||
| 卡 | 合同 | 直接前置 |
|
||
|---|---|---|
|
||
{links}
|
||
|
||
顺序:`A → B → C → D → E → F → G → X`;随后完成动作级卡,最后执行 `Y`。失败只新增同目录 `R` 恢复卡,不复制原卡全文。
|
||
"""
|
||
|
||
|
||
def atomic_card(module: str, row: dict, letter: str) -> str:
|
||
code = MODULE_IDS[module]
|
||
name = slug(module)
|
||
prefix = f"FW-P6-{code}"
|
||
titles = {
|
||
"A": "源码边界探针",
|
||
"B": "运行时注册与依赖闭包",
|
||
"C": "一个主要内核动作",
|
||
"D": "重算、非法输入与恢复",
|
||
"E": "Worker/Facade 消息合同",
|
||
"F": "Chrome 最小浏览器切片",
|
||
"G": "持久化或模块边界证据",
|
||
"X": "首切片出口",
|
||
"Y": "完整动作出口",
|
||
}
|
||
prev = {
|
||
"A": "`FW-P6-000`",
|
||
"B": f"`{prefix}-A`、`FW-P6-001`",
|
||
"C": f"`{prefix}-B`",
|
||
"D": f"`{prefix}-C`",
|
||
"E": f"`{prefix}-D`、`FW-P6-002`",
|
||
"F": f"`{prefix}-E`、`FW-P6-003`",
|
||
"G": f"`{prefix}-F`",
|
||
"X": f"`{prefix}-G`",
|
||
"Y": f"`{prefix}-X`",
|
||
}[letter]
|
||
if letter == "Y":
|
||
action_predecessors = [
|
||
f"`{prefix}-ACT-{number:02d}-G`"
|
||
for number, action_name in enumerate(row.get("user_actions", []), start=1)
|
||
if action_name != PRIMARY_ACTION[module]
|
||
]
|
||
if action_predecessors:
|
||
prev += " + " + " + ".join(action_predecessors)
|
||
task_type = {"A": "probe", "F": "slice", "X": "gate", "Y": "gate"}.get(letter, "contract")
|
||
bodies = {
|
||
"A": f"""- 合同:确认 `{module}` 的 CMake target、App/Gui 分界、直接依赖、类型注册入口和主要动作源码入口。
|
||
- 输入:`workbench-inventory.json`、`object-inventory.json`、`src/Mod/{module}/`。
|
||
- 命令/方法:在 `freecad-source` 上运行 `rg -n "BUILD_|Init|addObject|registerType|PyInit" src/Mod/{module}`,再对命中的 CMake 与源码行做最小闭包记录。
|
||
- 产物:`docs/p6/{name}-source-boundary.json` 与日志。
|
||
- 验收:每个入口有文件和行号;条件编译、缺失依赖和 GUI 符号单独列出;不把源码存在写成运行时通过。
|
||
- 不包含:代码修改、浏览器测试、完整 API 迁移。""",
|
||
"B": f"""- 合同:在独立 WASM probe 中完成 `{module}` 初始化、类型可见性和重复初始化检查。
|
||
- 输入:A 的边界 JSON、模块 App CMake、共享模块加载合同。
|
||
- 命令/方法:复制现有最小 Web probe 的 configure/build/run 流程;只替换模块 target 和一个代表类型;连续执行初始化两次并保存新的 build/smoke 日志,不覆盖成功证据。
|
||
- 产物:`docs/p6/{name}-runtime.json`、构建日志和 WASM SHA-256。
|
||
- 验收:初始化计数为 1;重复调用不重复注册;缺失依赖返回结构化错误;无未声明 GUI/动态库符号。
|
||
- 不包含:主要动作、Facade、Chrome UI。""",
|
||
"C": f"""- 合同:只验证一个最小 `{PRIMARY_ACTION[module]}` 合同,固定输入、输出和对象句柄。
|
||
- 输入:B 的 WASM、固定 fixture、对应源码 API;对象模块用代表类型,格式模块用一个最小样例。
|
||
- 命令/方法:从 A 的主要动作入口选择一个 deterministic fixture;在 C ABI/Worker probe 中执行一次成功路径和一次资源释放,记录输入 hash、结果摘要和返回码。
|
||
- 产物:`docs/p6/{name}-kernel.json` 与 smoke 日志。
|
||
- 验收:成功结果可重复;对象/几何/格式摘要符合源码语义;异常转为稳定错误码和消息;资源可释放。
|
||
- 不包含:第二个动作、全量对象覆盖、GUI 交互。""",
|
||
"D": f"""- 合同:覆盖一次重算/校验边界和一个非法输入,失败后保留上一次有效状态。
|
||
- 输入:C 的成功 fixture;只读模块改为缺少资源/不支持输入边界。
|
||
- 命令/方法:复用 C 的同一进程,先记录成功快照,再注入一个字段级非法值或缺失资源;校验错误 code、旧状态 hash、重复回放结果。
|
||
- 产物:`docs/p6/{name}-recovery.json` 与日志。
|
||
- 验收:错误定位到字段或步骤;有效对象/文件 hash 不污染;重复执行一致;缓存失败不升级为主流程失败。
|
||
- 不包含:跨文档事务、并发修改、性能预算。""",
|
||
"E": f"""- 合同:把 C/D 的一个动作接入 request id、typed result、错误和取消语义。
|
||
- 输入:共享 Facade envelope、D 的 fixture 和错误样例。
|
||
- 命令/方法:为一个 action 增加 typed request/result schema;顺序发送 success、expected error、duplicate id、cancel 四个 request,保存 Worker trace。
|
||
- 产物:`docs/p6/{name}-facade.json`、schema 测试和 Worker 日志。
|
||
- 验收:请求严格串行且可关联;JS 不持有原生指针;成功/失败/重复 request 均有固定结果;取消不破坏文档状态。
|
||
- 不包含:完整 UI、插件系统、跨工作台组合。""",
|
||
"F": f"""- 合同:在 Google Chrome 固定版本和 viewport 下完成一个真实 `{PRIMARY_ACTION[module]}` 用户动作回放。
|
||
- 输入:Facade bundle、固定 fixture、Chrome harness;只执行 Chrome,不切换 Firefox/WebKit/Safari。
|
||
- 命令/方法:调用 `FW-P6-003` harness 启动入口,拦截所有外部请求,执行 ready→action→expected-error→reset,并保存 console/network/截图证据。
|
||
- 产物:`docs/p6/{name}-chrome.json`、控制台/网络日志和截图(如适用)。
|
||
- 验收:ready、动作结果、错误路径和 Worker 消息通过;外部请求为 0;运行时异常为 0;证据含 Chrome 版本、模式和 viewport。
|
||
- 不包含:非 Chrome 兼容矩阵、移动适配、完整 UI。""",
|
||
"G": f"""- 合同:有保存/重开动作时验证一个最小 FCStd round-trip;否则验证 manifest/资源/只读重放边界。
|
||
- 输入:F 的成功状态、P2.3 FCStd 合同、模块 fixture。
|
||
- 命令/方法:有 `save/reopen` 时按 P2.3 的临时文件、hash、关闭、重开、对象/属性校验顺序执行;无该动作时运行资源 manifest 和只读重放检查。
|
||
- 产物:`docs/p6/{name}-persistence.json` 与日志。
|
||
- 验收:保存/重开对象和属性可校验;不适用时明确原因,不伪造通过;错误后旧状态仍有效。
|
||
- 不包含:跨版本迁移、全量格式覆盖、升级回滚。""",
|
||
"X": f"""- 合同:汇总本包 A-G 的独立证据,关闭 `{module}` 的首切片 gate。
|
||
- 输入:A-G JSON/日志、源码 revision、共享 gate 报告。
|
||
- 命令/方法:逐项读取本包 A-G 产物,核对 revision、输入/输出 hash、Chrome 证据和未覆盖动作;生成机器可读结果后再写人读 gate。
|
||
- 产物:`docs/p6/{name}-gate.md` 与 JSON。
|
||
- 验收:首个动作的每个原子合同有命令、输入、产物、hash 和结论;本卡只关闭首切片,不修改完整模块状态;其他动作保持 `pending`。
|
||
- 不包含:其他模块、全量发布 gate、法律或性能结论。""",
|
||
"Y": f"""- 合同:汇总首切片和全部动作级子链,决定 `{module}` 是否可以从 `pending` 晋级。
|
||
- 输入:X 的首切片 gate、所有动作级卡及其 C-G 子链产物、源码 revision。
|
||
- 命令/方法:逐动作核对源码入口、成功/错误/Chrome/持久化证据;任一声明动作缺证据即保持 `pending`,不得用动作计数推断完成。
|
||
- 产物:`docs/p6/{name}-module-gate.md` 与 JSON。
|
||
- 验收:本模块声明的 {len(row.get('user_actions', []))} 个动作全部有独立结论;未实现或不适用项显式列出;只有本卡可以晋级该模块 Web 状态。
|
||
- 不包含:其他模块、全量发布 gate、法律或性能结论。""",
|
||
}[letter]
|
||
return f"""# {prefix}-{letter}:{module} {titles[letter]}
|
||
|
||
> 最小执行上下文:只读取本卡、直接前置 {prev} 和本卡列出的产物。浏览器步骤只允许 Google Chrome。
|
||
|
||
- 状态:`{task_status(f"{prefix}-{letter}")}`
|
||
- 类型:`{task_type}`
|
||
- 前置:{prev}
|
||
{bodies}
|
||
- 失败恢复:保留首个错误、命令、输入 hash 和部分产物;恢复卡命名 `{prefix}-{letter}-R1`,不得覆盖成功证据。
|
||
"""
|
||
|
||
|
||
def action_card(module: str, action_name: str, action_number: int) -> str:
|
||
code = MODULE_IDS[module]
|
||
name = slug(module)
|
||
prefix = f"FW-P6-{code}-ACT-{action_number:02d}"
|
||
action_id = action_slug(action_name)
|
||
extra_prerequisite = {
|
||
"python-api": ";Python 运行时前置 `FW-P4-006-G`",
|
||
"save/reopen": ";持久化证据前置 `freecad-p2.3-persistence-transaction-gate.md`",
|
||
}.get(action_name, "")
|
||
return f"""# {prefix}:{module} `{action_name}` 动作拆分合同
|
||
|
||
> 这是动作级拆分卡,不等于功能已实现。完成本卡后,必须按卡内的 C→D→E→F→G 子链继续执行;只读取本卡、`FW-P6-{code}-B` 和本卡产物。
|
||
|
||
- 状态:`{task_status(prefix)}`
|
||
- 类型:`contract`
|
||
- 前置:`FW-P6-{code}-B`{extra_prerequisite};若首切片已完成,复用其 WASM、loader 和 fixture 约束。
|
||
- 合同:为 `{action_name}` 冻结一个可观察用户动作的输入、源码入口、结果摘要、错误边界、Worker request 和 Chrome 回放验收。
|
||
- 输入:`{name}-source-boundary.json`、对象/格式/API inventory 中与该动作对应的行、共享 Facade envelope 和 Chrome harness。
|
||
- 命令/方法:先在源码中定位动作入口,再写固定 fixture;把实现拆成 `{prefix}-C`(内核)、`{prefix}-D`(错误/恢复)、`{prefix}-E`(Facade)、`{prefix}-F`(Chrome)、`{prefix}-G`(持久化或适用边界)。
|
||
- 产物:`docs/p6/{name}-action-{action_number:02d}-{action_id}.json`,以及上述子卡各自的日志和 hash。
|
||
- 验收:动作有唯一 request id、成功和预期错误结果、无外部网络请求;未完成子链时本动作保持 `pending`。
|
||
- 不包含:其他动作、工作台 gate、全量对象覆盖、跨版本迁移。
|
||
- 失败恢复:只记录首个源码/fixture/schema 错误,新增 `{prefix}-R1`;不复制或覆盖首切片证据。
|
||
"""
|
||
|
||
|
||
def action_child_card(module: str, action_name: str, action_number: int, letter: str) -> str:
|
||
code = MODULE_IDS[module]
|
||
name = slug(module)
|
||
prefix = f"FW-P6-{code}-ACT-{action_number:02d}"
|
||
titles = {
|
||
"C": "内核动作",
|
||
"D": "错误与恢复",
|
||
"E": "Facade 接入",
|
||
"F": "Chrome 回放",
|
||
"G": "持久化或适用边界",
|
||
}
|
||
prev = {
|
||
"C": f"`{prefix}`、`FW-P6-{code}-B`",
|
||
"D": f"`{prefix}-C`",
|
||
"E": f"`{prefix}-D`、`FW-P6-002`",
|
||
"F": f"`{prefix}-E`、`FW-P6-003`",
|
||
"G": f"`{prefix}-F`",
|
||
}[letter]
|
||
bodies = {
|
||
"C": f"""- 合同:执行 `{action_name}` 的一个 deterministic 内核输入,返回对象/格式/API 摘要。
|
||
- 输入:动作拆分卡、B 的 WASM、动作 fixture 和源码入口。
|
||
- 命令/方法:单进程执行成功路径、释放路径和重复回放;记录输入 hash、结果 hash、错误 code。
|
||
- 产物:`docs/p6/{name}-action-{action_number:02d}-kernel.json` 与 smoke 日志。
|
||
- 验收:结果可重复,异常结构化,资源释放;不实现其他动作。""",
|
||
"D": f"""- 合同:为 `{action_name}` 注入一个字段级非法输入或缺少资源并保持旧状态。
|
||
- 输入:`{prefix}-C` 成功快照和错误 fixture。
|
||
- 命令/方法:成功→非法→恢复三段回放,比较旧状态 hash 和诊断字段。
|
||
- 产物:`docs/p6/{name}-action-{action_number:02d}-recovery.json` 与日志。
|
||
- 验收:首个错误可定位,旧状态未污染,重复回放一致。""",
|
||
"E": f"""- 合同:把 `{action_name}` 接入通用 request id、typed result、error 和 cancel。
|
||
- 输入:`{prefix}-D`、共享 envelope、成功和错误样例。
|
||
- 命令/方法:顺序发送 success、expected error、duplicate id、cancel;保存 Worker trace。
|
||
- 产物:`docs/p6/{name}-action-{action_number:02d}-facade.json` 与 schema 日志。
|
||
- 验收:请求可关联且严格串行;JS 不持有原生指针;取消不污染文档。""",
|
||
"F": f"""- 合同:在 Chrome 固定版本和 viewport 下回放真实 `{action_name}`。
|
||
- 输入:`{prefix}-E` bundle、fixture、`FW-P6-003` harness。
|
||
- 命令/方法:ready→action→expected-error→reset,拦截外部请求并保存 console/network/截图。
|
||
- 产物:`docs/p6/{name}-action-{action_number:02d}-chrome.json` 与日志。
|
||
- 验收:Chrome 版本、模式、viewport 齐全;外部请求和非预期异常为 0。""",
|
||
"G": f"""- 合同:为 `{action_name}` 完成 FCStd round-trip,或记录该动作不适用的边界证据。
|
||
- 输入:`{prefix}-F` 成功状态、P2.3 合同和 fixture。
|
||
- 命令/方法:有保存动作时执行临时文件、hash、关闭、重开、对象/属性校验;否则执行 manifest/只读重放。
|
||
- 产物:`docs/p6/{name}-action-{action_number:02d}-persistence.json` 与日志。
|
||
- 验收:对象/属性或“不适用”原因可校验;不伪造未覆盖能力。""",
|
||
}[letter]
|
||
task_type = "slice" if letter == "F" else "contract"
|
||
return f"""# {prefix}-{letter}:{module} `{action_name}` {titles[letter]}
|
||
|
||
> 最小执行上下文:只读取本卡、直接前置 {prev} 和本卡产物。浏览器只允许 Google Chrome。
|
||
|
||
- 状态:`{task_status(f"{prefix}-{letter}")}`
|
||
- 类型:`{task_type}`
|
||
- 前置:{prev}
|
||
{bodies}
|
||
- 不包含:其他用户动作、完整工作台 gate、跨版本迁移。
|
||
- 失败恢复:保留首个错误、输入 hash 和 trace;新增 `{prefix}-{letter}-R1`,不覆盖成功证据。
|
||
"""
|
||
|
||
|
||
def action_primary(module: str) -> str:
|
||
return PRIMARY_ACTION[module]
|
||
|
||
|
||
def main() -> None:
|
||
data = json.loads(MATRIX.read_text(encoding="utf-8"))
|
||
rows = {row["module"]: row for row in data["rows"]}
|
||
missing = set(MODULE_IDS) - set(rows)
|
||
if missing:
|
||
raise SystemExit(f"matrix missing modules: {sorted(missing)}")
|
||
OUT.mkdir(parents=True, exist_ok=True)
|
||
index = [
|
||
"# P6 功能扩展任务索引",
|
||
"",
|
||
"> P3 gate 已完成的是源码盘点,不是 32 个工作台的 Web 实现。P6 按工作台建立独立任务包;每个包内部再拆为 A-G、X 首切片和 Y 完整出口。",
|
||
"> 当前 32 个模块声明了 115 个用户动作;每个包先做一个首切片,其他动作必须继续拆独立合同,不能用首切片冒充完整对标。",
|
||
"> 状态由 `docs/current/task-state.json` 统一维护;本索引和卡片是生成投影。",
|
||
"> 机器可读文件清单和数量校验见 [`manifest.json`](manifest.json)。",
|
||
"> 矩阵更新后依次运行生成器、`sync_web_task_state.py` 和 `validate_web_context.py`;不要手改生成结构。",
|
||
"",
|
||
"## 共享前置",
|
||
"",
|
||
"| 编号 | 合同 | 产物 |",
|
||
"|---|---|---|",
|
||
"| [FW-P6-000](../FW-P6-000.md) | 锁定源码、矩阵、构建和 Chrome 环境 | `docs/p6/baseline.json` |",
|
||
"| [FW-P6-001](../FW-P6-001.md) | 模块初始化、类型注册和依赖错误 | `docs/p6/module-loader.json` |",
|
||
"| [FW-P6-002](../FW-P6-002.md) | Worker/Facade 通用消息 envelope | `docs/p6/facade-envelope.json` |",
|
||
"| [FW-P6-003](../FW-P6-003.md) | Chrome 回放 harness 和证据格式 | `docs/p6/chrome-harness.json` |",
|
||
"",
|
||
"## 工作台任务包",
|
||
"",
|
||
"| 包 | 模块 | 模块状态 | 首个动作 | 全部动作 | 对象 | 格式 | Python API | 直接前置 |",
|
||
"|---|---|---|---|---|---:|---|---:|---|",
|
||
]
|
||
for module in MODULE_IDS:
|
||
row = rows[module]
|
||
code = MODULE_IDS[module]
|
||
index.append(
|
||
f"| [FW-P6-{code}](FW-P6-{slug(module)}.md) | `{module}` | "
|
||
f"`{module_status(module, row.get('web_status', 'pending'))}` | `{PRIMARY_ACTION[module]}` | "
|
||
f"{', '.join(row['user_actions'])} | {row['object_type_count']} | "
|
||
f"{', '.join('`'+x+'`' for x in row['format_extensions']) or '-'} | "
|
||
f"{row['python_api_count']} | `FW-P6-000`, `001`, `002`, `003` |"
|
||
)
|
||
index += [
|
||
"",
|
||
"## 原子步骤语义",
|
||
"",
|
||
"| 步骤 | 单一合同 | 不读取的内容 |",
|
||
"|---|---|---|",
|
||
"| A | 源码边界探针 | 其他模块实现、浏览器日志 |",
|
||
"| B | WASM 初始化和类型注册 | 主要动作、UI |",
|
||
"| C | 一个主要内核动作 | 第二个动作、全量对象 |",
|
||
"| D | 重算/非法输入/恢复 | 并发和性能 |",
|
||
"| E | Worker/Facade 消息 | 完整 UI 和插件 |",
|
||
"| F | Chrome 用户动作 | 非 Chrome 浏览器 |",
|
||
"| G | FCStd round-trip 或适用边界 | 跨版本迁移 |",
|
||
"| X | 首切片 gate | 其他模块和发布 gate |",
|
||
"| Y | 完整动作 gate | 其他模块和发布 gate |",
|
||
"",
|
||
"失败恢复卡使用同一包内的 `R` 后缀,例如 `FW-P6-PRT-C-R1`;恢复卡只记录首个错误、最小修复和重放命令,不复制原卡全文。",
|
||
]
|
||
write_generated(OUT / "INDEX.md", "\n".join(index))
|
||
for module, row in rows.items():
|
||
module_slug = slug(module)
|
||
write_generated(OUT / f"FW-P6-{module_slug}.md", module_index(module, row))
|
||
module_dir = OUT / module_slug
|
||
module_dir.mkdir(parents=True, exist_ok=True)
|
||
for letter in "ABCDEFGXY":
|
||
write_generated(
|
||
module_dir / f"FW-P6-{MODULE_IDS[module]}-{letter}.md",
|
||
atomic_card(module, row, letter),
|
||
)
|
||
for action_number, action_name in enumerate(row.get("user_actions", []), start=1):
|
||
if action_name == PRIMARY_ACTION[module]:
|
||
continue
|
||
write_generated(
|
||
module_dir / f"FW-P6-{MODULE_IDS[module]}-ACT-{action_number:02d}.md",
|
||
action_card(module, action_name, action_number),
|
||
)
|
||
for letter in "CDEFG":
|
||
write_generated(
|
||
module_dir / f"FW-P6-{MODULE_IDS[module]}-ACT-{action_number:02d}-{letter}.md",
|
||
action_child_card(module, action_name, action_number, letter),
|
||
)
|
||
files = sorted(str(path.relative_to(ROOT)) for path in OUT.rglob("*.md"))
|
||
manifest = {
|
||
"schema": "freecad-web-p6-task-packet-manifest/v1",
|
||
"source_matrix": str(MATRIX.relative_to(ROOT)),
|
||
"source_matrix_sha256": hashlib.sha256(MATRIX.read_bytes()).hexdigest(),
|
||
"module_count": len(rows),
|
||
"declared_action_count": sum(len(row.get("user_actions", [])) for row in rows.values()),
|
||
"module_index_count": len(rows),
|
||
"atomic_card_count": len(rows) * 9,
|
||
"action_split_card_count": sum(
|
||
len([action for action in row.get("user_actions", []) if action != PRIMARY_ACTION[module]])
|
||
for module, row in rows.items()
|
||
),
|
||
"action_child_card_count": sum(
|
||
5 * len([action for action in row.get("user_actions", []) if action != PRIMARY_ACTION[module]])
|
||
for module, row in rows.items()
|
||
),
|
||
"markdown_file_count": len(files),
|
||
"files": files,
|
||
"shared_prerequisites": [
|
||
"docs/tasks/FW-P6-000.md",
|
||
"docs/tasks/FW-P6-001.md",
|
||
"docs/tasks/FW-P6-002.md",
|
||
"docs/tasks/FW-P6-003.md",
|
||
],
|
||
}
|
||
(OUT / "manifest.json").write_text(json.dumps(manifest, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|