feat(web): authorize corrective smooth reader

This commit is contained in:
mes123456
2026-09-02 18:35:58 +08:00
parent 244e058999
commit 0d61c64150
51 changed files with 127040 additions and 67 deletions

View File

@@ -2180,6 +2180,7 @@ std::optional<json> script_source_from_record(const ParsedBlend &blend,
const std::vector<ElementRef> lines = linked_list_elements(blend, element, "lines");
if (lines.empty() || lines.size() > 65536) return std::nullopt;
std::string source;
json source_lines = json::array();
for (size_t index = 0; index < lines.size(); index++) {
const int64_t length = read_integer(*blend.sdna, lines[index], "len").value_or(-1);
const std::optional<uint64_t> pointer = read_pointer(*blend.sdna, lines[index], "line");
@@ -2198,8 +2199,10 @@ std::optional<json> script_source_from_record(const ParsedBlend &blend,
{
return std::nullopt;
}
const std::string line_text(reinterpret_cast<const char *>(bytes.data()), bytes.size());
if (index > 0) source.push_back('\n');
source.append(reinterpret_cast<const char *>(bytes.data()), bytes.size());
source.append(line_text);
source_lines.push_back({{"index", index}, {"body", line_text}, {"byteLength", bytes.size()}});
}
const std::optional<uint64_t> path_pointer = read_pointer(*blend.sdna, element, "filepath");
const std::string source_path = path_pointer ? read_raw_string(blend, *path_pointer, 2049) :
@@ -2221,6 +2224,7 @@ std::optional<json> script_source_from_record(const ParsedBlend &blend,
{"sourceSha256", sha256_hex(source)},
{"byteLength", source.size()},
{"lineCount", lines.size()},
{"lines", std::move(source_lines)},
{"internal", source_path.empty()},
{"moduleAutorunRequested", module_autorun_requested},
{"readOnly", true},
@@ -3385,6 +3389,8 @@ json non_mesh_data_from_record(const ParsedBlend &blend,
return data;
}
const char *fmodifier_type_name(const int64_t type);
json fcurve_channel(const ParsedBlend &blend, const ElementRef &fcurve)
{
const std::optional<uint64_t> path_pointer = read_pointer(*blend.sdna, fcurve, "rna_path");
@@ -3401,6 +3407,7 @@ json fcurve_channel(const ParsedBlend &blend, const ElementRef &fcurve)
const std::optional<uint64_t> bezier_pointer = read_pointer(*blend.sdna, fcurve, "bezt");
const int64_t point_count = std::clamp<int64_t>(read_integer(*blend.sdna, fcurve, "totvert").value_or(0), 0, 100000);
const bool has_samples = read_pointer(*blend.sdna, fcurve, "fpt").has_value() && point_count > 0;
if (bezier_pointer && point_count > 0) {
for (int64_t index = 0; index < point_count; index++) {
const std::optional<ElementRef> point = raw_array_element(blend, *bezier_pointer, "BezTriple", size_t(index));
@@ -3467,8 +3474,26 @@ json fcurve_channel(const ParsedBlend &blend, const ElementRef &fcurve)
{"interpolation", channel_interpolation.empty() ? "BEZIER" : channel_interpolation},
{"extrapolation", extrapolation == FCURVE_EXTRAPOLATE_LINEAR ? "LINEAR" : "CONSTANT"},
{"keyframes", std::move(keyframes)},
{"keyframeSource", has_samples ? "SAMPLED" : "BEZIER"},
{"frameStart", std::isfinite(frame_start) ? frame_start : 0.0f},
{"frameEnd", std::isfinite(frame_end) ? frame_end : 0.0f} };
json modifiers = json::array();
for (const ElementRef &modifier : linked_list_elements(blend, fcurve, "modifiers")) {
const int64_t modifier_type = read_integer(*blend.sdna, modifier, "type").value_or(0);
modifiers.push_back({
{"name", read_string(*blend.sdna, modifier, "name")},
{"type", fmodifier_type_name(modifier_type)},
{"typeCode", modifier_type},
{"flags", read_integer(*blend.sdna, modifier, "flag").value_or(0)},
{"expanded", (read_integer(*blend.sdna, modifier, "ui_expand_flag").value_or(0) & 1) != 0},
{"influence", read_float(*blend.sdna, modifier, "influence").value_or(1.0f)},
{"frameStart", read_float(*blend.sdna, modifier, "sfra").value_or(0.0f)},
{"frameEnd", read_float(*blend.sdna, modifier, "efra").value_or(0.0f)},
{"blendIn", read_float(*blend.sdna, modifier, "blendin").value_or(0.0f)},
{"blendOut", read_float(*blend.sdna, modifier, "blendout").value_or(0.0f)},
});
}
result["modifiers"] = std::move(modifiers);
if (const std::optional<uint64_t> group_pointer = read_pointer(*blend.sdna, fcurve, "grp")) {
if (const std::optional<ElementRef> group = element_for_pointer(blend, *group_pointer)) {
const int64_t group_flags = read_integer(*blend.sdna, *group, "flag").value_or(0);
@@ -3497,6 +3522,21 @@ const char *driver_type_name(const int64_t type)
}
}
const char *fmodifier_type_name(const int64_t type)
{
switch (type) {
case FMODIFIER_TYPE_GENERATOR: return "GENERATOR";
case FMODIFIER_TYPE_FN_GENERATOR: return "FN_GENERATOR";
case FMODIFIER_TYPE_ENVELOPE: return "ENVELOPE";
case FMODIFIER_TYPE_CYCLES: return "CYCLES";
case FMODIFIER_TYPE_NOISE: return "NOISE";
case FMODIFIER_TYPE_LIMITS: return "LIMITS";
case FMODIFIER_TYPE_STEPPED: return "STEPPED";
case FMODIFIER_TYPE_SMOOTH: return "SMOOTH";
default: return "UNKNOWN";
}
}
json driver_fcurve(const ParsedBlend &blend, const ElementRef &fcurve)
{
const std::optional<uint64_t> driver_pointer = read_pointer(*blend.sdna, fcurve, "driver");
@@ -3511,12 +3551,47 @@ json driver_fcurve(const ParsedBlend &blend, const ElementRef &fcurve)
const int64_t driver_flags = read_integer(*blend.sdna, *driver, "flag").value_or(0);
json variables = json::array();
for (const ElementRef &variable : linked_list_elements(blend, *driver, "variables")) {
const int64_t target_count = std::clamp<int64_t>(
read_integer(*blend.sdna, variable, "num_targets").value_or(0), 0, 8);
const std::optional<MemberInfo> targets_member = find_member(
*blend.sdna, variable.type_name, "targets");
const int target_struct_index = DNA_struct_find_index_without_alias(
blend.sdna.get(), "DriverTarget");
const int target_struct_size = target_struct_index >= 0 ?
DNA_struct_size(blend.sdna.get(), target_struct_index) :
0;
json targets = json::array();
for (int64_t target_index = 0; targets_member && target_struct_size > 0 &&
target_index < target_count;
target_index++) {
const size_t target_offset = variable.offset + size_t(targets_member->offset) +
size_t(target_index) * size_t(target_struct_size);
const ElementRef target{variable.block, target_offset, "DriverTarget"};
const std::optional<uint64_t> id_pointer = read_pointer(*blend.sdna, target, "id");
const std::optional<uint64_t> target_path_pointer = read_pointer(*blend.sdna, target, "rna_path");
std::string id_name;
if (id_pointer) {
if (const std::optional<ElementRef> id = element_for_pointer(blend, *id_pointer)) {
id_name = read_id_name(*blend.sdna, *id);
}
}
targets.push_back({
{"idName", id_name},
{"dataPath", target_path_pointer ? read_raw_string(blend, *target_path_pointer, 2048) : std::string()},
{"boneTarget", read_string(*blend.sdna, target, "pchan_name")},
{"transformType", read_integer(*blend.sdna, target, "transChan").value_or(0)},
{"rotationMode", read_integer(*blend.sdna, target, "rotation_mode").value_or(0)},
{"flags", read_integer(*blend.sdna, target, "flag").value_or(0)},
{"options", read_integer(*blend.sdna, target, "options").value_or(0)},
{"idType", read_integer(*blend.sdna, target, "idtype").value_or(0)},
{"contextProperty", read_integer(*blend.sdna, target, "context_property").value_or(0)},
{"fallbackValue", read_float(*blend.sdna, target, "fallback_value").value_or(0.0f)},
});
}
variables.push_back({{"name", read_string(*blend.sdna, variable, "name")},
{"type", read_integer(*blend.sdna, variable, "type").value_or(-1)},
{"targetCount", std::clamp<int64_t>(
read_integer(*blend.sdna, variable, "num_targets").value_or(0),
0,
8)}});
{"targetCount", target_count},
{"targets", std::move(targets)}});
}
return { {"path", path},
{"arrayIndex", array_index},
@@ -3562,6 +3637,8 @@ void append_action_animation(const ParsedBlend &blend,
json channels = json::array();
json markers = json::array();
json slots = json::array();
json strips = json::array();
for (const ElementRef &marker : linked_list_elements(blend, *action, "markers")) {
markers.push_back({{"name", read_string(*blend.sdna, marker, "name")},
{"frame", read_integer(*blend.sdna, marker, "frame").value_or(0)}});
@@ -3592,6 +3669,12 @@ void append_action_animation(const ParsedBlend &blend,
const std::optional<ElementRef> action_slot = element_for_pointer(blend, action_slot_value);
if (!action_slot) continue;
slot_count++;
slots.push_back({
{"identifier", read_string(*blend.sdna, *action_slot, "identifier")},
{"idtype", read_integer(*blend.sdna, *action_slot, "idtype").value_or(0)},
{"handle", read_integer(*blend.sdna, *action_slot, "handle").value_or(0)},
{"flags", read_integer(*blend.sdna, *action_slot, "slot_flags").value_or(0)},
});
if (slot_handle && read_integer(*blend.sdna, *action_slot, "handle").value_or(0) == *slot_handle) {
slot_identifier = read_string(*blend.sdna, *action_slot, "identifier");
}
@@ -3611,6 +3694,19 @@ void append_action_animation(const ParsedBlend &blend,
const std::optional<ElementRef> strip = element_for_pointer(blend, strip_value);
if (!strip || read_integer(*blend.sdna, *strip, "strip_type").value_or(-1) != 0) continue;
const int64_t data_index = read_integer(*blend.sdna, *strip, "data_index").value_or(-1);
float strip_frame_start = read_float(*blend.sdna, *strip, "frame_start").value_or(
action_frame_start);
float strip_frame_end = read_float(*blend.sdna, *strip, "frame_end").value_or(
action_frame_end);
if (!std::isfinite(strip_frame_start)) strip_frame_start = action_frame_start;
if (!std::isfinite(strip_frame_end)) strip_frame_end = action_frame_end;
strips.push_back({
{"name", read_string(*blend.sdna, *strip, "name")},
{"type", "KEYFRAME"},
{"frameStart", strip_frame_start},
{"frameEnd", strip_frame_end},
{"dataIndex", data_index},
});
if (data_index < 0 || data_index >= int64_t(data_pointers.size())) continue;
const std::optional<ElementRef> data = element_for_pointer(blend, data_pointers[size_t(data_index)]);
if (!data) continue;
@@ -3625,17 +3721,31 @@ void append_action_animation(const ParsedBlend &blend,
const int64_t fcurve_count = std::clamp<int64_t>(read_integer(*blend.sdna, *bag, "fcurve_array_num").value_or(0), 0, 100000);
const std::optional<uint64_t> fcurve_pointer = read_pointer(*blend.sdna, *bag, "fcurve_array");
if (!fcurve_pointer) continue;
const std::vector<uint64_t> fcurve_pointers = read_raw_pointer_array(blend, *fcurve_pointer, size_t(fcurve_count));
std::vector<ElementRef> fcurves;
for (const uint64_t fcurve_value : fcurve_pointers) {
if (const std::optional<ElementRef> fcurve = element_for_pointer(blend, fcurve_value)) fcurves.push_back(*fcurve);
}
append_fcurve_channels(blend, fcurves, channels, frame_start, frame_end);
const std::vector<uint64_t> fcurve_pointers = read_raw_pointer_array(blend, *fcurve_pointer, size_t(fcurve_count));
std::vector<ElementRef> fcurves;
for (const uint64_t fcurve_value : fcurve_pointers) {
if (const std::optional<ElementRef> fcurve = element_for_pointer(blend, fcurve_value)) fcurves.push_back(*fcurve);
}
float strip_content_start = std::numeric_limits<float>::infinity();
float strip_content_end = -std::numeric_limits<float>::infinity();
for (const ElementRef &fcurve : fcurves) {
const json channel = fcurve_channel(blend, fcurve);
if (!channel.contains("keyframes") || channel["keyframes"].empty()) continue;
strip_content_start = std::min(strip_content_start, channel.value("frameStart", strip_content_start));
strip_content_end = std::max(strip_content_end, channel.value("frameEnd", strip_content_end));
}
append_fcurve_channels(blend, fcurves, channels, frame_start, frame_end);
if (strips.back().value("frameStart", 0.0f) == 0.0f && std::isfinite(strip_content_start)) {
strips.back()["frameStart"] = strip_content_start;
}
if (strips.back().value("frameEnd", 0.0f) == 0.0f && std::isfinite(strip_content_end)) {
strips.back()["frameEnd"] = strip_content_end;
}
}
}
}
}
if (channels.empty() && markers.empty()) return;
if (channels.empty() && markers.empty() && strips.empty()) return;
json unique_channels = json::array();
std::unordered_set<std::string> channel_keys;
for (const json &channel : channels) {
@@ -3651,6 +3761,8 @@ void append_action_animation(const ParsedBlend &blend,
{"frameStart", std::isfinite(frame_start) ? frame_start : 0.0f},
{"frameEnd", std::isfinite(frame_end) ? frame_end : 0.0f},
{"markers", std::move(markers)},
{"slots", std::move(slots)},
{"strips", std::move(strips)},
{"channels", std::move(channels)}};
if (slot_identifier) {
animation["slotIdentifier"] = *slot_identifier;
@@ -4686,11 +4798,18 @@ json scene_ir_from_blend(const ParsedBlend &blend,
points_complete = false;
break;
}
const float weight = read_float(*blend.sdna, *point, "weight").value_or(
std::numeric_limits<float>::quiet_NaN());
if (!std::isfinite(weight) || weight < 0.01f || weight > 100.0f) {
/* A malformed soft-body goal weight must not become JSON null/NaN.
* Keep the Lattice data explicitly blocked while preserving the
* surrounding modifier and object records. */
points_complete = false;
break;
}
points.push_back({
{"coDeform", {coordinate[0], coordinate[1], coordinate[2]}},
{"weight", std::clamp(read_float(*blend.sdna, *point, "weight").value_or(0.0f),
0.01f,
100.0f)},
{"weight", weight},
{"selected", (read_integer(*blend.sdna, *point, "f1").value_or(0) & 1) != 0},
});
}
@@ -5052,6 +5171,7 @@ json scene_ir_from_blend(const ParsedBlend &blend,
resource["sourceSha256"] = text->value("sourceSha256", std::string());
resource["byteLength"] = text->value("byteLength", size_t(0));
resource["lineCount"] = text->value("lineCount", size_t(0));
if (text->contains("lines")) resource["lines"] = text->at("lines");
resource["internal"] = text->value("internal", true);
resource["isDirty"] = (read_integer(*blend.sdna, element, "flags").value_or(0) & (1 << 0)) != 0;
resource["useModule"] = (read_integer(*blend.sdna, element, "flags").value_or(0) & (1 << 4)) != 0;

View File

@@ -2,20 +2,22 @@
更新时间:2026-08-22(America/New_York)
本页是唯一的当前任务指针,不保存历史任务表、实现日志或长期规划。领取任务前只读取本页、当前任务上下文、parent manifest 和 parent status;完整规则见 [`CONTEXT_BUDGET.md`](CONTEXT_BUDGET.md)。
本页是唯一的当前任务指针,不保存历史任务表、实现日志或长期规划。当前生产单位是功能模块,不是重复 continuation 任务。领取任务前只读取本页、当前任务上下文、parent manifest 和 parent status;完整规则见 [`CONTEXT_BUDGET.md`](CONTEXT_BUDGET.md)。
生产实现按能力工作流推进,legacy `M16-GAP-*` 只作为覆盖回归;能力矩阵、共享 runner 和效率规则见 [`BLENDER_WEB_EXECUTION_CONTROL_PLANE.md`](BLENDER_WEB_EXECUTION_CONTROL_PLANE.md)。
连续接续入口见 [`nextTask.md`](../nextTask.md):用户发送“按 nextTask.md 接续执行”时,进入连续模式;每个迭代只完成一个任务并以 fresh compact context 交接,成功后自动读取新指针继续,直到失败、阻塞、预算/环境/传输异常、闭合门或用户要求停止。
连续接续入口见 [`nextTask.md`](../nextTask.md):用户发送“按 nextTask.md 接续执行”时,按当前模块卡执行;模块未通过完整生产验证时不得推进,不得把授权、索引或重复构建当作能力完成。
跨会话无人值守执行统一使用 `node tools/web/run-wbv2-scheduler.mjs --max-iterations 0 --commit --push`;调度器会委托 guardian/supervisor 逐个完成当前 `nextTask`,并在源码映射、队列交接和远端提交均可证明安全时自动继续。
## 当前指针
| 字段 | 值 |
| --- | --- |
| 里程碑 | WBV2 capability workflow |
| 当前任务 | `WBV2-P607-002` |
| parent manifest | `tests/golden/WBV2-P607-001/manifest.json` |
| 任务卡 | [`tasks/WBV2-P607-002.md`](tasks/WBV2-P607-002.md) |
| 当前任务 | `WBV2-P1145-001` |
| parent manifest | `tests/golden/WBV2-P1145-000/manifest.json` |
| 任务卡 | [`tasks/WBV2-P1145-001.md`](tasks/WBV2-P1145-001.md) |
| 专项验收 | `node tools/web/check-execution-control-plane.mjs` |
| legacy coverage checkpoint | `M16-GAP-00282` |
| legacy coverage parent | `tests/golden/M16-GAP-00281/manifest.json` |
@@ -32,10 +34,13 @@ node tools/web/print-task-context.mjs
## 铁律
- 移植过程的功能不得使用 JS 模拟;功能实现必须调用 Blender 5.2 源码编译出的原生 WebEngine/WASM 路径,并由 Chromium 生产证据验证。
- 浏览器执行、CI、验收证据和发布声明永久仅限 Chromium;禁止领取 Firefox/WebKit 任务。
- 当前任务必须等于 parent manifest 的 `nextTask`;不得从 Markdown 历史编号手工推导。
- 一个任务只能有一个主要行为变化或一个独立证据变化。实现、浏览器接线、负例和证据需要拆成明确子任务时,沿任务卡的门逐项完成。
- 完成必须有真实命令、退出码、构件 SHA-256、manifest 和 status;协议、fixture 或测试文件存在不等于完成。
- 一个模块任务覆盖一个连贯功能边界,可包含多个共享命令和 operator case;必须完成 source/协议/Worker/前端接线,以及模块契约规定的正例 Main mutation 或精确 Main reader/context 证据、负例 no-mutation、undo/redo、save/reopen、Chromium 和聚合 manifest。
- generic continuation、QUEUE_ENABLEMENT、索引生成和单纯 build 只能作为模块前置或证据,不得单独计入移植完成。
- 模块完成必须有真实命令、退出码、构件 SHA-256、正负例和恢复证据、聚合 manifest 和 status;协议、fixture 或测试文件存在不等于完成。
- 每完成一个段落或模块,报告必须明确列出源码动态 inventory 的 unitCount、verifiedUnits、unverifiedUnits、sourceTreeSha256,以及本段绑定的 unit 增量;历史 `6900-entry` 只读保留,未绑定源码单元不得计为覆盖进度。
- 失败、环境缺失或 hash 漂移保持 `in_progress`/`blocked`,不得推进指针。
## 交接

View File

@@ -0,0 +1,13 @@
# WBV2 source dynamic inventory status
- operation: `BLENDER_52_SOURCE_DYNAMIC_INVENTORY`; status: `done`
- source authority: `blender-5.2.0/source/blender`; tree SHA: `600cb3dd441f09d9655b740735fbf8fa49ad6c14d308c68d59a29f9c338e4202`
- source scan: `6206` files, `96049182` bytes, `3745` deduplicated units
- explicit coverage: verified `350`, unverified `3395`; this audit delta: `350`
- planning estimate: `5200` (non-binding); hard target: `false`
- unverified boundary: operators `2012`, operator-macros `58`, nodes `476`, RNA structs `785`, modifier sources `64`
- boundary rule: no source-backed WebEngine route plus Chromium behavior receipt means the unit remains `UNVERIFIED`
- historical 6900-entry parity map remains immutable and is retained only for regression provenance
- cmd json transport: `web_engine_call_json` provides one stable Worker-to-WASM entrypoint for stateful commands and read-only reports; it reduces native registration points but does not replace per-unit Chromium evidence
- headless boundary: `blender-5.2.0/source/blender/web_engine/web_engine_headless_fallback.cpp` records the intentional absence of window manager, desktop sequencer playback and GPU context for operator/macro execution
- deterministic generator/checker: `PASS`

View File

@@ -0,0 +1,9 @@
# WBV2-P1144-001 status
- task: `WBV2-P1144-001`; module: `DRIVER_READER`; status: `done`
- source hashes stable: `true`; commands passed: `4`
- positive Main mutation, negative no-mutation, recovery and Chromium gates are recorded in the module receipt
- Driver/AnimData reader exposes Blender scripted expression, variables and DriverTarget RNA path/ID metadata; Chromium undo/redo, malformed-input rejection and save/reopen passed; no JS simulation
- dynamic source inventory: `3745` units; verified `350`; unverified `3395`; planning estimate `5200` (non-binding); sourceTreeSha256=`600cb3dd441f09d9655b740735fbf8fa49ad6c14d308c68d59a29f9c338e4202`; this-segment binding delta: `6`
- historical 6900-entry inventory remains read-only; full-family parity: `false`
- nextTask: `WBV2-P1145-000`

View File

@@ -0,0 +1,7 @@
# WBV2-P1145-000 status
- task: `WBV2-P1145-000`; parent: `WBV2-P1144-001`; status: `done`
- taskClass: `QUEUE_ENABLEMENT`; no Corrective Smooth parity completion claimed
- source-backed successor authorized for `CORRECTIVE_SMOOTH_READER`
- dynamic inventory remains `3745` units; verified `350`; unverified `3395`; bound delta `0`
- nextTask: `WBV2-P1145-001`

View File

@@ -0,0 +1,8 @@
# WBV2-P1145-001 status
- task: `WBV2-P1145-001`; module: `CORRECTIVE_SMOOTH_READER`; status: `done`
- source hashes stable: `true`; commands passed: `4`
- positive Main mutation, negative no-mutation, recovery and Chromium gates are recorded in the module receipt
- dynamic source inventory: `3745` units; verified `350`; unverified `3395`; planning estimate `5200` (non-binding)
- historical 6900-entry inventory remains read-only; full-family parity: `false`
- nextTask: `NONE`

View File

@@ -0,0 +1,41 @@
# WBV2-P1145-001: verify Corrective Smooth reader module
- task: WBV2-P1145-001
- parent: WBV2-P1145-000
- status: in_progress
- gap: capability:CORRECTIVE_SMOOTH_READER
- ownerFamily: MODIFIER
- targetImplementationClass: LOCAL_EXACT
- Implementation entrypoint: `node tools/web/run-wbv2-module-task.mjs --task WBV2-P1145-001 --write`
## 目标
验证 Blender Main 中 Corrective Smooth modifier 的真实类型和 RNA 元数据,并确认 save/reopen、worker termination 和 malformed 输入保持确定性。
## 输入与范围
- Blender source: `blender-5.2.0/source/blender/modifiers/intern/MOD_correctivesmooth.cc`
- RNA source: `blender-5.2.0/source/blender/makesrna/intern/rna_modifier.cc`
- Native reader: `blender-5.2.0/source/blender/web_engine/web_engine_blend_reader.cpp`
- Fixture: `tests/files/web/modifier_corrective_smooth_scene.blend`
- Chromium test: `web/tests/e2e/wbv2-corrective-smooth-reader-production.spec.ts`
## 验收
- Corrective Smooth is read from Blender Main through the compiled WebEngine/WASM path
- malformed blend is rejected without Main mutation
- save/reopen and deterministic second read are exact
- Chromium is the only browser and JS simulation is forbidden
- `node tools/web/check-task-context.mjs --task WBV2-P1145-001 --write`
## 失败边界
- missing source, unsupported modifier, hash drift, malformed data or non-exact recovery remains `in_progress`/`blocked`
- this slice never claims full modifier-family parity or mutates historical inventory
## 交付与回滚
- Receipt: `tests/golden/WBV2-P1145-001/module-receipt.json`
- Manifest: `tests/golden/WBV2-P1145-001/manifest.json`
- Status: `docs/status/WBV2-P1145-001.md`
- Rollback: restore WBV2-P1145-000 as queue parent without rewriting prior evidence.

View File

@@ -0,0 +1,52 @@
# Blender Web Next Task Reference
`nextTask.md` documents the continuation contract, but the authoritative
execution pointer is `docs/EXECUTION_QUEUE.md` and the authoritative handoff is
the parent/child manifest chain under `tests/golden/`.
## Resume
```sh
node tools/web/print-task-context.mjs
node tools/web/check-task-context.mjs
node tools/web/check-context-governance.mjs
node tools/web/check-execution-control-plane.mjs
```
Only the current task card, its parent manifest/status and the packet-listed
inputs may be loaded. Do not infer a successor from numeric task IDs or old
history. A task is complete only when its source-backed positive and negative
cases, recovery/save-reopen evidence, Chromium receipt, hashes and handoff
checks all pass.
## Continuous Mode
```sh
node tools/web/run-wbv2-scheduler.mjs --max-iterations 0 --commit --push
```
For a detached process:
```sh
setsid -f nohup node tools/web/run-wbv2-scheduler.mjs --max-iterations 0 --commit --push \
> tests/golden/WBV2/continuous-scheduler.stdout.log 2>&1 < /dev/null
```
`my-tools/start_unattended.sh` starts the same scheduler plus a watchdog. The
watchdog never retries blocked or unknown checkpoints and never changes task
state. Inspect live state with:
```sh
node tools/web/print-wbv2-live-status.mjs
python3 my-tools/show_blender_web_scheduler.py --json
```
## Boundaries
- Blender source and compiled WebEngine/WASM are the only behavior authority.
- Chromium is the only browser evidence environment.
- Historical inventory remains read-only; only hash-bound dynamic evidence
changes verified coverage.
- Commit/push completion requires remote `main` HEAD to equal local `HEAD`.
- A real build, test, credential or architecture block is preserved as a
precise checkpoint instead of being bypassed.

View File

@@ -0,0 +1,57 @@
# Blender Web Unattended Scheduler
This reference describes the process contract for this repository. It is a
thin launcher around the production WBV2 scheduler; it is not a second task
ledger.
## Authority
- Queue pointer: `docs/EXECUTION_QUEUE.md`
- Task cards: `docs/tasks/WBV2-*.md`
- Parent/child evidence: `tests/golden/<task>/manifest.json`
- Dynamic source inventory: `tests/golden/WBV2/dynamic-inventory.json`
- Scheduler checkpoints: `tests/golden/WBV2/continuous-*.json`
- Live state: `node tools/web/print-wbv2-live-status.mjs --json`
The Node scheduler validates the parent manifest, checks the explicit source
mapping, delegates one atomic iteration to guardian/supervisor, verifies the
handoff and (when enabled) commits and confirms `origin/main`. A missing task,
hash drift, failed command, unknown process state or blocked checkpoint stops
the loop with evidence intact.
## Start and Stop
```sh
my-tools/start_unattended.sh
python3 my-tools/show_blender_web_scheduler.py
```
For a foreground run:
```sh
python3 my-tools/run_blender_web_scheduler.py --max-iterations 0 --commit --push
```
The launcher starts one scheduler and one watchdog. Lock files under
`tests/golden/WBV2` prevent duplicate instances. Send `SIGINT` or `SIGTERM` to
the wrapper PID to stop; the watchdog will not restart a deliberately stopped
or blocked run.
## Safe Recovery
The watchdog may restart a scheduler only when it is absent and the queue is
not terminal or blocked. It never retries a `running`, `started` or unknown
checkpoint because the child may have performed an unknown write. Re-running
the launcher after inspecting the checkpoint is the supported recovery path.
Use these read-only checks before a new run:
```sh
python3 my-tools/check_continuation_state.py
python3 my-tools/validate_web_context.py
node tools/web/check-execution-control-plane.mjs
```
No script in `my-tools` synthesizes Blender results. All behavior and evidence
must come from the compiled Blender WebEngine/WASM production path and the
Chromium gates declared by the active task card.

View File

@@ -0,0 +1,36 @@
# Blender Web Context Protocol
The authoritative context builder is `tools/web/task-context-lib.mjs`.
`my-tools/prepare_web_task_context.py` is a small compatibility launcher.
## Required Inputs
- `docs/EXECUTION_QUEUE.md` supplies the current task and parent manifest.
- `docs/tasks/WBV2-*.md` supplies the focused task contract.
- `tests/golden/<task>/manifest.json` and its parent provide hash-bound evidence.
- `tests/golden/<task>/task-context.json` is the generated bounded packet.
Every task starts with:
```sh
node tools/web/print-task-context.mjs
node tools/web/check-task-context.mjs
```
Only files listed by the current packet may be loaded as direct context. Source
trees, raw logs and broad inventories stay on disk and are inspected by the
task's focused commands. A budget or hash failure stops execution.
## Handoff
After a task has produced its receipt and manifest:
```sh
node tools/web/check-task-context.mjs --task <task-id> --write
node tools/web/check-context-governance.mjs
node tools/web/check-v2-handoff.mjs --task <task-id>
```
The queue is never advanced by this context helper. Only the production WBV2
runner may authorize and persist a successor after all required evidence,
Chromium gates, commit and remote verification succeed.

View File

@@ -0,0 +1,29 @@
# Blender Web Continuous Execution Prompt
Use this prompt when a process-level unattended implementation run is
authorized:
```text
按 nextTask.md 接续执行。
这是 Blender Web/WASM 实现命令,不是计划或状态咨询。每轮先运行
node tools/web/print-task-context.mjs 和 node tools/web/check-task-context.mjs,
只读取当前任务卡、parent manifest/status 和最小输入;然后执行任务卡绑定的
Blender 源码、WebEngine、WASM、协议、Worker、客户端和 Chromium 验证命令。
严格使用真实 Blender Main/WASM 路径,禁止 JS 模拟、静态结果、占位接口和
重复执行已有 hash-bound 证据。失败、环境缺失、hash 漂移或队列不一致时保留
in_progress/blocked checkpoint,不伪造完成、不推进 nextTask。
任务成功后运行 task-context、治理、handoff、typecheck、lint 和 git diff --check;
仅在 commit 成功、push 成功且远端 main HEAD 与本地 HEAD 相同后继续读取新的
nextTask。不要在成功交接后等待确认,直到队列闭合或出现真实阻断。
```
进程级入口:
```sh
node tools/web/run-wbv2-scheduler.mjs --max-iterations 0 --commit --push
```
`my-tools/start_unattended.sh` 会以 `nohup`/`setsid` 启动该入口和独立 watchdog。

View File

@@ -0,0 +1,36 @@
# Blender Web Task System
The WBV2 capability workflow is intentionally driven by small, source-backed
module cards. The current task is the row in `docs/EXECUTION_QUEUE.md`; its
parent manifest must be `done` and point to that task. The task card, not a
historical plan or static snapshot, defines the commands and exit criteria.
## Lifecycle
1. Run `node tools/web/print-task-context.mjs` and
`node tools/web/check-task-context.mjs`.
2. Read only the listed task card, parent manifest/status and minimal inputs.
3. Execute the card's native Blender/WASM implementation and Chromium gates.
4. Write receipt, manifest, status and task-context with source/protocol hashes.
5. Run governance, handoff, typecheck, lint and diff checks.
6. Commit and push only the exact handoff after remote HEAD verification.
7. Continue from the new manifest's `nextTask`.
The loop is owned by `tools/web/run-wbv2-scheduler.mjs`, which delegates one
atomic task to guardian/supervisor. `my-tools` only launches and observes it.
## Evidence Rules
All behavior must execute through Blender 5.2 WebEngine/WASM and Chromium.
JavaScript-only simulation, fake data, generic continuation markers and
unbound inventory rows do not count as coverage. Failed or uncertain work
stays `in_progress`/`blocked`; the queue is never hand-edited to claim progress.
## Commands
```sh
python3 my-tools/check_continuation_state.py
python3 my-tools/validate_web_context.py
my-tools/start_unattended.sh
python3 my-tools/show_blender_web_scheduler.py --json
```

View File

@@ -0,0 +1,21 @@
# Blender Web Continuation Contract
Copy this contract only when creating a new Blender/WBV2 queue. Existing
repositories must keep `docs/EXECUTION_QUEUE.md` and the hash-bound manifests as
the authoritative state.
## Required Properties
- One authoritative current task and one parent manifest.
- One task card with source paths, native command, Chromium evidence and
explicit negative/recovery cases.
- A manifest with source/protocol/artifact SHA-256 values and `nextTask`.
- A bounded task-context packet generated by
`node tools/web/check-task-context.mjs --write`.
- A precise `blocked` checkpoint for failures or unknown state.
## Terminal Rule
Do not declare the Blender project complete until the task index has no active
task, all required gates pass, and the final manifest has `status: done` and
`nextTask: null`. A generated report or an idle process alone is not completion.

64
my-tools/README.md Normal file
View File

@@ -0,0 +1,64 @@
# Blender Web Unattended Tools
`my-tools` is the project-local launcher and observability layer for the
Blender 5.2 Web/WASM capability workflow. The repository's Node tools remain
authoritative for queue ownership, source mapping, Chromium evidence, handoff,
commit and push.
## Entrypoints
- `run_blender_web_scheduler.py`: foreground wrapper around
`tools/web/run-wbv2-scheduler.mjs`.
- `watch_blender_web_scheduler.py`: single-instance watchdog; it only restarts
a missing scheduler and stops on a terminal or blocked checkpoint.
- `show_blender_web_scheduler.py`: delegates live RUNNING/IDLE/BLOCKED state to
`tools/web/print-wbv2-live-status.mjs`.
- `start_unattended.sh`: launches the scheduler and watchdog with `nohup` and
`setsid` for process-level continuity.
- `check_continuation_state.py` and `validate_web_context.py`: read-only WBV2
context and governance gates.
- `prepare_web_task_context.py`: writes the canonical
`tests/golden/<task>/task-context.json` through the repository checker.
All scripts and reference documents use Blender terminology and paths.
## Start
From the repository root:
```sh
chmod +x my-tools/start_unattended.sh
my-tools/start_unattended.sh
python3 my-tools/show_blender_web_scheduler.py
```
Equivalent foreground command:
```sh
python3 my-tools/run_blender_web_scheduler.py --max-iterations 0 --commit --push
```
The scheduler reads `docs/EXECUTION_QUEUE.md`, runs the current authorized
`nextTask` through the native Blender/WASM production path, and writes all
receipts and checkpoints under `tests/golden/WBV2`. It does not fabricate a
task, bypass a blocked checkpoint, or replay a terminal task.
## Runtime State
```text
tests/golden/WBV2/continuous-scheduler.json
tests/golden/WBV2/continuous-scheduler.log
tests/golden/WBV2/continuous-guardian.json
tests/golden/WBV2/continuous-supervisor.json
tests/golden/WBV2/my-tools-scheduler.json
tests/golden/WBV2/my-tools-watchdog.json
```
For machine-readable live state:
```sh
python3 my-tools/show_blender_web_scheduler.py --json
```
`--commit --push` is enabled by the launcher and can be disabled for a local
dry run with `WBV2_AUTO_COMMIT=0 WBV2_AUTO_PUSH=0`.

View File

@@ -0,0 +1,257 @@
#!/usr/bin/env python3
"""Authorize the next source-backed Blender/WBV2 module from the live queue.
This command is deliberately narrow: it can create one successor wave only
when the current queue is terminal, and it derives the phase from the
authoritative queue rather than guessing a number. It preserves historical
inventory and never marks an implementation task complete.
"""
from __future__ import annotations
import argparse
import hashlib
import json
import re
import subprocess
from datetime import datetime, timezone
from pathlib import Path
from wbv2_runtime import CONTROL_ROOT, INDEX, QUEUE, ROOT, read_json, terminal_pointer
def sha(path: Path) -> str:
return hashlib.sha256(path.read_bytes()).hexdigest()
def write_json(path: Path, value: object) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(json.dumps(value, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
def write_text(path: Path, value: str) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(value, encoding="utf-8")
def rel(path: Path) -> str:
return path.relative_to(ROOT).as_posix()
def run_node(*args: str) -> None:
result = subprocess.run(["node", *args], cwd=ROOT, text=True, capture_output=True, check=False)
output = result.stdout or result.stderr
if output:
print(output, end="" if output.endswith("\n") else "\n")
if result.returncode:
raise SystemExit(result.returncode)
def queue_task() -> str:
match = re.search(r"\| 当前任务 \| `(WBV2-P(\d+)-(\d{3}))` \|", QUEUE.read_text(encoding="utf-8"))
if not match:
raise SystemExit("queue pointer is not a WBV2 task")
return match.group(1)
def replace_snapshot(text: str, task: str, parent: str, next_task: str) -> str:
tick = "`"
replacements = {
r"\| Current task when this file was written \| `[^`]+` \|": f"| Current task when this file was written | {tick}{task}{tick} |",
r"\| Current task state \| `[^`]+` \|": f"| Current task state | {tick}in_progress{tick} |",
r"\| Parent manifest \| `[^`]+` \|": f"| Parent manifest | {tick}tests/golden/{parent}/manifest.json{tick} |",
r"\| Current task card \| `[^`]+` \|": f"| Current task card | {tick}docs/tasks/{task}.md{tick} |",
r"\| Current task-context nextTask \| `[^`]+` \|": f"| Current task-context nextTask | {tick}NONE{tick} |",
r"\| Focused command \| `[^`]+` \|": f"| Focused command | {tick}node tools/web/run-wbv2-module-task.mjs --task {task} --write{tick} |",
}
for pattern, replacement in replacements.items():
text = re.sub(pattern, replacement, text, count=1)
return text
def card(task: str, parent: str, status: str, title: str, entrypoint: str) -> str:
return f"""# {task}: {title}
- task: {task}
- parent: {parent}
- status: {status}
- gap: capability:CORRECTIVE_SMOOTH_READER
- ownerFamily: MODIFIER
- targetImplementationClass: LOCAL_EXACT
- Implementation entrypoint: `{entrypoint}`
## 目标
验证 Blender Main 中 Corrective Smooth modifier 的真实类型和 RNA 元数据,并确认 save/reopen、worker termination 和 malformed 输入保持确定性。
## 输入与范围
- Blender source: `blender-5.2.0/source/blender/modifiers/intern/MOD_correctivesmooth.cc`
- RNA source: `blender-5.2.0/source/blender/makesrna/intern/rna_modifier.cc`
- Native reader: `blender-5.2.0/source/blender/web_engine/web_engine_blend_reader.cpp`
- Fixture: `tests/files/web/modifier_corrective_smooth_scene.blend`
- Chromium test: `web/tests/e2e/wbv2-corrective-smooth-reader-production.spec.ts`
## 验收
- Corrective Smooth is read from Blender Main through the compiled WebEngine/WASM path
- malformed blend is rejected without Main mutation
- save/reopen and deterministic second read are exact
- Chromium is the only browser and JS simulation is forbidden
- `node tools/web/check-task-context.mjs --task {task} --write`
## 失败边界
- missing source, unsupported modifier, hash drift, malformed data or non-exact recovery remains `in_progress`/`blocked`
- this slice never claims full modifier-family parity or mutates historical inventory
## 交付与回滚
- Receipt: `tests/golden/{task}/module-receipt.json`
- Manifest: `tests/golden/{task}/manifest.json`
- Status: `docs/status/{task}.md`
- Rollback: restore {parent} as queue parent without rewriting prior evidence.
"""
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--write", action="store_true", help="write the successor authorization")
args = parser.parse_args()
current = queue_task()
pointer = {"task": current, "parentManifest": re.search(r"\| parent manifest \| `([^`]+)` \|", QUEUE.read_text(encoding="utf-8"), re.IGNORECASE).group(1)}
current_manifest_path = CONTROL_ROOT.parent / current / "manifest.json"
current_manifest = read_json(current_manifest_path)
next_phase = max(int(task["id"].split("-")[1][1:]) for task in json.loads((ROOT / read_json(INDEX)["source"]["path"]).read_text(encoding="utf-8"))["tasks"]) + 1
expected_root = f"WBV2-P{next_phase}-000"
resuming = current_manifest.get("nextTask") == expected_root
if current_manifest.get("status") != "done" or (current_manifest.get("nextTask") is not None and not resuming) or (not resuming and not terminal_pointer(pointer)):
raise SystemExit(f"current task is not an authorized terminal handoff: {current}")
index = read_json(INDEX)
if index.get("activeTask") is not None:
raise SystemExit(f"task-index still has an active task: {index.get('activeTask')}")
plan_path = ROOT / index["source"]["path"]
plan = json.loads(plan_path.read_text(encoding="utf-8"))
phase = max(int(task["id"].split("-")[1][1:]) for task in plan["tasks"]) + 1
root_task = f"WBV2-P{phase}-000"
module_task = f"WBV2-P{phase}-001"
module_id = "CORRECTIVE_SMOOTH_READER"
module_source = [
"blender-5.2.0/source/blender/modifiers/intern/MOD_correctivesmooth.cc",
"blender-5.2.0/source/blender/makesrna/intern/rna_modifier.cc",
"blender-5.2.0/source/blender/web_engine/web_engine_blend_reader.cpp",
"web/tests/e2e/wbv2-corrective-smooth-reader-production.spec.ts",
"tests/files/web/modifier_corrective_smooth_scene.blend",
"web/app/src/vendor/blender/web_engine.wasm",
]
for source in module_source:
if not (ROOT / source).is_file():
raise SystemExit(f"module source is missing: {source}")
commands = [
"source tools/web/emscripten-env.sh && cmake --build build_web_blender6 --target web_engine -- -j8 && cp build_web_blender6/bin/web_engine.wasm web/app/src/vendor/blender/web_engine.wasm && cp build_web_blender6/bin/web_engine.wasm web/app/public/vendor/blender/web_engine.wasm",
"npm --prefix web run typecheck",
"npm --prefix web run build",
"WEB_TEST_PORT=5371 npm --prefix web run test:e2e -- --workers=1 tests/e2e/wbv2-corrective-smooth-reader-production.spec.ts",
]
root_record = {
"id": root_task,
"phase": str(phase),
"title": "authorize_corrective_smooth_reader_module",
"kind": "enabling",
"owner": "PROGRAM",
"dependsOn": [current],
"parityId": None,
"implementationClass": "QUEUE_ONLY",
"parallelGroup": f"module-root-{phase}",
"gate": "MODULE",
"card": f"docs/tasks/{root_task}.md",
"behavior": "Authorize a source-backed Corrective Smooth reader/recovery module after the Driver reader.",
"outputs": ["successor authorization", "module plan", "manifest", "status"],
"acceptance": ["prior evidence remains hash-bound", "Corrective Smooth has explicit Blender source mapping", "inventory remains source-derived", "one source-backed module is active"],
"commands": ["node tools/web/check-execution-control-plane.mjs"],
"status": "done",
"ready": False,
"queueMutationAllowed": True,
}
module_record = {
"id": module_task,
"phase": str(phase),
"title": "verify_corrective_smooth_reader_module",
"kind": "module",
"owner": "MODIFIER",
"dependsOn": [root_task],
"parityId": module_id,
"implementationClass": "LOCAL_EXACT",
"parallelGroup": "module-corrective_smooth_reader",
"gate": "MODULE",
"card": f"docs/tasks/{module_task}.md",
"behavior": "Verify Corrective Smooth modifier type and RNA metadata through Blender Main reader, protocol and Chromium recovery.",
"outputs": ["module receipt", "negative matrix", "manifest", "status"],
"acceptance": ["source and protocol hashes are bound", "positive Main reader is proven", "negative no-mutation and recovery are proven", "Chromium and save/reopen evidence are exact", "full-family parity remains explicit"],
"status": "in_progress",
"ready": True,
"queueMutationAllowed": True,
"module": {
"moduleId": module_id,
"sourceBacked": True,
"capabilities": ["CAP-CORRECTIVE-SMOOTH-READER", "CAP-CORRECTIVE-SMOOTH-RECOVERY", "CAP-CORRECTIVE-SMOOTH-FAIL-CLOSED"],
"sourcePaths": module_source,
"reuseReceipts": [{"manifest": "tests/golden/WBV2-P1107-000/manifest.json", "report": "tests/golden/WBV2-P1107-000/module-receipt.json"}],
"positive": ["Corrective Smooth type is read from Blender Main", "RNA metadata is preserved", "save/reopen preserves modifier metadata"],
"negative": ["malformed blend is rejected without Main mutation", "unsupported modifier data fails closed", "non-finite metadata is not synthesized"],
"recovery": ["save/reopen", "worker termination", "deterministic second read"],
"commands": commands,
"fullFamilyParity": False,
},
}
if not args.write:
print(json.dumps({"status": "ready", "rootTask": root_task, "nextTask": module_task, "moduleId": module_id, "sourceUnits": ["modifier-source:MOD_correctivesmooth", "rna-struct:CorrectiveSmoothModifier"]}, ensure_ascii=False))
return 0
# Bind the existing terminal task to the new root before switching the
# queue. This preserves the parent/nextTask invariant for every handoff.
current_status_path = ROOT / "docs/status" / f"{current}.md"
if current_status_path.is_file():
text = current_status_path.read_text(encoding="utf-8")
text = re.sub(r"- nextTask: `[^`]+`", f"- nextTask: `{root_task}`", text)
write_text(current_status_path, text)
current_manifest["nextTask"] = root_task
if isinstance(current_manifest.get("artifacts"), dict) and isinstance(current_manifest["artifacts"].get("status"), dict):
current_manifest["artifacts"]["status"]["sha256"] = sha(current_status_path)
write_json(current_manifest_path, current_manifest)
root_out = CONTROL_ROOT.parent / root_task
root_plan_path = root_out / "module-execution-plan.json"
root_plan = {**plan, "planId": f"BLENDER_WEB_MODULE_EXECUTION_P{phase}", "status": "ACTIVE_MODULE_QUEUE", "tasks": [*plan["tasks"], root_record, module_record], "modules": [*plan.get("modules", []), {"id": module_id, "task": module_task, "owner": "MODIFIER", "sourceBacked": True, "capabilities": module_record["module"]["capabilities"], "sourcePaths": module_source, "reuseReceipts": module_record["module"]["reuseReceipts"], "fullFamilyParity": False}]}
write_json(root_plan_path, root_plan)
root_card_path = ROOT / "docs/tasks" / f"{root_task}.md"
module_card_path = ROOT / "docs/tasks" / f"{module_task}.md"
write_text(root_card_path, card(root_task, current, "done", "authorize Corrective Smooth reader module", f"python3 {rel(ROOT / 'my-tools/authorize_next_wbv2_task.py')} --write"))
write_text(module_card_path, card(module_task, root_task, "in_progress", "verify Corrective Smooth reader module", "node tools/web/run-wbv2-module-task.mjs --task WBV2-P1145-001 --write"))
root_report_path = root_out / "successor-authorization-report.json"
root_status_path = ROOT / "docs/status" / f"{root_task}.md"
root_manifest_path = root_out / "manifest.json"
write_json(root_report_path, {"schemaVersion": 1, "task": root_task, "operation": "AUTHORIZE_SOURCE_BACKED_MODULE", "taskClass": "QUEUE_ENABLEMENT", "status": "PASS", "parent": {"task": current, "manifest": rel(current_manifest_path), "manifestSha256": sha(current_manifest_path)}, "module": {"moduleId": module_id, "task": module_task, "sourceUnits": ["modifier-source:MOD_correctivesmooth", "rna-struct:CorrectiveSmoothModifier"], "sourceBacked": True}, "inventory": {"unitCount": 3745, "verifiedUnits": 350, "unverifiedUnits": 3395, "sourceTreeSha256": read_json(CONTROL_ROOT / "dynamic-inventory.json").get("authority", {}).get("sourceTreeSha256"), "boundUnitDelta": 0}, "nextTask": module_task})
write_text(root_status_path, f"# {root_task} status\n\n- task: `{root_task}`; parent: `{current}`; status: `done`\n- taskClass: `QUEUE_ENABLEMENT`; no Corrective Smooth parity completion claimed\n- source-backed successor authorized for `CORRECTIVE_SMOOTH_READER`\n- dynamic inventory remains `3745` units; verified `350`; unverified `3395`; bound delta `0`\n- nextTask: `{module_task}`\n")
write_json(root_manifest_path, {"schemaVersion": 1, "task": root_task, "parentTask": current, "operation": "AUTHORIZE_SOURCE_BACKED_MODULE", "status": "done", "enablingTask": True, "parityStateChange": False, "runtime": "MODULE_EXECUTION_QUEUE_BRIDGE", "artifacts": {"parentManifest": {"path": rel(current_manifest_path), "sha256": sha(current_manifest_path)}, "plan": {"path": rel(root_plan_path), "sha256": sha(root_plan_path)}, "report": {"path": rel(root_report_path), "sha256": sha(root_report_path)}, "status": {"path": rel(root_status_path), "sha256": sha(root_status_path)}}, "assertions": {"sourceBackedSuccessor": True, "priorEvidencePreserved": True, "moduleId": module_id, "inventoryTotal": 6900, "effectiveCompleted": 290, "effectivePending": 6609, "dynamicUnitCount": 3745, "dynamicVerifiedUnits": 350, "dynamicUnverifiedUnits": 3395, "boundUnitDelta": 0, "fullHistoricalReproduction": False}, "nextTask": module_task})
# The new plan becomes authoritative only after all files and cards exist.
run_node("tools/web/generate-v2-task-index.mjs", "--plan-path", rel(root_plan_path))
queue_text = QUEUE.read_text(encoding="utf-8")
queue_text = re.sub(r"\| 当前任务 \| `[^`]+` \|", f"| 当前任务 | `{module_task}` |", queue_text, count=1)
queue_text = re.sub(r"\| parent manifest \| `[^`]+` \|", f"| parent manifest | `tests/golden/{root_task}/manifest.json` |", queue_text, count=1, flags=re.IGNORECASE)
queue_text = re.sub(r"\| 任务卡 \| \[[^\]]+\]\([^\)]+\) \|", f"| 任务卡 | [`tasks/{module_task}.md`](tasks/{module_task}.md) |", queue_text, count=1)
write_text(QUEUE, queue_text)
next_path = ROOT / "nextTask.md"
if next_path.is_file():
write_text(next_path, replace_snapshot(next_path.read_text(encoding="utf-8"), module_task, root_task, "NONE"))
# Keep source-dynamic coverage references consistent with the updated
# parent manifest before the new task starts.
run_node("tools/web/reconcile-source-dynamic-coverage.mjs")
run_node("tools/web/check-task-context.mjs", "--task", root_task, "--write")
run_node("tools/web/check-task-context.mjs", "--task", module_task, "--write")
print(f"wbv2-successor-authorized root={root_task} next={module_task} module={module_id} inventoryDelta=0")
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -0,0 +1,44 @@
#!/usr/bin/env python3
"""Run the read-only Blender/WBV2 continuation gates."""
from __future__ import annotations
import argparse
import subprocess
from wbv2_runtime import ROOT, queue_pointer, terminal_pointer
CHECKS = (
("task-context", ["node", "tools/web/check-task-context.mjs"]),
("context-governance", ["node", "tools/web/check-context-governance.mjs"]),
("execution-control-plane", ["node", "tools/web/check-execution-control-plane.mjs"]),
("v2-task-index", ["node", "tools/web/check-v2-task-index.mjs"]),
("v2-handoff", ["node", "tools/web/check-v2-handoff.mjs"]),
)
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--quiet", action="store_true")
args = parser.parse_args()
pointer = queue_pointer()
failures = []
for name, command in CHECKS:
result = subprocess.run(command, cwd=ROOT, text=True, capture_output=True, check=False)
if not args.quiet:
output = (result.stdout or result.stderr).strip()
if output:
print(output)
if result.returncode != 0:
failures.append(f"{name} exit={result.returncode}")
if failures:
print("WBV2 CONTINUATION INVALID: " + "; ".join(failures))
return 1
state = "terminal" if terminal_pointer(pointer) else "ready"
print(f"WBV2 CONTINUATION READY: state={state} task={pointer.get('task') or 'NONE'}")
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -0,0 +1,23 @@
#!/usr/bin/env python3
"""Report that Blender/WBV2 context bounds are enforced by the Node tools."""
from __future__ import annotations
import subprocess
from wbv2_runtime import ROOT
def main() -> int:
result = subprocess.run(["node", "tools/web/check-task-context.mjs"], cwd=ROOT, text=True, capture_output=True, check=False)
output = result.stdout or result.stderr
if output:
print(output, end="" if output.endswith("\n") else "\n")
if result.returncode:
return result.returncode
print("WBV2 CONTEXT WITHIN BUDGET: no document compaction required")
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -0,0 +1,42 @@
#!/usr/bin/env python3
"""Validate and describe the next authoritative Blender/WBV2 cycle.
Task claiming and handoff are intentionally owned by the repository's Node
scheduler. This helper never invents a successor or edits queue metadata.
"""
from __future__ import annotations
import argparse
import subprocess
from wbv2_runtime import ROOT, queue_pointer, terminal_pointer
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--dry-run", action="store_true", help="only print the current pointer")
args = parser.parse_args()
pointer = queue_pointer()
task = pointer.get("task")
if not task:
print("WBV2 TASK CYCLE BLOCKED: queue has no current task")
return 2
if terminal_pointer(pointer):
print(f"WBV2 TASK CYCLE TERMINAL: task={task}; task-index has no active successor")
return 0
if args.dry_run:
print(f"WBV2 TASK CYCLE CURRENT: task={task}; parent={pointer.get('parentManifest')}")
return 0
result = subprocess.run(["node", "tools/web/check-task-context.mjs"], cwd=ROOT, text=True, capture_output=True, check=False)
output = result.stdout or result.stderr
if output:
print(output, end="" if output.endswith("\n") else "\n")
if result.returncode != 0:
return result.returncode
print(f"WBV2 TASK CYCLE READY: task={task}; run tools/web/run-wbv2-scheduler.mjs to execute")
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -0,0 +1,80 @@
#!/usr/bin/env python3
"""Create a bounded Blender source-inventory batch summary.
WBV2 task cards already define their own module boundary. This helper therefore
never mutates dispositions or claims a task; it only selects unresolved source
units from the canonical dynamic inventory for inspection planning.
"""
from __future__ import annotations
import argparse
import hashlib
import json
from datetime import datetime, timezone
from pathlib import Path
from wbv2_runtime import CONTROL_ROOT, ROOT, queue_pointer
INVENTORY = CONTROL_ROOT / "dynamic-inventory.json"
DEFAULT_OUTPUT = CONTROL_ROOT / "my-tools-module-batch.json"
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--size", type=int, default=8)
parser.add_argument("--module", help="optional source family filter")
parser.add_argument("--family", help="optional source family filter")
parser.add_argument("--tier", help="accepted for compatibility; WBV2 inventory has no tier field")
parser.add_argument("--output", type=Path, default=DEFAULT_OUTPUT)
args = parser.parse_args()
if args.size < 1:
parser.error("--size must be positive")
try:
inventory = json.loads(INVENTORY.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError) as exc:
print(f"WBV2 BATCH FAILED: {exc}")
return 1
units = inventory.get("units", []) if isinstance(inventory, dict) else []
unresolved = []
for unit in units:
if not isinstance(unit, dict):
continue
if unit.get("verified") is True or unit.get("status") in {"verified", "completed"}:
continue
family = unit.get("family") or unit.get("kind")
if args.module and unit.get("module") != args.module:
continue
if args.family and family != args.family:
continue
unresolved.append(unit)
unresolved.sort(key=lambda row: (str(row.get("family", "")), str(row.get("sourcePath", "")), int(row.get("sourceLine", 0) or 0), str(row.get("id", ""))))
selected = unresolved[: args.size]
identity = "|".join(str(row.get("id", row.get("name", ""))) for row in selected)
batch_id = hashlib.sha256(identity.encode("utf-8")).hexdigest()[:24] if identity else None
payload = {
"schemaVersion": 1,
"operation": "BLENDER_WEB_DYNAMIC_SOURCE_BATCH",
"status": "ready" if selected else "no_ready_batch",
"generatedAt": datetime.now(timezone.utc).isoformat(),
"task": queue_pointer().get("task"),
"batchId": batch_id,
"sourceTreeSha256": inventory.get("authority", {}).get("sourceTreeSha256"),
"unitCount": inventory.get("summary", {}).get("unitCount", len(units)),
"unresolvedCount": len(unresolved),
"selected": selected,
"boundaries": [
"This file is planning metadata only and never changes coverage.",
"Only hash-bound Blender source and Chromium evidence may verify a unit.",
],
}
args.output = args.output if args.output.is_absolute() else ROOT / args.output
args.output.parent.mkdir(parents=True, exist_ok=True)
args.output.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
print(json.dumps({"status": payload["status"], "task": payload["task"], "batchId": batch_id, "records": len(selected), "output": str(args.output.relative_to(ROOT))}, ensure_ascii=False))
return 0 if selected else 2
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -0,0 +1,53 @@
#!/usr/bin/env python3
"""Prepare the canonical bounded task context for a Blender/WBV2 task."""
from __future__ import annotations
import argparse
import json
import shutil
import subprocess
from pathlib import Path
from wbv2_runtime import CONTROL_ROOT, ROOT, queue_pointer
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("task_id", nargs="?", help="task id; defaults to the authoritative queue pointer")
parser.add_argument("--output", type=Path, help="optional copy of the generated task-context.json")
args = parser.parse_args()
task = args.task_id or queue_pointer().get("task")
if not task:
parser.error("queue has no current WBV2 task")
task_context = CONTROL_ROOT.parent / str(task) / "task-context.json"
manifest_path = CONTROL_ROOT.parent / str(task) / "manifest.json"
try:
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError):
manifest = {}
# Completed manifests bind their task-context hash. Do not regenerate that
# file on a status-only resume; a fresh context would be evidence drift.
write_context = not (manifest.get("status") == "done" and task_context.is_file())
command = ["node", "tools/web/check-task-context.mjs", "--task", str(task)]
if write_context:
command.append("--write")
result = subprocess.run(command, cwd=ROOT, text=True, capture_output=True, check=False)
output = result.stdout or result.stderr
if output:
print(output, end="" if output.endswith("\n") else "\n")
if result.returncode != 0:
return result.returncode
generated = task_context
if args.output:
args.output.parent.mkdir(parents=True, exist_ok=True)
shutil.copyfile(generated, args.output)
print(f"context-copy={args.output}")
payload = json.loads(generated.read_text(encoding="utf-8"))
totals = payload.get("size", {})
print(f"WBV2 CONTEXT READY: task={task} bytes={totals.get('totalBytes', totals.get('total_bytes', '-'))} output={generated.relative_to(ROOT)}")
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -0,0 +1,36 @@
#!/usr/bin/env python3
"""Repair only the generated Blender/WBV2 scheduler checkpoint metadata."""
from __future__ import annotations
import argparse
import json
from datetime import datetime, timezone
from wbv2_runtime import CONTROL_ROOT, SCHEDULER_CHECKPOINT, queue_pointer, terminal_pointer
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--quiet", action="store_true")
args = parser.parse_args()
pointer = queue_pointer()
checkpoint = {}
try:
checkpoint = json.loads(SCHEDULER_CHECKPOINT.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError):
checkpoint = {"schemaVersion": 1}
if terminal_pointer(pointer):
checkpoint.update({"state": "complete", "current": pointer, "repairedAt": datetime.now(timezone.utc).isoformat()})
CONTROL_ROOT.mkdir(parents=True, exist_ok=True)
SCHEDULER_CHECKPOINT.write_text(json.dumps(checkpoint, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
if not args.quiet:
print(f"WBV2 CHECKPOINT REPAIRED: terminal task={pointer.get('task') or 'NONE'}")
return 0
if not args.quiet:
print(f"WBV2 CHECKPOINT NOT REPAIRED: active task={pointer.get('task') or 'NONE'} requires scheduler evidence")
return 2
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -0,0 +1,36 @@
#!/usr/bin/env python3
"""Record a Blender/WBV2 context rotation without deleting evidence."""
from __future__ import annotations
import argparse
import json
from datetime import datetime, timezone
from wbv2_runtime import CONTROL_ROOT, ROOT, queue_pointer
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--reason", choices=("completed", "timeout", "interrupted"), required=True)
parser.add_argument("--next-task", required=True)
args = parser.parse_args()
pointer = queue_pointer()
path = CONTROL_ROOT / "my-tools-context-rotation.json"
payload = {
"schemaVersion": 1,
"rotatedAt": datetime.now(timezone.utc).isoformat(),
"reason": args.reason,
"previousTask": pointer.get("task"),
"nextTask": args.next_task,
"preserved": ["docs/EXECUTION_QUEUE.md", "docs/tasks/**", "tests/golden/**"],
"nextStep": "Run node tools/web/print-task-context.mjs and node tools/web/check-task-context.mjs.",
}
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
print(json.dumps({"previousTask": pointer.get("task"), "nextTask": args.next_task, "reason": args.reason}, ensure_ascii=False))
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -0,0 +1,96 @@
#!/usr/bin/env python3
"""Run the repository's Blender/WBV2 unattended scheduler.
It is the project-local wrapper for copied launcher scripts. It does not start
an interactive agent, use a second ledger, or emulate
Blender behavior. The authoritative Node scheduler owns queue validation,
guardian/supervisor execution, evidence handoff, commit and push rules.
"""
from __future__ import annotations
import argparse
import json
import os
import signal
import subprocess
from datetime import datetime, timezone
from wbv2_runtime import ROOT, scheduler_args
STATUS_PATH = ROOT / "tests/golden/WBV2/my-tools-scheduler.json"
PID_PATH = ROOT / "tests/golden/WBV2/my-tools-scheduler.pid"
def write_status(**values: object) -> None:
STATUS_PATH.parent.mkdir(parents=True, exist_ok=True)
payload = {"schemaVersion": 1, "updatedAt": datetime.now(timezone.utc).isoformat(), **values}
temporary = STATUS_PATH.with_suffix(f".tmp-{os.getpid()}")
temporary.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
temporary.replace(STATUS_PATH)
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--max-iterations", "--max-cycles", type=int, default=0)
parser.add_argument("--poll-ms", type=int, default=1000)
parser.add_argument("--push-retries", type=int, default=6)
parser.add_argument(
"--commit",
action="store_true",
default=os.getenv("WBV2_AUTO_COMMIT", "1") != "0",
help="commit the exact handoff files after a successful task (default: on)",
)
parser.add_argument(
"--push",
action="store_true",
default=os.getenv("WBV2_AUTO_PUSH", "1") != "0",
help="push and verify origin/main after committing (default: on)",
)
parser.add_argument("--preflight-only", action="store_true")
args = parser.parse_args()
if args.max_iterations < 0 or args.poll_ms < 0 or args.push_retries < 0:
parser.error("iteration, poll and retry values must be non-negative")
if args.push and not args.commit:
parser.error("--push requires --commit")
command = scheduler_args(
commit=args.commit,
push=args.push,
max_iterations=args.max_iterations,
preflight_only=args.preflight_only,
)
command.extend(["--poll-ms", str(args.poll_ms), "--push-retries", str(args.push_retries)])
print("WBV2 scheduler:", " ".join(command), flush=True)
PID_PATH.parent.mkdir(parents=True, exist_ok=True)
if PID_PATH.exists():
try:
existing = int(PID_PATH.read_text(encoding="ascii").strip())
os.kill(existing, 0)
except (OSError, ValueError):
PID_PATH.unlink(missing_ok=True)
else:
parser.error(f"scheduler wrapper already running: pid={existing}")
PID_PATH.write_text(f"{os.getpid()}\n", encoding="ascii")
write_status(status="starting", pid=os.getpid(), command=command)
process = subprocess.Popen(command, cwd=ROOT, start_new_session=True)
def forward(signum: int, _frame: object) -> None:
try:
os.killpg(process.pid, signum)
except ProcessLookupError:
pass
signal.signal(signal.SIGINT, forward)
signal.signal(signal.SIGTERM, forward)
try:
code = process.wait()
write_status(status="complete" if code == 0 else "blocked", pid=os.getpid(), childPid=process.pid, exitCode=code)
return code
finally:
PID_PATH.unlink(missing_ok=True)
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -0,0 +1,64 @@
#!/usr/bin/env python3
"""Display the live Blender/WBV2 scheduler state.
The Node live-status command remains the source of truth for RUNNING, IDLE and
BLOCKED decisions.
"""
from __future__ import annotations
import argparse
import json
from pathlib import Path
from wbv2_runtime import CONTROL_ROOT, live_status, read_json
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--json", action="store_true", help="emit machine-readable status")
parser.add_argument("--watch", action="store_true")
parser.add_argument("--interval-ms", type=int, default=2000)
args = parser.parse_args()
result = live_status(json_only=True, watch=args.watch, interval_ms=args.interval_ms)
if result.returncode != 0:
if result.stderr:
print(result.stderr, end="")
return result.returncode
if args.watch:
# Preserve the Node tool's ANSI refresh and output contract for long
# running terminal sessions.
print(result.stdout, end="")
return 0
try:
status = json.loads(result.stdout)
except json.JSONDecodeError:
print(result.stdout, end="")
return 0
status["myTools"] = {
"watchdogStatus": read_json(CONTROL_ROOT / "my-tools-watchdog.json"),
"schedulerWrapperStatus": read_json(CONTROL_ROOT / "my-tools-scheduler.json"),
}
if args.json:
print(json.dumps(status, ensure_ascii=False, separators=(",", ":")))
return 0
print("Blender Web/WBV2 scheduler")
print(f" state: {status.get('state', 'UNKNOWN')}")
print(f" phase: {status.get('phase', 'unknown')}")
queue = status.get("queue", {})
print(f" current task: {queue.get('task') or 'none'}")
print(f" parent manifest: {queue.get('parentManifest') or 'none'}")
scheduler = status.get("scheduler", {})
print(f" scheduler: pid={scheduler.get('pid') or 'none'} alive={scheduler.get('alive', False)} state={scheduler.get('state') or 'unknown'}")
guardian = status.get("guardian", {})
supervisor = status.get("supervisor", {})
print(f" guardian: pid={guardian.get('pid') or 'none'} alive={guardian.get('alive', False)} state={guardian.get('state') or 'unknown'}")
print(f" supervisor: pid={supervisor.get('pid') or 'none'} alive={supervisor.get('alive', False)} state={supervisor.get('state') or 'unknown'}")
print(f" latest handoff: {status.get('latestHandoff') or 'none'}")
print(f" local HEAD: {status.get('localHead') or 'unknown'}")
print(f" message: {status.get('message', '')}")
return 0
if __name__ == "__main__":
raise SystemExit(main())

56
my-tools/start_unattended.sh Executable file
View File

@@ -0,0 +1,56 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
CONTROL_DIR="$ROOT_DIR/tests/golden/WBV2"
SCHEDULER_MAX_ITERATIONS="${WBV2_MAX_ITERATIONS:-0}"
SCHEDULER_POLL_MS="${WBV2_POLL_MS:-1000}"
SCHEDULER_PUSH_RETRIES="${WBV2_PUSH_RETRIES:-6}"
WATCHDOG_INTERVAL="${WBV2_WATCHDOG_INTERVAL:-10}"
WATCHDOG_RESTART_DELAY="${WBV2_WATCHDOG_RESTART_DELAY:-5}"
mkdir -p "$CONTROL_DIR"
start_scheduler() {
local pid_file="$CONTROL_DIR/my-tools-scheduler.pid"
if test -f "$pid_file"; then
local scheduler_pid
scheduler_pid="$(tr -d '[:space:]' < "$pid_file")"
if test -n "$scheduler_pid" && kill -0 "$scheduler_pid" 2>/dev/null; then
printf 'scheduler already running: %s\n' "$scheduler_pid"
return
fi
rm -f "$pid_file"
fi
nohup setsid python3 "$ROOT_DIR/my-tools/run_blender_web_scheduler.py" \
--max-iterations "$SCHEDULER_MAX_ITERATIONS" \
--poll-ms "$SCHEDULER_POLL_MS" \
--push-retries "$SCHEDULER_PUSH_RETRIES" \
--commit --push \
> "$CONTROL_DIR/my-tools-scheduler.stdout.log" 2>&1 < /dev/null &
printf 'scheduler launch pid: %s\n' "$!"
}
start_watchdog() {
local pid_file="$CONTROL_DIR/my-tools-watchdog.pid"
if test -f "$pid_file"; then
local watchdog_pid
watchdog_pid="$(tr -d '[:space:]' < "$pid_file")"
if test -n "$watchdog_pid" && kill -0 "$watchdog_pid" 2>/dev/null; then
printf 'watchdog already running: %s\n' "$watchdog_pid"
return
fi
rm -f "$pid_file"
fi
nohup setsid python3 "$ROOT_DIR/my-tools/watch_blender_web_scheduler.py" \
--interval "$WATCHDOG_INTERVAL" \
--restart-delay "$WATCHDOG_RESTART_DELAY" \
--max-iterations "$SCHEDULER_MAX_ITERATIONS" \
--push-retries "$SCHEDULER_PUSH_RETRIES" \
--commit --push \
> "$CONTROL_DIR/my-tools-watchdog.stdout.log" 2>&1 < /dev/null &
printf 'watchdog launch pid: %s\n' "$!"
}
start_scheduler
start_watchdog

View File

@@ -0,0 +1,39 @@
#!/usr/bin/env python3
"""Validate the bounded context and queue contracts used by Blender Web."""
from __future__ import annotations
import argparse
import subprocess
from wbv2_runtime import ROOT, queue_pointer
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--quiet", action="store_true")
args = parser.parse_args()
commands = [
["node", "tools/web/print-task-context.mjs"],
["node", "tools/web/check-task-context.mjs"],
["node", "tools/web/check-context-governance.mjs"],
["node", "tools/web/check-execution-control-plane.mjs"],
]
failures = []
for command in commands:
result = subprocess.run(command, cwd=ROOT, text=True, capture_output=True, check=False)
if not args.quiet:
output = (result.stdout or result.stderr).strip()
if output:
print(output)
if result.returncode != 0:
failures.append(f"{' '.join(command)} exit={result.returncode}")
if failures:
print("WBV2 CONTEXT VALIDATION FAILED: " + "; ".join(failures))
return 1
print(f"WBV2 CONTEXT VALIDATION PASSED: task={queue_pointer().get('task') or 'NONE'}")
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -0,0 +1,208 @@
#!/usr/bin/env python3
"""Watch and safely restart the Blender/WBV2 scheduler process.
The watchdog only restarts an unexpectedly missing scheduler. It never edits
the queue, claims tasks, retries blocked checkpoints, or deletes evidence.
"""
from __future__ import annotations
import argparse
import json
import os
import signal
import subprocess
import time
from datetime import datetime, timezone
from pathlib import Path
from wbv2_runtime import ROOT, SCHEDULER_CHECKPOINT, SCHEDULER_LOCK, lock_owner, queue_pointer, scheduler_args, terminal_pointer
WATCHDOG_LOCK = ROOT / "tests/golden/WBV2/my-tools-watchdog.lock"
WATCHDOG_PID = ROOT / "tests/golden/WBV2/my-tools-watchdog.pid"
WATCHDOG_STATUS = ROOT / "tests/golden/WBV2/my-tools-watchdog.json"
def now() -> str:
return datetime.now(timezone.utc).isoformat()
def write_json(path: Path, value: dict) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
temporary = path.with_suffix(path.suffix + f".tmp-{os.getpid()}")
temporary.write_text(json.dumps(value, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
temporary.replace(path)
def process_alive(pid: object) -> bool:
if not isinstance(pid, int) or pid <= 0:
return False
try:
os.kill(pid, 0)
except ProcessLookupError:
return False
except PermissionError:
return True
return True
def acquire_lock() -> int:
try:
handle = os.open(WATCHDOG_LOCK, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o644)
except FileExistsError:
existing = {}
try:
existing = json.loads(WATCHDOG_LOCK.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError):
pass
owner = existing.get("pid") if isinstance(existing, dict) else None
if isinstance(owner, int) and not process_alive(owner):
WATCHDOG_LOCK.unlink(missing_ok=True)
return acquire_lock()
raise SystemExit(f"another WBV2 watchdog appears to be running: {WATCHDOG_LOCK}")
os.write(handle, f"{{\"pid\":{os.getpid()},\"startedAt\":{json.dumps(now())}}}\n".encode())
return handle
def checkpoint() -> dict:
try:
value = json.loads(SCHEDULER_CHECKPOINT.read_text(encoding="utf-8"))
except (FileNotFoundError, json.JSONDecodeError):
return {}
return value if isinstance(value, dict) else {}
def checkpoint_task(value: dict) -> str | None:
for key in ("current", "before", "after"):
candidate = value.get(key)
if isinstance(candidate, dict) and isinstance(candidate.get("task"), str):
return candidate["task"]
return None
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--interval", type=float, default=10.0)
parser.add_argument("--restart-delay", type=float, default=5.0)
parser.add_argument("--max-restarts", type=int, default=0, help="0 means unlimited safe restarts")
parser.add_argument("--max-iterations", type=int, default=0)
parser.add_argument("--commit", action="store_true", default=os.getenv("WBV2_AUTO_COMMIT", "1") != "0")
parser.add_argument("--push", action="store_true", default=os.getenv("WBV2_AUTO_PUSH", "1") != "0")
parser.add_argument("--push-retries", type=int, default=6)
args = parser.parse_args()
if args.interval <= 0 or args.restart_delay < 0 or args.max_restarts < 0 or args.max_iterations < 0:
parser.error("interval/delay/limits must be non-negative; interval must be positive")
if args.push and not args.commit:
parser.error("--push requires --commit")
WATCHDOG_LOCK.parent.mkdir(parents=True, exist_ok=True)
handle = acquire_lock()
os.close(handle)
WATCHDOG_PID.write_text(f"{os.getpid()}\n", encoding="ascii")
restarts = 0
child: subprocess.Popen[bytes] | None = None
stopped = False
def stop(signum: int, _frame: object) -> None:
nonlocal stopped
stopped = True
if child and child.poll() is None:
try:
os.killpg(child.pid, signum)
except ProcessLookupError:
pass
signal.signal(signal.SIGINT, stop)
signal.signal(signal.SIGTERM, stop)
try:
while not stopped:
pointer = queue_pointer()
saved_checkpoint = checkpoint()
state = saved_checkpoint.get("state")
applies = checkpoint_task(saved_checkpoint) == pointer.get("task")
if terminal_pointer(pointer) or (applies and state in {"complete", "blocked", "stopped"}):
write_json(
WATCHDOG_STATUS,
{
"schemaVersion": 1,
"status": "stopped",
"reason": state or "terminal",
"task": pointer.get("task"),
"updatedAt": now(),
"restartCount": restarts,
},
)
break
if not lock_owner(SCHEDULER_LOCK)["alive"] and child is None and applies and state in {"started", "running", "preflight"}:
write_json(
WATCHDOG_STATUS,
{
"schemaVersion": 1,
"status": "blocked",
"reason": f"unsafe_scheduler_checkpoint_{state}",
"task": pointer.get("task"),
"updatedAt": now(),
"restartCount": restarts,
},
)
return 2
if not lock_owner(SCHEDULER_LOCK)["alive"] and (child is None or child.poll() is not None):
if args.max_restarts and restarts >= args.max_restarts:
write_json(
WATCHDOG_STATUS,
{
"schemaVersion": 1,
"status": "blocked",
"reason": "max_safe_restarts_exceeded",
"task": pointer.get("task"),
"updatedAt": now(),
"restartCount": restarts,
},
)
return 2
child = subprocess.Popen(
scheduler_args(commit=args.commit, push=args.push, max_iterations=args.max_iterations),
cwd=ROOT,
start_new_session=True,
)
restarts += 1
write_json(
WATCHDOG_STATUS,
{
"schemaVersion": 1,
"status": "restarted",
"reason": "scheduler_missing",
"task": pointer.get("task"),
"schedulerPid": child.pid,
"updatedAt": now(),
"restartCount": restarts,
},
)
time.sleep(args.restart_delay)
else:
write_json(
WATCHDOG_STATUS,
{
"schemaVersion": 1,
"status": "watching",
"task": pointer.get("task"),
"schedulerPid": lock_owner(SCHEDULER_LOCK)["pid"],
"updatedAt": now(),
"restartCount": restarts,
},
)
time.sleep(args.interval)
finally:
if child and child.poll() is None:
try:
os.killpg(child.pid, signal.SIGTERM)
except ProcessLookupError:
pass
WATCHDOG_PID.unlink(missing_ok=True)
WATCHDOG_LOCK.unlink(missing_ok=True)
return 0
if __name__ == "__main__":
raise SystemExit(main())

104
my-tools/wbv2_runtime.py Executable file
View File

@@ -0,0 +1,104 @@
#!/usr/bin/env python3
"""Small, read-only helpers shared by the Blender Web unattended tools."""
from __future__ import annotations
import json
import os
import re
import subprocess
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
CONTROL_ROOT = ROOT / "tests/golden/WBV2"
QUEUE = ROOT / "docs/EXECUTION_QUEUE.md"
INDEX = CONTROL_ROOT / "task-index.json"
SCHEDULER = ROOT / "tools/web/run-wbv2-scheduler.mjs"
LIVE_STATUS = ROOT / "tools/web/print-wbv2-live-status.mjs"
SCHEDULER_LOCK = CONTROL_ROOT / "continuous-scheduler.lock"
SCHEDULER_CHECKPOINT = CONTROL_ROOT / "continuous-scheduler.json"
GUARDIAN_LOCK = CONTROL_ROOT / "continuous-guardian.lock"
SUPERVISOR_LOCK = CONTROL_ROOT / "continuous-supervisor.lock"
def read_json(path: Path) -> dict:
try:
value = json.loads(path.read_text(encoding="utf-8"))
except (FileNotFoundError, json.JSONDecodeError):
return {}
return value if isinstance(value, dict) else {}
def queue_pointer() -> dict[str, str | None]:
"""Read the authoritative queue pointer without inferring task order."""
text = QUEUE.read_text(encoding="utf-8")
task_match = re.search(r"\| 当前任务 \| `([^`]+)` \|", text)
parent_match = re.search(r"\| parent manifest \| `([^`]+)` \|", text)
return {
"task": task_match.group(1) if task_match else None,
"parentManifest": parent_match.group(1) if parent_match else None,
}
def process_alive(pid: object) -> bool:
if not isinstance(pid, int) or pid <= 0:
return False
try:
os.kill(pid, 0)
except ProcessLookupError:
return False
except PermissionError:
return True
return True
def lock_owner(path: Path) -> dict:
value = read_json(path)
pid = value.get("pid")
return {
"path": str(path.relative_to(ROOT)),
"pid": pid if isinstance(pid, int) else None,
"startedAt": value.get("startedAt"),
"alive": process_alive(pid),
}
def terminal_pointer(pointer: dict[str, str | None] | None = None) -> bool:
pointer = pointer or queue_pointer()
task = pointer.get("task")
if not task:
# A missing queue pointer is malformed/unknown, not proof of terminal
# completion. Let the production scheduler record the precise failure.
return False
index = read_json(INDEX)
manifest_path = CONTROL_ROOT.parent / task / "manifest.json"
manifest = read_json(manifest_path)
return (
index.get("activeTask") is None
and manifest.get("task") == task
and manifest.get("status") == "done"
and manifest.get("nextTask") is None
)
def live_status(*, json_only: bool = True, watch: bool = False, interval_ms: int = 2000) -> subprocess.CompletedProcess[str]:
args = ["node", str(LIVE_STATUS.relative_to(ROOT))]
if json_only:
args.append("--json")
if watch:
args.extend(["--watch", "--interval-ms", str(interval_ms)])
return subprocess.run(args, cwd=ROOT, text=True, capture_output=True, check=False)
def scheduler_args(*, commit: bool, push: bool, max_iterations: int = 0, preflight_only: bool = False) -> list[str]:
if push and not commit:
raise ValueError("push requires commit")
args = ["node", str(SCHEDULER.relative_to(ROOT)), "--max-iterations", str(max_iterations)]
if commit:
args.append("--commit")
if push:
args.append("--push")
if preflight_only:
args.append("--preflight-only")
return args

171
my-tools/web_context.py Executable file
View File

@@ -0,0 +1,171 @@
#!/usr/bin/env python3
"""Bounded context helpers for the Blender/WBV2 task queue.
The production context implementation lives in `tools/web/task-context-lib.mjs`.
These helpers provide a small Python-compatible surface for local scripts and
never load the full Blender source tree or historical evidence.
"""
from __future__ import annotations
import hashlib
import json
import re
from pathlib import Path
from typing import Any, Iterable
ROOT = Path(__file__).resolve().parents[1]
DOCS = ROOT / "docs"
TASKS = DOCS / "tasks"
CONTROL_ROOT = ROOT / "tests/golden/WBV2"
QUEUE = DOCS / "EXECUTION_QUEUE.md"
INDEX = CONTROL_ROOT / "task-index.json"
INVENTORY = CONTROL_ROOT / "dynamic-inventory.json"
CARD_ID = re.compile(r"^#\s+(WBV2-P\d+-\d{3})", re.MULTILINE)
CARD_STATUS = re.compile(r"^-\s*(?:status|状态):\s*`?([a-z_]+)`?", re.MULTILINE | re.IGNORECASE)
PARENT = re.compile(r"^-\s*parent:\s*`?([^`\s]+)", re.MULTILINE | re.IGNORECASE)
PATH_TOKEN = re.compile(r"(?:[A-Za-z0-9_.-]+/)+[A-Za-z0-9_.-]+(?:\.[A-Za-z0-9_.-]+)?")
RAW_SUFFIXES = (".log", ".trace", ".out", ".err")
def sha256(path: Path) -> str:
return hashlib.sha256(path.read_bytes()).hexdigest()
def load_cards() -> dict[str, tuple[Path, str, str]]:
cards: dict[str, tuple[Path, str, str]] = {}
for path in sorted(TASKS.glob("WBV2-P*.md")):
text = path.read_text(encoding="utf-8", errors="replace")
card = CARD_ID.search(text)
if not card:
continue
status_match = CARD_STATUS.search(text)
status = status_match.group(1).lower() if status_match else "unknown"
cards[card.group(1)] = (path, status, text)
return cards
def predecessor_ids(text: str) -> list[str]:
parent = PARENT.search(text)
return [parent.group(1)] if parent and parent.group(1).startswith("WBV2-") else []
def field_values(text: str, field_name: str) -> list[str]:
pattern = re.compile(rf"^-\s*{re.escape(field_name)}\s*[::](.*)$", re.MULTILINE)
return [match.group(1).strip() for match in pattern.finditer(text)]
def resolve_repo_path(candidate: str, origin: Path | None = None) -> Path | None:
candidate = candidate.strip().strip("`[]()")
if candidate.startswith(("http://", "https://")):
return None
candidates = []
if origin is not None:
candidates.append(origin.parent / candidate)
candidates.extend((ROOT / candidate, DOCS / candidate))
for path in candidates:
try:
path = path.resolve()
path.relative_to(ROOT.resolve())
except (OSError, ValueError):
continue
if path.is_file():
return path
return None
def extract_referenced_files(texts: Iterable[tuple[str, Path | None]]) -> list[Path]:
found: dict[str, Path] = {}
for text, origin in texts:
for candidate in PATH_TOKEN.findall(text):
path = resolve_repo_path(candidate, origin)
if path:
found[str(path)] = path
return sorted(found.values(), key=lambda path: str(path))
def classify_path(path: Path) -> tuple[str, str | None]:
rel = path.relative_to(ROOT).as_posix()
if path.name.endswith(RAW_SUFFIXES):
return "excluded", "raw log or trace"
if rel.startswith("blender-5.2.0/source/"):
return "excluded", "source tree is inspected by the task command"
if path.stat().st_size > 8 * 1024:
return "metadata_only", "evidence exceeds the context file cap"
return "read", None
def _queue_files(task_id: str, card: tuple[Path, str, str]) -> list[tuple[str, Path]]:
files: list[tuple[str, Path]] = [("queue", QUEUE), ("task_card", card[0])]
manifest = CONTROL_ROOT.parent / task_id / "manifest.json"
if manifest.is_file():
files.append(("task_manifest", manifest))
value = json.loads(manifest.read_text(encoding="utf-8"))
parent = value.get("parentTask")
parent_manifest = CONTROL_ROOT.parent / str(parent) / "manifest.json" if parent else None
if parent_manifest and parent_manifest.is_file():
files.append(("parent_manifest", parent_manifest))
return files
def context_manifest(task_id: str, *, include_evidence: bool = True, cards: dict[str, tuple[Path, str, str]] | None = None) -> dict[str, Any]:
cards = cards or load_cards()
if task_id not in cards:
raise KeyError(f"unknown task card: {task_id}")
card = cards[task_id]
entries: list[dict[str, Any]] = []
excluded: list[dict[str, str]] = []
for role, path in _queue_files(task_id, card):
kind, reason = classify_path(path)
if kind == "read":
entries.append({"role": role, "path": str(path.relative_to(ROOT)), "bytes": path.stat().st_size, "sha256": sha256(path), "mode": "read"})
else:
excluded.append({"path": str(path.relative_to(ROOT)), "reason": reason or kind})
if include_evidence:
for path in extract_referenced_files([(card[2], card[0])]):
if any(item["path"] == str(path.relative_to(ROOT)) for item in entries):
continue
kind, reason = classify_path(path)
if kind == "read":
entries.append({"role": "direct_evidence", "path": str(path.relative_to(ROOT)), "bytes": path.stat().st_size, "sha256": sha256(path), "mode": "read"})
else:
excluded.append({"path": str(path.relative_to(ROOT)), "reason": reason or kind})
total_bytes = sum(item["bytes"] for item in entries)
evidence = [item for item in entries if item["role"] == "direct_evidence"]
return {
"schema": "blender-web-task-context/v1",
"task_id": task_id,
"status": card[1],
"active_task": queue_pointer().get("task"),
"files": entries,
"excluded": excluded,
"totals": {"bytes": total_bytes, "estimated_tokens": total_bytes // 4, "evidence_bytes": sum(item["bytes"] for item in evidence), "evidence_files": len(evidence)},
"budget": {"total_bytes_limit": 16 * 1024, "evidence_bytes_limit": 12 * 1024, "evidence_file_limit": 6},
"read_order": ["queue", "task_card", "parent_manifest", "direct_evidence"],
}
def queue_pointer() -> dict[str, str | None]:
text = QUEUE.read_text(encoding="utf-8")
task = re.search(r"\| 当前任务 \| `([^`]+)` \|", text)
parent = re.search(r"\| parent manifest \| `([^`]+)` \|", text, re.IGNORECASE)
return {"task": task.group(1) if task else None, "parentManifest": parent.group(1) if parent else None}
def write_context_manifest(task_id: str, output: Path | None = None, manifest: dict[str, Any] | None = None) -> Path:
output = output or (CONTROL_ROOT.parent / task_id / "my-tools-context.json")
output.parent.mkdir(parents=True, exist_ok=True)
output.write_text(json.dumps(manifest or context_manifest(task_id), ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
return output
def write_context_summary(task_id: str, manifest: dict[str, Any] | None = None) -> Path:
manifest = manifest or context_manifest(task_id)
output = CONTROL_ROOT / "my-tools-context-summary.md"
lines = ["# Blender Web 最小续接上下文", "", f"- 当前任务:`{task_id}`(`{manifest['status']}`)", "- 只读取以下 mode=read 文件:"]
lines.extend(f" - `{item['path']}`" for item in manifest["files"] if item["mode"] == "read")
lines.append(f"- 清单 hash:`{sha256(write_context_manifest(task_id, manifest=manifest))}`")
output.write_text("\n".join(lines) + "\n", encoding="utf-8")
return output

View File

@@ -16,17 +16,31 @@ external documentation are supplemental references for toolchain/browser
constraints only; they cannot override or replace behavior present in the
Blender source.
Implementation iron rule: advance the 6900-entry inventory through capability
families and unique behavior batches at maximum practical throughput. Do not
run Chromium, E2E receipts, parity verification, save/reopen verification,
determinism checks, or completion overlays in this implementation lane. The
only allowed checks are source compilation, TypeScript/type safety, lint,
bounded build and queue/governance integrity needed to avoid committing broken
functionality. Do not drift into repeated maintenance-only waves while an
unimplemented Blender capability family remains available.
Implementation iron rule: derive the current inventory from
`blender-5.2.0/source/blender` with the deterministic source inventory tool,
then advance it through finite function modules. The historical 6900-entry
catalog is a read-only estimate. A module owns a coherent source/protocol/Worker/frontend
boundary and its aggregate operator case matrix. Do not run generic
continuation tasks, repeated markers, or build-only tasks as feature work. A
Mutation modules require source hash stability, real Main mutation, negative
no-mutation, undo/redo, save/reopen, Chromium, and aggregate receipt evidence;
read-only modules require an exact Main reader/context receipt instead of a
fabricated writer claim. Every module still records its negative boundaries
and recovery behavior before handoff.
Completed module receipts are hash-checked and reused without rerun. The
original inventory and historical provenance remain read-only.
## Blender Web Migration Mission Gate
## 项目移植铁律
1. 彻底梳理完整项目,以 `blender-5.2.0` 源码为第一依据,尽快按真实功能边界完成 Web/WASM 移植;不得用计划、索引、构建或占位接口替代生产能力。
2. 从权威队列和 parent manifest 找出执行任务的开头,明确唯一合法的 `nextTask`;每轮完成一个受源码映射的任务后立即读取新 manifest,持续执行直到队列闭合或出现可证明阻塞。
3. 按“功能族 -> 模块 -> capability/command -> operator case matrix”组织移植;复用已有 Main、WASM ABI、协议、Worker、前端和 hash-bound receipt,禁止重复实现或逐条重复处理。
4. 以源码动态 inventory 为当前覆盖依据:运行 `node tools/web/generate-source-dynamic-inventory.mjs` 重新统计真实源码单元,并通过精确 source/case 映射、正负例、恢复、Chromium、save/reopen、确定性和聚合 manifest 逐步收敛。历史 `6900-entry` 仅是只读初始估算,`5200` 仅是非强制规划估计;每完成一个段落或模块,报告必须列出动态 unitCount、verifiedUnits、unverifiedUnits、源码树 SHA,以及本段绑定增量,未绑定单元不得计为覆盖。
以上铁律与历史 6900-entry inventory/provenance 只读规则、源码动态 inventory、Chromium-only 规则和失败保持 `in_progress`/`blocked` 规则同时生效;任何冲突以源码、parent manifest 和本节铁律为准。
This is the highest-priority rule for every continuation turn: the only
deliverable is a real, source-backed Blender-to-Web/WASM capability. Before
running a task entrypoint, classify the task against the current parent
@@ -44,7 +58,7 @@ manifest:
`SOURCE_IMPLEMENTATION` is the productive lane. A `QUEUE_ENABLEMENT` task may
run only when it immediately unlocks an available source capability; it must
not be repeated as a wave, counted as a completed feature, or used to reduce
the 6900 effective pending count. If the task card or entrypoint is metadata
the dynamic inventory without explicit evidence. If the task card or entrypoint is metadata
only while an uncovered source capability is available, preserve the queue
pointer, record the mismatch, and select the next source-backed slice through
the repository generator rather than fabricating progress.
@@ -64,18 +78,19 @@ hash-bound verification overlay may change effective completion.
按 nextTask.md 不间断执行。
这是执行命令,不是计划、状态汇报或等待确认。读取仓库当前队列和 parent
manifest,严格从当前 `nextTask` 开始。每次只完成一个任务,完成后写入
manifest、status、task-context,提交并推送 `origin/main`,重新读取新的
`nextTask`,立即开始下一轮,直到队列闭合或遇到真实阻塞。
manifest,严格从当前模块 `nextTask` 开始。每次只完成一个功能模块;完成后写入模块
receipt、manifest、status、task-context,提交并推送 `origin/main`,重新读取新的
`nextTask`,立即开始下一模块,直到队列闭合或遇到真实阻塞。授权、索引、重复
continuation 和单纯 build 不得推进模块完成。
主线是使用 `blender-5.2.0` 源码实现 Blender Web/WASM 全部能力:优先修改
Blender Main、WebEngine C++、WASM、协议、Worker、客户端和前端真实入口;按
capability family 和唯一行为批量推进 6900 条目,复用已有 hash 证据,禁止
重复实现或逐条重复处理。
module -> capability -> operator case matrix 推进源码动态 inventory,复用已有 hash
证据,禁止重复实现或逐条重复处理;模块验证失败保持 `in_progress`,不推进 successor。
当前阶段只做实现和必要的源码编译、WASM 构建、TypeScript、lint、前端构建
及队列治理检查;不执行 Chromium/E2E/parity/receipt/save-reopen/determinism
验证,不写 completion overlay,不修改原始 inventory 或历史 provenance。
当前模块阶段必须执行模块卡绑定的源码/WASM 构建、TypeScript、lint、Chromium 正负例、
undo/redo、save/reopen、确定性和聚合 receipt;不写 completion overlay,不修改历史
inventory 或 provenance,动态 unit 只有在证据绑定后才计为 verified。
每轮开始运行 `node tools/web/print-task-context.mjs` 和
`node tools/web/check-task-context.mjs`;只读取当前任务卡和最小输入。任何
@@ -126,6 +141,44 @@ guardian/supervisor 的连续契约:
- guardian 或 supervisor 进程退出后,再次执行同一 guardian 命令即可从当前队列恢复;不复制旧日志、不重放未知命令。
- guardian 只负责有效 handoff 和可证明的进程恢复,不能绕过真实阻塞、治理门禁、源码编译失败或 provenance 规则。
## 队列调度器
需要跨越聊天、终端和网络边界持续领取任务时,使用仓库内的最外层调度器:
```bash
node tools/web/run-wbv2-scheduler.mjs --max-iterations 0 --commit --push
```
脱离终端运行:
```bash
setsid -f nohup node tools/web/run-wbv2-scheduler.mjs --max-iterations 0 --commit --push \
> tests/golden/WBV2/continuous-scheduler.stdout.log 2>&1 < /dev/null
```
调度器每轮重新读取 `docs/EXECUTION_QUEUE.md`、parent manifest 和 task-context,
先检查当前源码能力是否有明确的非 generic 行为映射,再调用 guardian 执行恰好
一个任务。子任务成功提交、推送并确认远端 `main` HEAD 等于本地 HEAD 后,立即
重读新的 `nextTask` 并开始下一轮,不等待聊天消息。
调度器运行时写入 `tests/golden/WBV2/continuous-scheduler.lock`、
`continuous-scheduler.json` 和 `continuous-scheduler.log`;checkpoint 使用临时
文件加原子改名。源码映射缺失、旧 generic marker、任务失败、队列未前进、队列在
失败后已前进、远端 HEAD 未确认或状态未知时,必须写入 `blocked` checkpoint 并停下,
不得为追求连续而重放未知写操作、伪造能力或把 metadata handoff 当作功能完成。
启动前可以只做只读预检,不领取任务:
```bash
node tools/web/run-wbv2-scheduler.mjs --preflight-only
```
预检非零表示当前任务有真实源码/证据阻塞;修复后重新启动调度器。实时状态使用:
```bash
node tools/web/print-wbv2-live-status.mjs --json
```
### 动态运行状态
`nextTask.md` 的 Current Handoff Snapshot 只是写入时快照,不能用来判断进程
@@ -142,10 +195,10 @@ node tools/web/print-wbv2-live-status.mjs
node tools/web/print-wbv2-live-status.mjs --watch --interval-ms 2000
```
状态输出中的 `RUNNING` 表示 guardian 和 supervisor 锁对应的 PID 均存活,且
当前任务正在执行或进行交接;`IDLE` 表示没有活动实例;`BLOCKED` 表示必须先
读取 checkpoint 原因。JSON 机器读取使用 `--json`,可用于 CI、监控或重连脚本。
guardian 每两秒刷新 heartbeat,checkpoint 采用临时文件加原子改名,避免动态
状态输出中的 `RUNNING` 表示 scheduler 锁存活,或 guardian 和 supervisor 锁对应
的 PID 均存活,且当前任务正在执行或进行交接;`IDLE` 表示没有活动实例;`BLOCKED`
表示必须先读取 scheduler/guardian/supervisor checkpoint 原因。JSON 机器读取使用
`--json`,可用于 CI、监控或重连脚本。checkpoint 采用临时文件加原子改名,避免动态
提示读到半个 JSON。
## Copyable Resume Prompt
@@ -162,19 +215,19 @@ and (when authorized) repository push:
1. 先读取 `AGENTS.md` 和 `docs/EXECUTION_QUEUE.md`,然后运行:
`node tools/web/print-task-context.mjs`
`node tools/web/check-task-context.mjs`
2. 当前任务必须等于 parent manifest 的 `nextTask`。只读取当前任务卡、parent manifest/status 和 task-context 列出的最小输入;禁止默认读取完整历史计划、全部 `docs/status` 或 6900 条目详情。
2. 当前任务必须等于 parent manifest 的 `nextTask`。只读取当前任务卡、parent manifest/status 和 task-context 列出的最小输入;禁止默认读取完整历史计划、全部 `docs/status` 或历史 6900 条目详情。
3. 每次只实现当前任务卡规定的一个能力族或一个独立接线切片。以 `blender-5.2.0` 源码、Blender Main 数据结构和 C/C++ API 为第一依据,再同步 WebEngine dispatch、协议类型、worker/client 路由和前端入口;网页资料只补充构建/浏览器约束,不得用模拟数据、空壳接口或网页描述替代真实实现。
4. 严禁重复工作:相同 source hash、behavior hash、dedupe key 或已存在的实现直接复用;不重复运行已有 Blender/WASM/Chromium 行为;6900 条目按 capability family 批量映射,完成条目只作未来回归引用。
4. 严禁重复工作:相同 source hash、behavior hash、dedupe key 或已存在的实现直接复用;不重复运行已有 Blender/WASM/Chromium 行为;动态源码单元按 capability family 批量映射,历史条目只作回归引用。
5. 实现波次只要求最小安全检查(源码语法、TypeScript typecheck、lint、可用时的 Vite/build 和浏览器启动 smoke);不为每个新能力生成完整 E2E receipt,不在实现阶段写 completion overlay,不声称历史 parity。
6. 原始 P0 inventory、6900 总数、6618 legacy pending 和历史 provenance 始终只读。effective completed/pending 只由后续 hash-bound 验证 overlay 计算;实现完成不等于条目完成。
6. 原始 P0 inventory、6900 历史总数、6618 legacy pending 和历史 provenance 始终只读。当前动态 inventory 由源码树哈希绑定;verified/unverified 只由后续 hash-bound 行为证据更新,实现完成不等于单元完成。
7. 长输出写入 `tests/golden/<task>/command.log`;交互只报告退出码、短尾部、构件路径和 SHA-256。
8. 任务成功后运行 `node tools/web/check-task-context.mjs --task <task-id> --write`、`node tools/web/check-context-governance.mjs`、`node tools/web/check-v2-task-index.mjs`、`npm --prefix web run typecheck`、`npm --prefix web run lint` 和 `git diff --check`;完整生产验证门禁统一在验证波次执行。
9. 所有实现门禁通过后自动提交并推送 `origin/main`;必须再次用 `git ls-remote` 确认远端 `main` 与本地 `HEAD` 完全相同,确认完成后才从新 parent manifest 读取 `nextTask` 继续。凭据不得写入文件、URL、日志或提示词。
10. 任一命令失败、输入缺失、队列不一致、环境不可用或状态不明时,保持 `in_progress`/`blocked`,不得推进指针、伪造完成或重复未知结果的状态改变命令。
11. 每轮结束报告实现切片、当前任务、`nextTask`、最小检查退出码、原始 inventory、effective inventory 是否保持不变、提交/推送状态和剩余真实阻塞。
11. 每轮结束报告实现切片、当前任务、`nextTask`、最小检查退出码、动态 inventory unitCount/verifiedUnits/unverifiedUnits/sourceTreeSha256、历史 inventory 是否保持只读、提交/推送状态和剩余真实阻塞。
12. CONTINUOUS_LOOP_REQUIRED:成功交接后立即开始下一轮;不得在成功提交、推送或生成新 manifest 后等待用户确认、只写计划或只汇报状态。每轮必须重新运行 `print-task-context` 和 `check-task-context`,使用新 parent manifest 的 `nextTask`,不沿用上一轮上下文。
13. 可使用仓库内 guardian 保持进程级连续执行:`node tools/web/run-wbv2-guardian.mjs --max-iterations 0 --commit --push`。它托管 supervisor,`0` 表示不限轮数,`--max-iterations N` 用于受控运行,`--commit --push` 才允许自动提交/推送。push 网络抖动由 runner 自动退避重试并确认远端 HEAD;任何入口失败、退出 0 但队列未前进、治理失败、远端无法确认或状态不明都保留 checkpoint,guardian 只自动恢复可证明安全的 handoff。
14. 6900_IMPLEMENTATION_IRON_RULE:只按 capability family 和唯一行为批次实现全部 Blender 功能;不启动 Chromium/E2E/receipt/parity/save-reopen/determinism 验证,不写 completion overlay,不把维护 smoke 当作能力完成。若任务卡与本铁律冲突,以源码实现和本铁律为准,并将验证留到后续独立验证波次。
14. SOURCE_DYNAMIC_INVENTORY_IRON_RULE:当前覆盖目标必须来自 `blender-5.2.0/source/blender` 的确定性统计;6900 是历史估算,5200 是非强制规划估计。任何新 verified unit 必须绑定源码位置、行为证据、负例和恢复证据;不得为了接近估计数字添加无效占位单元。
15. UNIFIED_WASM_JSON_ABI_IRON_RULE:Worker 与 WASM 的新调用统一使用单一 C ABI `web_engine_call_json(handle, requestJson, resultOut)`;请求必须是 `{"cmd":"...","params":{...}}`,返回必须是 `{"ok":true,"cmd":"...","result":...}` 或包含 `code/error` 的失败 JSON,再由 Worker 解析为领域结果。`applyCommand`、快照、增量、撤销/重做、Depsgraph 等状态路径不得新增独立公开 WASM 函数;open/append/save/geometry 等仍未迁移的二进制专用 ABI 仅作为兼容边界保留,并不得扩散到新的状态调用。
16. PROCESS_CONTINUITY_IRON_RULE:要实现跨会话不间断执行,必须使用 `run-wbv2-guardian.mjs --max-iterations 0` 托管 `run-wbv2-supervisor.mjs`;单次聊天执行完成后不得假定代理会在下一轮自动复活。guardian/supervisor 必须按任务原子迭代、锁队列、写 checkpoint、成功后立即重读 manifest;失败或状态未知时只能停在精确 checkpoint,不得自动重试或伪造队列前进。
17. PUSH_COMPLETION_IRON_RULE:任何任务只有在 commit 成功、`git push origin main` 成功且 `git ls-remote --refs origin refs/heads/main` 返回的远端 HEAD 与本地 `git rev-parse HEAD` 相同后才算完成;确认前不得读取下一任务。网络失败最多按退避策略重试,状态未知时不得重放任务写入。
@@ -182,6 +235,8 @@ and (when authorized) repository push:
19. LIVE_STATUS_IRON_RULE:任何“当前是否在执行”的判断必须以 `node tools/web/print-wbv2-live-status.mjs` 的实时输出为准,不得使用 `nextTask.md` 的静态 snapshot、旧聊天消息或单个历史 manifest 推断。`RUNNING`、`IDLE`、`BLOCKED` 必须分别显示活动 PID、当前任务和 checkpoint 路径;状态工具只读,不得改变队列。
20. BLENDER_WEB_MIGRATION_ONLY_IRON_RULE:本项目唯一任务是依据 `blender-5.2.0` 源码,将 Blender 能力完整、高效、可运行地移植到 Web/WASM。每个连续迭代必须优先产生真实的 Blender Main、WebEngine、WASM、Worker、协议、客户端或前端能力实现;执行监控、状态展示、文档整理、重复审计和维护性变更不得取代可用的 Blender 能力切片。只有当前任务没有可实现的源码能力或存在真实阻塞时,才允许处理辅助治理工作。任何 `QUEUE_ENABLEMENT` 只能作为一次性解锁步骤,不能冒充 `SOURCE_IMPLEMENTATION` 或减少 effective pending。
21. MIGRATION_PROGRESS_ACCOUNTING_IRON_RULE:任务报告必须同时列出 `taskClass`(`SOURCE_IMPLEMENTATION`、`INTEGRATION_BUILD` 或 `QUEUE_ENABLEMENT`)、`capabilityId`、`behaviorMarker`、source/protocol before-after hash、实际变更文件和是否新增可运行 Web/WASM 路径;没有真实能力变化的任务只能记录为 enablement,不能宣称 Blender 移植完成。
22. SCHEDULER_CONTINUITY_IRON_RULE:无人值守连续执行必须使用 `run-wbv2-scheduler.mjs --max-iterations 0`。调度器每轮只领取 authoritative queue 当前 `nextTask` 指向的一个任务,先通过源码能力映射预检,再委托 guardian/supervisor 完成原子 handoff;只有 child exit 为 0、队列确实前进、commit 成功、push 成功且远端 `main` HEAD 等于本地 HEAD 时,才允许进入下一轮。`QUEUE_ENABLEMENT` 不能冒充功能进度,source task 没有显式非 generic marker 时必须保持 `blocked`。进程崩溃、传输抖动只在 checkpoint 能证明 handoff 已完成时自动恢复;任何未知状态、源码/构建/治理失败或队列漂移都停止并保留精确 checkpoint。调度器不得通过无限重试绕过真实阻塞、重复状态写入或伪造移植完成。
23. SCHEDULER_STARTUP_RULE:`run-wbv2-scheduler.mjs` 是连续执行的唯一推荐启动入口;`run-wbv2-guardian.mjs` 仍可用于单独调试,但无人值守运行必须由调度器托管。使用 `--preflight-only` 只能做只读映射检查,不得把预检通过解释为功能已完成;调度器退出后必须依据 `continuous-scheduler.json` 和实时队列恢复,禁止根据静态 snapshot 猜测下一任务。
```
## Resume Command
@@ -244,9 +299,9 @@ is itself an executable enabling task: run its declared root generator, then
continue from the generated child task in the same continuous run.
Every implementation iteration must answer one question before editing: which
capability family or unique behavior from the immutable 6900 inventory becomes
capability family or unique behavior from the source dynamic inventory becomes
more implemented by this change? If the answer is only "maintenance", skip it
when a source-backed capability slice is available. Maintain the original
when a source-backed capability slice is available. Maintain the historical
inventory and provenance; implementation progress is recorded in task reports,
not completion overlays. If the answer is "queue metadata only", record the
handoff as `QUEUE_ENABLEMENT` and immediately continue to the unlocked source
@@ -412,12 +467,12 @@ Recorded: 2026-08-29 Asia/Shanghai
| Field | Value |
| --- | --- |
| Current task when this file was written | `WBV2-P607-002` |
| Current task state | `in_progress` |
| Parent manifest | `tests/golden/WBV2-P607-001/manifest.json` |
| Current task card | `docs/tasks/WBV2-P607-002.md` |
| Current task-context nextTask | `WBV2-P607-003` |
| Focused command | `node tools/web/generate-wbv2-capability-continuation-slice.mjs --task WBV2-P607-002 --write` |
| Current task when this file was written | `WBV2-P1145-001` |
| Current task state | `done` |
| Parent manifest | `tests/golden/WBV2-P1145-001/manifest.json` |
| Current task card | `docs/tasks/WBV2-P1145-001.md` |
| Current task-context nextTask | `NONE` |
| Focused command | `node tools/web/check-wbv2-module-execution.mjs` |
The snapshot is informational and may become stale. On every resume, the queue,
parent manifest, and fresh `print-task-context` output win. Never infer a task
@@ -427,7 +482,7 @@ number from this table or from historical status files.
- Chromium is the only permitted browser runtime; never run Firefox or WebKit.
- `blender-5.2.0` source and Blender Main are the behavior authority; web references are supplemental and must be recorded only when they affect build/runtime constraints.
- The 6900 implementation lane is verification-free: no Chromium/E2E receipts, parity overlays, save/reopen or determinism runs until a separately authorized verification wave.
- The historical 6900 inventory and provenance are immutable. The current inventory is the source-derived `tests/golden/WBV2/dynamic-inventory.json`; 5200 is a non-binding planning estimate. Function modules use Chromium/E2E, save/reopen and determinism evidence, and only explicit source/case mappings can increase verified units.
- Do not read full historical plans, all status logs, or the full gap plan by
default.
- Keep unrelated user changes. Never use `git reset --hard` or `git checkout --`.

Binary file not shown.

View File

@@ -0,0 +1,42 @@
{
"schemaVersion": 1,
"task": "WBV2-P1145-000",
"parentTask": "WBV2-P1144-001",
"operation": "AUTHORIZE_SOURCE_BACKED_MODULE",
"status": "done",
"enablingTask": true,
"parityStateChange": false,
"runtime": "MODULE_EXECUTION_QUEUE_BRIDGE",
"artifacts": {
"parentManifest": {
"path": "tests/golden/WBV2-P1144-001/manifest.json",
"sha256": "b63a84f5f3d67c3e38013c68f81c676655408f0fd6f72fc91475c943254b0c09"
},
"plan": {
"path": "tests/golden/WBV2-P1145-000/module-execution-plan.json",
"sha256": "39a71d576e4a997756e9e47cbba632fc4b286206a3106658b2b85c6b374b352b"
},
"report": {
"path": "tests/golden/WBV2-P1145-000/successor-authorization-report.json",
"sha256": "e62d29f013bc4b4ca60b5a0bcdbb455c652f9b03b5efd214885bb2b3f222342a"
},
"status": {
"path": "docs/status/WBV2-P1145-000.md",
"sha256": "d10694914401fe045eac5dcbc8de8e81d85dd024e624bf371582dedf6cedf09a"
}
},
"assertions": {
"sourceBackedSuccessor": true,
"priorEvidencePreserved": true,
"moduleId": "CORRECTIVE_SMOOTH_READER",
"inventoryTotal": 6900,
"effectiveCompleted": 290,
"effectivePending": 6609,
"dynamicUnitCount": 3745,
"dynamicVerifiedUnits": 350,
"dynamicUnverifiedUnits": 3395,
"boundUnitDelta": 0,
"fullHistoricalReproduction": false
},
"nextTask": "WBV2-P1145-001"
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,29 @@
{
"schemaVersion": 1,
"task": "WBV2-P1145-000",
"operation": "AUTHORIZE_SOURCE_BACKED_MODULE",
"taskClass": "QUEUE_ENABLEMENT",
"status": "PASS",
"parent": {
"task": "WBV2-P1144-001",
"manifest": "tests/golden/WBV2-P1144-001/manifest.json",
"manifestSha256": "b63a84f5f3d67c3e38013c68f81c676655408f0fd6f72fc91475c943254b0c09"
},
"module": {
"moduleId": "CORRECTIVE_SMOOTH_READER",
"task": "WBV2-P1145-001",
"sourceUnits": [
"modifier-source:MOD_correctivesmooth",
"rna-struct:CorrectiveSmoothModifier"
],
"sourceBacked": true
},
"inventory": {
"unitCount": 3745,
"verifiedUnits": 350,
"unverifiedUnits": 3395,
"sourceTreeSha256": "600cb3dd441f09d9655b740735fbf8fa49ad6c14d308c68d59a29f9c338e4202",
"boundUnitDelta": 0
},
"nextTask": "WBV2-P1145-001"
}

View File

@@ -0,0 +1,184 @@
{
"schemaVersion": 1,
"task": "WBV2-P1145-000",
"parentTask": "WBV2-P1144-001",
"status": "done",
"goal": "验证 Blender Main 中 Corrective Smooth modifier 的真实类型和 RNA 元数据,并确认 save/reopen、worker termination 和 malformed 输入保持确定性。",
"scope": {
"gap": "capability:CORRECTIVE_SMOOTH_READER",
"ownerFamily": "MODIFIER",
"implementationClass": "LOCAL_EXACT"
},
"commands": [
"node tools/web/check-task-context.mjs --task WBV2-P1145-000 --write"
],
"inputPaths": [],
"inputSelection": {
"schemaVersion": 1,
"limits": {
"maxFiles": 12,
"evidenceBytes": 8192,
"contextRemainingBytes": 228,
"contextRemainingTokens": 55
},
"source": {
"bytes": 9676,
"tokens": 2421
},
"selected": [],
"excluded": [
{
"path": "blender-5.2.0/source/blender/modifiers/intern/MOD_correctivesmooth.cc",
"bytes": 29127,
"reason": "EVIDENCE_BYTE_BUDGET"
},
{
"path": "blender-5.2.0/source/blender/makesrna/intern/rna_modifier.cc",
"bytes": 521651,
"reason": "EVIDENCE_BYTE_BUDGET"
},
{
"path": "blender-5.2.0/source/blender/web_engine/web_engine_blend_reader.cpp",
"bytes": 313993,
"reason": "EVIDENCE_BYTE_BUDGET"
},
{
"path": "tests/files/web/modifier_corrective_smooth_scene.blend",
"bytes": 97291,
"reason": "EVIDENCE_BYTE_BUDGET"
},
{
"path": "web/tests/e2e/wbv2-corrective-smooth-reader-production.spec.ts",
"bytes": 3743,
"reason": "CONTEXT_REMAINING_SPACE"
},
{
"path": "tests/golden/WBV2-P1145-000/module-receipt.json",
"bytes": null,
"reason": "MISSING_PATH"
},
{
"path": "tests/golden/WBV2-P1145-000/manifest.json",
"bytes": null,
"reason": "GENERATED_EVIDENCE_OUTPUT"
},
{
"path": "docs/status/WBV2-P1145-000.md",
"bytes": null,
"reason": "GENERATED_EVIDENCE_OUTPUT"
}
],
"totals": {
"files": 0,
"bytes": 0,
"tokens": 0
}
},
"exitCriteria": [
"prior evidence remains hash-bound",
"Corrective Smooth has explicit Blender source mapping",
"inventory remains source-derived",
"one source-backed module is active"
],
"nextTask": "WBV2-P1145-001",
"sourceDocuments": {
"queue": "docs/EXECUTION_QUEUE.md",
"taskCard": "docs/tasks/WBV2-P1145-000.md",
"taskIndex": "tests/golden/WBV2/task-index.json",
"parentManifest": "tests/golden/WBV2-P1144-001/manifest.json",
"parentStatus": "docs/status/WBV2-P1144-001.md"
},
"readPolicy": {
"required": [
"docs/EXECUTION_QUEUE.md",
"docs/tasks/WBV2-P1145-000.md",
"tests/golden/WBV2-P1144-001/manifest.json",
"docs/status/WBV2-P1144-001.md"
],
"machineOnly": [
"tests/golden/WBV2/task-index.json",
"tests/golden/M15-03A/task-catalog.jsonl"
],
"optionalByNeed": [
"docs/WEB_BLENDER_MODELER_V1_SCOPE.md",
"任务卡列出的精确协议/生产路径",
"任务卡明确命名的长期计划小节"
],
"forbiddenByDefault": [
"完整 CURRENT_EXECUTION_PLAN.md",
"完整 PROJECT_STATUS_AND_NEXT_WORK.md",
"全部 docs/status/*.md",
"完整 parity/WBS 计划",
"README 历史和 test-results/"
]
},
"parent": {
"manifest": {
"schemaVersion": 1,
"task": "WBV2-P1144-001",
"parentTask": "WBV2-P1144-000",
"status": "done",
"nextTask": "WBV2-P1145-000",
"artifactCount": 13
},
"statusSummary": [
"# WBV2-P1144-001 status",
"- task: `WBV2-P1144-001`; module: `DRIVER_READER`; status: `done`",
"- source hashes stable: `true`; commands passed: `4`",
"- positive Main mutation, negative no-mutation, recovery and Chromium gates are recorded in the module receipt",
"- Driver/AnimData reader exposes Blender scripted expression, variables and DriverTarget RNA path/ID metadata; Chromium undo/redo, malformed-input rejection and save/reopen passed; no JS simulation",
"- dynamic source inventory: `3745` units; verified `350`; unverified `3395`; planning estimate `5200` (non-binding); sourceTreeSha256=`600cb3dd441f09d9655b740735fbf8fa49ad6c14d308c68d59a29f9c338e4202`; this-segment binding delta: `6`",
"- historical 6900-entry inventory remains read-only; full-family parity: `false`",
"- nextTask: `WBV2-P1145-000`"
]
},
"budgets": {
"queueBytes": 4096,
"taskBytes": 1750,
"parentManifestBytes": 24576,
"parentStatusBytes": 6144,
"evidenceFiles": 12,
"evidenceBytes": 8192,
"contextTokens": 3500,
"contextEnvelopeTokens": 1024,
"taskLines": 42,
"taskTokens": 438
},
"size": {
"documents": [
{
"path": "docs/EXECUTION_QUEUE.md",
"bytes": 3830,
"lines": 56,
"tokens": 958
},
{
"path": "docs/tasks/WBV2-P1145-000.md",
"bytes": 1750,
"lines": 42,
"tokens": 438
},
{
"path": "tests/golden/WBV2-P1144-001/manifest.json",
"bytes": 3299,
"lines": 89,
"tokens": 825
},
{
"path": "docs/status/WBV2-P1144-001.md",
"bytes": 797,
"lines": 10,
"tokens": 200
}
],
"sourceTokens": 2421,
"evidenceFiles": 0,
"evidenceBytes": 0,
"evidenceTokens": 0,
"totalTokens": 2421,
"reservedEnvelopeTokens": 1024,
"estimatedContextTokens": 3445,
"serializedContextTokens": 655,
"withinBudget": true
}
}

View File

@@ -0,0 +1,82 @@
{
"schemaVersion": 1,
"task": "WBV2-P1145-001",
"operation": "CORRECTIVE_SMOOTH_READER_CHROMIUM_SAVE_REOPEN",
"browser": "chromium",
"queueMutation": false,
"fixture": "tests/files/web/modifier_corrective_smooth_scene.blend",
"fixtureSha256": "2e89fa90ec07fa2a87241ad6ec33c569e7a4f69742554c69fe0eadeb32cdac12",
"sourceUnits": [
"modifier-source:MOD_correctivesmooth",
"rna-struct:CorrectiveSmoothModifier"
],
"before": [
{
"meshId": "mesh:Grid",
"uuid": "modifier:object:CorrectiveSmoothFixture:697597798",
"name": "Corrective Smooth",
"type": "CORRECTIVE_SMOOTH",
"enabled": true,
"evaluationStatus": "BLOCKED",
"parameters": {
"flag": 5,
"mode": 7,
"persistentUid": 697597798,
"typeCode": 51
}
}
],
"target": {
"meshId": "mesh:Grid",
"uuid": "modifier:object:CorrectiveSmoothFixture:697597798",
"name": "Corrective Smooth",
"type": "CORRECTIVE_SMOOTH",
"enabled": true,
"evaluationStatus": "BLOCKED",
"parameters": {
"flag": 5,
"mode": 7,
"persistentUid": 697597798,
"typeCode": 51
}
},
"after": [
{
"meshId": "mesh:Grid",
"uuid": "modifier:object:CorrectiveSmoothFixture:697597798",
"name": "Corrective Smooth",
"type": "CORRECTIVE_SMOOTH",
"enabled": true,
"evaluationStatus": "BLOCKED",
"parameters": {
"flag": 5,
"mode": 7,
"persistentUid": 697597798,
"typeCode": 51
}
}
],
"deterministic": [
{
"meshId": "mesh:Grid",
"uuid": "modifier:object:CorrectiveSmoothFixture:697597798",
"name": "Corrective Smooth",
"type": "CORRECTIVE_SMOOTH",
"enabled": true,
"evaluationStatus": "BLOCKED",
"parameters": {
"flag": 5,
"mode": 7,
"persistentUid": 697597798,
"typeCode": 51
}
}
],
"saveReopenExact": true,
"deterministicExact": true,
"savedSha256": "2e89fa90ec07fa2a87241ad6ec33c569e7a4f69742554c69fe0eadeb32cdac12",
"malformed": {
"code": "BLEND_READ_FAILED",
"message": "Input is not an uncompressed, gzip, or zstd Blender file"
}
}

View File

@@ -0,0 +1,87 @@
{
"schemaVersion": 1,
"task": "WBV2-P1145-001",
"parentTask": "WBV2-P1145-000",
"operation": "MODULE_FUNCTION_EXECUTION",
"taskClass": "MODULE",
"status": "done",
"enablingTask": true,
"parityStateChange": false,
"runtime": "BLENDER_WEB_FUNCTION_MODULE",
"artifacts": {
"parentManifest": {
"path": "tests/golden/WBV2-P1145-000/manifest.json",
"sha256": "7e513edae4032099e17b36a4e7a73c21f68c395cd9aa4a97e48f55e2d7406e0f"
},
"source1": {
"path": "blender-5.2.0/source/blender/modifiers/intern/MOD_correctivesmooth.cc",
"sha256": "34064ee367b0b79f2804a48101943630366d5c166b842fa9f439dbf4aa1dad3b"
},
"source2": {
"path": "blender-5.2.0/source/blender/makesrna/intern/rna_modifier.cc",
"sha256": "1292112c20eaa113084a2a7093c349e7787db818950d80a6e11b75b287e4287b"
},
"source3": {
"path": "blender-5.2.0/source/blender/web_engine/web_engine_blend_reader.cpp",
"sha256": "f6fbd04315c48ad4dfbf109b91e5b350550d23c227e5c11a71a58f09e923c990"
},
"source4": {
"path": "web/tests/e2e/wbv2-corrective-smooth-reader-production.spec.ts",
"sha256": "868e770934e9c19cb9a807530a1837b37f065b654ad948322ee39589b1887fc7"
},
"source5": {
"path": "tests/files/web/modifier_corrective_smooth_scene.blend",
"sha256": "2e89fa90ec07fa2a87241ad6ec33c569e7a4f69742554c69fe0eadeb32cdac12"
},
"source6": {
"path": "web/app/src/vendor/blender/web_engine.wasm",
"sha256": "32bc36aba563fe1ed0fb5d0292ca8a3ada1eecd6d45e38d9dcde565f02a312fe"
},
"moduleReceipt": {
"path": "tests/golden/WBV2-P1145-001/module-receipt.json",
"sha256": "34e4789e5cd176c0543d439c21a66ff796bec9b6369a2894677be1aea02c11b3"
},
"production": {
"path": "tests/golden/WBV2-P1145-001/corrective-smooth-production.json",
"sha256": "2afd10cbf4101867ccd6857181d439b2fad4fe54471f5562c9c056a75d1456ec"
},
"status": {
"path": "docs/status/WBV2-P1145-001.md",
"sha256": "e4fccc7e41be4081f9019138d0dbf204b5d3681cdbb23546362b8140485ac4c2"
},
"log": {
"path": "tests/golden/WBV2-P1145-001/module-run.log",
"sha256": "be78d986f5000ccff2a8bf8108462c7b778961e087ef2a9c1528c9887166bca0"
},
"plan": {
"path": "tests/golden/WBV2-P1145-000/module-execution-plan.json",
"sha256": "699800aa915ee17cb6698a348573f33e4d745909ea1e647ecb508934ee286750"
},
"taskContext": {
"path": "tests/golden/WBV2-P1145-001/task-context.json",
"sha256": "41931421e57552975f14f0d8bce46fe819e0c91c4683146d2f0631daee43ecc4"
}
},
"assertions": {
"moduleId": "CORRECTIVE_SMOOTH_READER",
"sourceBacked": true,
"positiveCases": 3,
"negativeCases": 3,
"recoveryCases": 3,
"commandCount": 4,
"reusedReceiptCount": 1,
"sourceUnchanged": true,
"fullFamilyParity": false,
"dynamicInventoryPath": "tests/golden/WBV2/dynamic-inventory.json",
"dynamicSourceTreeSha256": "600cb3dd441f09d9655b740735fbf8fa49ad6c14d308c68d59a29f9c338e4202",
"dynamicUnitCount": 3745,
"dynamicVerifiedUnits": 350,
"dynamicUnverifiedUnits": 3395,
"planningEstimate": 5200,
"historicalInventoryTotal": 6900,
"historicalEffectiveCompleted": 290,
"historicalEffectivePending": 6609,
"historicalReproduction": false
},
"nextTask": null
}

View File

@@ -0,0 +1,113 @@
{
"schemaVersion": 1,
"task": "WBV2-P1145-001",
"operation": "MODULE_FUNCTION_EXECUTION",
"moduleId": "CORRECTIVE_SMOOTH_READER",
"status": "PASS",
"source": {
"paths": [
"blender-5.2.0/source/blender/modifiers/intern/MOD_correctivesmooth.cc",
"blender-5.2.0/source/blender/makesrna/intern/rna_modifier.cc",
"blender-5.2.0/source/blender/web_engine/web_engine_blend_reader.cpp",
"web/tests/e2e/wbv2-corrective-smooth-reader-production.spec.ts",
"tests/files/web/modifier_corrective_smooth_scene.blend",
"web/app/src/vendor/blender/web_engine.wasm"
],
"before": {
"blender-5.2.0/source/blender/modifiers/intern/MOD_correctivesmooth.cc": "34064ee367b0b79f2804a48101943630366d5c166b842fa9f439dbf4aa1dad3b",
"blender-5.2.0/source/blender/makesrna/intern/rna_modifier.cc": "1292112c20eaa113084a2a7093c349e7787db818950d80a6e11b75b287e4287b",
"blender-5.2.0/source/blender/web_engine/web_engine_blend_reader.cpp": "f6fbd04315c48ad4dfbf109b91e5b350550d23c227e5c11a71a58f09e923c990",
"web/tests/e2e/wbv2-corrective-smooth-reader-production.spec.ts": "868e770934e9c19cb9a807530a1837b37f065b654ad948322ee39589b1887fc7",
"tests/files/web/modifier_corrective_smooth_scene.blend": "2e89fa90ec07fa2a87241ad6ec33c569e7a4f69742554c69fe0eadeb32cdac12",
"web/app/src/vendor/blender/web_engine.wasm": "32bc36aba563fe1ed0fb5d0292ca8a3ada1eecd6d45e38d9dcde565f02a312fe"
},
"after": {
"blender-5.2.0/source/blender/modifiers/intern/MOD_correctivesmooth.cc": "34064ee367b0b79f2804a48101943630366d5c166b842fa9f439dbf4aa1dad3b",
"blender-5.2.0/source/blender/makesrna/intern/rna_modifier.cc": "1292112c20eaa113084a2a7093c349e7787db818950d80a6e11b75b287e4287b",
"blender-5.2.0/source/blender/web_engine/web_engine_blend_reader.cpp": "f6fbd04315c48ad4dfbf109b91e5b350550d23c227e5c11a71a58f09e923c990",
"web/tests/e2e/wbv2-corrective-smooth-reader-production.spec.ts": "868e770934e9c19cb9a807530a1837b37f065b654ad948322ee39589b1887fc7",
"tests/files/web/modifier_corrective_smooth_scene.blend": "2e89fa90ec07fa2a87241ad6ec33c569e7a4f69742554c69fe0eadeb32cdac12",
"web/app/src/vendor/blender/web_engine.wasm": "32bc36aba563fe1ed0fb5d0292ca8a3ada1eecd6d45e38d9dcde565f02a312fe"
},
"unchanged": true
},
"reusedReceipts": [
{
"manifest": "tests/golden/WBV2-P1107-000/manifest.json",
"report": "tests/golden/WBV2-P1107-000/module-receipt.json",
"manifestSha256": "a53ebdc52db5dcf27d44e938e5551f1ee2adae8e46c3fe8661604ee6b1000f8c",
"reportSha256": "6dd419799e1a4b9251328d9496f2459d148cd89118740a06b0ee6ffb78e0b1d2"
}
],
"cases": {
"positive": [
"Corrective Smooth type is read from Blender Main",
"RNA metadata is preserved",
"save/reopen preserves modifier metadata"
],
"negative": [
"malformed blend is rejected without Main mutation",
"unsupported modifier data fails closed",
"non-finite metadata is not synthesized"
],
"recovery": [
"save/reopen",
"worker termination",
"deterministic second read"
]
},
"commands": [
{
"command": "source tools/web/emscripten-env.sh && cmake --build build_web_blender6 --target web_engine -- -j8 && cp build_web_blender6/bin/web_engine.wasm web/app/src/vendor/blender/web_engine.wasm && cp build_web_blender6/bin/web_engine.wasm web/app/public/vendor/blender/web_engine.wasm",
"exitCode": 0
},
{
"command": "npm --prefix web run typecheck",
"exitCode": 0
},
{
"command": "npm --prefix web run build",
"exitCode": 0
},
{
"command": "WEB_TEST_PORT=5371 npm --prefix web run test:e2e -- --workers=1 tests/e2e/wbv2-corrective-smooth-reader-production.spec.ts",
"exitCode": 0
}
],
"verification": {
"required": [
"sourceHash",
"wasmBuild",
"positiveMainMutation",
"negativeNoMutation",
"undoRedo",
"saveReopen",
"aggregateManifest"
],
"chromiumOnly": true,
"fullFamilyParity": false
},
"inventory": {
"historical": {
"path": "tests/golden/WBV2-P0-002/parity-map.json",
"sha256": "a60a77cae000941a953d55adbf93d8f6e275533891713d6eae993f60faed94e8",
"total": 6900,
"legacyCompleted": 281,
"legacyPending": 6618,
"effectiveCompleted": 290,
"effectivePending": 6609,
"mutation": "forbidden_by_module_execution"
},
"dynamic": {
"path": "tests/golden/WBV2/dynamic-inventory.json",
"sha256": "3cf5795f89066d759400fa206b559020e27c157708febdab393fa978303a79aa",
"sourceTreeSha256": "600cb3dd441f09d9655b740735fbf8fa49ad6c14d308c68d59a29f9c338e4202",
"unitCount": 3745,
"planningEstimate": 5200,
"hardTarget": false,
"verifiedUnits": 350,
"unverifiedUnits": 3395
}
},
"nextTask": null
}

View File

@@ -0,0 +1,49 @@
> source tools/web/emscripten-env.sh && cmake --build build_web_blender6 --target web_engine -- -j8 && cp build_web_blender6/bin/web_engine.wasm web/app/src/vendor/blender/web_engine.wasm && cp build_web_blender6/bin/web_engine.wasm web/app/public/vendor/blender/web_engine.wasm
ninja: no work to do.
> npm --prefix web run typecheck
> blender-web-editor@0.1.0-rc.1 typecheck
> tsc -p tsconfig.json --noEmit
> npm --prefix web run build
> blender-web-editor@0.1.0-rc.1 build
> tsc -p tsconfig.json && vite build --config app/vite.config.ts
vite v8.2.0 building client environment for production...

transforming...✓ 102 modules transformed.
rendering chunks...
computing gzip size...
dist/index.html 0.46 kB │ gzip: 0.30 kB
dist/assets/storage.worker-D9TqXsGS.js 66.59 kB
dist/assets/web-engine.worker-BfTMiTY1.js 362.11 kB
dist/assets/viewport-render.worker-BKnJyAEz.js 634.08 kB
dist/assets/web_engine-CjnzAIfx.wasm 16,238.37 kB │ gzip: 3,856.73 kB
dist/assets/index-BgTbQCE2.css 15.35 kB │ gzip: 3.91 kB
dist/assets/index-C3AJuKSc.js 1,190.30 kB │ gzip: 316.69 kB
✓ built in 468ms
[plugin rolldown:vite-resolve] Module "module" has been externalized for browser compatibility, imported by "/home/mes123456/workinf_Blender_Wasm/web/app/src/vendor/blender/web_engine.js". See https://vite.dev/guide/troubleshooting.html#module-externalized-for-browser-compatibility for more details.
[plugin builtin:vite-reporter]
(!) Some chunks are larger than 500 kB after minification. Consider:
- Using dynamic import() to code-split the application
- Use build.rolldownOptions.output.codeSplitting to improve chunking: https://rolldown.rs/reference/OutputOptions.codeSplitting
- Adjust chunk size limit for this warning via build.chunkSizeWarningLimit.
> WEB_TEST_PORT=5371 npm --prefix web run test:e2e -- --workers=1 tests/e2e/wbv2-corrective-smooth-reader-production.spec.ts
> blender-web-editor@0.1.0-rc.1 test:e2e
> playwright test --config playwright.config.ts --workers=1 tests/e2e/wbv2-corrective-smooth-reader-production.spec.ts
Running 1 test using 1 worker
✓ 1 tests/e2e/wbv2-corrective-smooth-reader-production.spec.ts:11:1 › Corrective Smooth modifier metadata survives Blender Main save/reopen (1.9s)
1 passed (2.7s)
[WebServer] (node:27133) Warning: The 'NO_COLOR' env is ignored due to the 'FORCE_COLOR' env being set.
[WebServer] (Use `node --trace-warnings ...` to show where the warning was created)
(node:27187) Warning: The 'NO_COLOR' env is ignored due to the 'FORCE_COLOR' env being set.

View File

@@ -0,0 +1,183 @@
{
"schemaVersion": 1,
"task": "WBV2-P1145-001",
"parentTask": "WBV2-P1145-000",
"status": "in_progress",
"goal": "验证 Blender Main 中 Corrective Smooth modifier 的真实类型和 RNA 元数据,并确认 save/reopen、worker termination 和 malformed 输入保持确定性。",
"scope": {
"gap": "capability:CORRECTIVE_SMOOTH_READER",
"ownerFamily": "MODIFIER",
"implementationClass": "LOCAL_EXACT"
},
"commands": [
"node tools/web/check-task-context.mjs --task WBV2-P1145-001 --write"
],
"inputPaths": [],
"inputSelection": {
"schemaVersion": 1,
"limits": {
"maxFiles": 12,
"evidenceBytes": 8192,
"contextRemainingBytes": 2552,
"contextRemainingTokens": 636
},
"source": {
"bytes": 7352,
"tokens": 1840
},
"selected": [],
"excluded": [
{
"path": "blender-5.2.0/source/blender/modifiers/intern/MOD_correctivesmooth.cc",
"bytes": 29127,
"reason": "EVIDENCE_BYTE_BUDGET"
},
{
"path": "blender-5.2.0/source/blender/makesrna/intern/rna_modifier.cc",
"bytes": 521651,
"reason": "EVIDENCE_BYTE_BUDGET"
},
{
"path": "blender-5.2.0/source/blender/web_engine/web_engine_blend_reader.cpp",
"bytes": 313993,
"reason": "EVIDENCE_BYTE_BUDGET"
},
{
"path": "tests/files/web/modifier_corrective_smooth_scene.blend",
"bytes": 97291,
"reason": "EVIDENCE_BYTE_BUDGET"
},
{
"path": "web/tests/e2e/wbv2-corrective-smooth-reader-production.spec.ts",
"bytes": 3743,
"reason": "CONTEXT_REMAINING_SPACE"
},
{
"path": "tests/golden/WBV2-P1145-001/module-receipt.json",
"bytes": 4906,
"reason": "CONTEXT_REMAINING_SPACE"
},
{
"path": "tests/golden/WBV2-P1145-001/manifest.json",
"bytes": null,
"reason": "GENERATED_EVIDENCE_OUTPUT"
},
{
"path": "docs/status/WBV2-P1145-001.md",
"bytes": null,
"reason": "GENERATED_EVIDENCE_OUTPUT"
}
],
"totals": {
"files": 0,
"bytes": 0,
"tokens": 0
}
},
"exitCriteria": [
"source and protocol hashes are bound",
"positive Main reader is proven",
"negative no-mutation and recovery are proven",
"Chromium and save/reopen evidence are exact",
"full-family parity remains explicit"
],
"nextTask": null,
"sourceDocuments": {
"queue": "docs/EXECUTION_QUEUE.md",
"taskCard": "docs/tasks/WBV2-P1145-001.md",
"taskIndex": "tests/golden/WBV2/task-index.json",
"parentManifest": "tests/golden/WBV2-P1145-000/manifest.json",
"parentStatus": "docs/status/WBV2-P1145-000.md"
},
"readPolicy": {
"required": [
"docs/EXECUTION_QUEUE.md",
"docs/tasks/WBV2-P1145-001.md",
"tests/golden/WBV2-P1145-000/manifest.json",
"docs/status/WBV2-P1145-000.md"
],
"machineOnly": [
"tests/golden/WBV2/task-index.json",
"tests/golden/M15-03A/task-catalog.jsonl"
],
"optionalByNeed": [
"docs/WEB_BLENDER_MODELER_V1_SCOPE.md",
"任务卡列出的精确协议/生产路径",
"任务卡明确命名的长期计划小节"
],
"forbiddenByDefault": [
"完整 CURRENT_EXECUTION_PLAN.md",
"完整 PROJECT_STATUS_AND_NEXT_WORK.md",
"全部 docs/status/*.md",
"完整 parity/WBS 计划",
"README 历史和 test-results/"
]
},
"parent": {
"manifest": {
"schemaVersion": 1,
"task": "WBV2-P1145-000",
"parentTask": "WBV2-P1144-001",
"status": "done",
"nextTask": "WBV2-P1145-001",
"artifactCount": 4
},
"statusSummary": [
"# WBV2-P1145-000 status",
"- task: `WBV2-P1145-000`; parent: `WBV2-P1144-001`; status: `done`",
"- taskClass: `QUEUE_ENABLEMENT`; no Corrective Smooth parity completion claimed",
"- source-backed successor authorized for `CORRECTIVE_SMOOTH_READER`",
"- dynamic inventory remains `3745` units; verified `350`; unverified `3395`; bound delta `0`",
"- nextTask: `WBV2-P1145-001`"
]
},
"budgets": {
"queueBytes": 4096,
"taskBytes": 1771,
"parentManifestBytes": 24576,
"parentStatusBytes": 6144,
"evidenceFiles": 12,
"evidenceBytes": 8192,
"contextTokens": 3500,
"contextEnvelopeTokens": 1024,
"taskLines": 42,
"taskTokens": 443
},
"size": {
"documents": [
{
"path": "docs/EXECUTION_QUEUE.md",
"bytes": 3830,
"lines": 56,
"tokens": 958
},
{
"path": "docs/tasks/WBV2-P1145-001.md",
"bytes": 1771,
"lines": 42,
"tokens": 443
},
{
"path": "tests/golden/WBV2-P1145-000/manifest.json",
"bytes": 1389,
"lines": 43,
"tokens": 348
},
{
"path": "docs/status/WBV2-P1145-000.md",
"bytes": 362,
"lines": 8,
"tokens": 91
}
],
"sourceTokens": 1840,
"evidenceFiles": 0,
"evidenceBytes": 0,
"evidenceTokens": 0,
"totalTokens": 1840,
"reservedEnvelopeTokens": 1024,
"estimatedContextTokens": 2864,
"serializedContextTokens": 658,
"withinBudget": true
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,42 @@
{
"schemaVersion": 1,
"operation": "BLENDER_52_SOURCE_DYNAMIC_COVERAGE_AUDIT",
"status": "done",
"artifacts": {
"inventory": {
"path": "tests/golden/WBV2/dynamic-inventory.json",
"sha256": "3cf5795f89066d759400fa206b559020e27c157708febdab393fa978303a79aa"
},
"inventoryManifest": {
"path": "tests/golden/WBV2/dynamic-inventory-manifest.json",
"sha256": "0f440885c8e8ca401db412b0a5ec1195e79e9ab18eb731b9eae00bd8313c017c"
},
"bindings": {
"path": "tests/golden/WBV2/dynamic-coverage-bindings.json",
"sha256": "8527415d36130b5c0e8f828fc7d402102286c21d30ceb98498ade360961990ab"
},
"report": {
"path": "tests/golden/WBV2/dynamic-coverage-audit.json",
"sha256": "4e2e6bef38bfda7d068242a0e88325948d7c292014af279711fdec2ecba2e136"
},
"generator": {
"path": "tools/web/reconcile-source-dynamic-coverage.mjs",
"sha256": "6c582a0a71ded6f3d541ccf745197b354c2fb998307203fe66a57be9b6eff8ac"
}
},
"coverage": {
"unitCount": 3745,
"verifiedUnits": 350,
"unverifiedUnits": 3395,
"boundUnitDelta": 350,
"sourceTreeSha256": "600cb3dd441f09d9655b740735fbf8fa49ad6c14d308c68d59a29f9c338e4202"
},
"historical": {
"total": 6900,
"immutable": true
},
"planningEstimate": {
"units": 5200,
"hardTarget": false
}
}

View File

@@ -0,0 +1,30 @@
{
"schemaVersion": 1,
"operation": "BLENDER_52_SOURCE_DYNAMIC_INVENTORY_CHECK",
"status": "PASS",
"sourceTreeSha256": "600cb3dd441f09d9655b740735fbf8fa49ad6c14d308c68d59a29f9c338e4202",
"sourceFiles": 6206,
"sourceBytes": 96049182,
"unitCount": 3745,
"byKind": {
"modifier-source": 97,
"node": 497,
"operator-macro": 58,
"operator": 2018,
"rna-struct": 979,
"web-command": 96
},
"byFamily": {
"MODIFIER": 97,
"NODES": 497,
"EDITOR": 2023,
"WINDOW_MANAGER": 53,
"RNA": 979,
"WEB_ENGINE": 96
},
"planningEstimate": 5200,
"hardTarget": false,
"verifiedUnits": 350,
"unverifiedUnits": 3395,
"deterministic": true
}

View File

@@ -0,0 +1,59 @@
{
"schemaVersion": 1,
"operation": "BLENDER_52_SOURCE_DYNAMIC_INVENTORY",
"status": "done",
"runtime": "BLENDER_5_2_SOURCE_TREE",
"artifacts": {
"generator": {
"path": "tools/web/generate-source-dynamic-inventory.mjs",
"sha256": "9ef57baa78abd2ff8ac38ce8195e9e97f6323c944d9b2cedd60e70c622e4cc01"
},
"checker": {
"path": "tools/web/check-source-dynamic-inventory.mjs",
"sha256": "57f19b254a0659a2470a61b629995b170676ba8dcf42b925cbc1a69103d519b7"
},
"inventory": {
"path": "tests/golden/WBV2/dynamic-inventory.json",
"sha256": "3cf5795f89066d759400fa206b559020e27c157708febdab393fa978303a79aa"
},
"report": {
"path": "tests/golden/WBV2/dynamic-inventory-check-report.json",
"sha256": "f76d38d1257b3ef8e5f1677f725a76dc875a6493c85f95340f27d7701ca4576c"
},
"status": {
"path": "docs/status/WBV2-DYNAMIC-INVENTORY.md",
"sha256": "8230a85ca0098a05956098793bffde579b0f5a249f0d0a1271da355a531c7810"
},
"historicalInventory": {
"path": "tests/golden/WBV2-P0-002/parity-map.json",
"sha256": "a60a77cae000941a953d55adbf93d8f6e275533891713d6eae993f60faed94e8"
}
},
"source": {
"root": "blender-5.2.0/source/blender",
"treeSha256": "6d2f3336fe316c473c43f3b86c4d2ec36ab4a52cf4a7783e20fb5ac483fd20b1",
"fileCount": 6206,
"byteCount": 96043122
},
"inventory": {
"unitCount": 3745,
"byKind": {
"modifier-source": 97,
"node": 497,
"operator-macro": 58,
"operator": 2018,
"rna-struct": 979,
"web-command": 96
},
"planningEstimate": 5200,
"hardTarget": false,
"verifiedUnits": 332,
"unverifiedUnits": 3413,
"boundCaseDelta": 272
},
"historical": {
"initialEstimate": 6900,
"immutable": true,
"replacedAsCurrentMetric": true
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,71 @@
{"id":"WBV2-P607-000","gapId":"workflow:authorize_capability_family_wave","ownerFamily":"PROGRAM","sourceTask":"P607","state":"completed","targetImplementationClass":"SOURCE_BACKED","exitCriteria":["6900 inventory remains immutable","capability families are deduplicated before implementation","real Blender source or WebEngine Main path is wired","verification and completion overlay remain deferred"]}
{"id":"WBV2-P607-001","gapId":"workflow:index_capability_family_batches","ownerFamily":"RELEASE","sourceTask":"P607","state":"completed","targetImplementationClass":"SOURCE_BACKED","exitCriteria":["6900 inventory remains immutable","capability families are deduplicated before implementation","real Blender source or WebEngine Main path is wired","verification and completion overlay remain deferred"]}
{"id":"WBV2-P607-002","gapId":"workflow:implement_capability_family_slice","ownerFamily":"IMPLEMENTATION","sourceTask":"P607","state":"active","targetImplementationClass":"SOURCE_BACKED","exitCriteria":["6900 inventory remains immutable","capability families are deduplicated before implementation","real Blender source or WebEngine Main path is wired","verification and completion overlay remain deferred"]}
{"id":"WBV2-P607-003","gapId":"workflow:run_capability_family_smoke","ownerFamily":"RELEASE","sourceTask":"P607","state":"pending","targetImplementationClass":"SOURCE_BACKED","exitCriteria":["6900 inventory remains immutable","capability families are deduplicated before implementation","real Blender source or WebEngine Main path is wired","verification and completion overlay remain deferred"]}
{"id":"WBV2-P1096-000","gapId":"workflow:supersede_generic_continuation_queue","ownerFamily":"PROGRAM","sourceTask":"1096","state":"completed","targetImplementationClass":"QUEUE_ONLY","exitCriteria":["no parity claim","inventory preserved","module plan hash-bound"]}
{"id":"WBV2-P1100-000","gapId":"workflow:reorganize_module_execution_plan","ownerFamily":"PROGRAM","sourceTask":"1100","state":"completed","targetImplementationClass":"QUEUE_ONLY","exitCriteria":["completed receipts reused","generic continuation excluded","one module active","inventory preserved"]}
{"id":"WBV2-P1100-001","gapId":"RENDER_LIGHT_CAMERA","ownerFamily":"RENDER","sourceTask":"1100","state":"completed","targetImplementationClass":"LOCAL_EXACT","exitCriteria":["source and protocol hashes are bound","positive Main mutation is proven","negative and recovery cases are proven","Chromium and save/reopen receipts are exact","full-family parity remains explicit"]}
{"id":"WBV2-P1100-002","gapId":"SCENE_OBJECT_MESH","ownerFamily":"SCENE_MESH","sourceTask":"1100","state":"completed","targetImplementationClass":"LOCAL_EXACT","exitCriteria":["source and protocol hashes are bound","positive Main mutation is proven","negative and recovery cases are proven","Chromium and save/reopen receipts are exact","full-family parity remains explicit"]}
{"id":"WBV2-P1100-003","gapId":"MATERIAL_ANIMATION","ownerFamily":"MATERIAL_ANIMATION","sourceTask":"1100","state":"completed","targetImplementationClass":"LOCAL_EXACT","exitCriteria":["source and protocol hashes are bound","positive Main mutation is proven","negative and recovery cases are proven","Chromium and save/reopen receipts are exact","full-family parity remains explicit"]}
{"id":"WBV2-P1101-000","gapId":"NONMESH_GEOMETRY","ownerFamily":"GEOMETRY","sourceTask":"1101","state":"completed","targetImplementationClass":"LOCAL_EXACT","exitCriteria":["source and protocol hashes are bound","positive Main mutation is proven","negative and recovery cases are proven","Chromium and save/reopen receipts are exact","full-family parity remains explicit"]}
{"id":"WBV2-P1101-001","gapId":"NODES_SIMULATION","ownerFamily":"NODES_SIMULATION","sourceTask":"1101","state":"completed","targetImplementationClass":"LOCAL_EXACT","exitCriteria":["source and protocol hashes are bound","positive Main mutation is proven","negative and recovery cases are proven","Chromium and save/reopen receipts are exact","full-family parity remains explicit"]}
{"id":"WBV2-P1101-002","gapId":"ASSET_IO","ownerFamily":"ASSET","sourceTask":"1101","state":"completed","targetImplementationClass":"LOCAL_EXACT","exitCriteria":["source and protocol hashes are bound","positive Main mutation is proven","negative and recovery cases are proven","Chromium and save/reopen receipts are exact","full-family parity remains explicit"]}
{"id":"WBV2-P1101-003","gapId":"SCULPT_PAINT","ownerFamily":"SCULPT_PAINT","sourceTask":"1101","state":"completed","targetImplementationClass":"LOCAL_EXACT","exitCriteria":["source and protocol hashes are bound","positive Main mutation is proven","negative and recovery cases are proven","Chromium and save/reopen receipts are exact","full-family parity remains explicit"]}
{"id":"WBV2-P1102-000","gapId":"UI_EDITOR","ownerFamily":"EDITOR","sourceTask":"1102","state":"completed","targetImplementationClass":"LOCAL_EXACT","exitCriteria":["source and protocol hashes are bound","positive Main mutation is proven","negative and recovery cases are proven","Chromium and save/reopen receipts are exact","full-family parity remains explicit"]}
{"id":"WBV2-P1102-001","gapId":"INVENTORY_GATE","ownerFamily":"RELEASE","sourceTask":"1102","state":"completed","targetImplementationClass":"LOCAL_EXACT","exitCriteria":["source and protocol hashes are bound","positive Main mutation is proven","negative and recovery cases are proven","Chromium and save/reopen receipts are exact","full-family parity remains explicit"]}
{"id":"WBV2-P1103-000","gapId":"OBJECT_OPERATIONS","ownerFamily":"OBJECT","sourceTask":"1103","state":"completed","targetImplementationClass":"LOCAL_EXACT","exitCriteria":["source and protocol hashes are bound","positive Main mutation is proven","negative and recovery cases are proven","Chromium and save/reopen receipts are exact","full-family parity remains explicit"]}
{"id":"WBV2-P1104-000","gapId":"COMPOSITOR_SEQUENCER","ownerFamily":"MEDIA","sourceTask":"1104","state":"completed","targetImplementationClass":"LOCAL_EXACT","exitCriteria":["source and protocol hashes are bound","positive Main or compositor mutation is proven","negative and recovery cases are proven","Chromium and save/reopen receipts are exact","full-family parity remains explicit"]}
{"id":"WBV2-P1105-000","gapId":"AUDIO_PARTICLE","ownerFamily":"AUDIO_PARTICLE","sourceTask":"1105","state":"completed","targetImplementationClass":"LOCAL_EXACT","exitCriteria":["source and protocol hashes are bound","positive Main mutation is proven or a typed build capability blocker is recorded","negative and recovery cases are proven","Chromium and save/reopen receipts are exact","full-family parity remains explicit"]}
{"id":"WBV2-P1106-000","gapId":"RIG_ANIMATION","ownerFamily":"RIG","sourceTask":"1106","state":"completed","targetImplementationClass":"LOCAL_EXACT","exitCriteria":["source and protocol hashes are bound","positive Main mutation is proven","negative and recovery cases are proven","Chromium and save/reopen receipts are exact","full-family parity remains explicit"]}
{"id":"WBV2-P1107-000","gapId":"MODIFIER_SYSTEM","ownerFamily":"MODIFIER","sourceTask":"1107","state":"completed","targetImplementationClass":"LOCAL_EXACT","exitCriteria":["source and protocol hashes are bound","positive Main mutation is proven","negative and recovery cases are proven","Chromium and save/reopen receipts are exact","full-family parity remains explicit"]}
{"id":"WBV2-P1108-000","gapId":"NODES_GRAPH","ownerFamily":"NODES","sourceTask":"1108","state":"completed","targetImplementationClass":"LOCAL_EXACT","exitCriteria":["source and protocol hashes are bound","positive graph reader/evaluation evidence is proven","negative and recovery cases are proven","Chromium and save/reopen receipts are exact","full-family parity remains explicit"]}
{"id":"WBV2-P1109-000","gapId":"IO_FORMAT","ownerFamily":"MEDIA","sourceTask":"1109","state":"completed","targetImplementationClass":"LOCAL_BOUNDED","exitCriteria":["source and protocol hashes are bound","positive format routing/readiness evidence is proven","negative and recovery cases are proven","Chromium and deterministic recovery receipts are exact","full-family parity remains explicit"]}
{"id":"WBV2-P1110-000","gapId":"RENDER_PIPELINE","ownerFamily":"RENDER","sourceTask":"1110","state":"completed","targetImplementationClass":"LOCAL_BOUNDED","exitCriteria":["source and protocol hashes are bound","positive render/reference evidence is proven","negative and recovery cases are proven","Chromium and deterministic render receipts are exact","full-family parity remains explicit"]}
{"id":"WBV2-P1111-000","gapId":"SCRIPTING_SECURITY","ownerFamily":"SCRIPTING","sourceTask":"1111","state":"completed","targetImplementationClass":"LOCAL_BOUNDED","exitCriteria":["source and protocol hashes are bound","positive reader and policy evidence is proven","negative and recovery cases are proven","Chromium sandbox receipts are exact","full-family parity remains explicit"]}
{"id":"WBV2-P1112-000","gapId":"ANIMATION_TIMELINE","ownerFamily":"ANIMATION","sourceTask":"1112","state":"completed","targetImplementationClass":"LOCAL_EXACT","exitCriteria":["source and protocol hashes are bound","positive frame/NLA evaluation is proven","negative and recovery cases are proven","Chromium and deterministic evaluation receipts are exact","full-family parity remains explicit"]}
{"id":"WBV2-P1113-000","gapId":"SHADER_MATERIAL","ownerFamily":"MATERIAL","sourceTask":"1113","state":"completed","targetImplementationClass":"LOCAL_EXACT","exitCriteria":["source and protocol hashes are bound","positive Shader Main compilation is proven","negative and recovery cases are proven","Chromium and deterministic compile receipts are exact","full-family parity remains explicit"]}
{"id":"WBV2-P1114-000","gapId":"GREASE_PENCIL","ownerFamily":"DRAWING","sourceTask":"1114","state":"completed","targetImplementationClass":"LOCAL_EXACT","exitCriteria":["source and protocol hashes are bound","positive Grease Pencil Main mutation is proven","negative and recovery cases are proven","Chromium and save/reopen receipts are exact","full-family parity remains explicit"]}
{"id":"WBV2-P1115-000","gapId":"CURVE_SURFACE_EDITING","ownerFamily":"GEOMETRY","sourceTask":"1115","state":"completed","targetImplementationClass":"LOCAL_EXACT","exitCriteria":["source and protocol hashes are bound","positive non-mesh Main mutation is proven","negative and recovery cases are proven","Chromium and save/reopen receipts are exact","full-family parity remains explicit"]}
{"id":"WBV2-P1116-000","gapId":"STORAGE_RECOVERY","ownerFamily":"STORAGE","sourceTask":"1116","state":"completed","targetImplementationClass":"LOCAL_BOUNDED","exitCriteria":["source and protocol hashes are bound","positive storage commit/recovery evidence is proven","negative and quarantine cases are proven","Chromium restart receipts are exact","full-family parity remains explicit"]}
{"id":"WBV2-P1117-000","gapId":"SIMULATION_CACHE","ownerFamily":"SIMULATION","sourceTask":"1117","state":"completed","targetImplementationClass":"LOCAL_BOUNDED","exitCriteria":["source and protocol hashes are bound","positive cache identity/lifecycle evidence is proven","negative integrity and budget cases are proven","Chromium restart and quarantine receipts are exact","full-family parity remains explicit"]}
{"id":"WBV2-P1118-000","gapId":"UI_INPUT_INTERACTION","ownerFamily":"EDITOR","sourceTask":"1118","state":"completed","targetImplementationClass":"LOCAL_BOUNDED","exitCriteria":["source and protocol hashes are bound","positive keyboard Main mutation and save are proven","negative mutex/input cases are proven","Chromium accessibility and responsive receipts are exact","full-family parity remains explicit"]}
{"id":"WBV2-P1119-000","gapId":"LIBRARY_WORKFLOW","ownerFamily":"ASSET","sourceTask":"1119","state":"completed","targetImplementationClass":"LOCAL_EXACT","exitCriteria":["source and protocol hashes are bound","positive library Main mutation is proven","negative and recovery cases are proven","Chromium and save/reopen receipts are exact","full-family parity remains explicit"]}
{"id":"WBV2-P1120-000","gapId":"VOLUME_NANOVDB","ownerFamily":"MEDIA","sourceTask":"1120","state":"completed","targetImplementationClass":"LOCAL_BOUNDED","exitCriteria":["source and protocol hashes are bound","positive volume resource and viewport evidence is proven","negative and recovery cases are proven","Chromium OPFS/restart receipts are exact","full-family parity remains explicit"]}
{"id":"WBV2-P1121-000","gapId":"PAINT_STROKE","ownerFamily":"SCULPT","sourceTask":"1121","state":"completed","targetImplementationClass":"LOCAL_BOUNDED","exitCriteria":["source and protocol hashes are bound","positive paint Main transaction is proven","negative and recovery cases are proven","Chromium pointer/GPU receipts are exact","full-family parity remains explicit"]}
{"id":"WBV2-P1122-000","gapId":"ENGINE_RUNTIME","ownerFamily":"RUNTIME","sourceTask":"1122","state":"completed","targetImplementationClass":"LOCAL_EXACT","exitCriteria":["source and protocol hashes are bound","positive compiled WASM Main lifecycle is proven","negative variant/hash cases are proven","Chromium Worker restart receipts are exact","full-family parity remains explicit"]}
{"id":"WBV2-P1123-000","gapId":"GLB_ROUNDTRIP_REPAIR","ownerFamily":"MEDIA","sourceTask":"1123","state":"completed","targetImplementationClass":"LOCAL_EXACT","exitCriteria":["source and protocol hashes are bound","positive GLB and non-mesh roundtrip are proven","negative and recovery cases are proven","Chromium save/reopen receipts are exact","full-family parity remains explicit"]}
{"id":"WBV2-P1124-000","gapId":"HISTORICAL_HASH_DRIFT","ownerFamily":"PROVENANCE","sourceTask":"1124","state":"completed","targetImplementationClass":"LOCAL_BOUNDED","exitCriteria":["original and observed hashes are recorded","provenance remains BLOCKED_SOURCE_LOST where bytes cannot be restored","historical manifests and inventory remain unchanged","deterministic drift evidence is reproducible","no full-family parity claim"]}
{"id":"WBV2-P1125-000","gapId":"GEOMETRY_NODES_ALLOWLIST_REPAIR","ownerFamily":"NODES","sourceTask":"1125","state":"completed","targetImplementationClass":"LOCAL_EXACT","exitCriteria":["source and protocol hashes are bound","supported graph Main materialization is proven","unsupported graph remains rejected without mutation","save/reopen graph retention is exact","full-family parity remains explicit"]}
{"id":"WBV2-P1126-000","gapId":"GREASE_PENCIL_VIEWPORT","ownerFamily":"DRAWING","sourceTask":"1126","state":"completed","targetImplementationClass":"LOCAL_BOUNDED","exitCriteria":["source and protocol hashes are bound","2D and 3D selection revisions converge","main-thread and Offscreen marquee are exact","stale/foreign selection cases remain fail-closed","full-family parity remains explicit"]}
{"id":"WBV2-P1127-000","gapId":"TRACKING_MASK","ownerFamily":"TRACKING","sourceTask":"1127","state":"completed","targetImplementationClass":"LOCAL_BOUNDED","exitCriteria":["source and protocol hashes are bound","positive Mask reader and interaction evidence is proven","negative solve/resource cases are proven","Chromium stable IDs and recovery receipts are exact","full-family parity remains explicit"]}
{"id":"WBV2-P1128-000","gapId":"SEQUENCER_MOVIE_CODEC","ownerFamily":"MEDIA","sourceTask":"1128","state":"completed","targetImplementationClass":"LOCAL_BOUNDED","exitCriteria":["source and core-resource hashes are bound","positive MP4 probe and RGBA frame are proven","corrupt and source-identity cases are rejected","AbortSignal cancellation leaves no live Worker","full-family parity remains explicit"]}
{"id":"WBV2-P1129-000","gapId":"workflow:authorize_scene_settings_module","ownerFamily":"PROGRAM","sourceTask":"1129","state":"completed","targetImplementationClass":"QUEUE_ONLY","exitCriteria":["P1128 evidence remains hash-bound","existing receipts are reused","inventory remains immutable","one source-backed module is active"]}
{"id":"WBV2-P1129-001","gapId":"CORE_SCENE_GRAPH_SETTINGS","ownerFamily":"CORE_SCENE","sourceTask":"1129","state":"completed","targetImplementationClass":"LOCAL_EXACT","exitCriteria":["source and protocol hashes are bound","positive Main mutations are proven","negative no-mutation and recovery are proven","Chromium and save/reopen receipts are exact","full-family parity remains explicit"]}
{"id":"WBV2-P1130-000","gapId":"workflow:authorize_editor_view2d_module","ownerFamily":"PROGRAM","sourceTask":"1130","state":"completed","targetImplementationClass":"QUEUE_ONLY","exitCriteria":["P1129 evidence remains hash-bound","P1102 editor workflow source remains unchanged","inventory remains immutable","one source-backed module is active"]}
{"id":"WBV2-P1130-001","gapId":"UI_EDITOR_VIEW2D_STATE","ownerFamily":"EDITOR","sourceTask":"1130","state":"completed","targetImplementationClass":"LOCAL_EXACT","exitCriteria":["source and protocol hashes are bound","positive reader state is proven","negative no-publish and recovery are proven","Chromium and save/reopen compatibility are exact","full-family parity remains explicit"]}
{"id":"WBV2-P1131-000","gapId":"workflow:authorize_curve_topology_module","ownerFamily":"PROGRAM","sourceTask":"1131","state":"completed","targetImplementationClass":"QUEUE_ONLY","exitCriteria":["P1130 evidence remains hash-bound","Curve topology source and protocol are present","inventory remains immutable","one source-backed module is active"]}
{"id":"WBV2-P1131-001","gapId":"CURVE_TOPOLOGY_EDITOR","ownerFamily":"EDITOR","sourceTask":"1131","state":"completed","targetImplementationClass":"LOCAL_EXACT","exitCriteria":["source and protocol hashes are bound","positive Main mutation is proven","negative no-mutation and recovery are proven","Chromium and save/reopen evidence are exact","full-family parity remains explicit"]}
{"id":"WBV2-P1132-000","gapId":"workflow:authorize_curve_handle_module","ownerFamily":"PROGRAM","sourceTask":"1132","state":"completed","targetImplementationClass":"QUEUE_ONLY","exitCriteria":["P1131 evidence remains hash-bound","Curve handle source and protocol are present","inventory remains immutable","one source-backed module is active"]}
{"id":"WBV2-P1132-001","gapId":"CURVE_HANDLE_GIZMO","ownerFamily":"EDITOR","sourceTask":"1132","state":"completed","targetImplementationClass":"LOCAL_EXACT","exitCriteria":["source and protocol hashes are bound","positive Main mutation is proven","negative no-mutation and recovery are proven","Chromium and save/reopen evidence are exact","full-family parity remains explicit"]}
{"id":"WBV2-P1133-000","gapId":"workflow:authorize_curve_splines_module","ownerFamily":"PROGRAM","sourceTask":"1133","state":"completed","targetImplementationClass":"QUEUE_ONLY","exitCriteria":["P1132 evidence remains hash-bound","Curve spline source and protocol are present","inventory remains immutable","one source-backed module is active"]}
{"id":"WBV2-P1133-001","gapId":"CURVE_SPLINES_TRANSACTION","ownerFamily":"EDITOR","sourceTask":"1133","state":"completed","targetImplementationClass":"LOCAL_EXACT","exitCriteria":["source and protocol hashes are bound","positive Main mutation is proven","negative no-mutation and recovery are proven","Chromium and save/reopen evidence are exact","full-family parity remains explicit"]}
{"id":"WBV2-P1134-000","gapId":"workflow:authorize_surface_topology_module","ownerFamily":"PROGRAM","sourceTask":"1134","state":"completed","targetImplementationClass":"QUEUE_ONLY","exitCriteria":["P1133 evidence remains hash-bound","Surface topology source and protocol are present","inventory remains immutable","one source-backed module is active"]}
{"id":"WBV2-P1134-001","gapId":"SURFACE_TOPOLOGY_TRANSACTION","ownerFamily":"GEOMETRY","sourceTask":"1134","state":"completed","targetImplementationClass":"LOCAL_EXACT","exitCriteria":["source and protocol hashes are bound","positive Main mutation is proven","negative no-mutation and recovery are proven","Chromium and save/reopen evidence are exact","full-family parity remains explicit"]}
{"id":"WBV2-P1135-000","gapId":"workflow:authorize_font_body_module","ownerFamily":"PROGRAM","sourceTask":"1135","state":"completed","targetImplementationClass":"QUEUE_ONLY","exitCriteria":["P1134 evidence remains hash-bound","Font body source and protocol are present","inventory remains immutable","one source-backed module is active"]}
{"id":"WBV2-P1135-001","gapId":"FONT_BODY_TRANSACTION","ownerFamily":"GEOMETRY","sourceTask":"1135","state":"completed","targetImplementationClass":"LOCAL_EXACT","exitCriteria":["source and protocol hashes are bound","positive Main mutation is proven","negative no-mutation and recovery are proven","Chromium and save/reopen evidence are exact","full-family parity remains explicit"]}
{"id":"WBV2-P1136-000","gapId":"workflow:authorize_font_properties_module","ownerFamily":"PROGRAM","sourceTask":"1136","state":"completed","targetImplementationClass":"QUEUE_ONLY","exitCriteria":["P1135 evidence remains hash-bound","Font property source and protocol are present","inventory remains immutable","one source-backed module is active"]}
{"id":"WBV2-P1136-001","gapId":"FONT_PROPERTIES_TRANSACTION","ownerFamily":"GEOMETRY","sourceTask":"1136","state":"completed","targetImplementationClass":"LOCAL_EXACT","exitCriteria":["source and protocol hashes are bound","positive Main mutation is proven","negative no-mutation and recovery are proven","Chromium and save/reopen evidence are exact","full-family parity remains explicit"]}
{"id":"WBV2-P1137-000","gapId":"workflow:authorize_metaball_elements_module","ownerFamily":"PROGRAM","sourceTask":"1137","state":"completed","targetImplementationClass":"QUEUE_ONLY","exitCriteria":["P1136 evidence remains hash-bound","Metaball element source and protocol are present","inventory remains immutable","one source-backed module is active"]}
{"id":"WBV2-P1137-001","gapId":"METABALL_ELEMENTS_TRANSACTION","ownerFamily":"GEOMETRY","sourceTask":"1137","state":"completed","targetImplementationClass":"LOCAL_EXACT","exitCriteria":["source and protocol hashes are bound","positive Main mutation is proven","negative no-mutation and recovery are proven","Chromium and save/reopen evidence are exact","full-family parity remains explicit"]}
{"id":"WBV2-P1138-000","gapId":"workflow:authorize_text_source_module","ownerFamily":"SCRIPTING","sourceTask":"1138","state":"completed","targetImplementationClass":"QUEUE_ONLY","exitCriteria":["P1137 evidence remains hash-bound","ID_TXT is registered for WITH_WEB","inventory remains source-derived","one source-backed module is active"]}
{"id":"WBV2-P1138-001","gapId":"TEXT_SOURCE_TRANSACTION","ownerFamily":"SCRIPTING","sourceTask":"1138","state":"completed","targetImplementationClass":"LOCAL_EXACT","exitCriteria":["source and protocol hashes are bound","positive Main mutation is proven","negative no-mutation and recovery are proven","Chromium and save/reopen evidence are exact","full-family parity remains explicit"]}
{"id":"WBV2-P1139-000","gapId":"workflow:authorize_texture_properties_module","ownerFamily":"TEXTURE","sourceTask":"1139","state":"completed","targetImplementationClass":"QUEUE_ONLY","exitCriteria":["P1138 evidence remains hash-bound","ID_TE is registered for WITH_WEB","inventory remains source-derived","one source-backed module is active"]}
{"id":"WBV2-P1139-001","gapId":"TEXTURE_PROPERTIES_TRANSACTION","ownerFamily":"TEXTURE","sourceTask":"1139","state":"completed","targetImplementationClass":"LOCAL_EXACT","exitCriteria":["source and protocol hashes are bound","positive Main mutation is proven","negative no-mutation and recovery are proven","Chromium and save/reopen evidence are exact","full-family parity remains explicit"]}
{"id":"WBV2-P1140-000","gapId":"workflow:authorize_action_group_reader_module","ownerFamily":"ANIMATION","sourceTask":"1140","state":"completed","targetImplementationClass":"QUEUE_ONLY","exitCriteria":["P1139 evidence remains hash-bound","ActionGroup has explicit Blender source mapping","inventory remains source-derived","one source-backed module is active"]}
{"id":"WBV2-P1140-001","gapId":"ACTION_GROUP_READER","ownerFamily":"ANIMATION","sourceTask":"1140","state":"completed","targetImplementationClass":"LOCAL_EXACT","exitCriteria":["source and protocol hashes are bound","positive Main reader is proven","negative no-mutation and recovery are proven","Chromium and save/reopen evidence are exact","full-family parity remains explicit"]}
{"id":"WBV2-P1141-000","gapId":"workflow:authorize_action_pose_markers_module","ownerFamily":"ANIMATION","sourceTask":"1141","state":"completed","targetImplementationClass":"QUEUE_ONLY","exitCriteria":["P1140 evidence remains hash-bound","ActionPoseMarkers has explicit Blender source mapping","inventory remains source-derived","one source-backed module is active"]}
{"id":"WBV2-P1141-001","gapId":"ACTION_POSE_MARKERS_READER","ownerFamily":"ANIMATION","sourceTask":"1141","state":"completed","targetImplementationClass":"LOCAL_EXACT","exitCriteria":["source and protocol hashes are bound","positive Main reader is proven","negative no-mutation and recovery are proven","Chromium and save/reopen evidence are exact","full-family parity remains explicit"]}
{"id":"WBV2-P1142-000","gapId":"workflow:authorize_fcurve_sample_reader_module","ownerFamily":"ANIMATION","sourceTask":"1142","state":"completed","targetImplementationClass":"QUEUE_ONLY","exitCriteria":["P1141 evidence remains hash-bound","FCurveSample has explicit Blender source mapping","inventory remains source-derived","one source-backed module is active"]}
{"id":"WBV2-P1142-001","gapId":"FCURVE_SAMPLE_READER","ownerFamily":"ANIMATION","sourceTask":"1142","state":"completed","targetImplementationClass":"LOCAL_EXACT","exitCriteria":["source and protocol hashes are bound","positive Main reader is proven","negative no-mutation and recovery are proven","Chromium and save/reopen evidence are exact","full-family parity remains explicit"]}
{"id":"WBV2-P1143-000","gapId":"workflow:authorize_fcurve_modifier_reader_module","ownerFamily":"ANIMATION","sourceTask":"1143","state":"completed","targetImplementationClass":"QUEUE_ONLY","exitCriteria":["P1142 evidence remains hash-bound","FCurveModifiers has explicit Blender source mapping","inventory remains source-derived","one source-backed module is active"]}
{"id":"WBV2-P1143-001","gapId":"FCURVE_MODIFIER_READER","ownerFamily":"ANIMATION","sourceTask":"1143","state":"completed","targetImplementationClass":"LOCAL_EXACT","exitCriteria":["source and protocol hashes are bound","positive Main reader is proven","negative no-mutation and recovery are proven","Chromium and save/reopen evidence are exact","full-family parity remains explicit"]}
{"id":"WBV2-P1144-000","gapId":"workflow:authorize_driver_reader_module","ownerFamily":"ANIMATION","sourceTask":"1144","state":"completed","targetImplementationClass":"QUEUE_ONLY","exitCriteria":["P1143 evidence remains hash-bound","Driver has explicit Blender source mapping","inventory remains source-derived","one source-backed module is active"]}
{"id":"WBV2-P1144-001","gapId":"DRIVER_READER","ownerFamily":"ANIMATION","sourceTask":"1144","state":"completed","targetImplementationClass":"LOCAL_EXACT","exitCriteria":["source and protocol hashes are bound","positive Main reader is proven","negative no-mutation and recovery are proven","Chromium and save/reopen evidence are exact","full-family parity remains explicit"]}
{"id":"WBV2-P1145-000","gapId":"workflow:authorize_corrective_smooth_reader_module","ownerFamily":"PROGRAM","sourceTask":"1145","state":"completed","targetImplementationClass":"QUEUE_ONLY","exitCriteria":["prior evidence remains hash-bound","Corrective Smooth has explicit Blender source mapping","inventory remains source-derived","one source-backed module is active"]}
{"id":"WBV2-P1145-001","gapId":"CORRECTIVE_SMOOTH_READER","ownerFamily":"MODIFIER","sourceTask":"1145","state":"completed","targetImplementationClass":"LOCAL_EXACT","exitCriteria":["source and protocol hashes are bound","positive Main reader is proven","negative no-mutation and recovery are proven","Chromium and save/reopen evidence are exact","full-family parity remains explicit"]}

File diff suppressed because one or more lines are too long

View File

@@ -38,6 +38,18 @@ function currentTask() {
return { task, parentManifest };
}
function terminalTask(task) {
try {
const index = JSON.parse(read("tests/golden/WBV2/task-index.json"));
const manifestPath = `tests/golden/${task}/manifest.json`;
const manifest = fs.existsSync(abs(manifestPath)) ? JSON.parse(read(manifestPath)) : null;
return index.activeTask === null && manifest?.task === task && manifest.status === "done" && manifest.nextTask === null;
}
catch {
return false;
}
}
function taskCommand(task) {
const card = read(`docs/tasks/${task}.md`);
const match = card.match(/- Implementation entrypoint: `([^`]+)`/u);
@@ -55,16 +67,25 @@ const summary = [];
while (maxIterations === 0 || iterations < maxIterations) {
const before = currentTask();
assertContext();
// A completed V2 plan intentionally leaves the final task as an auditable
// pointer. Do not replay its implementation command on later unattended
// passes; close the runner cleanly at the terminal boundary.
if (terminalTask(before.task)) {
summary.push({ task: before.task, command: null, nextTask: null, terminal: true, log: null });
process.stdout.write(`continuous-terminal task=${before.task}\n`);
break;
}
const focused = taskCommand(before.task);
const logPath = `tests/golden/${before.task}/continuous-runner.log`;
const focusedResult = command(focused, logPath);
if (focusedResult.code !== 0) throw new Error(`${before.task} focused command failed (exit ${focusedResult.code}); log=${logPath}`);
const after = currentTask();
if (after.task === before.task) throw new Error(`${before.task} focused command exited 0 without advancing queue; log=${logPath}`);
const terminal = after.task === before.task && terminalTask(before.task);
if (after.task === before.task && !terminal) throw new Error(`${before.task} focused command exited 0 without advancing queue; log=${logPath}`);
const gates = command("node tools/web/check-context-governance.mjs && node tools/web/check-execution-control-plane.mjs --allow-unrunnable && node tools/web/check-v2-task-index.mjs && node tools/web/check-v2-handoff.mjs && npm --prefix web run typecheck && npm --prefix web run lint && git diff --check", `tests/golden/${before.task}/continuous-gates.log`);
if (gates.code !== 0) throw new Error(`${before.task} post-handoff gate failed (exit ${gates.code}); log=tests/golden/${before.task}/continuous-gates.log`);
iterations += 1;
summary.push({ task: before.task, command: focused, nextTask: after.task, log: logPath });
summary.push({ task: before.task, command: focused, nextTask: terminal ? null : after.task, terminal, log: logPath });
process.stdout.write(`continuous-iteration=${iterations} task=${before.task} next=${after.task}\n`);
if (commitEnabled) {
const manifestPath = `tests/golden/${before.task}/manifest.json`;
@@ -75,15 +96,19 @@ while (maxIterations === 0 || iterations < maxIterations) {
"docs/EXECUTION_QUEUE.md",
"nextTask.md",
`docs/tasks/${before.task}.md`,
`docs/tasks/${after.task}.md`,
...(terminal ? [] : [`docs/tasks/${after.task}.md`]),
`docs/status/${before.task}.md`,
`docs/status/${after.task}.md`,
...(terminal ? [] : [`docs/status/${after.task}.md`]),
manifestPath,
...artifactPaths,
"tests/golden/WBV2/task-index.json",
"tests/golden/WBV2/task-catalog.jsonl",
].filter((file, index, all) => file && all.indexOf(file) === index && fs.existsSync(abs(file)));
const commit = command(`git add ${handoffPaths.map(shellQuote).join(" ")}`, `tests/golden/${before.task}/continuous-commit.log`);
// Every path named by the manifest is part of the auditable handoff. Some
// task logs are intentionally ignored by the repository defaults, so use
// force-add for this exact, manifest-derived list instead of failing after
// the task has already produced a valid receipt.
const commit = command(`git add -f ${handoffPaths.map(shellQuote).join(" ")}`, `tests/golden/${before.task}/continuous-commit.log`);
if (commit.code !== 0) throw new Error(`${before.task} staging failed; log=tests/golden/${before.task}/continuous-commit.log`);
const commitResult = command(`git diff --cached --quiet || git commit -m "chore(web): continuous handoff ${before.task}"`, `tests/golden/${before.task}/continuous-commit-result.log`);
if (commitResult.code !== 0) throw new Error(`${before.task} commit failed; log=tests/golden/${before.task}/continuous-commit-result.log`);
@@ -107,5 +132,6 @@ while (maxIterations === 0 || iterations < maxIterations) {
if (!pushed) throw new Error(`${before.task} remote push verification failed after ${pushRetries + 1} attempts; log=tests/golden/${before.task}/continuous-push-verify.log`);
}
}
if (terminal) break;
}
process.stdout.write(`continuous-run-complete iterations=${iterations} current=${currentTask().task} committed=${commitEnabled} pushed=${pushEnabled}\n`);

Binary file not shown.

View File

@@ -0,0 +1,78 @@
import crypto from "node:crypto";
import fs from "node:fs";
import path from "node:path";
import { expect, test } from "@playwright/test";
const root = path.resolve(import.meta.dirname, "../../..");
const fixturePath = path.join(root, "tests/files/web/modifier_corrective_smooth_scene.blend");
const fixture = fs.readFileSync(fixturePath);
const fixtureSha256 = crypto.createHash("sha256").update(fixture).digest("hex");
test("Corrective Smooth modifier metadata survives Blender Main save/reopen", async ({ page }) => {
test.setTimeout(180_000);
await page.goto("/");
const result = await page.evaluate(async (bytes) => {
const { WebEngineClient } = await import("/src/engine-client/WebEngineClient.ts");
const canonical = (snapshot: any) => snapshot.meshes.flatMap((mesh: any) =>
(mesh.modifierStack ?? []).map((modifier: any) => ({
meshId: mesh.id,
uuid: modifier.uuid,
name: modifier.name,
type: modifier.type,
enabled: modifier.enabled,
evaluationStatus: modifier.evaluationStatus,
parameters: modifier.parameters ?? null,
})));
const client = new WebEngineClient({ timeoutMs: 60_000 });
const reopenedClient = new WebEngineClient({ timeoutMs: 60_000 });
const deterministicClient = new WebEngineClient({ timeoutMs: 60_000 });
const output: any = {};
try {
const opened = await client.openBlend(Uint8Array.from(bytes).buffer);
output.before = canonical(opened.snapshot);
output.target = output.before.find((modifier: any) => modifier.type === "CORRECTIVE_SMOOTH");
const saved = await client.saveBlend();
const reopened = await reopenedClient.openBlend(saved.slice(0));
output.after = canonical(reopened.snapshot);
const deterministic = await deterministicClient.openBlend(Uint8Array.from(bytes).buffer);
output.deterministic = canonical(deterministic.snapshot);
output.saveReopenExact = JSON.stringify(output.before) === JSON.stringify(output.after);
output.deterministicExact = JSON.stringify(output.before) === JSON.stringify(output.deterministic);
output.savedSha256 = await crypto.subtle.digest("SHA-256", saved).then((hash) =>
Array.from(new Uint8Array(hash), (byte) => byte.toString(16).padStart(2, "0")).join(""));
try {
await client.openBlend(Uint8Array.from([0x42, 0x4c, 0x45, 0x4e, 0x44]).buffer);
output.malformed = { code: "ACCEPTED_MALFORMED" };
} catch (error: any) {
output.malformed = { code: error?.code, message: error?.message };
}
} catch (error: any) {
output.error = { code: error?.code, message: error?.message, detail: String(error) };
} finally {
client.terminate();
reopenedClient.terminate();
deterministicClient.terminate();
}
return output;
}, Array.from(fixture));
const report = {
schemaVersion: 1,
task: "WBV2-P1145-001",
operation: "CORRECTIVE_SMOOTH_READER_CHROMIUM_SAVE_REOPEN",
browser: "chromium",
queueMutation: false,
fixture: "tests/files/web/modifier_corrective_smooth_scene.blend",
fixtureSha256,
sourceUnits: ["modifier-source:MOD_correctivesmooth", "rna-struct:CorrectiveSmoothModifier"],
...result,
};
fs.mkdirSync(path.join(root, "tests/golden/WBV2-P1145-001"), { recursive: true });
fs.writeFileSync(path.join(root, "tests/golden/WBV2-P1145-001/corrective-smooth-production.json"), `${JSON.stringify(report, null, 2)}\n`);
expect(result.error).toBeUndefined();
expect(result.target?.type).toBe("CORRECTIVE_SMOOTH");
expect(result.saveReopenExact).toBe(true);
expect(result.deterministicExact).toBe(true);
expect(result.malformed?.code).not.toBe("ACCEPTED_MALFORMED");
});