feat(web): authorize corrective smooth reader
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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`,不得推进指针。
|
||||
|
||||
## 交接
|
||||
|
||||
13
docs/status/WBV2-DYNAMIC-INVENTORY.md
Normal file
13
docs/status/WBV2-DYNAMIC-INVENTORY.md
Normal 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`
|
||||
9
docs/status/WBV2-P1144-001.md
Normal file
9
docs/status/WBV2-P1144-001.md
Normal 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`
|
||||
7
docs/status/WBV2-P1145-000.md
Normal file
7
docs/status/WBV2-P1145-000.md
Normal 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`
|
||||
8
docs/status/WBV2-P1145-001.md
Normal file
8
docs/status/WBV2-P1145-001.md
Normal 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`
|
||||
41
docs/tasks/WBV2-P1145-001.md
Normal file
41
docs/tasks/WBV2-P1145-001.md
Normal 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.
|
||||
52
my-tools/BLENDER_NEXTTASK_REFERENCE.md
Normal file
52
my-tools/BLENDER_NEXTTASK_REFERENCE.md
Normal 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.
|
||||
57
my-tools/BLENDER_UNATTENDED_SCHEDULER_REFERENCE.md
Normal file
57
my-tools/BLENDER_UNATTENDED_SCHEDULER_REFERENCE.md
Normal 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.
|
||||
36
my-tools/BLENDER_WEB_CONTEXT_PROTOCOL_REFERENCE.md
Normal file
36
my-tools/BLENDER_WEB_CONTEXT_PROTOCOL_REFERENCE.md
Normal 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.
|
||||
@@ -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。
|
||||
36
my-tools/BLENDER_WEB_TASK_SYSTEM_REFERENCE.md
Normal file
36
my-tools/BLENDER_WEB_TASK_SYSTEM_REFERENCE.md
Normal 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
|
||||
```
|
||||
21
my-tools/CONTINUATION_PROTOCOL_TEMPLATE.md
Normal file
21
my-tools/CONTINUATION_PROTOCOL_TEMPLATE.md
Normal 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
64
my-tools/README.md
Normal 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`.
|
||||
257
my-tools/authorize_next_wbv2_task.py
Normal file
257
my-tools/authorize_next_wbv2_task.py
Normal 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())
|
||||
44
my-tools/check_continuation_state.py
Executable file
44
my-tools/check_continuation_state.py
Executable 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())
|
||||
23
my-tools/compact_continuation_docs.py
Executable file
23
my-tools/compact_continuation_docs.py
Executable 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())
|
||||
42
my-tools/continue_web_task_cycle.py
Executable file
42
my-tools/continue_web_task_cycle.py
Executable 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())
|
||||
80
my-tools/prepare_full_parity_module_batch.py
Executable file
80
my-tools/prepare_full_parity_module_batch.py
Executable 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())
|
||||
53
my-tools/prepare_web_task_context.py
Executable file
53
my-tools/prepare_web_task_context.py
Executable 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())
|
||||
36
my-tools/repair_continuation_checkpoint.py
Executable file
36
my-tools/repair_continuation_checkpoint.py
Executable 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())
|
||||
36
my-tools/rotate_web_task_context.py
Executable file
36
my-tools/rotate_web_task_context.py
Executable 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())
|
||||
96
my-tools/run_blender_web_scheduler.py
Executable file
96
my-tools/run_blender_web_scheduler.py
Executable 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())
|
||||
64
my-tools/show_blender_web_scheduler.py
Executable file
64
my-tools/show_blender_web_scheduler.py
Executable 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
56
my-tools/start_unattended.sh
Executable 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
|
||||
39
my-tools/validate_web_context.py
Executable file
39
my-tools/validate_web_context.py
Executable 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())
|
||||
208
my-tools/watch_blender_web_scheduler.py
Executable file
208
my-tools/watch_blender_web_scheduler.py
Executable 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
104
my-tools/wbv2_runtime.py
Executable 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
171
my-tools/web_context.py
Executable 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
|
||||
125
nextTask.md
125
nextTask.md
@@ -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 --`.
|
||||
|
||||
BIN
tests/files/web/modifier_corrective_smooth_scene.blend
Normal file
BIN
tests/files/web/modifier_corrective_smooth_scene.blend
Normal file
Binary file not shown.
42
tests/golden/WBV2-P1145-000/manifest.json
Normal file
42
tests/golden/WBV2-P1145-000/manifest.json
Normal 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"
|
||||
}
|
||||
5518
tests/golden/WBV2-P1145-000/module-execution-plan.json
Normal file
5518
tests/golden/WBV2-P1145-000/module-execution-plan.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
}
|
||||
184
tests/golden/WBV2-P1145-000/task-context.json
Normal file
184
tests/golden/WBV2-P1145-000/task-context.json
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
87
tests/golden/WBV2-P1145-001/manifest.json
Normal file
87
tests/golden/WBV2-P1145-001/manifest.json
Normal 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
|
||||
}
|
||||
113
tests/golden/WBV2-P1145-001/module-receipt.json
Normal file
113
tests/golden/WBV2-P1145-001/module-receipt.json
Normal 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
|
||||
}
|
||||
49
tests/golden/WBV2-P1145-001/module-run.log
Normal file
49
tests/golden/WBV2-P1145-001/module-run.log
Normal 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...
|
||||
[2K
|
||||
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.
|
||||
183
tests/golden/WBV2-P1145-001/task-context.json
Normal file
183
tests/golden/WBV2-P1145-001/task-context.json
Normal 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
|
||||
}
|
||||
}
|
||||
7217
tests/golden/WBV2/dynamic-coverage-audit.json
Normal file
7217
tests/golden/WBV2/dynamic-coverage-audit.json
Normal file
File diff suppressed because it is too large
Load Diff
7002
tests/golden/WBV2/dynamic-coverage-bindings.json
Normal file
7002
tests/golden/WBV2/dynamic-coverage-bindings.json
Normal file
File diff suppressed because it is too large
Load Diff
42
tests/golden/WBV2/dynamic-coverage-manifest.json
Normal file
42
tests/golden/WBV2/dynamic-coverage-manifest.json
Normal 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
|
||||
}
|
||||
}
|
||||
30
tests/golden/WBV2/dynamic-inventory-check-report.json
Normal file
30
tests/golden/WBV2/dynamic-inventory-check-report.json
Normal 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
|
||||
}
|
||||
59
tests/golden/WBV2/dynamic-inventory-manifest.json
Normal file
59
tests/golden/WBV2/dynamic-inventory-manifest.json
Normal 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
|
||||
}
|
||||
}
|
||||
104303
tests/golden/WBV2/dynamic-inventory.json
Normal file
104303
tests/golden/WBV2/dynamic-inventory.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -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`);
|
||||
|
||||
BIN
web/app/src/vendor/blender/web_engine.wasm
vendored
BIN
web/app/src/vendor/blender/web_engine.wasm
vendored
Binary file not shown.
@@ -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");
|
||||
});
|
||||
Reference in New Issue
Block a user