Advance Blender 5.2 web parity through M12-03D
This commit is contained in:
@@ -31,8 +31,9 @@ npm --prefix web run test:browser
|
||||
|
||||
See `docs/CURRENT_EXECUTION_PLAN.md` for the current task order,
|
||||
`docs/PROJECT_STATUS_AND_NEXT_WORK.md` for the implemented scope, and
|
||||
`docs/BLENDER_5_2_WEB_FEATURE_PARITY.md` for the long-term Blender parity ledger.
|
||||
The atomic full-product work breakdown is in
|
||||
`docs/BLENDER_5_2_FULL_WEB_PARITY_EXECUTION_PLAN.md` for the normative M12-M23
|
||||
full-parity execution plan. The domain overview and coverage cross-check are in
|
||||
`docs/BLENDER_5_2_WEB_FEATURE_PARITY.md` and
|
||||
`docs/BLENDER_5_2_FULL_PARITY_WBS.md`.
|
||||
|
||||
The release contract and explicit non-goals are defined in
|
||||
|
||||
@@ -157,7 +157,7 @@ if(WITH_WEB)
|
||||
# runtime only needs model/animation RNA needed by depsgraph evaluation.
|
||||
# Trim after generation so the generator itself remains source-compatible.
|
||||
list(FILTER GENSRC INCLUDE REGEX
|
||||
"rna_(ID|action|animation|armature|collection|constraint|curve|depsgraph|fcurve|grease_pencil|image|key|lattice|layer|main|material|mesh|object|packedfile|pose|scene|world)_gen\\.cc$")
|
||||
"rna_(ID|action|animation|armature|blendfile_import|collection|constraint|curve|depsgraph|fcurve|grease_pencil|image|key|lattice|layer|main|material|mesh|object|packedfile|pose|scene|world)_gen\\.cc$")
|
||||
list(APPEND GENSRC
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/rna_prototypes_gen.hh"
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/../RNA_prototypes.hh"
|
||||
|
||||
@@ -129,7 +129,7 @@ target_link_options(web_engine PRIVATE
|
||||
"-sINITIAL_MEMORY=67108864"
|
||||
"-sMAXIMUM_MEMORY=2147483648"
|
||||
"-sSTACK_SIZE=8388608"
|
||||
"-sEXPORTED_FUNCTIONS=['_malloc','_free','_web_engine_create','_web_engine_destroy','_web_engine_get_memory_stats','_web_engine_get_live_handles','_web_engine_get_allocated_bytes','_web_engine_open_blend','_web_engine_apply_command','_web_engine_undo','_web_engine_redo','_web_engine_get_scene_snapshot','_web_engine_get_scene_metadata','_web_engine_get_scene_geometry','_web_engine_get_scene_delta','_web_engine_get_packed_asset','_web_engine_evaluate_depsgraph','_web_engine_save_blend','_web_engine_free_buffer','_web_engine_last_error_code','_web_engine_last_error_message','_web_engine_decimate_apply']"
|
||||
"-sEXPORTED_FUNCTIONS=['_malloc','_free','_web_engine_create','_web_engine_destroy','_web_engine_get_memory_stats','_web_engine_get_live_handles','_web_engine_get_allocated_bytes','_web_engine_open_blend','_web_engine_apply_command','_web_engine_append_library_object','_web_engine_undo','_web_engine_redo','_web_engine_get_scene_snapshot','_web_engine_get_scene_metadata','_web_engine_get_scene_geometry','_web_engine_get_scene_delta','_web_engine_get_packed_asset','_web_engine_evaluate_depsgraph','_web_engine_save_blend','_web_engine_free_buffer','_web_engine_last_error_code','_web_engine_last_error_message','_web_engine_decimate_apply']"
|
||||
"-sEXPORTED_RUNTIME_METHODS=['ccall','cwrap','UTF8ToString']"
|
||||
)
|
||||
if(WEB_ENGINE_ENABLE_UNSAFE_DEPSGRAPH)
|
||||
|
||||
@@ -2145,6 +2145,139 @@ EMSCRIPTEN_KEEPALIVE int web_engine_apply_command(const int handle,
|
||||
}
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE int web_engine_append_library_object(const int handle,
|
||||
const uint8_t *source_data,
|
||||
const uint32_t source_length,
|
||||
const uint8_t *metadata,
|
||||
const uint32_t metadata_length)
|
||||
{
|
||||
EngineState *engine = find_engine(handle);
|
||||
if (engine == nullptr) return last_error_code;
|
||||
if (source_data == nullptr || source_length == 0 ||
|
||||
source_length > 64u * 1024u * 1024u || metadata == nullptr || metadata_length == 0)
|
||||
{
|
||||
set_error(WEB_ENGINE_INVALID_ARGUMENT, "ASSET_BUDGET_EXCEEDED: library append input is empty or exceeds 64 MiB");
|
||||
return last_error_code;
|
||||
}
|
||||
if (!engine->authoritative_commands || engine->authoritative_main == nullptr) {
|
||||
set_error(WEB_ENGINE_NOT_IMPLEMENTED,
|
||||
"COMMAND_REQUIRES_MAIN_AUTHORITY: library append requires an authoritative Main");
|
||||
return last_error_code;
|
||||
}
|
||||
try {
|
||||
const json request = json::parse(reinterpret_cast<const char *>(metadata),
|
||||
reinterpret_cast<const char *>(metadata) + metadata_length);
|
||||
if (!request.is_object() || request.size() != 5 || request.value("schemaVersion", 0) != 1 ||
|
||||
!request.contains("baseRevision") || !request["baseRevision"].is_number_unsigned() ||
|
||||
!request.contains("sourceLocator") || !request["sourceLocator"].is_string() ||
|
||||
!request.contains("sourceDataBlockId") || !request["sourceDataBlockId"].is_string() ||
|
||||
!request.contains("expectedClosure") || !request["expectedClosure"].is_object())
|
||||
{
|
||||
set_error(WEB_ENGINE_INVALID_ARGUMENT,
|
||||
"ASSET_MANIFEST_INVALID: native library append metadata is malformed");
|
||||
return last_error_code;
|
||||
}
|
||||
for (const char *key : {"schemaVersion", "baseRevision", "sourceLocator", "sourceDataBlockId", "expectedClosure"}) {
|
||||
if (!request.contains(key)) {
|
||||
set_error(WEB_ENGINE_INVALID_ARGUMENT,
|
||||
"ASSET_MANIFEST_INVALID: native library append metadata fields are not exact");
|
||||
return last_error_code;
|
||||
}
|
||||
}
|
||||
const uint64_t base_revision = request["baseRevision"].get<uint64_t>();
|
||||
if (base_revision != engine->revision) {
|
||||
set_error(WEB_ENGINE_INVALID_ARGUMENT,
|
||||
"REVISION_CONFLICT: library append base revision does not match the current SceneIR");
|
||||
return last_error_code;
|
||||
}
|
||||
const json &closure = request["expectedClosure"];
|
||||
if (closure.size() != 4) {
|
||||
set_error(WEB_ENGINE_INVALID_ARGUMENT, "ASSET_MANIFEST_INVALID: append closure fields are not exact");
|
||||
return last_error_code;
|
||||
}
|
||||
auto closure_name = [&](const char *field, const char *prefix) -> std::string {
|
||||
if (!closure.contains(field) || !closure[field].is_string()) return {};
|
||||
const std::string value = closure[field].get<std::string>();
|
||||
const size_t prefix_length = strlen(prefix);
|
||||
if (value.size() <= prefix_length || value.compare(0, prefix_length, prefix) != 0) return {};
|
||||
return value.substr(prefix_length);
|
||||
};
|
||||
const std::string object_name = closure_name("object", "Object/");
|
||||
const std::string mesh_name = closure_name("mesh", "Mesh/");
|
||||
const std::string material_name = closure_name("material", "Material/");
|
||||
const std::string image_name = closure_name("image", "Image/");
|
||||
if (object_name.empty() || mesh_name.empty() || material_name.empty() || image_name.empty() ||
|
||||
request["sourceDataBlockId"].get<std::string>() != "Object/" + object_name)
|
||||
{
|
||||
set_error(WEB_ENGINE_INVALID_ARGUMENT,
|
||||
"ASSET_SOURCE_HASH_MISMATCH: native append root does not match the expected closure");
|
||||
return last_error_code;
|
||||
}
|
||||
|
||||
HistoryEntry previous_state;
|
||||
std::string history_error;
|
||||
if (!capture_history_entry(*engine, previous_state, history_error)) {
|
||||
set_error(WEB_ENGINE_BLEND_WRITE_FAILED, history_error.c_str());
|
||||
return last_error_code;
|
||||
}
|
||||
const uint64_t previous_revision = engine->revision;
|
||||
const std::string previous_snapshot = engine->scene_snapshot.empty() ?
|
||||
std::string(empty_scene_snapshot_json()) :
|
||||
engine->scene_snapshot;
|
||||
const std::string previous_delta = engine->scene_delta;
|
||||
const auto previous_packed_assets = engine->packed_assets;
|
||||
auto rollback = [&]() {
|
||||
std::string rollback_error;
|
||||
if (restore_history_entry(*engine, previous_state, rollback_error)) {
|
||||
engine->revision = previous_revision;
|
||||
engine->scene_snapshot = previous_snapshot;
|
||||
engine->scene_delta = previous_delta;
|
||||
engine->packed_assets = previous_packed_assets;
|
||||
engine->binary_revision = std::numeric_limits<uint64_t>::max();
|
||||
}
|
||||
};
|
||||
|
||||
std::string object_id;
|
||||
std::string append_error;
|
||||
if (!web_engine_blend_main_append_object(engine->authoritative_main,
|
||||
source_data,
|
||||
source_length,
|
||||
request["sourceLocator"].get_ref<const std::string &>().c_str(),
|
||||
object_name.c_str(),
|
||||
mesh_name.c_str(),
|
||||
material_name.c_str(),
|
||||
image_name.c_str(),
|
||||
object_id,
|
||||
append_error))
|
||||
{
|
||||
rollback();
|
||||
set_error(append_error.rfind("BLEND_READ_FAILED:", 0) == 0 ?
|
||||
WEB_ENGINE_BLEND_READ_FAILED : WEB_ENGINE_INVALID_ARGUMENT,
|
||||
append_error.c_str());
|
||||
return last_error_code;
|
||||
}
|
||||
json snapshot;
|
||||
if (!refresh_scene_from_main(*engine, snapshot, object_id, append_error)) {
|
||||
rollback();
|
||||
set_error(WEB_ENGINE_BLEND_WRITE_FAILED, append_error.c_str());
|
||||
return last_error_code;
|
||||
}
|
||||
|
||||
engine->authoritative_dirty = true;
|
||||
push_history(engine->undo_history, std::move(previous_state));
|
||||
engine->redo_history.clear();
|
||||
engine->scene_snapshot = snapshot.dump();
|
||||
engine->revision++;
|
||||
engine->scene_delta = scene_delta_json(previous_snapshot, engine->scene_snapshot);
|
||||
set_error(WEB_ENGINE_OK, "ok");
|
||||
return WEB_ENGINE_OK;
|
||||
}
|
||||
catch (const std::exception &exception) {
|
||||
set_error(WEB_ENGINE_INVALID_ARGUMENT, exception.what());
|
||||
return last_error_code;
|
||||
}
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE int web_engine_undo(const int handle)
|
||||
{
|
||||
EngineState *engine = find_engine(handle);
|
||||
|
||||
@@ -28,6 +28,11 @@ uint32_t web_engine_get_live_handles(void);
|
||||
uint32_t web_engine_get_allocated_bytes(void);
|
||||
int web_engine_open_blend(int handle, const uint8_t *data, uint32_t length);
|
||||
int web_engine_apply_command(int handle, const uint8_t *data, uint32_t length);
|
||||
int web_engine_append_library_object(int handle,
|
||||
const uint8_t *source_data,
|
||||
uint32_t source_length,
|
||||
const uint8_t *metadata,
|
||||
uint32_t metadata_length);
|
||||
int web_engine_undo(int handle);
|
||||
int web_engine_redo(int handle);
|
||||
int web_engine_get_scene_snapshot(int handle, const uint8_t **data, uint32_t *length);
|
||||
|
||||
@@ -72,6 +72,13 @@ static void register_web_geometry_node_types()
|
||||
|
||||
}
|
||||
|
||||
namespace seq {
|
||||
|
||||
/* The browser engine does not load desktop sequencer playback state. */
|
||||
void doversion_250_sound_proxy_update(Main * /*bmain*/, Editing * /*editing*/) {}
|
||||
|
||||
} // namespace seq
|
||||
|
||||
void WEB_headless_node_system_init()
|
||||
{
|
||||
static bool initialized = false;
|
||||
@@ -411,4 +418,5 @@ const ComputeContext *compute_context_for_viewer_path_elem(
|
||||
|
||||
namespace blender {
|
||||
void WM_main_add_notifier(unsigned int /*type*/, void * /*reference*/) {}
|
||||
void WM_msg_publish_rna(wmMsgBus * /*mbus*/, PointerRNA * /*ptr*/, PropertyRNA * /*prop*/) {}
|
||||
} // namespace blender
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
#include "BKE_anim_data.hh"
|
||||
#include "BKE_attribute.hh"
|
||||
#include "BKE_attribute.h"
|
||||
#include "BKE_blendfile_link_append.hh"
|
||||
#include "BKE_layer.hh"
|
||||
#include "BKE_collection.hh"
|
||||
#include "BKE_curve.hh"
|
||||
@@ -1120,6 +1121,117 @@ bool web_engine_blend_main_create_primitive(WebBlendMainState *state,
|
||||
return true;
|
||||
}
|
||||
|
||||
bool web_engine_blend_main_append_object(WebBlendMainState *state,
|
||||
const uint8_t *source_data,
|
||||
const uint32_t source_length,
|
||||
const char *source_locator,
|
||||
const char *object_name,
|
||||
const char *mesh_name,
|
||||
const char *material_name,
|
||||
const char *image_name,
|
||||
std::string &object_id,
|
||||
std::string &error)
|
||||
{
|
||||
if (state == nullptr || state->main == nullptr || state->scene == nullptr ||
|
||||
state->view_layer == nullptr || source_data == nullptr || source_length == 0 ||
|
||||
source_length > 64u * 1024u * 1024u || source_locator == nullptr ||
|
||||
source_locator[0] == '\0' || object_name == nullptr || mesh_name == nullptr ||
|
||||
material_name == nullptr || image_name == nullptr || object_name[0] == '\0' ||
|
||||
mesh_name[0] == '\0' || material_name[0] == '\0' || image_name[0] == '\0')
|
||||
{
|
||||
error = "ASSET_MANIFEST_INVALID: append requires a bounded source and complete Object closure";
|
||||
return false;
|
||||
}
|
||||
if (find_object(state->main, ("object:" + std::string(object_name)).c_str()) != nullptr ||
|
||||
find_material(state->main, ("material:" + std::string(material_name)).c_str()) != nullptr ||
|
||||
find_image(state->main, ("image:" + std::string(image_name)).c_str()) != nullptr)
|
||||
{
|
||||
error = "ASSET_MANIFEST_INVALID: append closure collides with an existing local ID";
|
||||
return false;
|
||||
}
|
||||
for (const Mesh &mesh : state->main->meshes) {
|
||||
if (id_name(mesh.id) == mesh_name) {
|
||||
error = "ASSET_MANIFEST_INVALID: append closure collides with an existing local Mesh";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
BlendHandle *blend_handle = BLO_blendhandle_from_memory(
|
||||
source_data, int(source_length), nullptr);
|
||||
if (blend_handle == nullptr) {
|
||||
error = "BLEND_READ_FAILED: Blender rejected the append source buffer";
|
||||
return false;
|
||||
}
|
||||
|
||||
GlobalMainScope global_main_scope(state->main);
|
||||
BKE_main_id_tag_all(state->main, ID_TAG_PRE_EXISTING, true);
|
||||
LibraryLink_Params parameters{};
|
||||
BLO_library_link_params_init_with_context(¶meters,
|
||||
state->main,
|
||||
BLO_LIBLINK_APPEND_RECURSIVE,
|
||||
0,
|
||||
state->scene,
|
||||
state->view_layer,
|
||||
nullptr);
|
||||
BlendfileLinkAppendContext *context = BKE_blendfile_link_append_context_new(¶meters);
|
||||
if (context == nullptr) {
|
||||
BLO_blendhandle_close(blend_handle);
|
||||
BKE_main_id_tag_all(state->main, ID_TAG_PRE_EXISTING, false);
|
||||
error = "BLEND_READ_FAILED: Blender could not create the append context";
|
||||
return false;
|
||||
}
|
||||
BKE_blendfile_link_append_context_library_add(context, source_locator, blend_handle);
|
||||
BlendfileLinkAppendContextItem *item = BKE_blendfile_link_append_context_item_add(
|
||||
context, object_name, ID_OB, nullptr);
|
||||
BKE_blendfile_link_append_context_item_library_index_enable(context, item, 0);
|
||||
BKE_blendfile_link_append_context_init_done(context);
|
||||
BKE_blendfile_link(context, nullptr);
|
||||
BKE_blendfile_append(context, nullptr);
|
||||
BKE_blendfile_link_append_instantiate_loose(context, nullptr);
|
||||
BKE_blendfile_link_append_context_finalize(context);
|
||||
ID *new_id = BKE_blendfile_link_append_context_item_newid_get(context, item);
|
||||
BKE_blendfile_link_append_context_free(context);
|
||||
BKE_main_id_tag_all(state->main, ID_TAG_PRE_EXISTING, false);
|
||||
|
||||
Object *object = new_id != nullptr && GS(new_id->name) == ID_OB ?
|
||||
reinterpret_cast<Object *>(new_id) :
|
||||
nullptr;
|
||||
Mesh *mesh = object != nullptr && object->type == OB_MESH && object->data != nullptr ?
|
||||
reinterpret_cast<Mesh *>(object->data) :
|
||||
nullptr;
|
||||
Material *material = mesh != nullptr && mesh->totcol == 1 && mesh->mat != nullptr ?
|
||||
mesh->mat[0] :
|
||||
nullptr;
|
||||
Image *image = find_image(state->main, ("image:" + std::string(image_name)).c_str());
|
||||
bool material_uses_image = false;
|
||||
if (material != nullptr && material->nodetree != nullptr && image != nullptr) {
|
||||
for (const bNode &node : material->nodetree->nodes) {
|
||||
if (node.id == &image->id) {
|
||||
material_uses_image = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (object == nullptr || mesh == nullptr || material == nullptr || image == nullptr ||
|
||||
id_name(object->id) != object_name || id_name(mesh->id) != mesh_name ||
|
||||
id_name(material->id) != material_name || id_name(image->id) != image_name ||
|
||||
object->id.lib != nullptr || mesh->id.lib != nullptr || material->id.lib != nullptr ||
|
||||
image->id.lib != nullptr || object->id.override_library != nullptr ||
|
||||
mesh->id.override_library != nullptr || material->id.override_library != nullptr ||
|
||||
image->id.override_library != nullptr || !material_uses_image)
|
||||
{
|
||||
error = "ASSET_SOURCE_HASH_MISMATCH: appended Object closure is incomplete or not fully local";
|
||||
return false;
|
||||
}
|
||||
BKE_view_layer_synced_ensure(*state->main, state->scene, state->view_layer);
|
||||
object->id.recalc |= ID_RECALC_TRANSFORM | ID_RECALC_GEOMETRY;
|
||||
mesh->id.recalc |= ID_RECALC_GEOMETRY;
|
||||
material->id.recalc |= ID_RECALC_SYNC_TO_EVAL;
|
||||
image->id.recalc |= ID_RECALC_SOURCE;
|
||||
object_id = "object:" + id_name(object->id);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool web_engine_blend_main_create_curve(WebBlendMainState *state,
|
||||
const char *curve_type,
|
||||
const char *name,
|
||||
|
||||
@@ -128,6 +128,16 @@ bool web_engine_blend_main_create_primitive(WebBlendMainState *state,
|
||||
const std::vector<float> &location,
|
||||
std::string &object_id,
|
||||
std::string &error);
|
||||
bool web_engine_blend_main_append_object(WebBlendMainState *state,
|
||||
const uint8_t *source_data,
|
||||
uint32_t source_length,
|
||||
const char *source_locator,
|
||||
const char *object_name,
|
||||
const char *mesh_name,
|
||||
const char *material_name,
|
||||
const char *image_name,
|
||||
std::string &object_id,
|
||||
std::string &error);
|
||||
bool web_engine_blend_main_duplicate_object(WebBlendMainState *state,
|
||||
const char *object_id,
|
||||
const std::vector<float> &offset,
|
||||
|
||||
@@ -2,19 +2,23 @@
|
||||
|
||||
更新时间:2026-08-15
|
||||
|
||||
> 本文保留为 Blender 全产品覆盖检查表和 F00-F24 taxonomy 参考。M12-M23 的执行分类、
|
||||
> 原子任务、证据合同与发布门以 `BLENDER_5_2_FULL_WEB_PARITY_EXECUTION_PLAN.md` 为准;
|
||||
> 本文的 F 任务不能直接作为当前 `nextTask`,也不能独立改变机器状态。
|
||||
|
||||
## 1. 目标与事实边界
|
||||
|
||||
本文把“对标 Blender 全部功能”定义为:以仓库内 `blender-5.2.0` 的 RNA、operator、
|
||||
data-block、modifier、node、editor、文件格式和运行时后端为冻结基线,逐项给出
|
||||
`LOCAL_EXACT`、`LOCAL_BOUNDED`、`SERVER` 或 `EXCLUDED` 结论,并为结论保存可复验的正例、
|
||||
阻断例和数据保真证据。
|
||||
本文以仓库内 `blender-5.2.0` 的 RNA、operator、data-block、modifier、node、editor、
|
||||
文件格式和运行时后端为覆盖基线。本文原有 `LOCAL_EXACT/LOCAL_BOUNDED/SERVER/EXCLUDED` 是
|
||||
V1 盘点术语;完整对标结论必须按新计划转换为 `LOCAL_EXACT/LOCAL_EQUIVALENT/SERVER_EXACT`
|
||||
和独立 `parityStatus`,并保存可复验的正例、阻断例和数据保真证据。
|
||||
|
||||
当前 `Web Blender Modeler V1` 已达到其有界发布范围,但不等于完整 Blender。机器账本中
|
||||
N-015 至 N-026 的 12 个 family 全部仍为 `parityStatus=BLOCKED`。因此以下任务默认均为未完成;
|
||||
只有 `docs/status/parity-ledger.json` 和对应专项证据同时更新后才允许改变状态。
|
||||
|
||||
短周期唯一领取顺序仍以 `docs/CURRENT_EXECUTION_PLAN.md` 为准。本文负责完整产品范围和细粒度
|
||||
工作包,不取代当前执行队列。
|
||||
短周期唯一领取顺序仍以 `docs/CURRENT_EXECUTION_PLAN.md` 为准。本文负责验证新计划没有遗漏
|
||||
产品域;与新计划冲突时按新计划解释,不取代当前执行队列或长期实施事实源。
|
||||
|
||||
## 2. 原子任务完成定义
|
||||
|
||||
@@ -481,7 +485,8 @@ N-015 至 N-026 的 12 个 family 全部仍为 `parityStatus=BLOCKED`。因此
|
||||
- [ ] `F23-09` 完成 user preferences schema、migration 和项目无关 persistence。
|
||||
- [ ] `F23-10` 完成 locale 切换、数字/日期格式和翻译 fallback。
|
||||
- [ ] `F23-11` 完成 theme、font scale、reduced motion 和高对比偏好。
|
||||
- [ ] `F23-12` native window/file watcher/CUDA/Metal/HIP/OptiX 明确 EXCLUDED。
|
||||
- [ ] `F23-12` native window/file watcher/CUDA/Metal/HIP/OptiX 实现细节明确不复制,其用户能力
|
||||
按新计划映射到 platform-equivalent 或 server exact 路径。
|
||||
- [ ] `F23-13` 每个新增浏览器加入 quick/P0/full 三层 CI。
|
||||
- [ ] `F23-14` 1280x720、1440x900、4K 和窄屏无重叠截图门。
|
||||
|
||||
@@ -501,18 +506,22 @@ N-015 至 N-026 的 12 个 family 全部仍为 `parityStatus=BLOCKED`。因此
|
||||
- [ ] `F24-12` CI 报告绑定 commit、lockfile、inventory、ledger、engine、archive hash。
|
||||
- [ ] `F24-13` binary/source archive 在两个空临时目录独立复验。
|
||||
- [ ] `F24-14` 完成 SBOM、license、source offer 和供应链审计。
|
||||
- [ ] `F24-15` 每个 parity ID 都有 LOCAL 正例、SERVER 闭环或 EXCLUDED 保真证据。
|
||||
- [ ] `F24-15` 每个 parity ID 都有 LOCAL_EXACT、LOCAL_EQUIVALENT 或 SERVER_EXACT 完整证据。
|
||||
- [ ] `F24-16` inventory 未分类、隐式 loss、proxy-success 三项计数均为 0。
|
||||
- [ ] `F24-17` 独立机器复验全部构件和关键证据 hash。
|
||||
- [ ] `F24-18` 发布说明逐项列出支持、受限、server 和 excluded 能力。
|
||||
- [ ] `F24-18` 发布说明逐项列出 local exact、platform-equivalent 和 server exact 能力。
|
||||
|
||||
里程碑 F24:不能由聚合 release gate 反推 family 完成。只有 inventory 的每个单项都达到自身
|
||||
退出条件,N-015 至 N-026 才能逐项从 `BLOCKED` 改为 `COMPLETE`。
|
||||
退出条件,全部生成的 owner family 才能逐项从 `BLOCKED` 改为 `COMPLETE`。
|
||||
|
||||
## 29. 当前领取点
|
||||
|
||||
截至 2026-08-15,M6 已完成 35/71:single/pthread 选择与回退、真实 HTTP MIME/range、
|
||||
releaseId/hash 升级负例和缓存合同均有专项证据。HTML、manifest、stable engine 未变化时均返回
|
||||
`304`,换代后均返回 `200` 且 ETag 变化;production build 的 1 个入口 JS、1 个 CSS、3 个
|
||||
Worker 均使用 8 位内容 hash 文件名并返回 `immutable`。当前唯一下一任务为 `M6-09A`:从
|
||||
binary archive 解包后使用合同服务器启动,不读取工作区文件。
|
||||
截至 2026-08-17,M6-M11 已按当前计划完成,M11 最终以 Render、Compositor、Media 三域故障
|
||||
生命周期专项门收口,M12-01A-I 已完成 catalog schema/migration;M12-02A-H 又完成 PreviewImage
|
||||
inventory、identity、decode budget、OPFS commit、dedupe、quarantine、project-scoped reference GC
|
||||
与 desktop/browser 双显示路径像素闭环。本文的 F00-F24
|
||||
条目只作为覆盖检查表,不能直接领取;M12-03A 又冻结 Append/Link/Override 的 36 类 root 与
|
||||
实际 ID pointer dependency closure;M12-03B 固定 source/owner/read-only/invalidation identity;
|
||||
M12-03C 冻结 Blender 5.2 单 Object append 的 Object/Mesh/Material/Image local stable mapping;
|
||||
M12-03D 已通过 WASM Main 单 transaction 创建同一 local dependency closure。当前唯一下一任务为
|
||||
`BLENDER_5_2_FULL_WEB_PARITY_EXECUTION_PLAN.md` 中的 `M12-03E`。
|
||||
|
||||
615
docs/BLENDER_5_2_FULL_WEB_PARITY_EXECUTION_PLAN.md
Normal file
615
docs/BLENDER_5_2_FULL_WEB_PARITY_EXECUTION_PLAN.md
Normal file
@@ -0,0 +1,615 @@
|
||||
# Blender 5.2 完整 Web 对标实施计划
|
||||
|
||||
更新时间:2026-08-17
|
||||
|
||||
本文是 M12 及以后完整 Blender 5.2 Web 迁移的长期规范事实源,负责执行分类、原子任务、
|
||||
依赖、证据和完整发布门。它不覆盖 `WEB_BLENDER_MODELER_V1_SCOPE.md` 的 V1 产品契约,也不
|
||||
改写 V1 已冻结的 `LOCAL_EXACT/LOCAL_BOUNDED/SERVER/EXCLUDED` 发布分类。
|
||||
|
||||
文档职责固定如下:
|
||||
|
||||
| 事实 | 权威来源 |
|
||||
| --- | --- |
|
||||
| V1 承诺、非目标和发布标准 | `WEB_BLENDER_MODELER_V1_SCOPE.md` |
|
||||
| 当前唯一可领取任务 | `CURRENT_EXECUTION_PLAN.md` 与机器 `nextTask` |
|
||||
| M12-M23 完整对标规则和长期任务 | 本文 |
|
||||
| 当前实现事实和 family/slice 状态 | `status/parity-ledger.json` 与对应 evidence |
|
||||
| Blender 全域功能说明和覆盖参考 | `BLENDER_5_2_WEB_FEATURE_PARITY.md`、`BLENDER_5_2_FULL_PARITY_WBS.md` |
|
||||
|
||||
描述与机器证据冲突时不得用文字把失败改成成功。M12-01A-I 已收口,M12-02A-G preview
|
||||
inventory/identity/decode budget/OPFS commit/dedupe/quarantine/GC 已完成且未改变 parity 状态;
|
||||
desktop/browser 主线程与 Offscreen preview 像素闭环也已完成;Append/Link/Override 数据块与
|
||||
依赖闭包 inventory、source/owner/read-only/invalidation 合同、desktop 单 Object append fixture
|
||||
及 WASM Main 单 transaction append 已完成,当前唯一 `nextTask` 为 `M12-03E`。
|
||||
|
||||
## 1. 目标与边界
|
||||
|
||||
目标不是制作一个外观类似 Blender 的网页,而是让用户从浏览器完成 Blender 5.2 的可观察
|
||||
功能,并且每项声明都能用真实 Blender 5.2 执行结果复核。浏览器本地不适合承载的 Cycles、
|
||||
完整 Python、部分模拟和硬件后端,必须由隔离的 Blender 5.2 server job 执行;Three.js、
|
||||
WebGPU 或 JavaScript 近似结果不能冒充 Blender 结果。
|
||||
|
||||
“完整 Web Blender”只在以下条件同时满足后成立:
|
||||
|
||||
1. Blender 5.2 的 data-block、operator、node、modifier、editor workflow、import/export 和
|
||||
render/compute family 全部进入机器清单。
|
||||
2. 每个用户可观察能力被标为 `LOCAL_EXACT`、`LOCAL_EQUIVALENT` 或 `SERVER_EXACT`,且有成功
|
||||
证据;`BLOCKED`、`UNINVENTORIED` 或未声明项数量必须为零。
|
||||
3. `LOCAL_EXACT` 使用同一 fixture 比较 Blender desktop 与 WASM/Main 的结构、数值或像素。
|
||||
4. `LOCAL_EQUIVALENT` 只用于浏览器交互外壳;最终 Main、Depsgraph、保存文件和导出语义仍须
|
||||
与 Blender 一致。
|
||||
5. `SERVER_EXACT` 必须真的启动锁定版本 Blender,验证提交、进度、取消、资源隔离、结果 hash
|
||||
和项目绑定,不允许只返回“应走服务端”。
|
||||
6. 完整项目通过保存、关闭、Worker/页面重启、重新打开和 Blender desktop 再打开;未知数据
|
||||
不丢失,不支持的写操作不污染原文件。
|
||||
7. Chromium、Firefox、WebKit 的支持声明分别由自己的 quick/P0/full 证据决定,不互相推断。
|
||||
|
||||
Native window、CUDA/Metal/HIP/OptiX 实现细节不需要在浏览器复制,但它们提供的用户能力必须
|
||||
由 WebGPU、CPU 或 `SERVER_EXACT` 路径覆盖。若没有等价执行路径,就保持 `BLOCKED`,项目不得
|
||||
使用“完整对标”表述。
|
||||
|
||||
### 1.1 完整对标状态不是 V1 发布分类
|
||||
|
||||
全量盘点为每个 `parityId` 使用两个独立轴,禁止把 V1 family 的 `releaseClass` 直接升级为
|
||||
全量完成:
|
||||
|
||||
| 轴 | 允许值 | 含义 |
|
||||
| --- | --- | --- |
|
||||
| `implementationClass` | `LOCAL_EXACT` | Blender 语义在浏览器 Main/WASM 本地执行并经同 fixture 对标 |
|
||||
| `implementationClass` | `LOCAL_EQUIVALENT` | 仅浏览器平台或交互外壳不同,最终 Main、文件、导出和可见语义精确 |
|
||||
| `implementationClass` | `SERVER_EXACT` | 锁定版本的真实 Blender server 执行权威求值,浏览器负责提交和消费 |
|
||||
| `parityStatus` | `UNINVENTORIED` | Blender 基线项尚未进入机器清单 |
|
||||
| `parityStatus` | `BLOCKED` | 已盘点但缺实现、证据或存在超阈值差异 |
|
||||
| `parityStatus` | `IN_PROGRESS` | 唯一原子任务正在实现;仍按未完成计 |
|
||||
| `parityStatus` | `VERIFIED` | 对应 class 的全部强制证据通过 |
|
||||
|
||||
`LOCAL_EQUIVALENT` 不能用于 modifier、node、solver、render、import/export 或其他计算结果的
|
||||
近似实现。它只适用于 native window、文件选择器、菜单布局、输入映射等浏览器平台差异;
|
||||
若操作后的 Main 或文件语义不同,状态只能是 `BLOCKED`。
|
||||
|
||||
### 1.2 本地与 Blender server 路由规则
|
||||
|
||||
执行 class 在写实现前确定,不能根据某次运行是否方便临时切换:
|
||||
|
||||
| 能力特征 | 必选 class | 最低证据 |
|
||||
| --- | --- | --- |
|
||||
| Main/ID 读取写入、selection/context、undo/redo、增量编辑、保存恢复 | `LOCAL_EXACT` | desktop/WASM 同 fixture、单事务、保存重开、Chromium 用户路径 |
|
||||
| native window、系统文件对话框、菜单/快捷键表现等平台外壳 | `LOCAL_EQUIVALENT` | desktop 最终状态、Web Main/文件最终状态、可访问交互和恢复一致 |
|
||||
| WASM 已包含所需 Blender evaluator,且在声明预算内可确定性完成 | `LOCAL_EXACT` | desktop/WASM 语义或像素阈值、浏览器实际执行、故障恢复 |
|
||||
| Cycles、复杂 Eevee、Blender 完整 Compositor、重模拟/烘焙、tracking solve | `SERVER_EXACT`,除非该项另有完整本地证据 | 真实隔离 Blender job、版本/输入/设置/输出绑定、取消和恢复 |
|
||||
| 任意 Python/add-on、需原生库的 USD/Alembic/codec 或 OS/GPU 后端能力 | `SERVER_EXACT` | 权限策略、无隐式网络、资源限制、真实执行和结果复验 |
|
||||
| Three.js/WebGPU 预览、summary、proxy、capability probe、仅路由结果 | 不能单独领取成功 class | 只能作为已分类能力的中间证据;权威结果缺失时保持 `BLOCKED` |
|
||||
|
||||
同一 workflow 可以由多个 `parityId` 组成,例如本地编辑后提交服务端 Cycles 渲染;每个 ID
|
||||
只能有一个权威执行 class。server 路径必须向用户明确上传内容、版本、预算、进度和取消状态;
|
||||
未配置 server、用户不同意上传或 server receipt 失效时必须 fail-closed,不能回退到近似结果。
|
||||
|
||||
### 1.3 阻止“完整对标”声明的差异
|
||||
|
||||
以下任一项存在时,产品、README、发布说明和 UI 都不得使用“完整 Web Blender”或等价表述:
|
||||
|
||||
- inventory 有未映射、重复映射、版本漂移或 `UNINVENTORIED` 条目;
|
||||
- 任一 `parityId` 为 `BLOCKED/IN_PROGRESS`,或只有 summary/proxy/probe/route/mock 证据;
|
||||
- 计算结果超过已批准的数值、拓扑、时序、音频或像素阈值;
|
||||
- 不支持或未知数据在打开、编辑、保存、导出、append/link/override 中丢失或被重写;
|
||||
- writer 缺 undo/redo、保存重开、desktop 再打开,或失败路径改变 Main/项目 revision;
|
||||
- server 能力没有真实 Blender process、隔离、取消、预算、结果 hash 和项目绑定证据;
|
||||
- 用户可进入没有 capability gate 的 UI,或本地近似被显示为 Blender 权威结果;
|
||||
- 已声明浏览器、设备档位、输入方式或离线/server 模式缺自己的通过证据;
|
||||
- clean build、依赖/SBOM/license、安全、性能、故障或独立机器复验未通过。
|
||||
|
||||
## 2. 当前基线
|
||||
|
||||
- 当前交付物是可审计的 Chromium `Web Blender Modeler V1`,不是完整 Blender。
|
||||
- M6 至 M11 已完成;M12-01A-I、M12-02A-G enabling tasks 与 M12-02H preview display
|
||||
parity slice 及 M12-03A-D library-operation inventory/identity/desktop fixture/WASM Main append
|
||||
已完成,当前唯一下一任务为 M12-03E。
|
||||
- N-015 至 N-026 的 V1 `releaseStatus` 为 `READY`,但 12 个 family 的全域
|
||||
`parityStatus` 仍全部为 `BLOCKED`。
|
||||
- 这 12 个 family 是当前 post-V1 账本范围,不覆盖完整 Blender inventory;不能把它们未来的
|
||||
聚合状态当作 Main、Mesh、Modifier 等全域完成证明。M15 必须生成完整 owner family 集合。
|
||||
- 当前完成项只证明已声明的有界切片;任何 summary、proxy、capability probe 或路由结果都不能
|
||||
自动升级为 Blender 功能完成。
|
||||
|
||||
## 3. 真实 Blender 对标流水线
|
||||
|
||||
### 3.1 固定运行时
|
||||
|
||||
- [ ] `FULL-PARITY-RUNTIME-01` 固定官方 Blender 5.2.0 LTS archive SHA-256。
|
||||
- [ ] `FULL-PARITY-RUNTIME-02` 记录 `blender --version`、build hash、平台、Python、USD、
|
||||
Alembic、OpenVDB、Cycles device inventory。
|
||||
- [ ] `FULL-PARITY-RUNTIME-03` desktop runner 拒绝版本或 build hash 漂移。
|
||||
- [ ] `FULL-PARITY-RUNTIME-04` WASM manifest 绑定 Blender source commit、Emscripten、CMake
|
||||
cache、JS/WASM SHA-256。
|
||||
- [ ] `FULL-PARITY-RUNTIME-05` server image 绑定 Blender binary、启动参数、依赖和 image digest。
|
||||
- [ ] `FULL-PARITY-RUNTIME-06` fixture generator 只能在临时目录写文件,运行后逐项 hash。
|
||||
- [ ] `FULL-PARITY-RUNTIME-07` 禁止从开发机未声明插件、偏好设置或环境变量读取能力。
|
||||
- [ ] `FULL-PARITY-RUNTIME-08` CI 在每次 runner 启动时执行 runtime preflight 并写机器报告。
|
||||
|
||||
### 3.2 每项能力的固定证据链
|
||||
|
||||
能力原子执行单元固定为一个 `parityId × 单一用户可观察行为 × implementationClass`,内部走完
|
||||
本节的完整证据链。纯 runtime、inventory、generator 或 schema 基建任务必须标记
|
||||
`enablingTask=true`,列出它解除的依赖,但自身不能把任何 `parityId` 改为 `VERIFIED`。
|
||||
|
||||
M12 的字母 ID 只有在单一性检查通过后才能进入 `nextTask`;任务文字中出现“每种/全部/逐项”
|
||||
时,先按稳定 `parityId` 增加后缀并拆成多个任务。M16-M22 的“差距清零”项只是波次容器,
|
||||
必须先由 M15 展开,不能直接勾选容器。
|
||||
|
||||
每个原子功能任务必须按以下顺序执行,任一步失败都不能领取下一功能:
|
||||
|
||||
- [ ] `PARITY-TEMPLATE-01` 从 Blender RNA/operator/node 清单选取一个最小行为。
|
||||
- [ ] `PARITY-TEMPLATE-02` 写 capability 条目、输入 schema、预算、稳定错误码和保存策略。
|
||||
- [ ] `PARITY-TEMPLATE-03` 写 Blender 5.2 Python fixture generator,不手工伪造期望 JSON。
|
||||
- [ ] `PARITY-TEMPLATE-04` desktop Blender 执行操作并导出 canonical semantic report。
|
||||
- [ ] `PARITY-TEMPLATE-05` desktop 保存 `.blend`,新 Blender 进程重开并再次导出报告。
|
||||
- [ ] `PARITY-TEMPLATE-06` WASM/Main 对同一输入执行同一行为并导出同结构报告。
|
||||
- [ ] `PARITY-TEMPLATE-07` 比较 stable ID、Main revision、数据值、依赖和错误语义。
|
||||
- [ ] `PARITY-TEMPLATE-08` 对 writer 验证一次 transaction、undo、redo、save/reopen。
|
||||
- [ ] `PARITY-TEMPLATE-09` Chromium 主线程和 Offscreen 只消费权威结果并验证可见输出。
|
||||
- [ ] `PARITY-TEMPLATE-10` 加入 unsupported、malformed、stale revision 和预算超限负例。
|
||||
- [ ] `PARITY-TEMPLATE-11` 加入取消、Worker restart、资源释放和小任务恢复。
|
||||
- [ ] `PARITY-TEMPLATE-12` 将 command、duration、stdout 摘要和构件 SHA-256 写入 evidence。
|
||||
- [ ] `PARITY-TEMPLATE-13` 更新 family status、机器 ledger、实施记录和用户可见限制。
|
||||
- [ ] `PARITY-TEMPLATE-14` 独立运行 `typecheck`、lint、unit、专项 E2E 和状态一致性门。
|
||||
|
||||
### 3.3 原子任务与 evidence 机器合同
|
||||
|
||||
每个原子任务进入队列前必须含以下字段;字段不得留空,确实不适用时写 `NOT_APPLICABLE` 和
|
||||
可校验理由,不能直接删除:
|
||||
|
||||
| 字段 | 必填内容 |
|
||||
| --- | --- |
|
||||
| identity | `taskId`、parent ID、`parityId`、`enablingTask`、Blender source anchor、owner、dependency IDs |
|
||||
| classification | `implementationClass`、`parityStatus`、用户可观察行为、非目标 |
|
||||
| fixture | generator command/hash、`.blend`/asset hash、输入预算、license/source |
|
||||
| desktop | Blender command、runtime/build hash、canonical report/hash、保存后新进程重开报告 |
|
||||
| wasm | build manifest、command、Main revision、semantic report/hash;server 项记录本地拒绝/提交门 |
|
||||
| browser | 浏览器/OS/GPU、主线程/Offscreen 适用性、Chromium 用户动作和可见结果 |
|
||||
| persistence | undo/redo、save/close/reopen、Worker/page restart、desktop reopen、unknown-data report |
|
||||
| faults | malformed、unsupported、stale、cancel、timeout、OOM/quota、crash/restart、resource-zero |
|
||||
| server | image/binary digest、policy、job/request/result ID、progress/cancel、resource receipt、output hash |
|
||||
| comparison | comparator version、阈值来源、实际 max/RMS/P95/hash、pass/fail |
|
||||
| provenance | commit、dirty flag、commands、exit code、duration、stdout/stderr 摘要、artifact hashes |
|
||||
|
||||
evidence 只接受一次从干净输入开始的完整成功运行。重试前的失败保留为诊断,不得拼接不同
|
||||
commit、runtime 或 fixture 的局部成功。`parityStatus=VERIFIED` 必须由状态校验器重新计算,
|
||||
不能由 Markdown checkbox 单独决定。
|
||||
|
||||
### 3.4 class 对应的强制执行矩阵
|
||||
|
||||
| 证据 | `LOCAL_EXACT` | `LOCAL_EQUIVALENT` | `SERVER_EXACT` |
|
||||
| --- | --- | --- | --- |
|
||||
| Blender desktop 同 fixture | 必须 | 必须,比较最终语义 | 必须,作为 job 结果基线 |
|
||||
| WASM/Main 实际执行 | 必须 | 必须完成最终 Main/保存语义 | 必须完成读取、校验、提交和结果绑定;权威求值不冒充本地 |
|
||||
| Chromium 实际用户路径 | 必须 | 必须 | 必须,含提交/进度/取消/取回 |
|
||||
| 主线程与 Offscreen | 有可见 viewport 输出时都必须 | 有 viewport 输出时都必须 | 浏览器消费可见结果时按适用路径必须 |
|
||||
| undo/redo/save/reopen | writer 必须 | 改 Main/项目时必须 | 本地准备和结果绑定改变项目时必须 |
|
||||
| Worker/page restart | 必须 | 必须 | 必须,且不得重复提交或绑定迟到结果 |
|
||||
| OOM/quota/cancel/crash 后小任务恢复 | 必须 | 必须 | 浏览器与 server 两侧都必须 |
|
||||
| 真实 Blender server | 禁止作为本地成功替代 | 禁止作为等价外壳成功替代 | 必须 |
|
||||
|
||||
Firefox/WebKit 不在每个早期原子任务的默认完成门中;M14 声明某浏览器支持后,该浏览器对
|
||||
所有已验证 `parityId` 的适用 browser evidence 立即成为强制项,不能由 Chromium 结果代替。
|
||||
|
||||
### 3.5 比较器
|
||||
|
||||
- [ ] `COMPARE-01` 整数、枚举、布尔、ID、拓扑计数和离散索引使用精确比较。
|
||||
- [ ] `COMPARE-02` Float32/Float64 分别记录 max、RMS、P95、NaN/Inf 和阈值来源。
|
||||
- [ ] `COMPARE-03` transform 同时比较 location/rotation/scale 和 world matrix,处理旋转表示等价。
|
||||
- [ ] `COMPARE-04` Mesh 比较 topology hash、position/normal/UV/color/weight 和 attribute domain。
|
||||
- [ ] `COMPARE-05` 动画逐关键帧和区间采样,比较 FCurve、pose matrix、constraint 和 evaluated mesh。
|
||||
- [ ] `COMPARE-06` 图结构比较 node type、socket、default、link、group boundary 和未知节点保留。
|
||||
- [ ] `COMPARE-07` 图像固定色彩空间/alpha,输出 MAE、RMS、P95、坏像素和前景 IoU。
|
||||
- [ ] `COMPARE-08` 音频先统一 sample rate/channel layout,再比较时长、峰值、RMS 和 sample drift。
|
||||
- [ ] `COMPARE-09` 视频比较帧时间戳、关键帧映射、RGBA frame hash、音视频同步和丢帧。
|
||||
- [ ] `COMPARE-10` simulation 比较 frame/source/settings/cache hash 与每帧 canonical state。
|
||||
- [ ] `COMPARE-11` import/export 输出 loss report,未知或不可逆语义禁止静默丢弃。
|
||||
- [ ] `COMPARE-12` UI workflow 比较最终 Main/selection/context,而不是像素位置模仿 desktop UI。
|
||||
|
||||
## 4. 里程碑依赖
|
||||
|
||||
```text
|
||||
M11-14 跨域故障闭环
|
||||
-> M12 Asset / Library / IO / Editors
|
||||
-> M13 Scripting / Security / Server isolation
|
||||
-> M14 Firefox / WebKit / Device input
|
||||
-> M15 全域自动盘点
|
||||
-> M16-M22 按机器差距逐 family 清零
|
||||
-> M23 完整 Web Blender 候选发布
|
||||
```
|
||||
|
||||
M12 至 M15 是当前确定队列。M16 至 M22 的具体领取顺序由 M15 生成的机器差距决定;文档中的
|
||||
顺序是依赖上限,不得用它跳过最新 `nextTask`。
|
||||
|
||||
## 5. M11-14 Render、Compositor、Media 故障收口
|
||||
|
||||
- [x] `M11-14A` schema 固定三域 cancellation code、budget code 和资源归零字段。
|
||||
- [x] `M11-14B` Render 使用真实 `.blend` 验证 open 取消不提交、Worker generation 2 重开、
|
||||
texture aggregate budget 不替换已加载资源、dispose 回执和非空 WebGL 恢复。
|
||||
- [x] `M11-14C` Compositor 使用真实 Main graph 验证周期取消、超尺寸分配前阻断、LRU clear、
|
||||
Worker 重开和 Float32 output hash 恢复。
|
||||
- [x] `M11-14D` Media 使用真实 H.264 验证 proxy decode 取消、cache budget、clear、会话重建和
|
||||
RGBA8 payload hash 恢复。
|
||||
- [x] `M11-14E` Blender 5.2 Eevee 与 Compositor desktop golden、协议负例和 Chromium 三域门
|
||||
由一个专项命令串联。
|
||||
|
||||
本节已由 `npm --prefix web run test:render-compositor-media-recovery`、`M11-14` status、专项
|
||||
evidence 和 family ledger 联合收口。M12-03D 完成后,当前唯一 `nextTask` 为 `M12-03E`。
|
||||
|
||||
## 6. M12 Asset、Library、IO 与 Editor
|
||||
|
||||
### M12.1 Catalog schema 与迁移
|
||||
|
||||
- [x] `M12-01A` 盘点 Blender 5.2 AssetMetaData、catalog、tag、author、license、description 字段;
|
||||
machine inventory 绑定 8 个源码 hash 和真实 Blender 5.2 RNA,覆盖 14 DNA/10 RNA/3 catalog
|
||||
字段并记录 7 个 Web v1 缺口。本项为 enabling task,不改变 parity slice。
|
||||
- [x] `M12-01B` 生成 desktop catalog v1 fixture 和 canonical JSON;锁定 Blender 5.2 生成
|
||||
3 条 CDF v1 catalog、3 类资产及完整 metadata,独立新进程重开后 canonical JSON 精确一致。
|
||||
- [x] `M12-01C` 定义 Web catalog schema、stable catalog/asset ID 和文本/条目预算;schema v2
|
||||
使用 Blender UUID/path 与 AssetWeakReference identity,全部文本预算按 UTF-8 字节执行。
|
||||
- [x] `M12-01D` 定义 schema v1→v2 前向迁移;revision、source/preview/library graph 全部保留,
|
||||
legacy catalog/asset ID 生成确定性 mapping 和 source/target hash report。
|
||||
- [x] `M12-01E` 定义新版数据由旧版读取时的只读/阻断策略;v2 对 v1 reader 只开放
|
||||
hash-bound metadata snapshot,catalog/asset/save 写入统一阻断,未来 schema 完全拒绝。
|
||||
- [x] `M12-01F` 重复 ID、循环父 catalog、超长文本和未知字段负例;9 类 v1/v2 负例均返回
|
||||
稳定 code,失败后原始小正例继续通过。
|
||||
- [x] `M12-01G` IndexedDB migration 失败保持旧 transaction 和旧索引;两个真实 abort 点重开后
|
||||
v1 index、无关 migration 行、数据库版本均不变,成功重试才原子提交 v2 index 与 receipt。
|
||||
- [x] `M12-01H` 页面/Worker 重启后 catalog 顺序和 asset identity 不变;初始页面、reload、两个
|
||||
顺序启动的独立 Worker 返回相同 catalog/asset order、weak-reference identity 和 manifest hash。
|
||||
- [x] `M12-01I` schema fixture、migration report 和 hash 写入 M12-01 evidence;聚合 checker
|
||||
递归核对 A-H 任务链、desktop/Web fixture、canonical 双 hash、runtime fault/restart artifact。
|
||||
|
||||
### M12.2 Preview 与 content-addressed asset
|
||||
|
||||
- [x] `M12-02A` 盘点 Blender preview image 尺寸、颜色空间和无 preview 状态;锁定 DNA/RNA、
|
||||
ICON/PREVIEW 双槽、RGBA8 premultiplied alpha、无颜色空间字段及 3 个 null preview runtime 状态。
|
||||
- [x] `M12-02B` 定义 preview source/content SHA-256、MIME、width/height 和生成器 identity;
|
||||
Blender 5.2 确定性重编码绑定 executable/script/settings,source/content 独立且像素 exact。
|
||||
- [x] `M12-02C` 解码前检查 MIME、像素、字节和压缩比预算;schema 1 在任何 decoder/
|
||||
decoded allocation 前完成 identity、16 MiB encoded bytes、SHA-256、PNG/WebP header、尺寸、
|
||||
16,777,216 像素、64 MiB RGBA8 和 100:1 compression ratio 门,8 类负例后小正例仍可恢复。
|
||||
- [x] `M12-02D` preview 写 OPFS 后才提交 catalog 引用;production coordinator 先写入并回读复验
|
||||
content-addressed payload,再以一次 revision/full-row guarded IndexedDB transaction 提交 v2
|
||||
preview reference 与 hash receipt;三处 fault 均不发布引用,重开后 revision 8/payload 复验通过。
|
||||
- [x] `M12-02E` 相同内容去重,不同 metadata 不覆盖 payload;两个不同 asset/identity 在 revision
|
||||
8/9 绑定同一 content hash,第二次真实 OPFS write 去重且目录只有一份 payload,两条完整
|
||||
non-preview metadata 不变,重开后 head/reference/hash 一致。
|
||||
- [x] `M12-02F` 损坏 preview 隔离且 asset metadata 仍可读取;同长度 hash drift 从 content-addressed
|
||||
namespace 移入已复验 quarantine 文件,catalog revision/reference/两条 metadata 均不变;页面重开
|
||||
从 hash-bound receipt 返回 `QUARANTINED` 和完整 asset metadata,不再发布 bytes。
|
||||
- [x] `M12-02G` 删除引用只回收无其他项目引用的 payload;同项目首删在仍有一个 reference 时
|
||||
RETAINED,末删才 COLLECTED;OPFS project namespace 使另一项目的同 hash payload/reference
|
||||
保持 READY,revision 11 重开后两条 metadata 不变。
|
||||
- [x] `M12-02H` desktop/browser preview 指标与主线程/Offscreen 显示通过;Blender 5.2
|
||||
确定性 RGBA8 reference 与两条 Canvas2D 路径逐字节一致,错误像素由同一指标门阻断。
|
||||
|
||||
### M12.3 Append、Link、Override 语义
|
||||
|
||||
- [x] `M12-03A` 分别盘点 append、link、library override 涉及的数据块和依赖闭包;冻结 36 类
|
||||
selectable root、2 类 append-only、direct/indirect/transitive/embedded/override dependency
|
||||
角色及 17 个 override RNA 字段。
|
||||
- [x] `M12-03B` 为三种操作定义 source library ID、owner、read-only 和 invalidation token;
|
||||
locator/hash 绑定 source,operation/root/owner/generation/revision/closure 共同绑定 token。
|
||||
- [x] `M12-03C` desktop append 单 Object fixture,记录依赖 material/mesh/image stable mapping;
|
||||
Blender 5.2 append 后 Object/Mesh/Material/Image 全部成为可写 local owner,保存重开后依赖图、
|
||||
几何、UV、材质槽和 packed image Float32 像素 hash 保持一致。
|
||||
- [x] `M12-03D` WASM Main append 以一次 transaction 创建本地 owner 数据;Worker 先校验 source
|
||||
hash、closure 和 revision,Blender Main 递归导入 Object/Mesh/Material/packed Image,四个 ID
|
||||
均验证为可写 local owner,并只推进一个 SceneIR revision。
|
||||
- [ ] `M12-03E` append undo/redo/save/reopen 与 desktop canonical report 一致。
|
||||
- [ ] `M12-03F` desktop link fixture 保留 source library 和只读 ownership。
|
||||
- [ ] `M12-03G` linked data writer 全部返回 `LINKED_DATA_MUTATION_BLOCKED`。
|
||||
- [ ] `M12-03H` library reload 只替换匹配 generation 的 linked snapshot。
|
||||
- [ ] `M12-03I` missing library 保留 placeholder 和原始 source,不删除引用。
|
||||
- [ ] `M12-03J` desktop override fixture 记录 reference、local owner 和 property override path。
|
||||
- [ ] `M12-03K` 首个 override writer 只开放一个已验证属性。
|
||||
- [ ] `M12-03L` override stale source/revision 在 Main commit 前阻断。
|
||||
- [ ] `M12-03M` dependency cycle、ID collision、跨库环和重复 reload 负例。
|
||||
- [ ] `M12-03N` append/link/override 各有独立 desktop/WASM/Chromium 命令。
|
||||
|
||||
### M12.4 Origin、路径与 archive
|
||||
|
||||
- [ ] `M12-04A` library source schema 只接受声明的 HTTPS origin、项目 asset 或用户选择文件。
|
||||
- [ ] `M12-04B` 规范化 POSIX/Windows separator、`.`、`..`、percent encoding 和 Unicode 名称。
|
||||
- [ ] `M12-04C` 拒绝绝对路径、UNC、drive path、NUL、控制字符和 origin 逃逸。
|
||||
- [ ] `M12-04D` 符号链接/hardlink entry 在写入前解析并限制在临时根。
|
||||
- [ ] `M12-04E` archive 先读取 central directory/manifest,不先解压 payload。
|
||||
- [ ] `M12-04F` 单 entry 字节、总字节、entry 数、目录深度和文件名长度预算。
|
||||
- [ ] `M12-04G` 压缩比、重叠 range、重复路径、文件/目录前缀冲突负例。
|
||||
- [ ] `M12-04H` 解压取消删除 staging,不修改已提交项目。
|
||||
- [ ] `M12-04I` quota/OOM 后释放临时文件并允许小 archive 恢复。
|
||||
- [ ] `M12-04J` 恶意 ZIP/TAR fixture 进入长期安全回归。
|
||||
|
||||
### M12.5 格式 capability matrix
|
||||
|
||||
- [ ] `M12-05A` 从 Blender 5.2 build/runtime 生成 glTF/GLB、OBJ、STL、PLY、USD、Alembic 清单。
|
||||
- [ ] `M12-05B` 每种格式分别声明 import/export、local/server、geometry/material/animation 支持。
|
||||
- [ ] `M12-05C` matrix 未声明组合在文件选择器和 operator search 中不可执行。
|
||||
- [ ] `M12-05D` 能力由 runtime receipt 决定,不按扩展名推断。
|
||||
- [ ] `M12-05E` 每个 receipt 绑定 source/settings/runtime hash。
|
||||
- [ ] `M12-05F` 伪造、过期或跨版本 receipt 在使用前拒绝。
|
||||
|
||||
### M12.6 GLB round-trip
|
||||
|
||||
- [ ] `M12-06A` desktop 生成 Mesh/PBR/UV/skin/animation GLB fixture 组。
|
||||
- [ ] `M12-06B` Web import 比较 topology、attributes、materials、nodes 和 animations。
|
||||
- [ ] `M12-06C` import 后保存 `.blend`、重开并比较 stable ID。
|
||||
- [ ] `M12-06D` Web export 生成 machine loss report。
|
||||
- [ ] `M12-06E` desktop Blender 再导入 Web GLB 并比较 canonical report。
|
||||
- [ ] `M12-06F` sparse accessor、Draco/extension、外部 URI 和超预算负例。
|
||||
- [ ] `M12-06G` import/export 取消、Worker restart 和 OPFS quota 恢复。
|
||||
|
||||
### M12.7 OBJ、STL、PLY round-trip
|
||||
|
||||
- [ ] `M12-07A` OBJ 单 Mesh 正例:position/normal/UV/material group。
|
||||
- [ ] `M12-07B` OBJ 多对象、负索引、MTL/texture origin 和坏 face 负例。
|
||||
- [ ] `M12-07C` OBJ Web→desktop round-trip 与 loss report。
|
||||
- [ ] `M12-07D` STL binary/ASCII capability 分开声明。
|
||||
- [ ] `M12-07E` STL normal、unit、degenerate triangle 和 trailing bytes 对标。
|
||||
- [ ] `M12-07F` STL Web→desktop round-trip 与材质缺失 loss report。
|
||||
- [ ] `M12-07G` PLY ASCII/binary little-endian capability 分开声明。
|
||||
- [ ] `M12-07H` PLY vertex/face/color/custom property 映射与未知 property loss report。
|
||||
- [ ] `M12-07I` PLY big-endian/坏 list/超大 count 稳定阻断。
|
||||
- [ ] `M12-07J` 三格式分别执行取消、OOM、重启和小文件恢复。
|
||||
|
||||
### M12.8 USD 与 Alembic
|
||||
|
||||
- [ ] `M12-08A` 固定有 USD/Alembic 的 Blender 5.2 server runtime identity。
|
||||
- [ ] `M12-08B` 本地 runtime 缺能力时只能路由 `SERVER_EXACT`,不能静默降级 GLB。
|
||||
- [ ] `M12-08C` USD fixture 覆盖 hierarchy、xform、mesh、material、camera、animation。
|
||||
- [ ] `M12-08D` Alembic fixture 覆盖 topology sample、transform sample 和 frame range。
|
||||
- [ ] `M12-08E` server request 绑定 source/settings/build hash。
|
||||
- [ ] `M12-08F` server output 由新 Blender 进程重开并导出 canonical report。
|
||||
- [ ] `M12-08G` server cancel/timeout/OOM 不产生可绑定 output。
|
||||
- [ ] `M12-08H` 外部引用和不可映射语义进入 loss report。
|
||||
|
||||
### M12.9 统一 Editor context
|
||||
|
||||
- [ ] `M12-09A` 冻结 Workspace/Area/Region/Editor/Mode/Tool context schema。
|
||||
- [ ] `M12-09B` View3D、Outliner、Properties 读取同一 selection revision。
|
||||
- [ ] `M12-09C` 任一 editor selection 更新只提交一次共享 store transaction。
|
||||
- [ ] `M12-09D` stale editor event 不覆盖较新 Main/selection revision。
|
||||
- [ ] `M12-09E` hidden/locked/library object 在三 editor 中保持相同可选规则。
|
||||
- [ ] `M12-09F` frame、active object、mode 和 tool 在 Worker restart 后恢复。
|
||||
- [ ] `M12-09G` editor dispose 移除 DOM listener、pending request、GPU resource 和 timer。
|
||||
- [ ] `M12-09H` workspace/layout 保存只改 UI revision,不改 Blender scene revision。
|
||||
|
||||
### M12.10 专用 Editor 逐项开放
|
||||
|
||||
- [ ] `M12-10A` UV Editor 先只读显示 Main UV 与 selection。
|
||||
- [ ] `M12-10B` UV 单个 writer 通过 desktop/Main/undo/save 后开放。
|
||||
- [ ] `M12-10C` Shader Node Editor 只显示完整图,unsupported node 不可编辑但不得丢失。
|
||||
- [ ] `M12-10D` Geometry Node Editor 只开放已有 Main writer 的 allowlist node。
|
||||
- [ ] `M12-10E` Graph Editor 显示 FCurve/key/handle,单 operator 单独验收。
|
||||
- [ ] `M12-10F` Dope Sheet 与 NLA 共享 frame/selection/action revision。
|
||||
- [ ] `M12-10G` Sequencer editor 只开放已完成 Main writer 的 strip operation。
|
||||
- [ ] `M12-10H` Compositor editor 保留 unsupported graph 并显示执行 gate。
|
||||
- [ ] `M12-10I` Spreadsheet 只读取有预算的 evaluated attributes。
|
||||
- [ ] `M12-10J` 每个 editor 各有 context、dispose、键盘和窄屏 E2E。
|
||||
|
||||
### M12.11 Operator、keymap 与 workflow
|
||||
|
||||
- [ ] `M12-11A` operator search 直接由 capability registry 生成。
|
||||
- [ ] `M12-11B` registry、UI、协议和 Main writer 不维护第二份成功列表。
|
||||
- [ ] `M12-11C` operator poll 使用当前 area/region/mode/selection/tool context。
|
||||
- [ ] `M12-11D` keymap 按 editor→modal→tool→global 确定性解析。
|
||||
- [ ] `M12-11E` 冲突显示来源和胜出规则,不随机采用注册顺序。
|
||||
- [ ] `M12-11F` Escape、Enter、undo、redo 在所有 modal tool 保持一致。
|
||||
- [ ] `M12-11G` 建模 workflow:import→select→edit→undo/redo→save/reopen→export。
|
||||
- [ ] `M12-11H` 动画 workflow:key→curve→NLA→frame seek→save/reopen→export。
|
||||
- [ ] `M12-11I` asset workflow:catalog→append/link→override→reload→package。
|
||||
- [ ] `M12-11J` 三 workflow 各覆盖取消、Worker crash、quota 和诊断导出。
|
||||
|
||||
M12 退出条件:所有上述任务有独立报告;Asset/IO/Editor family 仍有未盘点项时不得宣称完整。
|
||||
|
||||
## 7. M13 Scripting、安全与 Server
|
||||
|
||||
### M13.1 默认拒绝与只读盘点
|
||||
|
||||
- [ ] `M13-01A` 盘点 Text、Python Console、autorun、driver expression、handler 和 add-on 入口。
|
||||
- [ ] `M13-01B` `.blend` 打开时只读取脚本 metadata,不执行任意内容。
|
||||
- [ ] `M13-01C` autorun、register、install、driver execution 默认返回稳定 policy code。
|
||||
- [ ] `M13-01D` UI 不提供绕过协议直接 eval 的入口。
|
||||
- [ ] `M13-01E` 保存/重开保留原 Text 数据块和未知脚本,不重写源码。
|
||||
- [ ] `M13-01F` malicious text、driver、handler 和 embedded module fixture 进入负例。
|
||||
|
||||
### M13.2 Script manifest 与签名
|
||||
|
||||
- [ ] `M13-02A` manifest 限制文本数量、总字节、module、path、dependency 和 permission。
|
||||
- [ ] `M13-02B` canonical serialization 固定签名输入。
|
||||
- [ ] `M13-02C` 定义 signer identity、key rotation、revocation 和 timestamp policy。
|
||||
- [ ] `M13-02D` signature 只批准声明内容,source hash 变化立即失效。
|
||||
- [ ] `M13-02E` permission 默认最小化,未知 permission 阻断。
|
||||
- [ ] `M13-02F` replay、key confusion、过期、撤销和多签顺序负例。
|
||||
|
||||
### M13.3 浏览器 sandbox
|
||||
|
||||
- [ ] `M13-03A` sandbox scope 不暴露 DOM、主 Worker、OPFS、IndexedDB 或网络。
|
||||
- [ ] `M13-03B` CPU、wall time、memory、message 和 output byte budget 固定。
|
||||
- [ ] `M13-03C` host call 使用显式 allowlist 和结构化参数。
|
||||
- [ ] `M13-03D` sandbox crash/timeout 终止当前 job,不污染 Main revision。
|
||||
- [ ] `M13-03E` cancellation 后不得发布迟到 message 或 cache。
|
||||
- [ ] `M13-03F` dispose 后 Worker、port、timer 和 buffer 归零。
|
||||
- [ ] `M13-03G` 小脚本在同会话恢复,审计链保持连续。
|
||||
|
||||
### M13.4 Blender server job isolation
|
||||
|
||||
- [ ] `M13-04A` 每个 job 创建不可预测的一次性目录。
|
||||
- [ ] `M13-04B` source 只读挂载,output 写独立目录。
|
||||
- [ ] `M13-04C` CPU、内存、进程、文件、时间和 output budget 由 OS/container 强制。
|
||||
- [ ] `M13-04D` 默认无网络;声明 origin 使用单独 policy。
|
||||
- [ ] `M13-04E` Blender 只以 background/factory-startup 和固定 startup script 启动。
|
||||
- [ ] `M13-04F` stdout/stderr 截断并过滤凭据/绝对内部路径。
|
||||
- [ ] `M13-04G` cancel 终止 Blender process tree 并清理临时目录。
|
||||
- [ ] `M13-04H` timeout/OOM/exit signal 转换为稳定错误码。
|
||||
- [ ] `M13-04I` result 验证 source/settings/build/output hash 后才进入 OPFS。
|
||||
- [ ] `M13-04J` 同一 request 重试保持幂等,不绑定两个冲突结果。
|
||||
|
||||
### M13.5 CSP、供应链与恶意输入
|
||||
|
||||
- [ ] `M13-05A` CSP 禁止 inline script、eval、data script 和未声明 origin。
|
||||
- [ ] `M13-05B` Worker、WASM、font、image、media 的 CSP 分别验证。
|
||||
- [ ] `M13-05C` 生产依赖、构建依赖、测试依赖分别生成 inventory。
|
||||
- [ ] `M13-05D` severity 门和例外包含 owner、期限、理由和替代控制。
|
||||
- [ ] `M13-05E` SBOM、license、source offer 与 archive/commit hash 绑定。
|
||||
- [ ] `M13-05F` malicious blend/image/font/media/archive/node graph/manifest 全矩阵。
|
||||
- [ ] `M13-05G` fuzz crash 先保存最小样本,再修复,再进入长期回归。
|
||||
- [ ] `M13-05H` audit record 使用严格时间顺序、request ID 和防篡改 hash chain。
|
||||
|
||||
M13 退出条件:任意 Python/add-on 能力只有在本地 sandbox 或 server isolation 实跑后才可从
|
||||
`BLOCKED` 改为成功;仅签名验证通过不等于执行安全。
|
||||
|
||||
## 8. M14 跨浏览器与设备
|
||||
|
||||
### M14.1 Capability probe
|
||||
|
||||
- [ ] `M14-01A` 冻结当前 Chromium release/engine/archive hash。
|
||||
- [ ] `M14-01B` Firefox probe:WASM、Worker、OPFS、IndexedDB、WebGL2、WebGPU、Offscreen、isolation。
|
||||
- [ ] `M14-01C` WebKit probe采用相同字段与错误码。
|
||||
- [ ] `M14-01D` probe 记录浏览器/OS/GPU adapter,不按 user-agent 猜测能力。
|
||||
- [ ] `M14-01E` 缺 WebGPU 只阻断依赖 WebGPU 的功能,不影响可验证 WebGL2/Main。
|
||||
|
||||
### M14.2 Firefox
|
||||
|
||||
- [ ] `M14-02A` Firefox quick:type/protocol/static asset 启动。
|
||||
- [ ] `M14-02B` Firefox P0:open/edit/undo/redo/save/reopen/export。
|
||||
- [ ] `M14-02C` Firefox 主线程 WebGL2 像素和 context loss。
|
||||
- [ ] `M14-02D` Firefox Offscreen 只在主线程通过后领取。
|
||||
- [ ] `M14-02E` Firefox OPFS quota、Worker crash、OOM 和 network interruption。
|
||||
- [ ] `M14-02F` Firefox full family 命令逐项 PASS/BLOCKED,禁止静默 fallback。
|
||||
- [ ] `M14-02G` Firefox quick/P0/full 进入 CI 后才更新支持声明。
|
||||
|
||||
### M14.3 WebKit
|
||||
|
||||
- [ ] `M14-03A` WebKit quick 启动和本地 asset 完整性。
|
||||
- [ ] `M14-03B` WebKit P0 用户闭环。
|
||||
- [ ] `M14-03C` WebKit 主线程 viewport。
|
||||
- [ ] `M14-03D` WebKit Offscreen 独立 gate。
|
||||
- [ ] `M14-03E` WebKit storage/fault/recovery。
|
||||
- [ ] `M14-03F` WebKit full family matrix。
|
||||
- [ ] `M14-03G` WebKit CI 和发布说明。
|
||||
|
||||
### M14.4 设备档位与输入
|
||||
|
||||
- [ ] `M14-04A` GPU/内存预算按声明设备档位选择,不自动扩容。
|
||||
- [ ] `M14-04B` DPR 1/1.5/2/3 下 canvas、raycast、gizmo 和截图一致。
|
||||
- [ ] `M14-04C` pointer mouse、touch、pen 分开记录 pressure/tilt/button/cancel。
|
||||
- [ ] `M14-04D` IME composition 不触发未完成 operator。
|
||||
- [ ] `M14-04E` US、非 US、dead key 和 modifier keymap fixture。
|
||||
- [ ] `M14-04F` 触控 modal cancel、双指导航和笔 stroke 只提交一次 Main transaction。
|
||||
- [ ] `M14-04G` 1440x900、1280x720、平板、手机无重叠/溢出。
|
||||
- [ ] `M14-04H` keyboard-only、screen reader name/role 和 focus restore。
|
||||
|
||||
## 9. M15 全域自动盘点
|
||||
|
||||
- [ ] `M15-01A` 从 Blender 5.2 source/RNA 生成 data-block type 清单。
|
||||
- [ ] `M15-01B` 生成 operator idname、poll context 和主要 property 清单。
|
||||
- [ ] `M15-01C` 生成 modifier、constraint、shader/GN/compositor node 清单。
|
||||
- [ ] `M15-01D` 生成 sequencer strip/effect、physics family 和 import/export 清单。
|
||||
- [ ] `M15-01E` 生成 editor/space/region/workspace/keymap 清单。
|
||||
- [ ] `M15-01F` 为 Main/Scene/Mesh/Depsgraph 和其余现有 V1 core slice 生成同级全量清单,
|
||||
不把“当前范围完成”映射为全量完成。
|
||||
- [ ] `M15-02A` 将每项映射到 owner family、implementation class、tests 和 evidence。
|
||||
- [ ] `M15-02B` 找出未映射、重复映射、只有 summary、只有 proxy 和只有 route 的条目。
|
||||
- [ ] `M15-02C` `LOCAL_EXACT` 缺 desktop/WASM 同 fixture 时降为 `BLOCKED`。
|
||||
- [ ] `M15-02D` `LOCAL_EQUIVALENT` 缺 Main/save 证据时降为 `BLOCKED`。
|
||||
- [ ] `M15-02E` `SERVER_EXACT` 缺真实 job/cancel/isolation/result binding 时降为 `BLOCKED`。
|
||||
- [ ] `M15-02F` unknown data 保存破坏原文件时对应 reader/writer family 降为 `BLOCKED`。
|
||||
- [ ] `M15-03A` 为每个 gap 生成一个最小 `nextTask`,不生成“完成整个 family”大任务。
|
||||
- [ ] `M15-03B` nextTask 包含 fixture、desktop command、Web command、比较器和退出条件。
|
||||
- [ ] `M15-03C` 依赖图无缺失、无环、每次只允许一个 active task。
|
||||
- [ ] `M15-03D` 机器统计分别输出 inventoried/completed/blocked/uninventoried。
|
||||
- [ ] `M15-03E` 所有生成的 owner family(包括当前 N-015 至 N-026)分别达到零 gap,禁止由
|
||||
12-family 或 release 聚合 gate 反推未纳入当前账本的 core family 已完成。
|
||||
|
||||
## 10. M16-M22 全域差距清零波次
|
||||
|
||||
这些里程碑只定义 owner 和顺序。具体任务由 M15 生成,每个任务仍使用第 3 节的 14 步模板。
|
||||
|
||||
### M16 Main、Mesh、Modifier、Sculpt
|
||||
|
||||
- [ ] `M16-01` `.blend` reader/writer unknown-data preservation 差距清零。
|
||||
- [ ] `M16-02` Object/Collection/parent/transform operator 差距清零。
|
||||
- [ ] `M16-03` Mesh create/delete/select/topology/attribute operator 差距清零。
|
||||
- [ ] `M16-04` UV/normal/data-transfer operator 差距清零。
|
||||
- [ ] `M16-05` Modifier family 与 Depsgraph 求值差距清零。
|
||||
- [ ] `M16-06` PBVH Sculpt brush/mask/face-set 差距清零。
|
||||
|
||||
### M17 Rigging、Animation、Constraint、NLA
|
||||
|
||||
- [ ] `M17-01` Armature edit/pose/bone collection 差距清零。
|
||||
- [ ] `M17-02` Constraint family 与 dependency order 差距清零。
|
||||
- [ ] `M17-03` Shape key、driver 安全执行和 property animation 差距清零。
|
||||
- [ ] `M17-04` FCurve/key/handle/modifier/extrapolation 差距清零。
|
||||
- [ ] `M17-05` NLA track/strip/blend/transition/meta/time 差距清零。
|
||||
- [ ] `M17-06` animation export/import loss 差距清零。
|
||||
|
||||
### M18 Non-mesh、Grease Pencil、Paint
|
||||
|
||||
- [ ] `M18-01` Curve/Surface/Text/Metaball 完整 data/operator 差距清零。
|
||||
- [ ] `M18-02` Curves/Hair/PointCloud/Volume data/operator 差距清零。
|
||||
- [ ] `M18-03` Grease Pencil layer/frame/drawing/material/modifier/editor 差距清零。
|
||||
- [ ] `M18-04` Vertex/Weight/Texture Paint 与 PBVH/UDIM 差距清零。
|
||||
- [ ] `M18-05` desktop/browser/export golden 差距清零。
|
||||
|
||||
### M19 Geometry Nodes、Shader、Simulation
|
||||
|
||||
- [ ] `M19-01` Geometry Nodes node/socket/field/domain/group 清单差距清零。
|
||||
- [ ] `M19-02` Geometry Nodes external resource/instance/lazy-function 差距清零。
|
||||
- [ ] `M19-03` Shader node/texture/color/sampler/compiler 差距清零。
|
||||
- [ ] `M19-04` Rigid/Soft/Cloth/Fluid/Dynamic Paint/Particle/Hair simulation 差距清零。
|
||||
- [ ] `M19-05` local/server bake、cache、playback 和 fault 差距清零。
|
||||
|
||||
### M20 Render、Compositor、Sequencer、Tracking
|
||||
|
||||
- [ ] `M20-01` Camera/Light/World/Color Management 完整字段差距清零。
|
||||
- [ ] `M20-02` Eevee/WebGPU/Cycles/server render 差距清零。
|
||||
- [ ] `M20-03` Compositor node/resource/HDR/tile/server 差距清零。
|
||||
- [ ] `M20-04` Sequencer strip/effect/modifier/proxy/mix/encode 差距清零。
|
||||
- [ ] `M20-05` Audio device/mix/sync/waveform 差距清零。
|
||||
- [ ] `M20-06` Tracking/Mask editor/solve/compositor binding 差距清零。
|
||||
|
||||
### M21 Asset、IO、Editors、Scripting
|
||||
|
||||
- [ ] `M21-01` Asset/Library/Override 全清单差距清零。
|
||||
- [ ] `M21-02` Blender 启用的 import/export format 差距清零。
|
||||
- [ ] `M21-03` Editor/context/operator/keymap/workspace 差距清零。
|
||||
- [ ] `M21-04` Python/Text/Add-on/driver 的 local/server 安全能力差距清零。
|
||||
- [ ] `M21-05` help/i18n/accessibility/preferences 差距清零。
|
||||
|
||||
### M22 平台与跨浏览器
|
||||
|
||||
- [ ] `M22-01` Chromium 全矩阵零 gap。
|
||||
- [ ] `M22-02` Firefox 声明矩阵零 gap。
|
||||
- [ ] `M22-03` WebKit 声明矩阵零 gap。
|
||||
- [ ] `M22-04` touch/pen/HiDPI/IME/keymap 零 gap。
|
||||
- [ ] `M22-05` local/server platform capability 零未声明项。
|
||||
|
||||
## 11. M23 完整候选发布
|
||||
|
||||
- [ ] `M23-01` clean checkout 重建 WASM、Web app、server image 和所有 fixture。
|
||||
- [ ] `M23-02` 运行全部 desktop/WASM semantic golden。
|
||||
- [ ] `M23-03` 运行 Chromium/Firefox/WebKit quick、P0、full。
|
||||
- [ ] `M23-04` 运行 geometry/texture/volume/simulation/media 性能矩阵。
|
||||
- [ ] `M23-05` 运行 OOM/quota/network/device/Worker/server fault 矩阵。
|
||||
- [ ] `M23-06` 运行 malicious/fuzz/CSP/dependency/security 矩阵。
|
||||
- [ ] `M23-07` inventory 中每个 `parityId` 均为 `VERIFIED`,所有生成的 owner family 均为
|
||||
`COMPLETE`,blocked/in-progress/uninventoried 均为零。
|
||||
- [ ] `M23-08` binary/source/server archives 在独立机器复验。
|
||||
- [ ] `M23-09` SBOM、license、source offer、runtime 和 archive hash 联合校验。
|
||||
- [ ] `M23-10` 发布说明逐项列出 local/server 实现,不隐去架构差异。
|
||||
- [ ] `M23-11` 从空环境部署、升级、回滚并恢复真实项目。
|
||||
- [ ] `M23-12` 只有上述报告绑定同一 commit 后才允许“完整 Web Blender”声明。
|
||||
|
||||
## 12. 每轮执行规则
|
||||
|
||||
1. 从机器队列领取唯一 `nextTask`。
|
||||
2. 先运行现有正例,确认不是在旧失败上继续扩展。
|
||||
3. 写 fixture/golden 时实际启动锁定 Blender 5.2,不手工填写运行结果。
|
||||
4. 先完成协议和 fail-closed,再接 Main writer,再接浏览器 UI。
|
||||
5. 失败结果记录到工作日志,但不写成成功 evidence。
|
||||
6. 代码完成后运行专项;专项通过后运行全量 Node、typecheck、lint、build、status/evidence。
|
||||
7. 更新 ledger 时只增加本轮真实完成的 slice;family 全域状态保持 `BLOCKED` 直到 gap 为零。
|
||||
8. 每轮保留下一任务的可执行入口,不提前实现无依赖保证的后续功能。
|
||||
@@ -3,7 +3,9 @@
|
||||
更新时间:2026-08-13
|
||||
|
||||
本文件以仓库内 `blender-5.2.0/source/blender/` 为基线,记录 Blender 功能域在 Web
|
||||
项目中的迁移方式和唯一领取顺序。`completed_current_scope` 只表示声明子集通过验收,
|
||||
项目中的迁移方式和当前差距。M12-M23 的唯一长期实施规则和原子任务位于
|
||||
`BLENDER_5_2_FULL_WEB_PARITY_EXECUTION_PLAN.md`;本文不再定义领取顺序。`completed_current_scope`
|
||||
只表示声明子集通过验收,
|
||||
不表示桌面 Blender 全量等价。任何未列入白名单的 operator、node、strip、physics
|
||||
类型或数据块都必须返回机器可读阻断,不能由 Three.js 静默近似。
|
||||
|
||||
@@ -206,8 +208,8 @@ operator search;这不等价于 Blender 全量 operator registry、context men
|
||||
|
||||
## N-026 全域发布门
|
||||
|
||||
1. 生成 machine-readable parity manifest:每个 Blender family 为
|
||||
`LOCAL_EXACT`、`LOCAL_BOUNDED`、`SERVER` 或 `BLOCKED`。
|
||||
1. 当前 V1 machine-readable manifest 保留 `LOCAL_EXACT`、`LOCAL_BOUNDED`、`SERVER` 或
|
||||
`BLOCKED`;完整对标 inventory 按新计划转换为独立 `implementationClass/parityStatus`。
|
||||
2. 全部正例和阻断 fixture 在 Chromium 主线程/OffscreenCanvas、离线包和 Worker restart 下运行。
|
||||
3. 100k/1M/10M 几何、4k/8k texture、100/1000 frame、长媒体和 simulation cache 基准。
|
||||
4. `.blend`/image/media/script fuzz、OPFS quota、OOM、设备丢失、网络中断和恢复验证。
|
||||
|
||||
@@ -18,9 +18,9 @@ React 工作区和编辑器
|
||||
-> OPFS 大文件 + IndexedDB 元数据
|
||||
```
|
||||
|
||||
本计划是当前唯一的短周期领取队列。长期 Blender 功能对标仍记录在
|
||||
`BLENDER_5_2_WEB_FEATURE_PARITY.md` 和 `status/parity-ledger.json`,但不以长期全域差距
|
||||
阻断已明确限定范围的 V1。
|
||||
本计划是当前唯一的短周期领取队列。M12-M23 的长期 Blender 全功能实施规范以
|
||||
`BLENDER_5_2_FULL_WEB_PARITY_EXECUTION_PLAN.md` 为准;当前实现状态仍由
|
||||
`status/parity-ledger.json` 和对应 evidence 给出。长期全域差距不阻断已明确限定范围的 V1。
|
||||
|
||||
## 2. 文档与事实源
|
||||
|
||||
@@ -29,7 +29,9 @@ React 工作区和编辑器
|
||||
| `WEB_BLENDER_MODELER_V1_SCOPE.md` | V1 产品契约、支持矩阵、非目标、发布标准 |
|
||||
| `CURRENT_EXECUTION_PLAN.md` | 当前任务顺序、最小任务、依赖和退出条件 |
|
||||
| `PROJECT_STATUS_AND_NEXT_WORK.md` | 已实现能力、风险、验证命令总览 |
|
||||
| `BLENDER_5_2_WEB_FEATURE_PARITY.md` | Blender 5.2 全域长期差距 |
|
||||
| `BLENDER_5_2_FULL_WEB_PARITY_EXECUTION_PLAN.md` | M12-M23 完整 Web 对标的长期规范事实源 |
|
||||
| `BLENDER_5_2_WEB_FEATURE_PARITY.md` | Blender 5.2 功能域说明和当前差距参考 |
|
||||
| `BLENDER_5_2_FULL_PARITY_WBS.md` | 完整产品覆盖检查表和历史 F 域映射参考 |
|
||||
| `status/parity-ledger.json` | 功能域和 V1 slice 的机器事实 |
|
||||
| `status/release-evidence.json` | 最近一次可审计发布证据快照 |
|
||||
|
||||
@@ -156,8 +158,8 @@ loss、恶意 blend、zip bomb、离线包和 V1 用户闭环均已在最终 M5
|
||||
| M8 VDB 自动分页 | GPU 缺页反馈、range/OPFS、LRU、双视口恢复 | 已完成,20/20 | 联合重开与 desktop/main/Offscreen 三轴 golden 通过 |
|
||||
| M9 非 Mesh/GP/Paint | 字体、Curve、Grease Pencil、Paint 增量闭环 | 已完成,14/14 | 每个新增 writer 独立通过 Main/undo/save/golden;三域故障恢复闭环通过 |
|
||||
| M10 GN/Shader/NLA/Simulation | 白名单求值、cache、编译与阻断 | 已完成,15/15 | 四域分别通过 desktop/WASM/fault 门 |
|
||||
| M11 Render/Compositor/Media | 灯光、渲染、合成、媒体执行边界 | 进行中,13/14 | 本地白名单与 server 边界均可审计 |
|
||||
| M12 Asset/IO/Editors | 资产、格式、编辑器和上下文工作流 | 未开始 | 每个格式/editor 有独立 round-trip 或稳定阻断 |
|
||||
| M11 Render/Compositor/Media | 灯光、渲染、合成、媒体执行边界 | 已完成,14/14 | 本地白名单、server 边界与三域故障恢复均可审计 |
|
||||
| M12 Asset/IO/Editors | 资产、格式、编辑器和上下文工作流 | 进行中,M12-01A-I、M12-02A-H 与 M12-03A-D 完成 | 每个格式/editor 有独立 round-trip 或稳定阻断 |
|
||||
| M13 Scripting/Security | 脚本默认拒绝、服务端隔离、CSP、供应链 | 未开始 | 恶意输入矩阵和 release 安全门通过 |
|
||||
| M14 跨浏览器/设备 | Firefox、WebKit、触控、笔、HiDPI、IME | 未开始 | 新浏览器进入 quick/P0/full CI 后才声明支持 |
|
||||
| M15 全域审计 | Blender 5.2 全域差距和下一发布 | 未开始 | 逐 family 审计,不由聚合 release gate 反推完成 |
|
||||
@@ -790,7 +792,7 @@ CI 可从 lockfile 和对应源码重现当前 binary/source hash。
|
||||
- [x] `M10-14` 每类 cache 验证 source hash、frame range、字节预算和版本。
|
||||
- [x] `M10-15` GN/Shader/NLA/Simulation 分别建立浏览器性能、OOM 和恶意图输入门。
|
||||
|
||||
### M11 Lighting、Render、Compositor 与 Sequencer(13/14)
|
||||
### M11 Lighting、Render、Compositor 与 Sequencer(14/14)
|
||||
|
||||
- [x] `M11-01` Camera/Light/World/Scene color management 建立字段级 parity 表。
|
||||
- [x] `M11-02` 每个支持字段通过 Main edit、undo、save/reopen 和 viewport 映射。
|
||||
@@ -820,10 +822,16 @@ CI 可从 lockfile 和对应源码重现当前 binary/source hash。
|
||||
- [x] `M11-13` audio context suspend/resume、设备缺失和静音恢复有专项测试;schema 1
|
||||
区分 context/output 状态,真实 Chromium `AudioContext` 通过用户手势、挂起、静音恢复、
|
||||
二次挂起/恢复和 close,缺设备与 resume failure 保持结构化静音阻断。
|
||||
- [ ] `M11-14` render/compositor/media 全部覆盖取消、重启、预算释放和恢复。
|
||||
- [x] `M11-14` render/compositor/media 全部覆盖取消、重启、预算释放和恢复;schema 1
|
||||
固定三域 cancellation/budget code 和资源归零字段,真实 `.blend`、H.264 与 WebGL 输出通过
|
||||
generation 2 重开、失败不提交、显式 clear/dispose 和恢复后 output SHA-256 门。
|
||||
|
||||
### M12 Asset、IO、Editor 与工作流
|
||||
|
||||
本节 M12-M15 仅保留短周期计划形成时的里程碑摘要,不再作为原子任务定义。M12-03D
|
||||
机器证据完成后,唯一下一任务为 `BLENDER_5_2_FULL_WEB_PARITY_EXECUTION_PLAN.md` 中的
|
||||
`M12-03E`;后续只能领取该文档或机器差距生成器给出的精确字母任务,不得领取下列整行摘要。
|
||||
|
||||
- [ ] `M12-01` asset catalog schema migration 有向前/向后兼容 fixture。
|
||||
- [ ] `M12-02` append/link/override 分别定义 stable ID、所有权和失效语义。
|
||||
- [ ] `M12-03` library source 只允许声明 origin,路径穿越和符号链接逃逸被拒绝。
|
||||
@@ -872,13 +880,15 @@ CI 可从 lockfile 和对应源码重现当前 binary/source hash。
|
||||
- [ ] `M15-01` 每季度从 Blender 5.2 operator/data-block 清单重新生成 parity 差距。
|
||||
- [ ] `M15-02` 每个 family 的 `COMPLETE` 必须没有 summary-only、proxy-success 或隐式 loss。
|
||||
- [ ] `M15-03` LOCAL_EXACT 必须有相同 fixture 的 desktop/WASM 语义或像素 golden。
|
||||
- [ ] `M15-04` LOCAL_BOUNDED 必须同时验证白名单正例和非白名单稳定阻断。
|
||||
- [ ] `M15-05` SERVER 必须验证提交、取消、隔离、预算、hash 和结果绑定。
|
||||
- [ ] `M15-06` EXCLUDED 必须验证 UI 不开放、文件数据不丢失、保存不破坏原项目。
|
||||
- [ ] `M15-07` 12 个 family 分别达到自身退出条件,禁止由 N-026 聚合状态反推完成。
|
||||
- [ ] `M15-04` LOCAL_EQUIVALENT 只用于平台外壳,必须验证最终 Main/文件语义与 desktop 一致。
|
||||
- [ ] `M15-05` SERVER_EXACT 必须验证真实 Blender 提交、取消、隔离、预算、hash 和结果绑定。
|
||||
- [ ] `M15-06` BLOCKED/UNINVENTORIED 必须保持 UI fail-closed、文件保真和可诊断状态。
|
||||
- [ ] `M15-07` 全部生成的 owner family 分别达到自身退出条件,禁止由当前 12-family 或 N-026
|
||||
聚合状态反推完整 Blender core 已完成。
|
||||
- [ ] `M15-08` 全域发布候选重新跑 clean build、acceptance、性能、fault 和安全矩阵。
|
||||
- [ ] `M15-09` 在独立机器复验 binary/source archive 和所有 hash。
|
||||
- [ ] `M15-10` 发布说明逐项列出支持、受限、server 和 excluded 能力。
|
||||
- [ ] `M15-10` 发布说明逐项列出 local exact、platform-equivalent 和 server exact 能力。
|
||||
|
||||
实际领取顺序固定为:先完成 M5;随后 M6、M7;再按产品需求在 M8 至 M13 中一次选择一个
|
||||
最小闭环;M14 只在 Chromium RC 稳定后开始;M15 是状态审计,不是把剩余功能一次性打包。
|
||||
历史领取顺序为先完成 M5,随后 M6、M7,再在 M8-M11 中一次选择一个最小闭环。当前只领取
|
||||
机器 `nextTask`;M12 起使用完整对标计划的原子任务,M15 是状态审计和差距生成,不是把剩余
|
||||
功能一次性打包。
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
|
||||
当前短周期任务、领取顺序和阶段退出条件统一维护在
|
||||
`docs/CURRENT_EXECUTION_PLAN.md`。本文件保留实现事实、长期能力台账和完整验收命令,不再作为
|
||||
V1 的逐项领取顺序;V1 范围以 `docs/WEB_BLENDER_MODELER_V1_SCOPE.md` 为准。
|
||||
V1 的逐项领取顺序;V1 范围以 `docs/WEB_BLENDER_MODELER_V1_SCOPE.md` 为准,M12-M23 全功能
|
||||
实施规则以 `docs/BLENDER_5_2_FULL_WEB_PARITY_EXECUTION_PLAN.md` 为准。
|
||||
|
||||
## 1. 当前结论
|
||||
|
||||
@@ -29,19 +30,21 @@ SQLite WASM 和 Bitbybit/OCCT 均不在当前依赖范围内。Three.js、WASM
|
||||
| --- | --- | --- | --- |
|
||||
| V1 release family | 12/12 `READY` | 0 | V1 已声明功能闭环完成 |
|
||||
| Blender 5.2 全域 parity | 0/12 complete | 12/12 `BLOCKED` | 不是完整 Blender Web 移植 |
|
||||
| slice 台账 | 191 completed | 58 blocked | blocked 项均留在 M9-M15 长期路线 |
|
||||
| slice 台账 | 195 completed | 58 blocked | blocked 项均留在 M9-M15 长期路线 |
|
||||
| acceptance | 50 declarations / 49 unique passed | 0 failed | 本地 V1 RC 证据完整 |
|
||||
| M6 可部署 RC | 71/71 原子任务 | 0 | 可部署 RC 已冻结 |
|
||||
| M7 核心体验硬化 | 18/18 原子任务 | 0 | 已完成并进入持续回归 |
|
||||
| M8 VDB 自动分页 | 20/20 原子任务 | 0 | 联合重开与 desktop/main/Offscreen 三轴 golden 通过 |
|
||||
| M9 非 Mesh/GP/Paint | 14/14 原子任务 | 0 | `TOGGLE_CYCLIC` 已通过 Main/undo/save/reopen 与 Blender 5.2 golden;GP current-drawing marquee、2D/3D 共享 selection revision 及 layer/frame reorder 已通过 Main/undo/save/reopen 门;Paint 主线程/Offscreen 真实 GPU depth、单 undo 分块 pointer session、normalize/limit/mirror 权重与 Blender 5.2 golden、packed/UDIM dirty tile 原子资产绑定,以及 46 项 PBVH brush 的 WASM 入口显式阻断已通过;Curve、Grease Pencil、Paint 三域的 Worker restart、OOM、GPU release、小场景恢复已通过;PBVH/桌面 brush 求值仍 BLOCKED |
|
||||
| M10 GN/Shader/NLA/Simulation | 15/15 原子任务 | 0 | GN/Simulation cache、Shader、NLA 和 Physics 有界闭环全部完成;M10-15 以四个隔离 Chromium Worker 分别通过性能、超预算/OOM-prevention、恶意输入和同会话小输入恢复门 |
|
||||
| M11 Lighting/Render/Compositor/Media | 13/14 原子任务 | 1 | M11-01/02 已冻结字段 parity 并完成支持字段闭环;M11-03 已让双 viewport 共用资源预算;M11-04 已完成 Blender Eevee reference 图像指标;M11-05/06 已完成最终渲染路由/provenance;M11-07/08 已完成有限 Compositor golden 与 Unsupported 全图阻断;M11-09/10/11/12 已完成 codec/proxy/revision/export gate;M11-13 已完成实时 AudioContext 恢复门 |
|
||||
| M11 Lighting/Render/Compositor/Media | 14/14 原子任务 | 0 | M11-01/02 已冻结字段 parity 并完成支持字段闭环;M11-03 已让双 viewport 共用资源预算;M11-04 已完成 Blender Eevee reference 图像指标;M11-05/06 已完成最终渲染路由/provenance;M11-07/08 已完成有限 Compositor golden 与 Unsupported 全图阻断;M11-09/10/11/12 已完成 codec/proxy/revision/export gate;M11-13 已完成实时 AudioContext 恢复门;M11-14 已完成 Render/Compositor/Media 三域取消、重启、预算、释放和恢复门 |
|
||||
| M12 Asset/IO/Editors | M12-01A-I + M12-02A-H + M12-03A-D 共 21 项 | Append undo/redo/save/reopen、Link/Override 与后续 IO/Editor 仍未完成 | WASM Main 已有单 transaction append slice,但 N-023 全域 parity 仍阻断 |
|
||||
|
||||
当前优先级不是扩 Blender 全域功能。single/pthread、真实 HTTP、缓存升级、离线闭环、
|
||||
独立归档复验、运维 runbook、RC 文档和最终三条 CI lane 均已通过;M7 核心项目体验硬化
|
||||
18/18 已完成并进入持续回归;M9 已完成 14/14;M10 已完成 15/15 并进入持续回归;M11 当前
|
||||
13/14;后续领取点只读取机器队列最新 `nextTask`。
|
||||
18/18 已完成并进入持续回归;M9 已完成 14/14;M10 已完成 15/15 并进入持续回归;M11 已完成
|
||||
14/14;M12-01A-I、M12-02A-H 和 M12-03A-D 已完成,机器队列的当前唯一 `nextTask` 为完整对标计划中的
|
||||
`M12-03E`。
|
||||
|
||||
## 2. 已完成并有测试覆盖的能力
|
||||
|
||||
|
||||
64
docs/status/M11-14.md
Normal file
64
docs/status/M11-14.md
Normal file
@@ -0,0 +1,64 @@
|
||||
# M11-14 Status
|
||||
|
||||
status: done
|
||||
task: close render, compositor, and media cancellation/restart/resource recovery
|
||||
updated: 2026-08-17 America/New_York
|
||||
|
||||
## Scope
|
||||
|
||||
Schema 1 evidence fixes one report for each of `RENDER`, `COMPOSITOR`, and `MEDIA`.
|
||||
Every report binds real source bytes, a domain-specific cancellation code, a domain-specific
|
||||
budget code, generation 1/2 identity and output hashes, positive release receipts, zero
|
||||
owned resources after release, and a non-empty recovered output. The suite parser rejects
|
||||
undeclared fields, missing or duplicate domains, published cancellation results, restart
|
||||
drift, budget mutation, and incomplete release.
|
||||
|
||||
The Chromium production-path case opens real Blender Main fixtures for Render and
|
||||
Compositor and decodes a real H.264 fixture for Media. Render reads an actual WebGL RGBA8
|
||||
frame, Compositor executes the real Main GraphIR into Float32 output, and Media generates
|
||||
the initial RGBA8 proxy frame. This task closes only bounded lifecycle behavior; it does not
|
||||
claim full Eevee/Cycles parity, arbitrary compositor nodes, frame-exact long-video decode,
|
||||
or complete audio/video rendering.
|
||||
|
||||
## Evidence
|
||||
|
||||
- `WEB_TEST_PORT=5592 npm --prefix web run test:render-compositor-media-recovery` passed:
|
||||
protocol negatives 3/3, Blender 5.2 Eevee reference checker, Blender 5.2 Compositor
|
||||
golden checker, and Chromium 1/1.
|
||||
- Render used `m11_render_reference.blend`, rejected a pre-cancelled open with
|
||||
`OPEN_CANCELLED`, preserved the active Main, restarted WebEngine at generation 2, and
|
||||
reproduced the same scene identity and WebGL output SHA-256. Aggregate texture overflow
|
||||
returned `GPU_TEXTURE_BUDGET_EXCEEDED` without replacing the retained texture; dispose
|
||||
reported positive released bytes/resources followed by zero owned textures.
|
||||
- Compositor used `m11_compositor_allowlist.blend`, returned `COMPOSITOR_CANCELLED` from the
|
||||
periodic executor check, rejected width 8193 with `COMPOSITOR_BUDGET_EXCEEDED` before
|
||||
cache mutation, reproduced GraphIR/Float32 output after Worker restart, and cleared the
|
||||
LRU to zero.
|
||||
- Media probed and decoded `sequencer-probe.mp4`, rejected a pre-cancelled proxy decode with
|
||||
`SEQUENCER_CANCELLED`, rejected a cache one byte below the frame size with
|
||||
`SEQUENCER_BUDGET_EXCEEDED`, cleared the first cache, then re-probed and re-decoded the
|
||||
H.264 source before reproducing the source/decode/profile identity and RGBA8 payload
|
||||
SHA-256 in a rebuilt session.
|
||||
- Regression passed: full Node 193/193, typecheck, lint, production build (75 modules),
|
||||
render resource budget unit 3/3 plus Chromium main/Offscreen 3/3, Compositor golden unit
|
||||
3/3 plus Chromium 1/1, media proxy cache unit 3/3 plus Chromium 1/1, status consistency,
|
||||
release evidence, and `git diff --check`.
|
||||
|
||||
## Artifact Hashes
|
||||
|
||||
- evidence protocol: `a1b0b36cf93173347d55a955ff2e02f3666f7c28ef7f06442b86f0ee2b272286`
|
||||
- Chromium production suite: `ef9cd58ce3bbd522c72075e82d6423387b61039509996851d8d5ebf3a0d86789`
|
||||
- media proxy cache: `7a1dbf8e838c47e2b99bc8932997e79d3c3cf2636b6285ace083800d833e2e64`
|
||||
- GPU texture store: `a87adefa67b243cfd0cfcad0573a1b646f1e8be8abd0a187e1dc29cab2954d44`
|
||||
- compositor cache: `3ce4819dadb2d6de7305c3676b2708689cd8be95e6fa7894651d6a02c4658b31`
|
||||
- unit: `3cec4a33f28a76423a9678bcb61244cc463f5f2f5939c3f0b6865d34063e1f55`
|
||||
- Chromium spec: `1578848eda7cb26cdf1584fa13f4cc2e252e782959d489dc4454e45c8d06595a`
|
||||
- golden manifest: `87f17b0437d819e00c4a0902146d6549cf713482667ead627a45ba2b46050247`
|
||||
- package: `0e731a5c826a999eef372f875c6c5ce9d697fd369a929025370a74cf18a93582`
|
||||
|
||||
## Rollback
|
||||
|
||||
Remove the recovery protocol, browser suite, unit/Chromium tests, M11-14 golden manifest,
|
||||
and package command. Restore the media proxy generator signature, Compositor cache clear,
|
||||
and GPU texture release receipt only if no later caller depends on them. Remove the three
|
||||
family lifecycle slices, restore M11 to 13/14, and move the queue back from M12-01A.
|
||||
59
docs/status/M12-01A.md
Normal file
59
docs/status/M12-01A.md
Normal file
@@ -0,0 +1,59 @@
|
||||
# M12-01A Status
|
||||
|
||||
status: done
|
||||
task: inventory Blender 5.2 AssetMetaData, tag, and catalog fields
|
||||
updated: 2026-08-17 America/New_York
|
||||
enablingTask: true
|
||||
parityStateChange: false
|
||||
|
||||
## Scope
|
||||
|
||||
The machine inventory freezes the Blender 5.2 storage and public API surface needed before
|
||||
designing a full-parity Web catalog schema. It covers every `AssetMetaData` DNA member,
|
||||
the `AssetTag` storage and collection operations, actual `AssetMetaData` RNA properties,
|
||||
Blend read/write participation, `AssetCatalog` semantic/runtime fields, catalog definition
|
||||
file version/record order, path hierarchy rules, and the current Web v1 interface snapshot.
|
||||
|
||||
This task deliberately does not create the desktop catalog fixture, Web schema v2,
|
||||
migrations, IndexedDB behavior, or a new verified parity slice. In particular, a source
|
||||
inventory is not evidence that Web preserves or writes the inventoried fields.
|
||||
|
||||
## Findings
|
||||
|
||||
- `AssetMetaData` has 14 DNA members: 10 persisted semantic/UI members, custom
|
||||
`IDProperty` metadata, one runtime type pointer, one derived tag count, and ABI padding.
|
||||
The real Blender runtime exposes 10 RNA properties.
|
||||
- `author`, `description`, `copyright`, and `license` are optional dynamic strings. Empty
|
||||
values are valid Blender state; the existing Web v1 manifest currently requires non-empty
|
||||
author/license and has no description/copyright fields.
|
||||
- `catalog_id` is the authoritative RFC4122 identity. `catalog_simple_name` is a read-only
|
||||
recovery copy and cannot replace the UUID-to-path mapping.
|
||||
- Catalog hierarchy is implicit in a cleaned UTF-8 slash path. CDF v1 records
|
||||
`UUID:path:simple_name`; no parent UUID is stored. Duplicate paths are legal and resolve
|
||||
deterministically by first-loaded state and UUID.
|
||||
- Existing Web v1 directly represents catalog ID, tags, author, and license, but misses seven
|
||||
inventoried metadata semantics and models hierarchy as an explicit `parentId` graph.
|
||||
|
||||
## Evidence
|
||||
|
||||
- `npm --prefix web run test:asset-catalog-inventory` passed on the first complete run:
|
||||
`dna=14 rna=10 tag=3 catalog=3 gaps=7 next=M12-01B`.
|
||||
- The checker binds eight source SHA-256 values, extracts the RNA property declarations,
|
||||
launches `Blender 5.2.0 LTS --factory-startup --background`, compares the actual runtime
|
||||
property identifiers/types/read-only flags, verifies Blend IO markers and CDF v1 format,
|
||||
and uses the TypeScript AST to freeze the current Web interfaces.
|
||||
- The inventory manifest SHA-256 is
|
||||
`194d5fd0fef5044f5e82a51931a061e226445f0f53410af9dc0594c70a79e936`;
|
||||
the checker SHA-256 is
|
||||
`38839f47dbe82ad7931e5e444e124e87b64a604d9358be6d71cce7769439e57b`.
|
||||
|
||||
## Next Task
|
||||
|
||||
`M12-01B`: generate a desktop Blender 5.2 catalog v1 fixture and canonical JSON from the
|
||||
inventoried fields. Do not begin Web schema v2 until that desktop artifact is verified.
|
||||
|
||||
## Rollback
|
||||
|
||||
Remove the inventory manifest, checker, package command, and this status entry; restore
|
||||
M12-01A to pending and move `nextTask` back from M12-01B. No parity ledger rollback is
|
||||
needed because this enabling task did not change a family slice.
|
||||
55
docs/status/M12-01B.md
Normal file
55
docs/status/M12-01B.md
Normal file
@@ -0,0 +1,55 @@
|
||||
# M12-01B Status
|
||||
|
||||
status: done
|
||||
task: generate a Blender 5.2 catalog v1 fixture and canonical desktop report
|
||||
updated: 2026-08-17 America/New_York
|
||||
enablingTask: true
|
||||
parityStateChange: false
|
||||
|
||||
## Scope
|
||||
|
||||
The locked Blender 5.2 runtime generates a catalog definition v1 file and a `.blend`
|
||||
containing Object, Material, and World assets. A separate Blender process reopens the
|
||||
fixture and exports one canonical JSON report from the inventoried AssetMetaData fields.
|
||||
The report covers catalog hierarchy, assigned and nil catalog UUIDs, optional empty text,
|
||||
ordered tags, active tag, preferred import method, custom ID properties, and Blender's
|
||||
automatically generated Object `dimensions` metadata.
|
||||
|
||||
This is an enabling desktop baseline. It does not define Web schema v2, migration behavior,
|
||||
stable Web asset IDs, or catalog mutation parity, and it does not change a ledger slice.
|
||||
The read-only `catalog_simple_name` remains empty because Blender's RNA catalog UUID setter
|
||||
clears it outside an Asset Browser context; the fixture records that behavior rather than
|
||||
inventing a recovery name.
|
||||
|
||||
## Evidence
|
||||
|
||||
- `npm --prefix web run test:asset-catalog-v1-fixture` passed from a fresh temporary
|
||||
directory: `catalogs=3 assets=3 metadata=complete next=M12-01C`.
|
||||
- The checker verified the locked Blender version and every artifact hash, regenerated the
|
||||
CDF and `.blend`, opened the generated file in a second Blender process, and compared its
|
||||
canonical report exactly with the checked-in report. It then reopened the checked-in
|
||||
fixture in another Blender process and repeated the exact comparison.
|
||||
- Catalog definition v1 contains three deterministic UUID/path/simple-name records, including
|
||||
one nested path. The canonical report contains three assets across MATERIAL, OBJECT, and
|
||||
WORLD, preserves tag order and custom metadata, and records an uncataloged nil UUID.
|
||||
|
||||
## Artifact Hashes
|
||||
|
||||
- generator: `4156ec6293562d44f87ad5a268de52fbfa31a65da8263a67a2bc45bf22999307`
|
||||
- canonical exporter: `3dbccd8df54d8686aa526f7967f58fd1008336c4c5b1deed01a136ba15aa3e41`
|
||||
- checker: `c2813aa36482eff0b23ca0209f488cfe70ea11e4f39cc6c6036d6c47bbc8e17b`
|
||||
- desktop `.blend`: `8facfe82e3ca0a0605c139ef5bdcbb82d27a6953a6add4e008b68af90bc1211f`
|
||||
- catalog definition: `a72f542acb2d2239951a2b20cda0f92305e1d6d8222f84072ad1aab52e3e4c98`
|
||||
- canonical report: `6fc6def0b9d0e4f51ef40c1d87a1f17074f6340c126a01aee49e5e53271c961e`
|
||||
- manifest: `1ee0abf961b82195e5fb586eb9e59302e4f7f839615f1ebbdf5cdcba8b60accd`
|
||||
|
||||
## Next Task
|
||||
|
||||
`M12-01C`: define Web catalog schema v2, stable catalog/asset identity, and bounded text and
|
||||
collection limits from the verified desktop baseline.
|
||||
|
||||
## Rollback
|
||||
|
||||
Remove the M12-01B generator, exporter, checker, fixture, CDF, canonical report, manifest,
|
||||
package command, and this status entry. Restore M12-01B to pending and `nextTask` to
|
||||
M12-01B. No parity ledger rollback is required.
|
||||
50
docs/status/M12-01C.md
Normal file
50
docs/status/M12-01C.md
Normal file
@@ -0,0 +1,50 @@
|
||||
# M12-01C Status
|
||||
|
||||
status: done
|
||||
task: define Web catalog schema v2, stable identities, and budgets
|
||||
updated: 2026-08-17 America/New_York
|
||||
enablingTask: true
|
||||
parityStateChange: false
|
||||
|
||||
## Scope
|
||||
|
||||
Schema v2 represents Blender catalogs with canonical RFC4122 UUIDs and cleaned slash paths;
|
||||
hierarchy is derived from `parentPath`, matching CDF semantics instead of retaining the Web v1
|
||||
`parentId` graph. Asset identity binds Blender's `AssetWeakReference` fields
|
||||
(`asset_library_identifier`, `relative_asset_identifier`) into a SHA-256 Web ID. A Blender
|
||||
rename changes the relative identifier and therefore the stable ID, matching Blender's weak
|
||||
reference behavior rather than promising an identity Blender does not provide.
|
||||
|
||||
All text budgets use UTF-8 bytes. Catalog/tag recovery strings keep Blender's 63-byte payload
|
||||
limit, dynamic metadata permits empty strings, and catalog, asset, tag, custom-property,
|
||||
custom-array, per-string, and aggregate-text counts are bounded. The positive schema fixture
|
||||
is generated from the verified M12-01B desktop canonical report through production protocol
|
||||
code. This task does not implement v1 migration, legacy-reader policy, IndexedDB migration, or
|
||||
catalog mutation.
|
||||
|
||||
## Evidence
|
||||
|
||||
- `npm --prefix web run test:asset-catalog-v2` passed 4/4.
|
||||
- The suite binds the desktop canonical report and Blender 5.2 weak-reference source, regenerates
|
||||
schema v2 from the desktop report, parses the generated golden, and verifies deterministic
|
||||
local/external identity hashes and exposed UTF-8/collection budgets.
|
||||
- `npm --prefix web run typecheck`, `npm --prefix web run lint`, and `git diff --check` passed.
|
||||
|
||||
## Artifact Hashes
|
||||
|
||||
- protocol: `0949838efb2f69466368185b80bb1d2e851672d78311fcd5270eb0814576e5d7`
|
||||
- generator: `b20e7dbd8508ca8ecf17d7da507b45b1225315bac7759c55c7b324683d660881`
|
||||
- unit suite: `73038022124ddc7e683602b47a4804297b3a64ecd4819160c7ec086d2f6190ae`
|
||||
- schema golden: `2abe279cebbffd1341dc61b1fdc5366dd1f6f42769ba36171782019ca61fb929`
|
||||
- manifest: `612892b03a689ebe19804e495bd29744634ab43a3b29fff41adc2d7a5d0b67b9`
|
||||
- Blender weak-reference source: `ace3d6468bcdb2f6190431e967495794b7afa35758fd82fc3941b1ce7d374635`
|
||||
|
||||
## Next Task
|
||||
|
||||
`M12-01D`: define and verify schema v1 to v2 forward migration.
|
||||
|
||||
## Rollback
|
||||
|
||||
Remove the schema v2 protocol, generator, unit suite, M12-01C golden/manifest, package command,
|
||||
and this status entry. Restore M12-01C to pending and `nextTask` to M12-01C. No parity ledger
|
||||
rollback is required.
|
||||
48
docs/status/M12-01D.md
Normal file
48
docs/status/M12-01D.md
Normal file
@@ -0,0 +1,48 @@
|
||||
# M12-01D Status
|
||||
|
||||
status: done
|
||||
task: define and verify schema v1 to v2 forward migration
|
||||
updated: 2026-08-17 America/New_York
|
||||
enablingTask: true
|
||||
parityStateChange: false
|
||||
|
||||
## Scope
|
||||
|
||||
The forward migration first validates the current production schema v1 manifest and rejects
|
||||
undeclared fields before conversion. Canonical UUIDs are preserved; legacy catalog IDs map
|
||||
deterministically through RFC 4122 UUIDv5 with a fixed namespace. The v1 parent graph becomes
|
||||
Blender cleaned slash paths, and assets receive SHA-256 IDs from Blender weak-reference identity.
|
||||
|
||||
Logical revision is preserved. Author/license/tag/source bindings, optional source paths,
|
||||
preview receipts, and the complete library dependency graph survive migration. Metadata absent
|
||||
from v1 receives explicit defaults recorded in the migration report. The report binds canonical
|
||||
source/target hashes and every legacy-to-v2 catalog/asset identity mapping.
|
||||
|
||||
## Evidence
|
||||
|
||||
- `npm --prefix web run test:asset-catalog-migration` passed 3/3.
|
||||
- A schema v1 fixture with three catalogs, two asset kinds, one preview, two source bindings,
|
||||
and two dependent libraries migrated to the checked-in schema v2 golden and exact report.
|
||||
- Repeating the migration on a cloned source produced byte-equivalent semantic objects and the
|
||||
same source/target SHA-256 values. A canonical source UUID remained unchanged.
|
||||
- M12-01C regression, typecheck, lint, and `git diff --check` passed.
|
||||
|
||||
## Artifact Hashes
|
||||
|
||||
- migration protocol: `6d74bc5605f94e05208eda82830192d38f88066dfc409058555a8876c785f226`
|
||||
- generator: `954d15c02d68e3f644b57c8c4c136ce09bd037a0af2012bf74b97f681c77dd60`
|
||||
- unit suite: `6b927e7a8238e4e0a77cc0fdf783b099d8add22c5f875df2299012eb32730186`
|
||||
- v1 fixture: `1cc43d181afefd63b53cb9c3626bb58195fad20187d6e534adc1ecbe65f75da5`
|
||||
- v2 fixture: `38e5d1771b3d2d63ce5e10268eac4513f2043b0cbf71ab1799c1868bc2aa97fb`
|
||||
- migration report: `57b972c1bbb9ae326c3f264c225c21546682442ff3194fbc343e1c46a2cd410a`
|
||||
- manifest: `fea9e01b51f64a7e9c15dcfc521b3e84636525ffbe0a849f7a6f1b0be2b02e9d`
|
||||
|
||||
## Next Task
|
||||
|
||||
`M12-01E`: define the read-only/block policy when a schema v1 reader encounters schema v2.
|
||||
|
||||
## Rollback
|
||||
|
||||
Remove the migration protocol, generator, unit suite, M12-01D fixtures/report/manifest, package
|
||||
command, and this status entry. Restore M12-01D to pending and `nextTask` to M12-01D. No parity
|
||||
ledger rollback is required.
|
||||
45
docs/status/M12-01E.md
Normal file
45
docs/status/M12-01E.md
Normal file
@@ -0,0 +1,45 @@
|
||||
# M12-01E Status
|
||||
|
||||
status: done
|
||||
task: define schema v2 behavior for a schema v1 reader
|
||||
updated: 2026-08-17 America/New_York
|
||||
enablingTask: true
|
||||
parityStateChange: false
|
||||
|
||||
## Scope
|
||||
|
||||
A native schema v1 document remains `READY`. A valid schema v2 document exposed to a v1
|
||||
reader yields a bounded `READ_ONLY` snapshot for metadata inspection, while catalog writes,
|
||||
asset writes, and save all return `ASSET_SCHEMA_DOWNGRADE_BLOCKED`. Unknown future schema
|
||||
versions return non-recoverable `PROTOCOL_MISMATCH` with no snapshot.
|
||||
|
||||
Every result binds a canonical source SHA-256. Inspection does not mutate or downgrade the
|
||||
source document, and no write path can serialize the lossy read-only projection. This task
|
||||
defines compatibility behavior only; it does not implement IndexedDB migration or catalog
|
||||
mutation parity.
|
||||
|
||||
## Evidence
|
||||
|
||||
- `npm --prefix web run test:asset-catalog-legacy-reader` passed 4/4.
|
||||
- Generated evidence covers v2 read-only metadata, all three write/save blocks, native v1
|
||||
readiness, future-schema rejection, equal source hashes, and source object immutability.
|
||||
- M12-01C/D regressions, typecheck, lint, and `git diff --check` passed.
|
||||
|
||||
## Artifact Hashes
|
||||
|
||||
- compatibility protocol: `5d51029b232e03f08fc0778d4933910b2a54818b4d653fbcc185c8d764641349`
|
||||
- error contract: `309ab84d5c755a69466ddb73e4b54e87caf62cbcac0f10f34d904e9f6f4a5f34`
|
||||
- generator: `208f568db092e1712095d77ab28db92d5a1eb11fe62813b237ce466f9268fd4e`
|
||||
- unit suite: `f6477222412044e6784b64c1be83dc9bd6050fe6a955ba5430a4ea963b20dbb4`
|
||||
- compatibility report: `3c2f9241b56ad5bd742dc4152dec703bdc56eef4e6d8d60e6edf163c4571ad54`
|
||||
- manifest: `cf55ea1f9430df04fb55bf07c68c1b2099b044b0da0fdb07a7251b13c459724b`
|
||||
|
||||
## Next Task
|
||||
|
||||
`M12-01F`: add duplicate ID, cyclic parent catalog, overlong text, and unknown-field negatives.
|
||||
|
||||
## Rollback
|
||||
|
||||
Remove the compatibility protocol, generator, unit suite, M12-01E report/manifest, error code,
|
||||
package command, and this status entry. Restore M12-01E to pending and `nextTask` to M12-01E.
|
||||
No parity ledger rollback is required.
|
||||
40
docs/status/M12-01F.md
Normal file
40
docs/status/M12-01F.md
Normal file
@@ -0,0 +1,40 @@
|
||||
# M12-01F Status
|
||||
|
||||
status: done
|
||||
task: reject duplicate IDs, catalog cycles, oversized text, and unknown fields
|
||||
updated: 2026-08-17 America/New_York
|
||||
enablingTask: true
|
||||
parityStateChange: false
|
||||
|
||||
## Scope
|
||||
|
||||
The negative matrix executes production schema v2 and migration parsers against duplicate
|
||||
catalog/asset IDs, a legacy parent cycle, an inconsistent v2 `parentPath`, 64-byte UTF-8
|
||||
simple-name/tag values, and unknown top-level/nested fields in both v1 and v2. The byte cases
|
||||
use multi-byte text to prove limits are not JavaScript character counts.
|
||||
|
||||
Every failure returns a stable asset error code. The valid v2 fixture is parsed after each
|
||||
negative case to prove the failed input did not poison parser state. This task adds negative
|
||||
evidence only and does not change catalog parity status.
|
||||
|
||||
## Evidence
|
||||
|
||||
- `npm --prefix web run test:asset-catalog-negatives` passed 2/2 with 9/9 negative cases.
|
||||
- Duplicate/cycle/parent/unknown cases returned `ASSET_MANIFEST_INVALID`; both 64-byte UTF-8
|
||||
payloads returned `ASSET_BUDGET_EXCEEDED`.
|
||||
- Typecheck, lint, and `git diff --check` passed.
|
||||
|
||||
## Artifact Hashes
|
||||
|
||||
- unit suite: `3a11605ac8666c8c4e1e2e85ccfd423115915820d0de6f92bdf72dff1f01ce41`
|
||||
- negative cases: `74672fa0aec248fd5d9a265743ef4dfe4d417fce8a24622b8b3d6be2f95c8874`
|
||||
- manifest: `127b2b605ac79a891cd6ab516ee031c02624a512e1043b111bfac2ebf1d8278d`
|
||||
|
||||
## Next Task
|
||||
|
||||
`M12-01G`: prove an IndexedDB migration failure preserves the old transaction and index.
|
||||
|
||||
## Rollback
|
||||
|
||||
Remove the M12-01F negative suite, golden/manifest, package command, and this status entry.
|
||||
Restore M12-01F to pending and `nextTask` to M12-01F. No parity ledger rollback is required.
|
||||
47
docs/status/M12-01G.md
Normal file
47
docs/status/M12-01G.md
Normal file
@@ -0,0 +1,47 @@
|
||||
# M12-01G Status
|
||||
|
||||
status: done
|
||||
task: preserve the old catalog index when an IndexedDB migration fails
|
||||
updated: 2026-08-17 America/New_York
|
||||
enablingTask: true
|
||||
parityStateChange: false
|
||||
|
||||
## Scope
|
||||
|
||||
The production IndexedDB migration prepares schema v2 with the existing schema v1 parser and
|
||||
migrator before opening a write transaction. It then rechecks the source row and atomically
|
||||
writes the v2 index and migration receipt while deleting the v1 row. A concurrent source change
|
||||
returns `REVISION_CONFLICT`; a transaction failure returns `STORAGE_TRANSACTION`.
|
||||
|
||||
The browser suite injects failures after the v2 target write and after the v1 source delete.
|
||||
After each abort it closes and reopens the database, proving the database version, complete v1
|
||||
index, unrelated legacy migration row, absent v2 row, and absent receipt are byte-for-byte
|
||||
equivalent to the baseline. A subsequent retry commits the v2 index and receipt together.
|
||||
|
||||
## Evidence
|
||||
|
||||
- `WEB_TEST_PORT=5593 npm --prefix web run test:asset-catalog-indexeddb-migration` passed the
|
||||
M12-01D production migration unit suite 3/3, the M12-01G artifact binding unit 1/1, and
|
||||
Chromium 1/1.
|
||||
- Both `AFTER_TARGET_PUT` and `AFTER_SOURCE_DELETE` returned `STORAGE_TRANSACTION`; reopening
|
||||
IndexedDB after either fault produced the exact pre-migration snapshot.
|
||||
- The successful retry preserved revision 7 and the M12-01D source/target hashes, removed only
|
||||
the v1 row, and retained the unrelated migration row.
|
||||
- Typecheck and `git diff --check` passed.
|
||||
|
||||
## Artifact Hashes
|
||||
|
||||
- production IndexedDB migration: `283a08bccb5ea301bd2aef8f01d4496972e3962f75e340479336c50e3539ba57`
|
||||
- browser suite: `82c40f38898277b4053a9654537ec9c21bc01a784d1091831e139361575a81e9`
|
||||
- artifact unit: `a4366a16f28b91b3ff213e0232308eaecd392731f27972cfa49a11568eb0feca`
|
||||
- manifest: `2596067956211698f1b7a07d9423411e2db830b72e6b78593354953a568738e7`
|
||||
|
||||
## Next Task
|
||||
|
||||
`M12-01H`: prove catalog ordering and asset identity survive page and Worker restarts.
|
||||
|
||||
## Rollback
|
||||
|
||||
Remove the IndexedDB migration module, M12-01G unit/browser suites, manifest, package command,
|
||||
and this status entry. Restore M12-01G to pending and `nextTask` to M12-01G. No parity ledger
|
||||
rollback is required.
|
||||
47
docs/status/M12-01H.md
Normal file
47
docs/status/M12-01H.md
Normal file
@@ -0,0 +1,47 @@
|
||||
# M12-01H Status
|
||||
|
||||
status: done
|
||||
task: preserve catalog order and asset identity across page and Worker restarts
|
||||
updated: 2026-08-17 America/New_York
|
||||
enablingTask: true
|
||||
parityStateChange: false
|
||||
|
||||
## Scope
|
||||
|
||||
The production catalog index loader validates the stored schema v2 manifest, recomputes its
|
||||
canonical SHA-256, and compares it with the committed migration receipt before publishing a
|
||||
snapshot. The snapshot exposes persisted catalog order, asset order, stable asset IDs, Blender
|
||||
weak-reference components, revision, and manifest identity.
|
||||
|
||||
A page migrates the M12-01D v1 fixture, reloads, and reads the same database again. Two separate
|
||||
module Workers then open the database in sequence, with generation 1 terminated before
|
||||
generation 2 starts. All four contexts must return the same complete snapshot.
|
||||
|
||||
## Evidence
|
||||
|
||||
- `WEB_TEST_PORT=5594 npm --prefix web run test:asset-catalog-restart` passed Node 5/5 and
|
||||
Chromium 2/2, including the full M12-01G abort/reopen regression.
|
||||
- Initial page, reloaded page, Worker generation 1, and Worker generation 2 returned exactly the
|
||||
same 3 catalog IDs in order, 2 asset IDs in order, weak-reference components, revision 7, and
|
||||
manifest SHA-256.
|
||||
- A stored receipt whose target hash or revision drifts from the parsed v2 index is rejected as
|
||||
`ASSET_MANIFEST_INVALID` before a snapshot is published.
|
||||
- Typecheck, lint, and `git diff --check` passed.
|
||||
|
||||
## Artifact Hashes
|
||||
|
||||
- production IndexedDB reader: `283a08bccb5ea301bd2aef8f01d4496972e3962f75e340479336c50e3539ba57`
|
||||
- restart Worker: `e6007d352555be046b89d0becd719692652ee0418cc97087bf7599155f2a538e`
|
||||
- browser suite: `7945ff38dd474a74c265594b66534a280eb69407509137e60d41f1bea3b3c19b`
|
||||
- artifact unit: `eb22b5cc852fa47d9bdd30afec2977facfca0dfa554e7ce738dc1bf8c4bf2122`
|
||||
- manifest: `59138eb83d63ae0d397b152e587fbe005d02bc2d229ee484c279b452ea3f2eae`
|
||||
|
||||
## Next Task
|
||||
|
||||
`M12-01I`: bind the schema fixture, migration report, and hashes into M12-01 evidence.
|
||||
|
||||
## Rollback
|
||||
|
||||
Remove the restart snapshot reader, Worker/browser/unit suites, M12-01H manifest, package
|
||||
command, and this status entry. Restore M12-01H to pending and `nextTask` to M12-01H. No parity
|
||||
ledger rollback is required.
|
||||
44
docs/status/M12-01I.md
Normal file
44
docs/status/M12-01I.md
Normal file
@@ -0,0 +1,44 @@
|
||||
# M12-01I Status
|
||||
|
||||
status: done
|
||||
task: bind the schema fixture, migration report, and hashes into M12-01 evidence
|
||||
updated: 2026-08-17 America/New_York
|
||||
enablingTask: true
|
||||
parityStateChange: false
|
||||
|
||||
## Scope
|
||||
|
||||
The M12-01 aggregate evidence binds all eight predecessor manifests in exact queue order and
|
||||
recursively verifies their declared source and artifact hashes. It separately binds the locked
|
||||
Blender 5.2 desktop canonical fixture, Web schema v2 golden and protocol, schema v1/v2 migration
|
||||
fixtures, production migration report, IndexedDB transaction implementation, and restart Worker.
|
||||
|
||||
The checker recomputes canonical source and target manifest SHA-256 values, matches them to the
|
||||
migration report, verifies revision and preserved counts, and compares the persisted restart
|
||||
catalog/asset order with the target fixture. This closes the schema/migration evidence section;
|
||||
it does not claim catalog mutation or preview parity.
|
||||
|
||||
## Evidence
|
||||
|
||||
- `WEB_TEST_PORT=5595 npm --prefix web run test:asset-catalog-m12-evidence` passed the Blender
|
||||
5.2 inventory and regenerated fixture checks, schema v2 4/4, migration 3/3, legacy-reader 4/4,
|
||||
negatives 2/2, transaction/restart Node 5/5, Chromium 2/2, and final aggregate checker.
|
||||
- The final checker reported `subtasks=8 catalogs=3 assets=2 next=M12-02A`.
|
||||
- Source revision 7 and target revision 7 canonical hashes match the production migration report;
|
||||
all A-H task links and recursive artifact hashes resolve without gaps.
|
||||
- Typecheck, lint, status consistency, and `git diff --check` passed.
|
||||
|
||||
## Artifact Hashes
|
||||
|
||||
- M12-01 evidence: `2b99f64a207c2ffb966963be8a2150feef67d5bf78946939d3a49a6335b55bc3`
|
||||
- aggregate checker: `7d0d9f5390648d4af79a8da6226627edb1bf99945bae1a7b1acfb2c5e2419a8b`
|
||||
- package command: `a810fec6a9a24cad94cb01475f9529ac2f85da827d391a70ab7ddb57421003f8`
|
||||
|
||||
## Next Task
|
||||
|
||||
`M12-02A`: inventory Blender preview image dimensions, color space, and no-preview state.
|
||||
|
||||
## Rollback
|
||||
|
||||
Remove the M12-01 aggregate evidence, checker, package command, and this status entry. Restore
|
||||
M12-01I to pending and `nextTask` to M12-01I. No parity ledger rollback is required.
|
||||
43
docs/status/M12-02A.md
Normal file
43
docs/status/M12-02A.md
Normal file
@@ -0,0 +1,43 @@
|
||||
# M12-02A Status
|
||||
|
||||
status: done
|
||||
task: inventory Blender preview dimensions, color semantics, and no-preview state
|
||||
updated: 2026-08-17 America/New_York
|
||||
enablingTask: true
|
||||
parityStateChange: false
|
||||
|
||||
## Scope
|
||||
|
||||
The machine inventory binds Blender 5.2 PreviewImage DNA, slot enum, BKE/RNA implementation,
|
||||
Blend reader, asset preview generator, render-size constants, current Web v1/v2 preview protocols,
|
||||
and two runtime fixtures. It records both ICON and PREVIEW slots, persisted dimensions/flags/pixel
|
||||
arrays, runtime-only state, and absent-pointer behavior.
|
||||
|
||||
Preview pixels are packed 32-bit RGBA with four byte components and Blender's deferred load path
|
||||
premultiplies alpha. PreviewImage has no persisted or RNA color-space field, and float access only
|
||||
divides byte components by 255 without a color transform. The inventory therefore records color
|
||||
space as unknown instead of assuming sRGB.
|
||||
|
||||
## Evidence
|
||||
|
||||
- `npm --prefix web run test:asset-preview-inventory` launched locked Blender 5.2 twice and
|
||||
reported `slots=2 rna=9 absent=3 loaded=8x8 next=M12-02B`.
|
||||
- The checked-in asset fixture reopened with Object, Material, and World `preview == null`.
|
||||
- A real 8x8 PNG loaded through Blender's preview API produced an 8x8 PREVIEW slot, 32x32 ICON
|
||||
slot, 64/256 image packed/float counts, and 1024 icon packed pixels.
|
||||
- Typecheck, lint, status consistency, and `git diff --check` passed.
|
||||
|
||||
## Artifact Hashes
|
||||
|
||||
- inventory: `a4427426d94afdf1a2ccf5379d76d1797f8604a742a3f0d15bdca629272864bd`
|
||||
- checker: `c9cc83d21bac5f34c6d68cb1225dffcd92a5ec76c158ddce909b620cbbcb245c`
|
||||
- package command: `04f832a3c5773c8cc5294ecdcf175478eed9710496e48c63698d79043384e328`
|
||||
|
||||
## Next Task
|
||||
|
||||
`M12-02B`: define preview source/content SHA-256, MIME, dimensions, and generator identity.
|
||||
|
||||
## Rollback
|
||||
|
||||
Remove the M12-02A inventory, checker, package command, and this status entry. Restore M12-02A
|
||||
to pending and `nextTask` to M12-02A. No parity ledger rollback is required.
|
||||
48
docs/status/M12-02B.md
Normal file
48
docs/status/M12-02B.md
Normal file
@@ -0,0 +1,48 @@
|
||||
# M12-02B Status
|
||||
|
||||
status: done
|
||||
task: define preview source, content, dimensions, MIME, and generator identity
|
||||
updated: 2026-08-17 America/New_York
|
||||
enablingTask: true
|
||||
parityStateChange: false
|
||||
|
||||
## Scope
|
||||
|
||||
The schema binds source bytes and encoded content as separate SHA-256/length/MIME identities.
|
||||
Encoded content additionally binds width, height, RGBA8, explicit sRGB, and alpha mode. Generator
|
||||
identity binds name, Blender version, executable hash, generator script hash, and canonical
|
||||
settings hash. A domain-separated canonical hash covers the complete record.
|
||||
|
||||
The locked Blender 5.2 generator re-encodes a real 8x8 PNG with explicit PNG RGBA8, compression,
|
||||
display, view-transform, exposure, and gamma settings. The source and content encodings have
|
||||
different hashes and lengths while a second Blender process confirms decoded pixels are exact.
|
||||
|
||||
## Evidence
|
||||
|
||||
- `npm --prefix web run test:asset-preview-identity` passed 3/3 protocol tests and the locked
|
||||
Blender generator checker.
|
||||
- Two independent generation runs matched the checked-in 513-byte content SHA-256 exactly; the
|
||||
261-byte source has a different SHA-256, and decoded pixel maximum error is zero.
|
||||
- Source bytes, content bytes, generator settings, dimensions, identity hash, and unknown fields
|
||||
each have fail-closed negative coverage.
|
||||
- Typecheck and `git diff --check` passed.
|
||||
|
||||
## Artifact Hashes
|
||||
|
||||
- protocol: `2a1d877af42424014097c669e9d44c21b27e3171be185320554693cbefcd7438`
|
||||
- generator: `753a26be90f7d97828737ebc3a4ab88275a7c655b4f9992406c7c35fba6535ce`
|
||||
- checker: `4d8ece1a5440d202f4e79a48f1f26bc75b3fb11a02d821b8ebc5aee14b11e41a`
|
||||
- unit suite: `5b7b3ccaf410d627365579fa82bf2b48fed9ef17144aad963cdbd4bb7fbd4b5a`
|
||||
- identity golden: `1b1eb8416ce4aa4d83b0dc42b9cb375f1da965e3f0cb9650991289fc619a0a7b`
|
||||
- content PNG: `ff139c4a1c5d388c78c5168dfd3272e80ea0d1b15b3743e29a38cd908e8c70e7`
|
||||
- manifest: `096f125461b963ee2c414f98f6bb00f02034a380a0cc9c43e0d84754324adb17`
|
||||
|
||||
## Next Task
|
||||
|
||||
`M12-02C`: enforce MIME, pixel, byte, and compression-ratio budgets before preview decode.
|
||||
|
||||
## Rollback
|
||||
|
||||
Remove the preview identity protocol, generator/checker, unit suite, M12-02B goldens, package
|
||||
command, error code, and this status entry. Restore M12-02B to pending and `nextTask` to M12-02B.
|
||||
No parity ledger rollback is required.
|
||||
46
docs/status/M12-02C.md
Normal file
46
docs/status/M12-02C.md
Normal file
@@ -0,0 +1,46 @@
|
||||
# M12-02C Status
|
||||
|
||||
status: done
|
||||
task: enforce MIME, pixel, byte, and compression-ratio budgets before preview decode
|
||||
updated: 2026-08-17 America/New_York
|
||||
enablingTask: true
|
||||
parityStateChange: false
|
||||
|
||||
## Scope
|
||||
|
||||
The schema 1 pre-decode planner accepts only the M12-02B content identity and an exact
|
||||
`ArrayBuffer` payload. It checks the 16 MiB encoded-byte limit before hashing, then binds the
|
||||
content SHA-256, declared MIME to the PNG/WebP container header, header dimensions to the identity,
|
||||
and dimensions to pixel and RGBA8 decoded-byte budgets. The final gate limits decoded-to-encoded
|
||||
compression ratio to 100 before any browser image decoder or decoded allocation is invoked.
|
||||
|
||||
PNG dimensions come only from a valid signature and first IHDR header. WebP dimensions come only
|
||||
from a size-bound RIFF/WEBP VP8X, VP8L, or VP8 header. Unsupported headers, content drift, and
|
||||
dimension drift fail closed; a rejected input does not affect a following valid preview plan.
|
||||
|
||||
## Evidence
|
||||
|
||||
- `npm --prefix web run test:asset-preview-decode-budget` passed 5/5 M12-02B/C protocol tests.
|
||||
- The positive 513-byte Blender-generated PNG plans 64 pixels and 256 decoded RGBA8 bytes without
|
||||
calling a decoder.
|
||||
- Eight negative cases cover payload hash/length drift, MIME mismatch, dimension mismatch,
|
||||
over-width/height and decoded-byte budget, compression ratio, encoded-byte budget, and corrupt
|
||||
container signature. The checked-in small preview passes immediately after each rejection.
|
||||
- `npm --prefix web run typecheck`, `npm --prefix web run lint`, and `git diff --check` passed.
|
||||
|
||||
## Artifact Hashes
|
||||
|
||||
- decode protocol: `3d1c491af2503a7a6012541ed43400d680f8e666cc12a54db284aeb6ac18ff63`
|
||||
- unit suite: `1a65df86d568ae8defd7cd9242f426ac900d92637eb3bed2cbb4a0fae2e86c20`
|
||||
- package commands: `8563916637fc3cb29b53432ae9b80d1dcf6269f2f5c092b595e72c6b0ada55ab`
|
||||
- manifest: `934b3ca29d905ad2f5c46f35acff4cf4b9ea291c423fb3aad4afe7e9f10e6244`
|
||||
|
||||
## Next Task
|
||||
|
||||
`M12-02D`: commit a catalog preview reference only after its content-addressed payload is durably
|
||||
written to OPFS.
|
||||
|
||||
## Rollback
|
||||
|
||||
Remove the preview decode planner, unit suite, M12-02C manifest, package command, and this status
|
||||
entry. Restore M12-02C to pending and `nextTask` to M12-02C. No parity ledger rollback is required.
|
||||
49
docs/status/M12-02D.md
Normal file
49
docs/status/M12-02D.md
Normal file
@@ -0,0 +1,49 @@
|
||||
# M12-02D Status
|
||||
|
||||
status: done
|
||||
task: commit a catalog preview reference only after verified OPFS persistence
|
||||
updated: 2026-08-17 America/New_York
|
||||
enablingTask: true
|
||||
parityStateChange: false
|
||||
|
||||
## Scope
|
||||
|
||||
The production coordinator first runs the M12-02C pre-decode gate, writes the content-addressed
|
||||
preview to OPFS, reads it back, and repeats identity verification. Only then may one IndexedDB
|
||||
transaction advance the catalog revision and write both the v2 preview reference and an M12-02D
|
||||
head receipt. The receipt binds project, asset, base/current revision, base/current manifest hash,
|
||||
preview identity, payload identity, and OPFS path.
|
||||
|
||||
The transaction rereads and compares the complete source catalog row before publication. A stale
|
||||
revision or changed row returns `REVISION_CONFLICT`; OPFS and transaction faults return
|
||||
`STORAGE_TRANSACTION`. An OPFS payload left by a failure is unreferenced, never presented as a
|
||||
committed catalog preview.
|
||||
|
||||
## Evidence
|
||||
|
||||
- `WEB_TEST_PORT=5596 npm --prefix web run test:asset-preview-opfs-commit` passed the artifact unit
|
||||
test 1/1 and the real Chromium OPFS/IndexedDB test 1/1.
|
||||
- `BEFORE_OPFS_WRITE`, `AFTER_OPFS_WRITE`, and `AFTER_CATALOG_PUT` faults all preserved the complete
|
||||
revision 7 catalog and left no commit receipt. Only the latter two left an unreferenced payload.
|
||||
- Success persisted and reread the 513-byte payload before advancing the catalog to revision 8;
|
||||
closing and reopening IndexedDB reproduced the same preview and receipt and rehashed OPFS bytes.
|
||||
- `npm --prefix web run typecheck`, `npm --prefix web run lint`, and `git diff --check` passed.
|
||||
|
||||
## Artifact Hashes
|
||||
|
||||
- commit coordinator: `36418e480b108eb885b5d8da6633174ed1face1fa62742a64372a45280e727a2`
|
||||
- Chromium suite: `043b9b06d4c0275938cc1d4f9837df95f58f281130f3c0f2b90c10540244592e`
|
||||
- unit suite: `f489a1d9a4c5fb24a99bce6d31841534c7cf3981b53076a0d9e19d106e9d0d74`
|
||||
- manifest: `34fe6c6ab097e58f7b648dd84cb919ecb407a5742e387c513bb3ed53db50eed6`
|
||||
- package commands: `7363a8a03cb5f6b1d6bf40d8eabc90b9d48e91aeb4787b175deb4f242c676243`
|
||||
|
||||
## Next Task
|
||||
|
||||
`M12-02E`: deduplicate identical preview content without allowing different asset metadata to
|
||||
overwrite the shared payload.
|
||||
|
||||
## Rollback
|
||||
|
||||
Remove the OPFS/catalog commit coordinator, Chromium and unit suites, M12-02D manifest, package
|
||||
command, and this status entry. Restore M12-02D to pending and `nextTask` to M12-02D. No parity
|
||||
ledger rollback is required.
|
||||
45
docs/status/M12-02E.md
Normal file
45
docs/status/M12-02E.md
Normal file
@@ -0,0 +1,45 @@
|
||||
# M12-02E Status
|
||||
|
||||
status: done
|
||||
task: deduplicate identical preview content without merging asset metadata
|
||||
updated: 2026-08-17 America/New_York
|
||||
enablingTask: true
|
||||
parityStateChange: false
|
||||
|
||||
## Scope
|
||||
|
||||
Two catalog assets with distinct Blender weak-reference identities and distinct catalog metadata
|
||||
may bind the same verified preview content. The M12-02D coordinator keys OPFS storage only by the
|
||||
content SHA-256, while catalog entries and preview identity hashes remain asset-specific. A second
|
||||
commit of identical bytes reuses the existing payload after a full readback/hash check.
|
||||
|
||||
No catalog metadata is derived from or stored inside the shared payload record. Each transaction
|
||||
updates only its targeted asset's preview field and advances the guarded catalog revision.
|
||||
|
||||
## Evidence
|
||||
|
||||
- `WEB_TEST_PORT=5597 npm --prefix web run test:asset-preview-dedup` passed the artifact unit test
|
||||
1/1 and real Chromium OPFS/IndexedDB test 1/1.
|
||||
- Two distinct asset/identity hashes committed at revisions 8 and 9; the second commit reported
|
||||
`deduplicated=true` and the OPFS content directory contained exactly one final file.
|
||||
- Both catalog entries referenced the same 513-byte/hash payload while their complete non-preview
|
||||
metadata remained byte-for-byte equal to their different baseline records.
|
||||
- Database reopen reproduced revision 9, the second asset head receipt, and the shared preview;
|
||||
stored payload bytes rehashed to the declared content digest.
|
||||
|
||||
## Artifact Hashes
|
||||
|
||||
- Chromium suite: `4e53b7a35eb109b152bd6dc0c3348dcb08879d17619968d37fc761c67aa38b40`
|
||||
- unit suite: `e023fdd33e0f40c1331dfdd73cd287dc684256b9d134971ab17db8cfe2a83835`
|
||||
- manifest: `7d25f1d8335481795257c643c8589d1607a865634b27a7e5d145a57ce2a92a62`
|
||||
- package commands: `5a83676e821bb768f6d3e282ea4728413cf84c5f3d57672fc664fac3883873e5`
|
||||
|
||||
## Next Task
|
||||
|
||||
`M12-02F`: quarantine corrupt preview content while keeping asset metadata readable.
|
||||
|
||||
## Rollback
|
||||
|
||||
Remove the deduplication Chromium/unit suites, M12-02E manifest, package command, and this status
|
||||
entry. Restore M12-02E to pending and `nextTask` to M12-02E. The shared coordinator and parity
|
||||
ledger do not require rollback.
|
||||
49
docs/status/M12-02F.md
Normal file
49
docs/status/M12-02F.md
Normal file
@@ -0,0 +1,49 @@
|
||||
# M12-02F Status
|
||||
|
||||
status: done
|
||||
task: quarantine corrupt preview content while keeping asset metadata readable
|
||||
updated: 2026-08-17 America/New_York
|
||||
enablingTask: true
|
||||
parityStateChange: false
|
||||
|
||||
## Scope
|
||||
|
||||
The preview inspector validates payload byte length and SHA-256 independently from the catalog.
|
||||
Healthy bytes return `READY`. Missing or mismatched bytes return a structured
|
||||
`ASSET_SOURCE_HASH_MISMATCH` quarantine receipt; present corrupt bytes are copied, verified, and
|
||||
then removed from the content-addressed namespace before the receipt is published.
|
||||
|
||||
The catalog row and its preview provenance are not rewritten on corruption. Consumers receive the
|
||||
complete parsed asset metadata together with `preview=null/data=null`, so a broken optional image
|
||||
cannot make the asset name, type, catalog, license, tags, or source metadata unreadable. A matching
|
||||
quarantine receipt is stable across database reopen.
|
||||
|
||||
## Evidence
|
||||
|
||||
- `WEB_TEST_PORT=5598 npm --prefix web run test:asset-preview-quarantine` passed the artifact unit
|
||||
test 1/1 and real Chromium OPFS/IndexedDB test 1/1.
|
||||
- The checked-in 513-byte PNG first returned `READY`; same-length byte tampering then returned
|
||||
`QUARANTINED` with a different actual SHA-256 and stable error code.
|
||||
- The corrupt file was absent from the SHA-256 namespace and present as a verified 513-byte file in
|
||||
the quarantine directory. Database reopen returned the exact same receipt without reading it.
|
||||
- Catalog revision 8, the target asset metadata/reference, and the unrelated asset all remained
|
||||
exactly unchanged.
|
||||
|
||||
## Artifact Hashes
|
||||
|
||||
- quarantine protocol: `a5f3330dbd16d9f578d85d9504c980fffea85b834b77340a307ce21ce9b1cb04`
|
||||
- Chromium suite: `d8e1d0741765e8f8bfd47b45cf03213d8629efdbbf77dfd33846785a3ec80c27`
|
||||
- unit suite: `79c836c7a3be4e9f305a962775444c766577cca87fa6d1698811e75eb21f2d9a`
|
||||
- manifest: `d1a9feeb98c5332b1a14a14c5b813d75a29f172601590e1b5cde254c5059a7c5`
|
||||
- package commands: `77b5cd3d3af68ac390d8fbdb9b2f64764fb4521a1d2bf22cd8a89e40d69424f6`
|
||||
|
||||
## Next Task
|
||||
|
||||
`M12-02G`: reclaim a preview payload only after its final project-scoped reference is removed,
|
||||
without touching another project's copy.
|
||||
|
||||
## Rollback
|
||||
|
||||
Remove the quarantine protocol, Chromium/unit suites, M12-02F manifest, package command, and this
|
||||
status entry. Restore M12-02F to pending and `nextTask` to M12-02F. Catalog and parity ledger data
|
||||
do not require rollback.
|
||||
47
docs/status/M12-02G.md
Normal file
47
docs/status/M12-02G.md
Normal file
@@ -0,0 +1,47 @@
|
||||
# M12-02G Status
|
||||
|
||||
status: done
|
||||
task: reclaim preview payload only after the final project-scoped reference is removed
|
||||
updated: 2026-08-17 America/New_York
|
||||
enablingTask: true
|
||||
parityStateChange: false
|
||||
|
||||
## Scope
|
||||
|
||||
Preview reference removal is a guarded catalog mutation: it validates the v2 row, checks the exact
|
||||
base revision, clears only the target asset's preview, advances revision, and commits a manifest
|
||||
hash receipt. Payload reclamation happens only after that reference transaction has completed and
|
||||
only when the committed manifest has zero remaining references to the content hash.
|
||||
|
||||
OPFS payload ownership remains project-scoped (`projects/<projectId>/assets/sha256/...`). Therefore
|
||||
collection can target only the requested project's file and cannot delete an equal-hash payload
|
||||
owned and referenced by another project. Non-preview asset metadata is preserved.
|
||||
|
||||
## Evidence
|
||||
|
||||
- `WEB_TEST_PORT=5599 npm --prefix web run test:asset-preview-reference-gc` passed the artifact unit
|
||||
test 1/1 and real Chromium OPFS/IndexedDB test 1/1.
|
||||
- Project A began with two references to one deduplicated payload. Removing the first at revision
|
||||
9->10 returned `RETAINED`, one remaining reference, and kept the file.
|
||||
- Removing the final reference at revision 10->11 returned `COLLECTED` and removed project A's
|
||||
payload. Project B's equal-hash payload remained present and its preview returned READY/513 bytes.
|
||||
- Reopening project A reproduced revision 11, two null preview references, and unchanged metadata.
|
||||
|
||||
## Artifact Hashes
|
||||
|
||||
- reference GC: `76b9d8ccd3e7992356f33adc694001f9b0ff364788234389eacdb09198d1415e`
|
||||
- Chromium suite: `2e9e9b47883997092c5afb0fb7859a484754764de19006e24a8ff2bdeb24f35a`
|
||||
- unit suite: `c5de3122b5d8aab17daf0179cb4f5bb0b5d299408818849cbe7b18e40a47706e`
|
||||
- manifest: `3449f2ea9488f66ea5e3f5f2423413e8e290d8ffed3b3d941bdeee3f76addd0a`
|
||||
- package commands: `7d1db0932fbe7405ffdbd44b770ce7bf9815b9dfb829c1906a06bfff635ae702`
|
||||
|
||||
## Next Task
|
||||
|
||||
`M12-02H`: compare desktop/browser preview pixels with explicit metrics and pass both main-thread
|
||||
and Offscreen display paths.
|
||||
|
||||
## Rollback
|
||||
|
||||
Remove the reference-GC protocol, Chromium/unit suites, M12-02G manifest, package command, and this
|
||||
status entry. Restore M12-02G to pending and `nextTask` to M12-02G. Catalog and parity ledger data
|
||||
do not require rollback.
|
||||
51
docs/status/M12-02H.md
Normal file
51
docs/status/M12-02H.md
Normal file
@@ -0,0 +1,51 @@
|
||||
# M12-02H Status
|
||||
|
||||
status: done
|
||||
task: compare desktop/browser preview pixels and pass main-thread and Offscreen display paths
|
||||
updated: 2026-08-17 America/New_York
|
||||
enablingTask: false
|
||||
parityStateChange: true
|
||||
|
||||
## Scope
|
||||
|
||||
The display path validates the content identity, encoded SHA-256, image header, dimensions, decoded
|
||||
byte budget, and compression ratio before invoking a decoder. It then decodes with disabled browser
|
||||
color conversion and alpha premultiplication, draws with copy composition and no smoothing, and
|
||||
publishes a receipt bound to the identity, content, backend, dimensions, RGBA8 hash, and alpha count.
|
||||
|
||||
Blender 5.2 deterministically regenerates the 8x8 `SRGB/RGBA8/STRAIGHT` reference. Main-thread
|
||||
Canvas2D and a dedicated Worker OffscreenCanvas execute the same production display implementation
|
||||
and match all 256 reference bytes. This closes only the bounded preview display slice; arbitrary
|
||||
profiles, Asset Browser UI, and library operations remain outside this task.
|
||||
|
||||
## Evidence
|
||||
|
||||
- `WEB_TEST_PORT=5600 npm --prefix web run test:asset-preview-display` passed the evidence unit 1/1,
|
||||
Blender 5.2 desktop regeneration/hash checker, and Chromium main-thread/Offscreen test 1/1.
|
||||
- Desktop, main-thread, and Offscreen output SHA-256 was
|
||||
`c2ff81750b41193ce1a06d47d0ff18168a756136dbf0f2586cd0638d4bf0ef00`.
|
||||
- Both browser paths reported 8x8, 256 RGBA8 bytes, 64 non-transparent pixels, and closed bitmap
|
||||
ownership. Their MAE, RMS, P95, maximum channel error, bad-pixel ratio, and alpha coverage delta
|
||||
were zero; foreground IoU was 1.
|
||||
- A one-channel mutation returned `BLOCKED/RENDER_REFERENCE_MISMATCH` under the same metric contract.
|
||||
|
||||
## Artifact Hashes
|
||||
|
||||
- display runtime: `e4a46a40291427afc185caf016b02120ea71639ab4ab4ac250a5c3de28ee8d28`
|
||||
- Offscreen Worker: `6322562f54b608b7eaac4ca3bc0f8d861f47bb8e9b37b3c2ae3d61163108724f`
|
||||
- desktop checker: `d03d766e69ea0547be2c247b5d7ef8fd5ceab6b399dc996eba711fa0cac0ca54`
|
||||
- desktop report: `cdf841f5d27362f6df380772ada8b7b1427d051a8b46b5c2c20aab0b597c25d0`
|
||||
- Chromium suite: `340d386c6a854c44cd6f6207e3699f870b8ded2aada4747afd6ff1103c719520`
|
||||
- unit suite: `f10a05900c4d1282251f7a76e7c413a18602910f55fe8400f1713f548eba3873`
|
||||
- manifest: `30e6e4b6f53e07768d2ae8d853fe08a5c0782f8608320a72e41db77f6d87f2c4`
|
||||
- package commands: `2510242fbc0c2da787f558365733a4db6faf9f490e2594f69dc978920ed09f21`
|
||||
|
||||
## Next Task
|
||||
|
||||
`M12-03A`: inventory the data-block and dependency closures for append, link, and library override.
|
||||
|
||||
## Rollback
|
||||
|
||||
Remove the display runtime, Worker, desktop checker/report, unit/Chromium suites, M12-02H manifest,
|
||||
package command, completed ledger slice, and this status entry. Restore M12-02H to pending and the
|
||||
queue to M12-02H.
|
||||
55
docs/status/M12-03A.md
Normal file
55
docs/status/M12-03A.md
Normal file
@@ -0,0 +1,55 @@
|
||||
# M12-03A Status
|
||||
|
||||
status: done
|
||||
task: inventory append, link, and library override data-blocks and dependency closures
|
||||
updated: 2026-08-17 America/New_York
|
||||
enablingTask: true
|
||||
parityStateChange: false
|
||||
|
||||
## Scope
|
||||
|
||||
The machine inventory separately records Append, Link, and Library Override entry points,
|
||||
selectable data-block roots, non-root ID roles, direct/indirect/transitive/embedded dependency
|
||||
classes, terminal ownership actions, and override reference/hierarchy/property-operation fields.
|
||||
|
||||
The exact closure is an actual Blender Main ID pointer graph walked by
|
||||
`BKE_library_foreach_ID_link`, not a hard-coded Object-to-Mesh table. The inventory therefore
|
||||
preserves cross-library and override-only dependency distinctions needed by later ownership and
|
||||
invalidation contracts. It does not perform a library mutation or add a parity slice.
|
||||
|
||||
## Findings
|
||||
|
||||
- Blender 5.2 exposes 36 selectable library data-block collections. Screen and WorkSpace are
|
||||
append-only; Library is source/transitive metadata, while embedded Key IDs follow their owner.
|
||||
- Append can keep linked, reuse local, make local, or copy local after classifying direct,
|
||||
indirect, and liboverride-only dependencies. Recursive cross-library localization is explicit.
|
||||
- Link retains the source `Library`, direct `ID_TAG_EXTERN`, indirect `ID_TAG_INDIRECT`, and
|
||||
transitive parent-library relationships without localization.
|
||||
- Override retains a linked reference alongside a local override and hierarchy root. Its 17 RNA
|
||||
fields cover reference/root flags, property paths, operations, and local/reference subitems.
|
||||
- Current Web metadata represents only library source, dependency IDs, and read-only state; eight
|
||||
per-ID ownership, override, and invalidation semantics remain absent and mutation stays blocked.
|
||||
|
||||
## Evidence
|
||||
|
||||
- `npm --prefix web run test:library-operation-inventory` passed with
|
||||
`roots=36 appendOnly=2 operations=3 overrideFields=17 gaps=8 next=M12-03B`.
|
||||
- The checker binds nine Blender/Web source files and one real nested-library fixture by SHA-256,
|
||||
verifies link/append action and dependency-walk tokens, launches Blender 5.2, and compares the
|
||||
actual append/link operator properties, library collections, nested source, and override RNA.
|
||||
|
||||
## Artifact Hashes
|
||||
|
||||
- inventory: `0dcb16233b52b3c0be2373f489d6bf3c771c9d6564993ff98b5d63dcd322fef7`
|
||||
- checker: `1581490d1923578293202d27a1c379eb2a8c5e13be418547d148cf09c6a783fd`
|
||||
- package commands: `a7cf339b0cf6fab17ef63e9394b2e540af60327f7bf1f7fe95823d182e0d3e63`
|
||||
|
||||
## Next Task
|
||||
|
||||
`M12-03B`: define source library ID, owner, read-only state, and invalidation token for all three
|
||||
operations.
|
||||
|
||||
## Rollback
|
||||
|
||||
Remove the library operation inventory, checker, package command, and this status entry. Restore
|
||||
M12-03A to pending and the queue to M12-03A. No parity ledger rollback is required.
|
||||
47
docs/status/M12-03B.md
Normal file
47
docs/status/M12-03B.md
Normal file
@@ -0,0 +1,47 @@
|
||||
# M12-03B Status
|
||||
|
||||
status: done
|
||||
task: define source library identity, owner, read-only state, and invalidation tokens
|
||||
updated: 2026-08-17 America/New_York
|
||||
enablingTask: true
|
||||
parityStateChange: false
|
||||
|
||||
## Scope
|
||||
|
||||
Schema 1 derives `sourceLibraryId` from a canonical source locator and source `.blend` SHA-256.
|
||||
Each operation binding then fixes one source data-block, its operation-specific owner and read-only
|
||||
state, source generation/revision, dependency-closure SHA-256, and a hash-derived invalidation token.
|
||||
|
||||
APPEND owns a writable local Main ID. LINK remains owned by the matching source library and is
|
||||
fully read-only. LIBRARY_OVERRIDE owns a writable local override while retaining a read-only linked
|
||||
reference and hierarchy root. This is a pure protocol task and does not mutate Blender Main.
|
||||
|
||||
## Evidence
|
||||
|
||||
- `npm --prefix web run test:library-operation-identity` passed 6/6.
|
||||
- Stable source identity changes when either locator or source bytes change.
|
||||
- APPEND/LINK/LIBRARY_OVERRIDE produce distinct invalidation tokens for the same source root and
|
||||
reject owner substitution, read-only drift, reference substitution, unknown fields, and forgery.
|
||||
- Source library/hash, generation, revision, and dependency closure drift each return
|
||||
`REVISION_CONFLICT`; the unchanged state revalidates the original binding exactly.
|
||||
- The one initial test expectation was corrected to the actual fail-fast exact-schema code, then
|
||||
the complete suite was rerun successfully.
|
||||
|
||||
## Artifact Hashes
|
||||
|
||||
- protocol: `5371133bd98e05935cd75bab582abedec32a36559af8d212f0bc8a534ef19451`
|
||||
- golden bindings: `b39c0f26697ac688c1203ae4e3e27d8a02686c7c9da9afc438c89a0b4d69370f`
|
||||
- unit suite: `0e5b2c82d481180eb8eb22de16ad8cdef5e58fa7423c26011faaffbac788e547`
|
||||
- manifest: `f388aedb9c0901932cf90c058f2f2b83259d9797cd6b144566a25e37a348c64b`
|
||||
- package commands: `48cf41f0d0041bf4f497bc2a454cfa9469874eded1775cea50fe3249d4e0b59e`
|
||||
|
||||
## Next Task
|
||||
|
||||
`M12-03C`: create a desktop single-Object append fixture and record stable Object, Mesh, Material,
|
||||
and Image mappings.
|
||||
|
||||
## Rollback
|
||||
|
||||
Remove the library operation identity protocol, golden, unit suite, manifest, package command, and
|
||||
this status entry. Restore M12-03B to pending and the queue to M12-03B. No parity ledger rollback is
|
||||
required.
|
||||
50
docs/status/M12-03C.md
Normal file
50
docs/status/M12-03C.md
Normal file
@@ -0,0 +1,50 @@
|
||||
# M12-03C Status
|
||||
|
||||
status: done
|
||||
task: create a desktop single-Object append fixture and stable dependency mapping
|
||||
updated: 2026-08-17 America/New_York
|
||||
enablingTask: true
|
||||
parityStateChange: false
|
||||
|
||||
## Scope
|
||||
|
||||
Blender 5.2 creates a source library containing one Object whose dependency closure is one Mesh,
|
||||
one Material, and one packed Image. A clean desktop Main appends only the Object, saves the target,
|
||||
reopens it, and exports the same canonical graph before and after reopen.
|
||||
|
||||
The fixture records stable source-to-local mappings, ownership, read-only state, dependency edges,
|
||||
geometry/UV/material structure, and packed-image metadata and Float32 pixel SHA-256. Container hashes
|
||||
remain session-bound Blender artifacts; the canonical semantic report must reproduce exactly.
|
||||
|
||||
## Evidence
|
||||
|
||||
- `npm --prefix web run test:library-append-desktop` passed.
|
||||
- The generator appended one selected Object and resolved four stable local IDs with three dependency
|
||||
edges.
|
||||
- Object, Mesh, Material, and Image all have null library pointers, no library override, writable
|
||||
`LOCAL_MAIN` ownership, and identical source/local stable names.
|
||||
- Save/reopen preserved 4 vertices, 4 edges, 1 polygon, 4 loops, `UVMap`, the material slot, packed
|
||||
2x2 sRGB RGBA image metadata, and Float32 pixel SHA-256
|
||||
`6f0f8c231d65149e69e6ed12d370bcfa095ef90adc202065492ea8c8ef17e45a`.
|
||||
- A new temporary source/target pair reproduced the canonical report after normalizing only the
|
||||
session-bound `.blend` container hashes.
|
||||
|
||||
## Artifact Hashes
|
||||
|
||||
- generator: `b9d8e3bd24966b0d7d419382d7ef0b35ae8def9ee2c2236dfd3ad1a90aebe66f`
|
||||
- checker: `6f8cc0d51be19423cf6fb7203c3c16f8d27e0fbb3b5b357bad5dec6c5ceb432d`
|
||||
- source `.blend`: `5b60d02926efd588a6ca300ba31414cdf37dbc48786c17b383a70319057c0606`
|
||||
- target `.blend`: `5cde4927f47e33f0338de38184ce5ba971825fcefd1d8dc6feb48d41a6cdb60c`
|
||||
- desktop report: `b1d7b8b9e832d18d69081f63981062800e0f00f0c9aec025b18274510ed76e2a`
|
||||
- manifest: `cbfbd8c919125108334dd24925b9ef8e69880983515e56841e6ead4a2b182aed`
|
||||
- package commands: `b68f2bce48fcd42e4cc1399b47ba33c35495248a6c0405c2280b5b73e3ce581a`
|
||||
|
||||
## Next Task
|
||||
|
||||
`M12-03D`: append the same closure into WASM Main with one transaction and local ownership.
|
||||
|
||||
## Rollback
|
||||
|
||||
Remove the desktop append generator/checker, source and target fixtures, golden report/manifest,
|
||||
package command, and this status entry. Restore M12-03C to pending and the queue to M12-03C. No
|
||||
parity ledger rollback is required.
|
||||
52
docs/status/M12-03D.md
Normal file
52
docs/status/M12-03D.md
Normal file
@@ -0,0 +1,52 @@
|
||||
# M12-03D Status
|
||||
|
||||
status: done
|
||||
task: append one dependency closure through WASM Main in one transaction
|
||||
updated: 2026-08-17 America/New_York
|
||||
enablingTask: false
|
||||
parityStateChange: false
|
||||
|
||||
## Scope
|
||||
|
||||
The browser Worker validates a source-hash-bound APPEND binding and sends one Object closure to
|
||||
the authoritative Blender Main. Blender's native link/append context recursively imports the
|
||||
Object, Mesh, Material, and packed Image, verifies that every resulting ID is local and writable,
|
||||
and publishes one SceneIR revision. The existing Main history transaction is used for rollback and
|
||||
undo/redo; the source is never treated as a linked read-only snapshot.
|
||||
|
||||
## Evidence
|
||||
|
||||
- `EM_CACHE=/home/mes123456/workinf_Blender_Wasm/.emcache cmake --build build_web_blender6 --target web_engine -- -j2` passed;
|
||||
the generated module exports `_web_engine_append_library_object`.
|
||||
- `npm --prefix web run typecheck` passed.
|
||||
- `WEB_TEST_PORT=5194 npm --prefix web run test:library-main-append` passed in Chromium.
|
||||
- The test opens a clean `.blend`, appends the desktop fixture closure through `WebEngineClient`,
|
||||
verifies `transactionCount=1`, four `LOCAL_MAIN` mappings, stable Object/Mesh/Material/Image
|
||||
IDs, the material-to-image dependency, and a single revision increment.
|
||||
- A stale base revision returns `REVISION_CONFLICT`; a second append colliding with the local IDs
|
||||
returns `ASSET_MANIFEST_INVALID` without changing the revision. Undo removes the closure, redo
|
||||
restores it, and a saved buffer reopens with the same local image and Object dependency.
|
||||
- The Worker closure check accepts both the reader's `material.imageIds` representation and the
|
||||
Blender node-level `imageId` representation; it does not infer ownership from a preview.
|
||||
|
||||
## Artifact hashes
|
||||
|
||||
- test: `7130453cbc81f22eb1bcf598ff3745672ac8177dccb11cef667f5b68b43e87c6`
|
||||
- worker: `e3bb430e401144bf17cdcbd0de13cde0a4f904f1f9ff9d5b10a73d90f0d4c648`
|
||||
- protocol: `bfd98561cbe797d7b8f07c92c53a25460c839a64ab7222b7795cf58d54dff8d7`
|
||||
- source `.blend`: `5b60d02926efd588a6ca300ba31414cdf37dbc48786c17b383a70319057c0606`
|
||||
- clean target `.blend`: `9b1ecbcc3d7f8079ee5469de64193ffcaa0a7b01e0f2ac340bbb18aa88734a63`
|
||||
- WASM JS: `9c831d0351ac9d0714cce3b1da748cba416ced3086e3c59595cf24ac4da459ed`
|
||||
- WASM binary: `7821f408687e455c6f4e185fc3741c199c1c60d409836a2fe1b32f521d81e0ea`
|
||||
- Main append API: `5a71260c542fa559f73c6b82e23e72d39bd2844f5ebe12c6edc809a49d79791b`
|
||||
- Main append implementation: `d0c484ab62e6ec051c8c2f67c49d750a456578aaccc2aa4f461026307af0db95`
|
||||
|
||||
## Next task
|
||||
|
||||
`M12-03E`: append undo/redo/save/reopen and the desktop canonical report must agree.
|
||||
|
||||
## Rollback
|
||||
|
||||
Remove the M12-03D Worker/e2e test, package command, WASM append export and Main append entrypoint,
|
||||
restore the material closure check, and move the machine queue back to `M12-03D`. No V1 release
|
||||
ledger rollback is required.
|
||||
@@ -1,8 +1,8 @@
|
||||
# N-019 灯光与渲染
|
||||
|
||||
状态:`BLOCKED`(Camera/Light/World/Scene metadata、Camera/Light/World 有界 Main 写回、
|
||||
Three exposure/shadow 映射和一条有界 Blender reference 图像指标闭环已落地;Scene 颜色管理
|
||||
writer、全域渲染等价和高级后端仍未实现)
|
||||
Three exposure/shadow 映射、一条有界 Blender reference 图像指标和 Render 故障生命周期闭环
|
||||
已落地;Scene 颜色管理 writer、全域渲染等价和高级后端仍未实现)
|
||||
|
||||
## 已验证切片
|
||||
|
||||
@@ -49,6 +49,10 @@ writer、全域渲染等价和高级后端仍未实现)
|
||||
绑定同一 provenance 并公开 output MIME/byte length/output SHA-256/result SHA-256。真实
|
||||
loopback server 由 Blender 5.2 headless 打开 fixture 并输出 PNG;source、settings、build
|
||||
和 output 篡改均在提交或消费前结构化拒绝。
|
||||
14. M11-14 Render fault lifecycle:真实 Eevee `.blend` 在 Chromium WebEngine/Main 打开后生成
|
||||
非空 WebGL RGBA8 输出;预取消 open 返回 `OPEN_CANCELLED` 且不替换当前 Main,Worker
|
||||
generation 2 重开保持 scene identity 和输出 SHA-256。纹理 aggregate budget 超限不替换
|
||||
已加载资源,`GPUTextureStore.dispose()` 返回正数 byte/resource 回执并归零所有 owned texture。
|
||||
|
||||
## 仍然阻断
|
||||
|
||||
@@ -75,5 +79,6 @@ WEB_TEST_PORT=5548 npm --prefix web run test:render-resource-budget
|
||||
WEB_TEST_PORT=5552 npm --prefix web run test:render-reference
|
||||
WEB_TEST_PORT=5556 npm --prefix web run test:render-routing
|
||||
WEB_TEST_PORT=5560 npm --prefix web run test:server-render-job
|
||||
WEB_TEST_PORT=5592 npm --prefix web run test:render-compositor-media-recovery
|
||||
WEB_TEST_PORT=5319 npm --prefix web run test:e2e -- --grep "N-019 Scene exposure"
|
||||
```
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
# N-020 Compositor
|
||||
|
||||
状态:`BLOCKED`(GraphIR、有界 CPU executor、真实 Main graph reader、四节点 WebGPU golden
|
||||
与 Unsupported 全图执行门已落地;完整节点参数映射、通用 WebGPU、HDR golden 和服务端执行未实现)
|
||||
状态:`BLOCKED`(GraphIR、有界 CPU executor、真实 Main graph reader、四节点 WebGPU golden、
|
||||
Unsupported 全图执行门和故障生命周期闭环已落地;完整节点参数映射、通用 WebGPU、HDR golden
|
||||
和服务端执行未实现)
|
||||
|
||||
## 已验证切片
|
||||
|
||||
@@ -35,6 +36,10 @@
|
||||
完整 graph;即使 Unsupported 节点未连接到 Composite,也返回
|
||||
`COMPOSITOR_NODE_UNSUPPORTED`。真实 Blender 5.2 Main fixture 的 Glare node name/type/
|
||||
`blenderType`、graph JSON 与 revision 在失败前后完全不变;预置 cache 不能绕过该门。
|
||||
10. M11-14 Compositor fault lifecycle:真实 Main graph 的 CPU executor 周期取消返回
|
||||
`COMPOSITOR_CANCELLED`,8193 像素超尺寸输入在分配/缓存前返回
|
||||
`COMPOSITOR_BUDGET_EXCEEDED`。generation 2 重开保持 GraphIR identity 与 Float32 output
|
||||
SHA-256,LRU `clear()` 返回已释放字节并把 entries/bytes 归零。
|
||||
|
||||
## 仍然阻断
|
||||
|
||||
@@ -54,4 +59,5 @@ WEB_TEST_PORT=5320 npm --prefix web run test:e2e -- --grep "N-020 CPU compositor
|
||||
npm --prefix web run test:compositor-main-reader
|
||||
WEB_TEST_PORT=5562 npm --prefix web run test:compositor-node-golden
|
||||
WEB_TEST_PORT=5565 npm --prefix web run test:compositor-unsupported-gate
|
||||
WEB_TEST_PORT=5592 npm --prefix web run test:render-compositor-media-recovery
|
||||
```
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
状态:`BLOCKED`(strip schema、真实 Main reader、确定性时间编辑、长媒体有界索引/seek/cache、
|
||||
三类实际 runtime codec probe、source/decode-bound 首帧 proxy cache、三类迟到结果 revision
|
||||
gate、最终编码 server-export 路由和实时 AudioContext 恢复门已落地;Main 写回、完整媒体
|
||||
解码/渲染、音频波形、A/V sync、实际混音和真正服务端编码 job 仍未实现)
|
||||
gate、最终编码 server-export 路由、实时 AudioContext 恢复门和媒体故障生命周期闭环已落地;
|
||||
Main 写回、完整媒体解码/渲染、音频波形、A/V sync、实际混音和真正服务端编码 job 仍未实现)
|
||||
|
||||
## 已验证切片
|
||||
|
||||
@@ -63,6 +63,10 @@ gate、最终编码 server-export 路由和实时 AudioContext 恢复门已落
|
||||
mute 将 gain 归零,unmute 恢复 0.75,close 再归零并断开节点。API/构造器缺失与 resume
|
||||
failure 分别稳定返回 `SEQUENCER_AUDIO_DEVICE_UNAVAILABLE` 和
|
||||
`SEQUENCER_AUDIO_RESUME_FAILED`,不会误报输出启用。
|
||||
17. M11-14 Media fault lifecycle:真实 H.264 先通过 HTMLMedia probe 并解出 8x8 RGBA8 frame;
|
||||
预取消 proxy decode 返回 `SEQUENCER_CANCELLED` 且不发布 payload。低于一帧的 cache budget
|
||||
返回 `SEQUENCER_BUDGET_EXCEEDED` 且 entries/bytes 保持 0;首会话 clear 返回正数释放字节,
|
||||
重建会话后 source/decode/profile identity 和 payload SHA-256 保持不变。
|
||||
|
||||
## 仍然阻断
|
||||
|
||||
@@ -86,4 +90,5 @@ WEB_TEST_PORT=5573 npm --prefix web run test:sequencer-media-cache
|
||||
WEB_TEST_PORT=5576 npm --prefix web run test:sequencer-media-revision
|
||||
WEB_TEST_PORT=5579 npm --prefix web run test:sequencer-final-export
|
||||
WEB_TEST_PORT=5586 npm --prefix web run test:sequencer-audio-recovery
|
||||
WEB_TEST_PORT=5592 npm --prefix web run test:render-compositor-media-recovery
|
||||
```
|
||||
|
||||
@@ -3,6 +3,97 @@
|
||||
状态:`BLOCKED`(asset catalog、来源/许可证元数据、真实 Main library inventory、库依赖与
|
||||
IO 安全门已落地;Append/Link/Override Main、非 GLB 本地导入和跨桌面重导入未实现)
|
||||
|
||||
## 完整对标盘点基线
|
||||
|
||||
M12-01A 已从 Blender 5.2 DNA、RNA、Blend IO、AssetCatalog/CDF 源码及真实 RNA runtime 盘点
|
||||
14 个 `AssetMetaData` DNA 字段、10 个公开 RNA 属性、3 个 `AssetTag` 存储字段和 3 个 catalog
|
||||
持久语义字段。M12-01B 随后用锁定 Blender 5.2 生成 3 条 CDF v1 catalog、Object/Material/World
|
||||
资产和 canonical JSON,并由独立 Blender 新进程精确重开复验。M12-01C 以 Blender UUID/path
|
||||
和 AssetWeakReference 定义 Web schema v2 stable identity,并冻结 UTF-8 文本及条目预算。三项均为
|
||||
`enablingTask`。M12-01D 又完成 v1→v2 无损前向迁移,保留 revision、source、preview 与 library
|
||||
graph,并输出确定性 ID mapping/hash report。四项只冻结完整对标输入与迁移,不增加本节已验证
|
||||
slice。M12-01E 又固定 v2 对 v1 reader 的 read-only/write-block/future-reject 策略。五项仍不改变
|
||||
`parityStatus=BLOCKED`。
|
||||
|
||||
M12-01F 已补齐 duplicate catalog/asset ID、legacy parent cycle、v2 parentPath drift、UTF-8
|
||||
simple-name/tag budget 和 v1/v2 unknown-field 共 9 类稳定负例;该 enabling evidence 不改变 slice。
|
||||
|
||||
M12-01G 已让 v1→v2 IndexedDB index migration 在同一 readwrite transaction 中提交 target、
|
||||
receipt 和 source delete;写 target 后或删 source 后注入失败均 abort,关闭并重开后旧 v1 index、
|
||||
无关 migration 行和数据库版本精确不变。该 enabling evidence 仍不改变 slice。
|
||||
|
||||
M12-01H 又证明初始页面、reload 和两个顺序重建的独立 Worker 读取相同 catalog/asset 顺序、
|
||||
AssetWeakReference stable identity、revision 和 manifest SHA-256;receipt/hash drift 在发布前阻断。
|
||||
该 enabling evidence 仍不改变 slice。
|
||||
|
||||
M12-01I 将 A-H 八项任务链、Blender 5.2 desktop fixture、Web v2 fixture、production migration
|
||||
report、canonical 双 hash 和 IndexedDB fault/restart artifact 递归绑定为一个 READY evidence。
|
||||
M12.1 schema/migration 已收口,但仍是 enabling evidence,不把 catalog mutation/preview 计为完成。
|
||||
|
||||
M12-02A 已盘点 PreviewImage 的 ICON/PREVIEW 双槽、持久尺寸/rect/flag、RGBA8 packed pixel、
|
||||
premultiplied alpha、runtime deferred/invalid 状态和真正的 null pointer。原生结构/RNA 没有颜色空间
|
||||
字段;现有 Web v1/v2 也缺 slot/colorSpace/alpha/generator 语义。本项仍是 inventory evidence。
|
||||
|
||||
M12-02B 定义 source/content 各自的 byte length、MIME、SHA-256,并让 content 绑定尺寸、RGBA8、
|
||||
sRGB/alpha profile,generator 绑定 Blender version/executable/script/settings。真实 Blender 两次重编码
|
||||
确定性相同,source/content 编码不同但解码像素 exact;本项仍是 enabling identity evidence。
|
||||
|
||||
M12-02C 在任何图像 decoder 或 decoded allocation 前依次检查 identity、16 MiB encoded bytes、
|
||||
content SHA-256、PNG/WebP header、identity-bound dimensions、16,777,216 pixels、64 MiB RGBA8
|
||||
decoded bytes 和 100:1 compression ratio。8 类失败均 fail-closed,随后小 preview 可立即恢复;
|
||||
本项仍是 enabling budget evidence,不把 preview storage/display 计为完成。
|
||||
|
||||
M12-02D 先把 content-addressed preview 写入 OPFS 并回读复验,再用一次 revision/full-row guarded
|
||||
IndexedDB transaction 提交 catalog v2 preview reference 和 hash receipt。OPFS 前后及 catalog put
|
||||
后的三处 fault 都保持旧 catalog;重开后 revision、receipt 和 payload hash 一致。本项仍不把
|
||||
dedupe、损坏隔离或 viewport display 计为完成。
|
||||
|
||||
M12-02E 又让两个不同 Blender weak-reference identity 和不同 metadata 的 asset 共享相同 content
|
||||
SHA-256;第二次写入只复验并复用 OPFS payload,物理文件计数为 1,两个 catalog entry 的完整
|
||||
non-preview metadata 不变,revision 9 重开一致。本项仍不把损坏隔离或 display 计为完成。
|
||||
|
||||
M12-02F 将同长度但 SHA-256 漂移的 bytes 从 content-addressed namespace 移入复验后的 quarantine
|
||||
文件,并写入与 catalog revision/reference 绑定的 receipt。catalog 本身及另一 asset 均不变;重开后
|
||||
仍可读取完整 metadata,只把 preview/data 返回 null。本项仍不把 reference GC/display 计为完成。
|
||||
|
||||
M12-02G 在 catalog reference transaction 完成后按 committed manifest 计数:同项目仍有一个引用时
|
||||
保留 payload,最后一个引用删除后才回收。OPFS project namespace 保证另一项目的同 hash payload
|
||||
和 READY preview 不受影响;revision 11 重开 metadata 不变。本项仍不把 display 计为完成。
|
||||
|
||||
M12-02H 已把 Blender 5.2 确定性重编码的 `SRGB/RGBA8/STRAIGHT` preview 固定为逐字节 reference,
|
||||
主线程 `HTMLCanvasElement` 与独立 Worker `OffscreenCanvas` 共用生产 decode/display 实现。两条路径
|
||||
均先通过 M12-02C 的 identity/header/预算门,再以禁用色彩转换和预乘的 `ImageBitmap` 解码;输出
|
||||
256-byte RGBA8 hash 与 desktop reference 完全一致,MAE/RMS/P95/max/bad-pixel/alpha 指标均为零,
|
||||
前景 IoU 为 1。该项新增一个已验证 preview display parity slice,但不扩大到任意尺寸、色彩空间、
|
||||
asset browser UI 或 Append/Link/Override。
|
||||
|
||||
M12-03A 已分别冻结 Append、Link 与 Library Override 的数据块/闭包边界。真实 Blender runtime
|
||||
公开 36 类 selectable root,其中 Screen/WorkSpace 为 append-only;Library 是来源/传递依赖元数据,
|
||||
Key 等 embedded ID 随 owner 进入闭包。Append 由实际 ID pointer graph 区分 direct、indirect、
|
||||
cross-library、override dependency 与 reusable local;Link 保留 `ID.lib`、EXTERN/INDIRECT 和
|
||||
`Library.runtime.parent`;Override 另记录 linked reference、local owner、hierarchy root、property/
|
||||
operation 与 system dependency。该 inventory 仍不创建 ownership contract 或 Main transaction。
|
||||
|
||||
M12-03B 已定义 schema 1 library operation identity。`sourceLibraryId` 同时绑定 canonical source
|
||||
locator 与 `.blend` SHA-256;APPEND 只能由 `LOCAL_MAIN` owner 持有且可写,LINK 只能由匹配的
|
||||
`SOURCE_LIBRARY` owner 持有且 root/reference 均只读,LIBRARY_OVERRIDE 由 `LOCAL_OVERRIDE`
|
||||
持有、local 可写但 linked reference 只读。invalidation token 还绑定 operation、root、owner、
|
||||
source generation/revision 与 dependency-closure hash;任一漂移稳定返回 `REVISION_CONFLICT`。
|
||||
|
||||
M12-03C 已生成真实 Blender 5.2 desktop 单 Object append fixture。选中的 Object 自动带入 Mesh、
|
||||
Material 和 packed Image 依赖,四个 ID 在 target Main 中均无 source library/override 指针并映射为
|
||||
可写 `LOCAL_MAIN` owner。新 Blender load 路径重开 target 后,3 条依赖边、4/4/1 geometry、UVMap、
|
||||
material slot、2x2 sRGB RGBA image 和 Float32 pixel SHA-256 均与 source canonical report 一致。
|
||||
本项只冻结 desktop 权威 fixture,不提前声明 WASM Main append、undo/redo 或完整 append parity。
|
||||
该合同没有提前执行 Append 或开放 writer。
|
||||
|
||||
M12-03D 已把同一 source-hash-bound closure 送入 WASM Worker 的 authoritative Blender Main。Native
|
||||
link/append context 递归导入 Object、Mesh、Material 和 packed Image,Main commit 前核对 source
|
||||
locator、root、closure 与 base revision;四个 ID 均无 `ID.lib`/override、映射为可写 `LOCAL_MAIN`,
|
||||
并只产生一个新的 SceneIR revision。stale revision 与 local ID collision 在 Main mutation 前阻断,
|
||||
失败路径回滚完整 history state。该项只证明单 transaction Main append,undo/redo/save/reopen 与
|
||||
desktop canonical report 的联合一致性仍由 M12-03E 验证。
|
||||
|
||||
## 已验证切片
|
||||
|
||||
1. N-023-A:版本化 manifest 覆盖 catalog、asset kind/tag、preview、author、license、
|
||||
@@ -29,11 +120,13 @@ IO 安全门已落地;Append/Link/Override Main、非 GLB 本地导入和跨
|
||||
已完成;大 bundle 性能仍未实现。
|
||||
9. VDB 独立资源库包含 generated/official/source/derived/report/manifest/license,14 条记录逐项绑定
|
||||
byte length 与 SHA-256;官方 sphere 使用 CC-BY-4.0 和 Git LFS 权威 hash,未放入仓库工作树。
|
||||
10. N-023-A(preview display):Blender 5.2 desktop RGBA8 reference、主线程 Canvas2D 与
|
||||
OffscreenCanvas 的像素 SHA-256 精确一致;错误像素返回 `RENDER_REFERENCE_MISMATCH`。
|
||||
|
||||
## 仍然阻断
|
||||
|
||||
- N-023-B:Append/Link/Library Override、reload/relocate 和真实 Main transaction;只读 library
|
||||
inventory 已完成。
|
||||
- N-023-B:Append undo/redo/save/reopen、Link/Library Override、reload/relocate 和完整真实 Main
|
||||
parity;M12-03D 的单 transaction append 已有独立证据,但不解除本项阻断。
|
||||
- N-023-C/D:GLTF/OBJ/PLY/STL、USD/Alembic import/export/save/reopen/desktop reimport。
|
||||
- N-023-E:真实 zip decoder/fuzz、OPFS quota/recovery、license/source offer 发布审计和大文件
|
||||
流式性能;archive 路径冲突、双向字节预算与确定性 range plan 已完成。
|
||||
@@ -44,6 +137,23 @@ IO 安全门已落地;Append/Link/Override Main、非 GLB 本地导入和跨
|
||||
```bash
|
||||
WEB_TEST_PORT=5323 npm --prefix web run test:e2e -- --grep "N-023 asset"
|
||||
npm --prefix web run test:library-main-reader
|
||||
npm --prefix web run test:asset-catalog-inventory
|
||||
npm --prefix web run test:asset-catalog-v1-fixture
|
||||
npm --prefix web run test:asset-catalog-v2
|
||||
npm --prefix web run test:asset-catalog-migration
|
||||
npm --prefix web run test:asset-catalog-legacy-reader
|
||||
npm --prefix web run test:asset-catalog-negatives
|
||||
npm --prefix web run test:asset-preview-inventory
|
||||
npm --prefix web run test:asset-preview-identity
|
||||
npm --prefix web run test:asset-preview-decode-budget
|
||||
npm --prefix web run test:asset-preview-opfs-commit
|
||||
npm --prefix web run test:asset-preview-dedup
|
||||
npm --prefix web run test:asset-preview-quarantine
|
||||
npm --prefix web run test:asset-preview-reference-gc
|
||||
npm --prefix web run test:asset-preview-display
|
||||
npm --prefix web run test:library-operation-inventory
|
||||
npm --prefix web run test:library-operation-identity
|
||||
npm --prefix web run test:library-main-append
|
||||
npm --prefix web run test:vdb
|
||||
npm --prefix web run test:vdb-native
|
||||
```
|
||||
|
||||
@@ -72,7 +72,7 @@
|
||||
"v1RequiredSlices": ["B-world-scene-render-delta-roundtrip", "B-C-nanovdb-float32-wgsl-bounded-ray-integration"],
|
||||
"v1ExcludedSlices": ["A", "B", "C", "D", "E", "B-C-nanovdb-production-viewport-paging-advanced-material", "E-volume-desktop-chromium-pixel-golden"],
|
||||
"roadmapStatus": "planned",
|
||||
"completedSlices": ["A-camera-light-world-scene-reader-partial", "A-main-camera-dof-properties-partial", "A-main-light-world-properties-partial", "A-white-balance-integrity-gate", "B-three-exposure-shadow-mapping-partial", "B-bounded-kelvin-linear-light-color", "B-world-scene-render-delta-roundtrip", "B-volume-material-manifest-boundary", "B-C-nanovdb-float32-wgsl-bounded-ray-integration", "D-final-render-server-routing-fail-closed", "D-server-render-source-build-settings-output-hash-binding", "E-bounded-realtime-blender-reference-image-metrics"],
|
||||
"completedSlices": ["A-camera-light-world-scene-reader-partial", "A-main-camera-dof-properties-partial", "A-main-light-world-properties-partial", "A-white-balance-integrity-gate", "B-three-exposure-shadow-mapping-partial", "B-bounded-kelvin-linear-light-color", "B-world-scene-render-delta-roundtrip", "B-volume-material-manifest-boundary", "B-C-nanovdb-float32-wgsl-bounded-ray-integration", "D-final-render-server-routing-fail-closed", "D-server-render-source-build-settings-output-hash-binding", "E-bounded-realtime-blender-reference-image-metrics", "E-render-fault-lifecycle-recovery"],
|
||||
"blockedSlices": ["A", "B", "C", "D", "E", "B-C-nanovdb-production-viewport-paging-advanced-material", "E-volume-desktop-chromium-pixel-golden"],
|
||||
"acceptance": ["web:test:lighting-roundtrip", "web:e2e:N-019 Scene exposure"],
|
||||
"dependencies": ["N-018"]
|
||||
@@ -86,7 +86,7 @@
|
||||
"v1RequiredSlices": ["B-real-main-exposure-invert-cpu-chain", "D-unsupported-node-preservation-gate"],
|
||||
"v1ExcludedSlices": ["A", "B", "C", "D", "E"],
|
||||
"roadmapStatus": "planned",
|
||||
"completedSlices": ["A-graph-resource-cycle-schema", "A-main-graph-structure-reader-partial", "A-main-exposure-default-invert-parameter-reader", "B-bounded-cpu-executor-partial", "B-real-main-exposure-invert-cpu-chain", "B-webgpu-constant-exposure-invert-golden-partial", "B-D-unsupported-full-graph-execution-block", "C-image-operation-budget-cancel-partial", "C-content-addressed-frame-lru-cache", "D-unsupported-node-preservation-gate"],
|
||||
"completedSlices": ["A-graph-resource-cycle-schema", "A-main-graph-structure-reader-partial", "A-main-exposure-default-invert-parameter-reader", "B-bounded-cpu-executor-partial", "B-real-main-exposure-invert-cpu-chain", "B-webgpu-constant-exposure-invert-golden-partial", "B-D-unsupported-full-graph-execution-block", "C-image-operation-budget-cancel-partial", "C-content-addressed-frame-lru-cache", "C-E-compositor-fault-lifecycle-recovery", "D-unsupported-node-preservation-gate"],
|
||||
"blockedSlices": ["A", "B", "C", "D", "E"],
|
||||
"acceptance": ["web:test:compositor-main-reader", "web:e2e:N-020 CPU compositor"],
|
||||
"dependencies": ["N-019"]
|
||||
@@ -100,7 +100,7 @@
|
||||
"v1RequiredSlices": ["B-real-main-cross-still-frame-resolution", "C-runtime-codec-probe-gate"],
|
||||
"v1ExcludedSlices": ["A", "B", "C", "D", "E"],
|
||||
"roadmapStatus": "planned",
|
||||
"completedSlices": ["A-strip-resource-schema", "A-main-strip-timeline-reader-partial", "B-deterministic-move-trim-split-partial", "B-source-frame-seek", "B-active-frame-dependency-resolution", "B-cross-transition-progress-input-resolution", "B-real-main-cross-still-frame-resolution", "C-runtime-codec-probe-gate", "C-runtime-image-sound-movie-byte-decode-probe", "C-source-decode-bound-movie-proxy-cache", "C-seek-scrub-decode-revision-gate", "C-audio-context-suspend-device-mute-recovery", "D-final-export-server-routing-fail-closed"],
|
||||
"completedSlices": ["A-strip-resource-schema", "A-main-strip-timeline-reader-partial", "B-deterministic-move-trim-split-partial", "B-source-frame-seek", "B-active-frame-dependency-resolution", "B-cross-transition-progress-input-resolution", "B-real-main-cross-still-frame-resolution", "C-runtime-codec-probe-gate", "C-runtime-image-sound-movie-byte-decode-probe", "C-source-decode-bound-movie-proxy-cache", "C-seek-scrub-decode-revision-gate", "C-audio-context-suspend-device-mute-recovery", "C-media-fault-lifecycle-recovery", "D-final-export-server-routing-fail-closed"],
|
||||
"blockedSlices": ["A", "B", "C", "D", "E"],
|
||||
"acceptance": ["web:test:sequencer-main-reader", "web:e2e:N-021 sequencer"],
|
||||
"dependencies": ["N-020"]
|
||||
@@ -128,7 +128,7 @@
|
||||
"v1RequiredSlices": ["C-glb-export-usd-analysis-gates", "E-archive-path-conflict-compressed-budget-range-plan", "E-nanovdb-opfs-tamper-rollback-quota-recovery"],
|
||||
"v1ExcludedSlices": ["A", "B", "C", "D", "E", "E-nanovdb-large-bundle-performance"],
|
||||
"roadmapStatus": "planned",
|
||||
"completedSlices": ["A-catalog-asset-license-source-schema", "A-content-addressed-opfs-capability", "A-preview-content-signature-dimension-verification", "A-vdb-external-resource-license-sha-catalog", "B-library-dependency-order-partial", "B-main-library-inventory-reader", "C-glb-export-usd-analysis-gates", "E-archive-path-ratio-budget", "E-archive-path-conflict-compressed-budget-range-plan", "E-nanovdb-http206-range-hash-streaming-boundary", "E-nanovdb-http-retry-if-range-body-resume", "E-nanovdb-opfs-atomic-binding-worker-reopen-bundle-lru", "E-nanovdb-opfs-tamper-rollback-quota-recovery", "E-nanovdb-gpu-resident-page-lru"],
|
||||
"completedSlices": ["A-catalog-asset-license-source-schema", "A-content-addressed-opfs-capability", "A-preview-content-signature-dimension-verification", "A-preview-desktop-browser-main-offscreen-display", "A-vdb-external-resource-license-sha-catalog", "B-library-dependency-order-partial", "B-main-library-inventory-reader", "C-glb-export-usd-analysis-gates", "E-archive-path-ratio-budget", "E-archive-path-conflict-compressed-budget-range-plan", "E-nanovdb-http206-range-hash-streaming-boundary", "E-nanovdb-http-retry-if-range-body-resume", "E-nanovdb-opfs-atomic-binding-worker-reopen-bundle-lru", "E-nanovdb-opfs-tamper-rollback-quota-recovery", "E-nanovdb-gpu-resident-page-lru"],
|
||||
"blockedSlices": ["A", "B", "C", "D", "E", "E-nanovdb-large-bundle-performance"],
|
||||
"acceptance": ["web:test:library-main-reader", "web:test:vdb-faults", "web:e2e:N-023 asset"],
|
||||
"dependencies": ["N-022"]
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"schemaVersion": 4,
|
||||
"source": "docs/status/parity-ledger.json",
|
||||
"sourceSha256": "0a9b4c1026c42b5f77869f222b1531601ed229184a1d1737e74047c7b3406730",
|
||||
"generatedAt": "2026-08-17T08:22:00.415Z",
|
||||
"sourceSha256": "c8d73374c338f46c2016f6089d99efdd7b3331fc0100d64dad0f416fe66e3eb6",
|
||||
"generatedAt": "2026-08-17T15:09:53.894Z",
|
||||
"families": [
|
||||
{
|
||||
"id": "N-015",
|
||||
@@ -277,7 +277,8 @@
|
||||
"B-C-nanovdb-float32-wgsl-bounded-ray-integration",
|
||||
"D-final-render-server-routing-fail-closed",
|
||||
"D-server-render-source-build-settings-output-hash-binding",
|
||||
"E-bounded-realtime-blender-reference-image-metrics"
|
||||
"E-bounded-realtime-blender-reference-image-metrics",
|
||||
"E-render-fault-lifecycle-recovery"
|
||||
],
|
||||
"blockedSlices": [
|
||||
"A",
|
||||
@@ -324,6 +325,7 @@
|
||||
"B-D-unsupported-full-graph-execution-block",
|
||||
"C-image-operation-budget-cancel-partial",
|
||||
"C-content-addressed-frame-lru-cache",
|
||||
"C-E-compositor-fault-lifecycle-recovery",
|
||||
"D-unsupported-node-preservation-gate"
|
||||
],
|
||||
"blockedSlices": [
|
||||
@@ -372,6 +374,7 @@
|
||||
"C-source-decode-bound-movie-proxy-cache",
|
||||
"C-seek-scrub-decode-revision-gate",
|
||||
"C-audio-context-suspend-device-mute-recovery",
|
||||
"C-media-fault-lifecycle-recovery",
|
||||
"D-final-export-server-routing-fail-closed"
|
||||
],
|
||||
"blockedSlices": [
|
||||
@@ -456,6 +459,7 @@
|
||||
"A-catalog-asset-license-source-schema",
|
||||
"A-content-addressed-opfs-capability",
|
||||
"A-preview-content-signature-dimension-verification",
|
||||
"A-preview-desktop-browser-main-offscreen-display",
|
||||
"A-vdb-external-resource-license-sha-catalog",
|
||||
"B-library-dependency-order-partial",
|
||||
"B-main-library-inventory-reader",
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
# This is an Asset Catalog Definition file for Blender.
|
||||
#
|
||||
# Generated by M12-01B from a locked Blender 5.2 runtime.
|
||||
|
||||
VERSION 1
|
||||
|
||||
11111111-1111-4111-8111-111111111111:Characters:Characters
|
||||
22222222-2222-4222-8222-222222222222:Characters/Heroes:Heroes
|
||||
33333333-3333-4333-8333-333333333333:Materials/Metal:Metal
|
||||
BIN
tests/files/web/m12_asset_catalog_v1/m12_asset_catalog_v1.blend
Normal file
BIN
tests/files/web/m12_asset_catalog_v1/m12_asset_catalog_v1.blend
Normal file
Binary file not shown.
BIN
tests/files/web/m12_library_append_v1/m12_append_source.blend
Normal file
BIN
tests/files/web/m12_library_append_v1/m12_append_source.blend
Normal file
Binary file not shown.
BIN
tests/files/web/m12_library_append_v1/m12_append_target.blend
Normal file
BIN
tests/files/web/m12_library_append_v1/m12_append_target.blend
Normal file
Binary file not shown.
48
tests/golden/M11-14/render-compositor-media-recovery.json
Normal file
48
tests/golden/M11-14/render-compositor-media-recovery.json
Normal file
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"task": "M11-14",
|
||||
"blenderVersion": "5.2.0",
|
||||
"domains": [
|
||||
{
|
||||
"domain": "RENDER",
|
||||
"fixture": "tests/files/web/m11_render_reference.blend",
|
||||
"sha256": "d2bea55fe4de0b00e73a9241b4d1b0c802c2eb0e6159c0cd48e007b97b9d3963",
|
||||
"desktopGolden": "tests/golden/M11-04/manifest.json",
|
||||
"cancellationCode": "OPEN_CANCELLED",
|
||||
"budgetCode": "GPU_TEXTURE_BUDGET_EXCEEDED"
|
||||
},
|
||||
{
|
||||
"domain": "COMPOSITOR",
|
||||
"fixture": "tests/files/web/m11_compositor_allowlist.blend",
|
||||
"sha256": "e844eba69002101c8cf4f376cdd844c4be78729091627d898634088707374b29",
|
||||
"desktopGolden": "tests/golden/M11-07/compositor-node-golden.json",
|
||||
"cancellationCode": "COMPOSITOR_CANCELLED",
|
||||
"budgetCode": "COMPOSITOR_BUDGET_EXCEEDED"
|
||||
},
|
||||
{
|
||||
"domain": "MEDIA",
|
||||
"fixture": "tests/files/web/media/sequencer-probe.mp4",
|
||||
"sha256": "f35a5a2765aef9d0fed7146108c699d0beb03f34414cb27e6b7a7d4871187b65",
|
||||
"desktopGolden": "tests/golden/M11-09/sequencer-codec-probe.json",
|
||||
"cancellationCode": "SEQUENCER_CANCELLED",
|
||||
"budgetCode": "SEQUENCER_BUDGET_EXCEEDED"
|
||||
}
|
||||
],
|
||||
"textureFixture": {
|
||||
"path": "tests/files/web/resources/udim_1001.png",
|
||||
"sha256": "0c66d053412de9e99f1fb83fa8f199d061df874ad1b6bda86c76cf3f499382cf"
|
||||
},
|
||||
"requiredStages": ["CANCELLATION", "RESTART", "BUDGET", "RELEASE", "RECOVERY"],
|
||||
"invariants": {
|
||||
"cancellation": "zero result publication and zero temporary resources",
|
||||
"restart": "generation 1 and 2 preserve domain identity and output SHA-256",
|
||||
"budget": "stable domain-specific block before committed state mutation",
|
||||
"release": "positive resource and byte release followed by zero owned resources",
|
||||
"recovery": "a bounded real fixture produces non-empty output after restart"
|
||||
},
|
||||
"verification": {
|
||||
"unit": "web/tests/unit/render-compositor-media-recovery.test.mjs",
|
||||
"browser": "web/tests/e2e/render-compositor-media-recovery.spec.ts",
|
||||
"command": "npm --prefix web run test:render-compositor-media-recovery"
|
||||
}
|
||||
}
|
||||
53
tests/golden/M12-01/evidence.json
Normal file
53
tests/golden/M12-01/evidence.json
Normal file
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"task": "M12-01I",
|
||||
"parentTask": "M12-01",
|
||||
"parityId": "N-023-ASSET-CATALOG-SCHEMA",
|
||||
"status": "READY",
|
||||
"enablingTask": true,
|
||||
"parityStateChange": false,
|
||||
"implementationClass": "NOT_APPLICABLE",
|
||||
"subtasks": [
|
||||
{ "task": "M12-01A", "path": "tests/golden/M12-01A/asset-catalog-field-inventory.json", "sha256": "194d5fd0fef5044f5e82a51931a061e226445f0f53410af9dc0594c70a79e936" },
|
||||
{ "task": "M12-01B", "path": "tests/golden/M12-01B/manifest.json", "sha256": "1ee0abf961b82195e5fb586eb9e59302e4f7f839615f1ebbdf5cdcba8b60accd" },
|
||||
{ "task": "M12-01C", "path": "tests/golden/M12-01C/manifest.json", "sha256": "612892b03a689ebe19804e495bd29744634ab43a3b29fff41adc2d7a5d0b67b9" },
|
||||
{ "task": "M12-01D", "path": "tests/golden/M12-01D/manifest.json", "sha256": "fea9e01b51f64a7e9c15dcfc521b3e84636525ffbe0a849f7a6f1b0be2b02e9d" },
|
||||
{ "task": "M12-01E", "path": "tests/golden/M12-01E/manifest.json", "sha256": "cf55ea1f9430df04fb55bf07c68c1b2099b044b0da0fdb07a7251b13c459724b" },
|
||||
{ "task": "M12-01F", "path": "tests/golden/M12-01F/manifest.json", "sha256": "127b2b605ac79a891cd6ab516ee031c02624a512e1043b111bfac2ebf1d8278d" },
|
||||
{ "task": "M12-01G", "path": "tests/golden/M12-01G/manifest.json", "sha256": "2596067956211698f1b7a07d9423411e2db830b72e6b78593354953a568738e7" },
|
||||
{ "task": "M12-01H", "path": "tests/golden/M12-01H/manifest.json", "sha256": "59138eb83d63ae0d397b152e587fbe005d02bc2d229ee484c279b452ea3f2eae" }
|
||||
],
|
||||
"schema": {
|
||||
"desktopCanonical": { "path": "tests/golden/M12-01B/canonical.json", "sha256": "6fc6def0b9d0e4f51ef40c1d87a1f17074f6340c126a01aee49e5e53271c961e" },
|
||||
"webV2Golden": { "path": "tests/golden/M12-01C/asset-catalog-v2.json", "sha256": "2abe279cebbffd1341dc61b1fdc5366dd1f6f42769ba36171782019ca61fb929" },
|
||||
"productionProtocol": { "path": "web/protocol/asset-catalog-v2.ts", "sha256": "0949838efb2f69466368185b80bb1d2e851672d78311fcd5270eb0814576e5d7" }
|
||||
},
|
||||
"migration": {
|
||||
"sourceFixture": { "path": "tests/golden/M12-01D/catalog-v1.json", "sha256": "1cc43d181afefd63b53cb9c3626bb58195fad20187d6e534adc1ecbe65f75da5" },
|
||||
"targetFixture": { "path": "tests/golden/M12-01D/catalog-v2.json", "sha256": "38e5d1771b3d2d63ce5e10268eac4513f2043b0cbf71ab1799c1868bc2aa97fb" },
|
||||
"report": { "path": "tests/golden/M12-01D/migration-report.json", "sha256": "57b972c1bbb9ae326c3f264c225c21546682442ff3194fbc343e1c46a2cd410a" },
|
||||
"productionProtocol": { "path": "web/protocol/asset-catalog-migration.ts", "sha256": "6d74bc5605f94e05208eda82830192d38f88066dfc409058555a8876c785f226" },
|
||||
"sourceManifestSha256": "cbdc8b03dc9ce1f38b44bae1c7459eb273126cbd6c7dd33115d2b1966e975881",
|
||||
"targetManifestSha256": "0c08ed1af1dd0998c809f76d969fd21d8fdae28ad37cf5571bdc99b16bf3ec90",
|
||||
"revision": 7
|
||||
},
|
||||
"runtime": {
|
||||
"indexedDB": { "path": "web/app/src/storage/asset-catalog-indexeddb.ts", "sha256": "283a08bccb5ea301bd2aef8f01d4496972e3962f75e340479336c50e3539ba57" },
|
||||
"restartWorker": { "path": "web/app/src/workers/asset-catalog-restart-test.worker.ts", "sha256": "e6007d352555be046b89d0becd719692652ee0418cc97087bf7599155f2a538e" },
|
||||
"catalogCount": 3,
|
||||
"assetCount": 2,
|
||||
"faultPoints": ["AFTER_TARGET_PUT", "AFTER_SOURCE_DELETE"],
|
||||
"restartContexts": ["INITIAL_PAGE", "RELOADED_PAGE", "WORKER_GENERATION_1", "WORKER_GENERATION_2"]
|
||||
},
|
||||
"claims": [
|
||||
"BLENDER_5_2_FIELD_INVENTORY_BOUND",
|
||||
"DESKTOP_FIXTURE_REOPENED",
|
||||
"SCHEMA_V2_UTF8_BUDGETED",
|
||||
"V1_TO_V2_MIGRATION_HASH_BOUND",
|
||||
"LEGACY_READER_FAIL_CLOSED",
|
||||
"NEGATIVE_MATRIX_RECOVERS",
|
||||
"INDEXEDDB_ABORT_PRESERVES_V1",
|
||||
"PAGE_WORKER_RESTART_IDENTITY_STABLE"
|
||||
],
|
||||
"nextTask": "M12-02A"
|
||||
}
|
||||
264
tests/golden/M12-01A/asset-catalog-field-inventory.json
Normal file
264
tests/golden/M12-01A/asset-catalog-field-inventory.json
Normal file
@@ -0,0 +1,264 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"task": "M12-01A",
|
||||
"enablingTask": true,
|
||||
"parityStateChange": false,
|
||||
"blenderVersion": "5.2.0",
|
||||
"sources": [
|
||||
{
|
||||
"role": "DNA_ASSET_STORAGE",
|
||||
"path": "blender-5.2.0/source/blender/makesdna/DNA_asset_types.h",
|
||||
"sha256": "de97f44c71d46049b0effdf0fed6aba3f0df72d6d4ba89b88f61e7624d3b9a49"
|
||||
},
|
||||
{
|
||||
"role": "RNA_ASSET_API",
|
||||
"path": "blender-5.2.0/source/blender/makesrna/intern/rna_asset.cc",
|
||||
"sha256": "92c9cd5ce718d5b250a02eaefbd1c356a962e2f69b235d23fc97447a9a567a22"
|
||||
},
|
||||
{
|
||||
"role": "ASSET_BLEND_IO",
|
||||
"path": "blender-5.2.0/source/blender/blenkernel/intern/asset.cc",
|
||||
"sha256": "eb66d1bfb3efb39cd6c6a6be9e6ff23f2be10a1c427e5b998a2dbcd1ec5b9adc"
|
||||
},
|
||||
{
|
||||
"role": "CATALOG_MODEL",
|
||||
"path": "blender-5.2.0/source/blender/asset_system/AS_asset_catalog.hh",
|
||||
"sha256": "6fd93dc78d0634b9c4d0ee03a23d3233be591204aea7957d1b9768b7c3508110"
|
||||
},
|
||||
{
|
||||
"role": "CATALOG_PATH",
|
||||
"path": "blender-5.2.0/source/blender/asset_system/AS_asset_catalog_path.hh",
|
||||
"sha256": "9579b74fac1cb3208c96c8aa56b51c9e44d96e3416b718d77270d5f8269bc7b9"
|
||||
},
|
||||
{
|
||||
"role": "CATALOG_FILE_API",
|
||||
"path": "blender-5.2.0/source/blender/asset_system/intern/asset_catalog_definition_file.hh",
|
||||
"sha256": "880f5ae42fa6c532d46b70d5264f3ac7f8e8abab8c6cf189a4e8aebdd9dd04b6"
|
||||
},
|
||||
{
|
||||
"role": "CATALOG_FILE_FORMAT",
|
||||
"path": "blender-5.2.0/source/blender/asset_system/intern/asset_catalog_definition_file.cc",
|
||||
"sha256": "4316b683125df87e07e4f1116b44f58eb725b654ac9941118a148c08ee916a18"
|
||||
},
|
||||
{
|
||||
"role": "CURRENT_WEB_V1_SNAPSHOT",
|
||||
"path": "web/protocol/asset-library-io.ts",
|
||||
"sha256": "5c62539db3833a2113007dfbdfd12ca06704d86e0cf0e57ad97f7fbf6acda7f4"
|
||||
}
|
||||
],
|
||||
"assetTag": {
|
||||
"fields": [
|
||||
{
|
||||
"name": "next",
|
||||
"cType": "AssetTag*",
|
||||
"sourceToken": "struct AssetTag *next",
|
||||
"persistence": "LIST_LINKAGE",
|
||||
"semantic": "NON_USER_LINKAGE",
|
||||
"rnaProperty": null
|
||||
},
|
||||
{
|
||||
"name": "prev",
|
||||
"cType": "AssetTag*",
|
||||
"sourceToken": "*prev = nullptr",
|
||||
"persistence": "LIST_LINKAGE",
|
||||
"semantic": "NON_USER_LINKAGE",
|
||||
"rnaProperty": null
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"cType": "char[64]",
|
||||
"sourceToken": "char name[/*MAX_NAME*/ 64]",
|
||||
"persistence": "BLEND",
|
||||
"semantic": "USER_VALUE",
|
||||
"rnaProperty": "name",
|
||||
"maximumStorageBytesIncludingNull": 64
|
||||
}
|
||||
],
|
||||
"collectionOperations": ["new", "remove"],
|
||||
"newSupportsSkipIfExists": true,
|
||||
"orderIsPersistent": true
|
||||
},
|
||||
"assetMetaData": {
|
||||
"dnaFields": [
|
||||
{
|
||||
"name": "local_type_info",
|
||||
"cType": "AssetTypeInfo*",
|
||||
"sourceToken": "struct AssetTypeInfo *local_type_info",
|
||||
"persistence": "RUNTIME_ONLY",
|
||||
"semantic": "CALLBACK_TYPE",
|
||||
"rnaProperty": null
|
||||
},
|
||||
{
|
||||
"name": "properties",
|
||||
"cType": "IDProperty*",
|
||||
"sourceToken": "struct IDProperty *properties",
|
||||
"persistence": "BLEND_IDPROPERTY",
|
||||
"semantic": "CUSTOM_METADATA_NO_ID_POINTERS",
|
||||
"rnaProperty": "CUSTOM_ID_PROPERTIES"
|
||||
},
|
||||
{
|
||||
"name": "catalog_id",
|
||||
"cType": "bUUID",
|
||||
"sourceToken": "struct bUUID catalog_id",
|
||||
"persistence": "BLEND",
|
||||
"semantic": "AUTHORITATIVE_CATALOG_ID",
|
||||
"rnaProperty": "catalog_id"
|
||||
},
|
||||
{
|
||||
"name": "catalog_simple_name",
|
||||
"cType": "char[64]",
|
||||
"sourceToken": "char catalog_simple_name[/*MAX_NAME*/ 64]",
|
||||
"persistence": "BLEND",
|
||||
"semantic": "RECOVERY_ONLY_NOT_AUTHORITY",
|
||||
"rnaProperty": "catalog_simple_name"
|
||||
},
|
||||
{
|
||||
"name": "author",
|
||||
"cType": "char*",
|
||||
"sourceToken": "char *author = nullptr",
|
||||
"persistence": "BLEND_DYNAMIC_STRING",
|
||||
"semantic": "OPTIONAL_DISPLAY_TEXT",
|
||||
"rnaProperty": "author"
|
||||
},
|
||||
{
|
||||
"name": "description",
|
||||
"cType": "char*",
|
||||
"sourceToken": "char *description = nullptr",
|
||||
"persistence": "BLEND_DYNAMIC_STRING",
|
||||
"semantic": "OPTIONAL_DISPLAY_TEXT",
|
||||
"rnaProperty": "description"
|
||||
},
|
||||
{
|
||||
"name": "copyright",
|
||||
"cType": "char*",
|
||||
"sourceToken": "char *copyright = nullptr",
|
||||
"persistence": "BLEND_DYNAMIC_STRING",
|
||||
"semantic": "OPTIONAL_LEGAL_TEXT",
|
||||
"rnaProperty": "copyright"
|
||||
},
|
||||
{
|
||||
"name": "license",
|
||||
"cType": "char*",
|
||||
"sourceToken": "char *license = nullptr",
|
||||
"persistence": "BLEND_DYNAMIC_STRING",
|
||||
"semantic": "OPTIONAL_LEGAL_TEXT",
|
||||
"rnaProperty": "license"
|
||||
},
|
||||
{
|
||||
"name": "tags",
|
||||
"cType": "ListBaseT<AssetTag>",
|
||||
"sourceToken": "ListBaseT<AssetTag> tags",
|
||||
"persistence": "BLEND_ORDERED_COLLECTION",
|
||||
"semantic": "FILTER_TOKENS",
|
||||
"rnaProperty": "tags"
|
||||
},
|
||||
{
|
||||
"name": "active_tag",
|
||||
"cType": "short",
|
||||
"sourceToken": "short active_tag = 0",
|
||||
"persistence": "BLEND",
|
||||
"semantic": "EDITOR_SELECTION_INDEX",
|
||||
"rnaProperty": "active_tag"
|
||||
},
|
||||
{
|
||||
"name": "tot_tags",
|
||||
"cType": "short",
|
||||
"sourceToken": "short tot_tags = 0",
|
||||
"persistence": "BLEND_DERIVED_COUNT",
|
||||
"semantic": "MUST_EQUAL_TAG_LIST_COUNT",
|
||||
"rnaProperty": null
|
||||
},
|
||||
{
|
||||
"name": "flag",
|
||||
"cType": "AssetMetaDataFlag",
|
||||
"sourceToken": "AssetMetaDataFlag flag = {}",
|
||||
"persistence": "BLEND",
|
||||
"semantic": "USE_OWN_IMPORT_METHOD_BIT",
|
||||
"rnaProperty": "use_preferred_import_method"
|
||||
},
|
||||
{
|
||||
"name": "preferred_import_method",
|
||||
"cType": "eAssetImportMethod",
|
||||
"sourceToken": "eAssetImportMethod preferred_import_method",
|
||||
"persistence": "BLEND",
|
||||
"semantic": "CONDITIONAL_IMPORT_POLICY",
|
||||
"rnaProperty": "preferred_import_method"
|
||||
},
|
||||
{
|
||||
"name": "_pad",
|
||||
"cType": "char[4]",
|
||||
"sourceToken": "char _pad[4] = {}",
|
||||
"persistence": "ABI_PADDING",
|
||||
"semantic": "NON_USER_PADDING",
|
||||
"rnaProperty": null
|
||||
}
|
||||
],
|
||||
"rnaProperties": [
|
||||
{ "identifier": "author", "type": "STRING", "isReadonly": false, "isRuntime": false },
|
||||
{ "identifier": "description", "type": "STRING", "isReadonly": false, "isRuntime": false },
|
||||
{ "identifier": "copyright", "type": "STRING", "isReadonly": false, "isRuntime": false },
|
||||
{ "identifier": "license", "type": "STRING", "isReadonly": false, "isRuntime": false },
|
||||
{ "identifier": "tags", "type": "COLLECTION", "isReadonly": true, "isRuntime": false },
|
||||
{ "identifier": "active_tag", "type": "INT", "isReadonly": false, "isRuntime": false },
|
||||
{ "identifier": "catalog_id", "type": "STRING", "isReadonly": false, "isRuntime": false },
|
||||
{ "identifier": "catalog_simple_name", "type": "STRING", "isReadonly": true, "isRuntime": false },
|
||||
{ "identifier": "use_preferred_import_method", "type": "BOOLEAN", "isReadonly": false, "isRuntime": false },
|
||||
{ "identifier": "preferred_import_method", "type": "ENUM", "isReadonly": false, "isRuntime": false }
|
||||
],
|
||||
"preferredImportMethods": ["LINK", "APPEND", "APPEND_REUSE", "PACK"],
|
||||
"emptyAuthorDescriptionCopyrightLicenseAreValid": true,
|
||||
"catalogIdFormat": "RFC4122_UUID_OR_NIL",
|
||||
"customPropertiesMayNotReferenceDataBlocks": true
|
||||
},
|
||||
"catalog": {
|
||||
"semanticFields": [
|
||||
{ "name": "catalog_id", "cType": "CatalogID", "sourceToken": "const CatalogID catalog_id", "persistence": "CATALOG_DEFINITION_FILE" },
|
||||
{ "name": "path", "cType": "AssetCatalogPath", "sourceToken": "AssetCatalogPath path", "persistence": "CATALOG_DEFINITION_FILE" },
|
||||
{ "name": "simple_name", "cType": "std::string", "sourceToken": "std::string simple_name", "persistence": "CATALOG_DEFINITION_FILE_AND_ASSET_RECOVERY_COPY" }
|
||||
],
|
||||
"runtimeFlags": [
|
||||
{ "name": "is_deleted", "sourceToken": "bool is_deleted = false" },
|
||||
{ "name": "is_first_loaded", "sourceToken": "bool is_first_loaded = false" },
|
||||
{ "name": "has_unsaved_changes", "sourceToken": "bool has_unsaved_changes = false" }
|
||||
],
|
||||
"definitionFile": {
|
||||
"defaultFilename": "blender_assets.cats.txt",
|
||||
"supportedVersion": 1,
|
||||
"versionMarker": "VERSION ",
|
||||
"recordFields": ["catalog_id", "path", "simple_name"],
|
||||
"delimiter": ":",
|
||||
"writeOrder": ["path", "is_first_loaded", "catalog_id"],
|
||||
"emptyAndCommentLinesIgnored": true,
|
||||
"firstSemanticLineMustBeVersion": true
|
||||
},
|
||||
"path": {
|
||||
"encoding": "UTF8_BYTES",
|
||||
"separator": "/",
|
||||
"hierarchy": "IMPLICIT_PATH_COMPONENTS",
|
||||
"leadingSlash": false,
|
||||
"componentColonAllowed": false,
|
||||
"cleanup": ["TRIM_COMPONENTS", "REMOVE_EMPTY_COMPONENTS", "REPLACE_INVALID_CHARACTERS"]
|
||||
},
|
||||
"identityRules": {
|
||||
"assetReferenceAuthority": "catalog_id",
|
||||
"simpleNameAuthority": false,
|
||||
"parentIdStored": false,
|
||||
"duplicatePathsAllowed": true,
|
||||
"duplicatePathSelectionOrder": ["is_first_loaded", "catalog_id"]
|
||||
}
|
||||
},
|
||||
"currentWebSnapshot": {
|
||||
"assetCatalogIRFields": ["id", "name", "parentId"],
|
||||
"assetEntryIRFields": ["id", "name", "kind", "catalogId", "tags", "author", "license", "sourceSha256", "sourcePath", "preview"],
|
||||
"directlyRepresentedMetadata": ["catalog_id", "tags", "author", "license"],
|
||||
"missingMetadata": ["properties", "description", "copyright", "catalog_simple_name", "active_tag", "use_preferred_import_method", "preferred_import_method"],
|
||||
"semanticDrift": [
|
||||
"Blender author and license are optional; Web v1 requires both to be non-empty",
|
||||
"Blender catalog hierarchy is an implicit cleaned path; Web v1 stores an explicit parentId graph",
|
||||
"Blender catalog simple_name is recovery metadata; Web v1 name is the only catalog label",
|
||||
"Blender tag storage is 64 bytes including the terminator; Web v1 bounds JavaScript string length to 64 code units",
|
||||
"Blender custom ID properties are persisted but Web v1 has no preservation field"
|
||||
]
|
||||
},
|
||||
"nextTask": "M12-01B"
|
||||
}
|
||||
116
tests/golden/M12-01B/canonical.json
Normal file
116
tests/golden/M12-01B/canonical.json
Normal file
@@ -0,0 +1,116 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"task": "M12-01B",
|
||||
"enablingTask": true,
|
||||
"parityStateChange": false,
|
||||
"blenderVersion": "5.2.0",
|
||||
"catalogDefinition": {
|
||||
"fileName": "blender_assets.cats.txt",
|
||||
"version": 1,
|
||||
"records": [
|
||||
{
|
||||
"catalogId": "11111111-1111-4111-8111-111111111111",
|
||||
"path": "Characters",
|
||||
"simpleName": "Characters",
|
||||
"parentPath": null
|
||||
},
|
||||
{
|
||||
"catalogId": "22222222-2222-4222-8222-222222222222",
|
||||
"path": "Characters/Heroes",
|
||||
"simpleName": "Heroes",
|
||||
"parentPath": "Characters"
|
||||
},
|
||||
{
|
||||
"catalogId": "33333333-3333-4333-8333-333333333333",
|
||||
"path": "Materials/Metal",
|
||||
"simpleName": "Metal",
|
||||
"parentPath": "Materials"
|
||||
}
|
||||
]
|
||||
},
|
||||
"assets": [
|
||||
{
|
||||
"idType": "MATERIAL",
|
||||
"name": "M12 Brushed Metal",
|
||||
"catalogId": "33333333-3333-4333-8333-333333333333",
|
||||
"catalogSimpleName": "",
|
||||
"author": "",
|
||||
"description": "",
|
||||
"copyright": "",
|
||||
"license": "",
|
||||
"tags": [
|
||||
"metal"
|
||||
],
|
||||
"activeTag": 0,
|
||||
"usePreferredImportMethod": false,
|
||||
"preferredImportMethod": "APPEND",
|
||||
"customProperties": [
|
||||
{
|
||||
"name": "roughness",
|
||||
"type": "FLOAT",
|
||||
"value": 0.35
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"idType": "OBJECT",
|
||||
"name": "M12 Hero",
|
||||
"catalogId": "22222222-2222-4222-8222-222222222222",
|
||||
"catalogSimpleName": "",
|
||||
"author": "M12 Artist",
|
||||
"description": "Desktop catalog v1 object fixture",
|
||||
"copyright": "Copyright 2026 M12 Fixture Authors",
|
||||
"license": "CC0-1.0",
|
||||
"tags": [
|
||||
"character",
|
||||
"hero",
|
||||
"rig-ready"
|
||||
],
|
||||
"activeTag": 1,
|
||||
"usePreferredImportMethod": true,
|
||||
"preferredImportMethod": "APPEND",
|
||||
"customProperties": [
|
||||
{
|
||||
"name": "approved",
|
||||
"type": "BOOL",
|
||||
"value": true
|
||||
},
|
||||
{
|
||||
"name": "dimensions",
|
||||
"type": "IDPROPERTYARRAY",
|
||||
"value": [
|
||||
2.0,
|
||||
2.0,
|
||||
0.0
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "rating",
|
||||
"type": "INT",
|
||||
"value": 5
|
||||
},
|
||||
{
|
||||
"name": "source",
|
||||
"type": "STR",
|
||||
"value": "M12-01B"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"idType": "WORLD",
|
||||
"name": "M12 Uncataloged World",
|
||||
"catalogId": "00000000-0000-0000-0000-000000000000",
|
||||
"catalogSimpleName": "",
|
||||
"author": "M12 Artist",
|
||||
"description": "Asset without a catalog assignment",
|
||||
"copyright": "",
|
||||
"license": "CC0-1.0",
|
||||
"tags": [],
|
||||
"activeTag": 0,
|
||||
"usePreferredImportMethod": true,
|
||||
"preferredImportMethod": "LINK",
|
||||
"customProperties": []
|
||||
}
|
||||
],
|
||||
"nextTask": "M12-01C"
|
||||
}
|
||||
60
tests/golden/M12-01B/manifest.json
Normal file
60
tests/golden/M12-01B/manifest.json
Normal file
@@ -0,0 +1,60 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"task": "M12-01B",
|
||||
"parentTask": "M12-01",
|
||||
"parityId": "N-023-ASSET-CATALOG-SCHEMA",
|
||||
"enablingTask": true,
|
||||
"parityStateChange": false,
|
||||
"implementationClass": "NOT_APPLICABLE",
|
||||
"blenderVersion": "5.2.0",
|
||||
"artifacts": {
|
||||
"generator": {
|
||||
"path": "tools/web/generate-asset-catalog-v1.py",
|
||||
"sha256": "4156ec6293562d44f87ad5a268de52fbfa31a65da8263a67a2bc45bf22999307"
|
||||
},
|
||||
"exporter": {
|
||||
"path": "tools/web/export-asset-catalog-v1.py",
|
||||
"sha256": "3dbccd8df54d8686aa526f7967f58fd1008336c4c5b1deed01a136ba15aa3e41"
|
||||
},
|
||||
"checker": {
|
||||
"path": "tools/web/check-asset-catalog-v1-fixture.mjs",
|
||||
"sha256": "c2813aa36482eff0b23ca0209f488cfe70ea11e4f39cc6c6036d6c47bbc8e17b"
|
||||
},
|
||||
"fixture": {
|
||||
"path": "tests/files/web/m12_asset_catalog_v1/m12_asset_catalog_v1.blend",
|
||||
"sha256": "8facfe82e3ca0a0605c139ef5bdcbb82d27a6953a6add4e008b68af90bc1211f"
|
||||
},
|
||||
"catalogDefinition": {
|
||||
"path": "tests/files/web/m12_asset_catalog_v1/blender_assets.cats.txt",
|
||||
"sha256": "a72f542acb2d2239951a2b20cda0f92305e1d6d8222f84072ad1aab52e3e4c98"
|
||||
},
|
||||
"canonicalReport": {
|
||||
"path": "tests/golden/M12-01B/canonical.json",
|
||||
"sha256": "6fc6def0b9d0e4f51ef40c1d87a1f17074f6340c126a01aee49e5e53271c961e"
|
||||
}
|
||||
},
|
||||
"coverage": {
|
||||
"catalogRecords": 3,
|
||||
"assetRecords": 3,
|
||||
"assetIdTypes": ["MATERIAL", "OBJECT", "WORLD"],
|
||||
"metadata": [
|
||||
"properties",
|
||||
"catalog_id",
|
||||
"catalog_simple_name",
|
||||
"author",
|
||||
"description",
|
||||
"copyright",
|
||||
"license",
|
||||
"tags",
|
||||
"active_tag",
|
||||
"use_preferred_import_method",
|
||||
"preferred_import_method"
|
||||
]
|
||||
},
|
||||
"nonGoals": [
|
||||
"Web catalog schema v2",
|
||||
"Web migration behavior",
|
||||
"Catalog or asset mutation parity"
|
||||
],
|
||||
"nextTask": "M12-01C"
|
||||
}
|
||||
127
tests/golden/M12-01C/asset-catalog-v2.json
Normal file
127
tests/golden/M12-01C/asset-catalog-v2.json
Normal file
@@ -0,0 +1,127 @@
|
||||
{
|
||||
"schemaVersion": 2,
|
||||
"revision": 1,
|
||||
"catalogs": [
|
||||
{
|
||||
"catalogId": "11111111-1111-4111-8111-111111111111",
|
||||
"path": "Characters",
|
||||
"simpleName": "Characters",
|
||||
"parentPath": null
|
||||
},
|
||||
{
|
||||
"catalogId": "22222222-2222-4222-8222-222222222222",
|
||||
"path": "Characters/Heroes",
|
||||
"simpleName": "Heroes",
|
||||
"parentPath": "Characters"
|
||||
},
|
||||
{
|
||||
"catalogId": "33333333-3333-4333-8333-333333333333",
|
||||
"path": "Materials/Metal",
|
||||
"simpleName": "Metal",
|
||||
"parentPath": "Materials"
|
||||
}
|
||||
],
|
||||
"assets": [
|
||||
{
|
||||
"assetId": "asset:0ecbd393de19b0cc4fe26956ba658e782aaad4a859b4a9647161f1405ddadfd5",
|
||||
"assetLibraryIdentifier": null,
|
||||
"relativeAssetIdentifier": "Material/M12 Brushed Metal",
|
||||
"idType": "MATERIAL",
|
||||
"name": "M12 Brushed Metal",
|
||||
"catalogId": "33333333-3333-4333-8333-333333333333",
|
||||
"catalogSimpleName": "",
|
||||
"author": "",
|
||||
"description": "",
|
||||
"copyright": "",
|
||||
"license": "",
|
||||
"tags": [
|
||||
"metal"
|
||||
],
|
||||
"activeTag": 0,
|
||||
"usePreferredImportMethod": false,
|
||||
"preferredImportMethod": "APPEND",
|
||||
"customProperties": [
|
||||
{
|
||||
"name": "roughness",
|
||||
"type": "FLOAT",
|
||||
"value": 0.35
|
||||
}
|
||||
],
|
||||
"sourceSha256": null,
|
||||
"sourcePath": null,
|
||||
"preview": null
|
||||
},
|
||||
{
|
||||
"assetId": "asset:4e9e5a3755581b0f3b604748fc5c64f4f51f14b5c2efd485a7093edeae4dbabd",
|
||||
"assetLibraryIdentifier": null,
|
||||
"relativeAssetIdentifier": "Object/M12 Hero",
|
||||
"idType": "OBJECT",
|
||||
"name": "M12 Hero",
|
||||
"catalogId": "22222222-2222-4222-8222-222222222222",
|
||||
"catalogSimpleName": "",
|
||||
"author": "M12 Artist",
|
||||
"description": "Desktop catalog v1 object fixture",
|
||||
"copyright": "Copyright 2026 M12 Fixture Authors",
|
||||
"license": "CC0-1.0",
|
||||
"tags": [
|
||||
"character",
|
||||
"hero",
|
||||
"rig-ready"
|
||||
],
|
||||
"activeTag": 1,
|
||||
"usePreferredImportMethod": true,
|
||||
"preferredImportMethod": "APPEND",
|
||||
"customProperties": [
|
||||
{
|
||||
"name": "approved",
|
||||
"type": "BOOL",
|
||||
"value": true
|
||||
},
|
||||
{
|
||||
"name": "dimensions",
|
||||
"type": "FLOAT_ARRAY",
|
||||
"value": [
|
||||
2,
|
||||
2,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "rating",
|
||||
"type": "INT",
|
||||
"value": 5
|
||||
},
|
||||
{
|
||||
"name": "source",
|
||||
"type": "STRING",
|
||||
"value": "M12-01B"
|
||||
}
|
||||
],
|
||||
"sourceSha256": null,
|
||||
"sourcePath": null,
|
||||
"preview": null
|
||||
},
|
||||
{
|
||||
"assetId": "asset:7ef9ef809269e187e988effca992a2249e6340bd65bba2da2c4b2545c8a7bd8c",
|
||||
"assetLibraryIdentifier": null,
|
||||
"relativeAssetIdentifier": "World/M12 Uncataloged World",
|
||||
"idType": "WORLD",
|
||||
"name": "M12 Uncataloged World",
|
||||
"catalogId": null,
|
||||
"catalogSimpleName": "",
|
||||
"author": "M12 Artist",
|
||||
"description": "Asset without a catalog assignment",
|
||||
"copyright": "",
|
||||
"license": "CC0-1.0",
|
||||
"tags": [],
|
||||
"activeTag": 0,
|
||||
"usePreferredImportMethod": true,
|
||||
"preferredImportMethod": "LINK",
|
||||
"customProperties": [],
|
||||
"sourceSha256": null,
|
||||
"sourcePath": null,
|
||||
"preview": null
|
||||
}
|
||||
],
|
||||
"libraries": []
|
||||
}
|
||||
45
tests/golden/M12-01C/manifest.json
Normal file
45
tests/golden/M12-01C/manifest.json
Normal file
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"task": "M12-01C",
|
||||
"parentTask": "M12-01",
|
||||
"parityId": "N-023-ASSET-CATALOG-SCHEMA",
|
||||
"enablingTask": true,
|
||||
"parityStateChange": false,
|
||||
"implementationClass": "NOT_APPLICABLE",
|
||||
"blenderVersion": "5.2.0",
|
||||
"identityContract": {
|
||||
"catalog": "CANONICAL_RFC4122_UUID",
|
||||
"asset": "SHA256_OF_BLENDER_ASSET_WEAK_REFERENCE_V1",
|
||||
"renameBehavior": "RELATIVE_IDENTIFIER_AND_ASSET_ID_CHANGE_TO_MATCH_BLENDER",
|
||||
"catalogHierarchy": "CLEANED_SLASH_PATH"
|
||||
},
|
||||
"artifacts": {
|
||||
"protocol": {
|
||||
"path": "web/protocol/asset-catalog-v2.ts",
|
||||
"sha256": "0949838efb2f69466368185b80bb1d2e851672d78311fcd5270eb0814576e5d7"
|
||||
},
|
||||
"generator": {
|
||||
"path": "tools/web/generate-asset-catalog-v2.mjs",
|
||||
"sha256": "b20e7dbd8508ca8ecf17d7da507b45b1225315bac7759c55c7b324683d660881"
|
||||
},
|
||||
"desktopCanonical": {
|
||||
"path": "tests/golden/M12-01B/canonical.json",
|
||||
"sha256": "6fc6def0b9d0e4f51ef40c1d87a1f17074f6340c126a01aee49e5e53271c961e"
|
||||
},
|
||||
"schemaGolden": {
|
||||
"path": "tests/golden/M12-01C/asset-catalog-v2.json",
|
||||
"sha256": "2abe279cebbffd1341dc61b1fdc5366dd1f6f42769ba36171782019ca61fb929"
|
||||
},
|
||||
"blenderWeakReferenceSource": {
|
||||
"path": "blender-5.2.0/source/blender/asset_system/intern/asset_representation.cc",
|
||||
"sha256": "ace3d6468bcdb2f6190431e967495794b7afa35758fd82fc3941b1ce7d374635"
|
||||
}
|
||||
},
|
||||
"nonGoals": [
|
||||
"Schema v1 to v2 migration",
|
||||
"Schema v2 legacy-reader policy",
|
||||
"IndexedDB migration",
|
||||
"Catalog mutation parity"
|
||||
],
|
||||
"nextTask": "M12-01D"
|
||||
}
|
||||
58
tests/golden/M12-01D/catalog-v1.json
Normal file
58
tests/golden/M12-01D/catalog-v1.json
Normal file
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"revision": 7,
|
||||
"catalogs": [
|
||||
{ "id": "catalog:root", "name": "Characters", "parentId": null },
|
||||
{ "id": "catalog:heroes", "name": "Heroes", "parentId": "catalog:root" },
|
||||
{ "id": "44444444-4444-4444-8444-444444444444", "name": "Animation", "parentId": null }
|
||||
],
|
||||
"assets": [
|
||||
{
|
||||
"id": "legacy:hero",
|
||||
"name": "Legacy Hero",
|
||||
"kind": "OBJECT",
|
||||
"catalogId": "catalog:heroes",
|
||||
"tags": ["character", "hero"],
|
||||
"author": "M12 Artist",
|
||||
"license": "CC0-1.0",
|
||||
"sourceSha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
"sourcePath": "assets/legacy-hero.blend",
|
||||
"preview": {
|
||||
"assetId": "legacy:hero",
|
||||
"sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
|
||||
"mimeType": "image/png",
|
||||
"width": 64,
|
||||
"height": 64,
|
||||
"byteLength": 4096
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "legacy:walk",
|
||||
"name": "Legacy Walk",
|
||||
"kind": "ACTION",
|
||||
"catalogId": "44444444-4444-4444-8444-444444444444",
|
||||
"tags": ["walk"],
|
||||
"author": "M12 Artist",
|
||||
"license": "CC-BY-4.0",
|
||||
"sourceSha256": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
|
||||
}
|
||||
],
|
||||
"libraries": [
|
||||
{
|
||||
"id": "library:characters",
|
||||
"name": "Characters",
|
||||
"sourcePath": "libraries/characters.blend",
|
||||
"sourceSha256": "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd",
|
||||
"dependencyIds": ["library:materials"],
|
||||
"readOnly": true
|
||||
},
|
||||
{
|
||||
"id": "library:materials",
|
||||
"name": "Materials",
|
||||
"sourcePath": "libraries/materials.blend",
|
||||
"sourceSha256": "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee",
|
||||
"dependencyIds": [],
|
||||
"readOnly": true
|
||||
}
|
||||
]
|
||||
}
|
||||
99
tests/golden/M12-01D/catalog-v2.json
Normal file
99
tests/golden/M12-01D/catalog-v2.json
Normal file
@@ -0,0 +1,99 @@
|
||||
{
|
||||
"schemaVersion": 2,
|
||||
"revision": 7,
|
||||
"catalogs": [
|
||||
{
|
||||
"catalogId": "44444444-4444-4444-8444-444444444444",
|
||||
"path": "Animation",
|
||||
"simpleName": "Animation",
|
||||
"parentPath": null
|
||||
},
|
||||
{
|
||||
"catalogId": "fe3ca14c-95d7-549f-a13b-1bb4c07b6074",
|
||||
"path": "Characters",
|
||||
"simpleName": "Characters",
|
||||
"parentPath": null
|
||||
},
|
||||
{
|
||||
"catalogId": "b4bb3608-9267-5aba-bb9e-9e97b006d5ab",
|
||||
"path": "Characters/Heroes",
|
||||
"simpleName": "Heroes",
|
||||
"parentPath": "Characters"
|
||||
}
|
||||
],
|
||||
"assets": [
|
||||
{
|
||||
"assetId": "asset:ac88c6147ada877adccf4df8ca54d64e1a9cb50a13514212f6d19b1da1660f8d",
|
||||
"assetLibraryIdentifier": null,
|
||||
"relativeAssetIdentifier": "Action/Legacy Walk",
|
||||
"idType": "ACTION",
|
||||
"name": "Legacy Walk",
|
||||
"catalogId": "44444444-4444-4444-8444-444444444444",
|
||||
"catalogSimpleName": "Animation",
|
||||
"author": "M12 Artist",
|
||||
"description": "",
|
||||
"copyright": "",
|
||||
"license": "CC-BY-4.0",
|
||||
"tags": [
|
||||
"walk"
|
||||
],
|
||||
"activeTag": 0,
|
||||
"usePreferredImportMethod": false,
|
||||
"preferredImportMethod": "APPEND",
|
||||
"customProperties": [],
|
||||
"sourceSha256": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc",
|
||||
"sourcePath": null,
|
||||
"preview": null
|
||||
},
|
||||
{
|
||||
"assetId": "asset:bb91ef0dddf69bb7d87dd4217a01e7f549b50ad628db0367d3896c6840b3a326",
|
||||
"assetLibraryIdentifier": null,
|
||||
"relativeAssetIdentifier": "Object/Legacy Hero",
|
||||
"idType": "OBJECT",
|
||||
"name": "Legacy Hero",
|
||||
"catalogId": "b4bb3608-9267-5aba-bb9e-9e97b006d5ab",
|
||||
"catalogSimpleName": "Heroes",
|
||||
"author": "M12 Artist",
|
||||
"description": "",
|
||||
"copyright": "",
|
||||
"license": "CC0-1.0",
|
||||
"tags": [
|
||||
"character",
|
||||
"hero"
|
||||
],
|
||||
"activeTag": 0,
|
||||
"usePreferredImportMethod": false,
|
||||
"preferredImportMethod": "APPEND",
|
||||
"customProperties": [],
|
||||
"sourceSha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
"sourcePath": "assets/legacy-hero.blend",
|
||||
"preview": {
|
||||
"sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
|
||||
"mimeType": "image/png",
|
||||
"width": 64,
|
||||
"height": 64,
|
||||
"byteLength": 4096
|
||||
}
|
||||
}
|
||||
],
|
||||
"libraries": [
|
||||
{
|
||||
"libraryId": "library:characters",
|
||||
"name": "Characters",
|
||||
"sourcePath": "libraries/characters.blend",
|
||||
"sourceSha256": "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd",
|
||||
"dependencyIds": [
|
||||
"library:materials"
|
||||
],
|
||||
"readOnly": true
|
||||
},
|
||||
{
|
||||
"libraryId": "library:materials",
|
||||
"name": "Materials",
|
||||
"sourcePath": "libraries/materials.blend",
|
||||
"sourceSha256": "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee",
|
||||
"dependencyIds": [],
|
||||
"readOnly": true
|
||||
}
|
||||
]
|
||||
}
|
||||
44
tests/golden/M12-01D/manifest.json
Normal file
44
tests/golden/M12-01D/manifest.json
Normal file
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"task": "M12-01D",
|
||||
"parentTask": "M12-01",
|
||||
"parityId": "N-023-ASSET-CATALOG-SCHEMA",
|
||||
"enablingTask": true,
|
||||
"parityStateChange": false,
|
||||
"implementationClass": "NOT_APPLICABLE",
|
||||
"migration": {
|
||||
"sourceSchemaVersion": 1,
|
||||
"targetSchemaVersion": 2,
|
||||
"catalogIdMapping": "RFC4122_UUIDV5_FIXED_NAMESPACE",
|
||||
"assetIdMapping": "BLENDER_ASSET_WEAK_REFERENCE_V1_SHA256",
|
||||
"revisionPolicy": "PRESERVE",
|
||||
"unknownFieldPolicy": "BLOCK_BEFORE_MIGRATION"
|
||||
},
|
||||
"artifacts": {
|
||||
"migrationProtocol": {
|
||||
"path": "web/protocol/asset-catalog-migration.ts",
|
||||
"sha256": "6d74bc5605f94e05208eda82830192d38f88066dfc409058555a8876c785f226"
|
||||
},
|
||||
"targetProtocol": {
|
||||
"path": "web/protocol/asset-catalog-v2.ts",
|
||||
"sha256": "0949838efb2f69466368185b80bb1d2e851672d78311fcd5270eb0814576e5d7"
|
||||
},
|
||||
"generator": {
|
||||
"path": "tools/web/generate-asset-catalog-migration.mjs",
|
||||
"sha256": "954d15c02d68e3f644b57c8c4c136ce09bd037a0af2012bf74b97f681c77dd60"
|
||||
},
|
||||
"sourceFixture": {
|
||||
"path": "tests/golden/M12-01D/catalog-v1.json",
|
||||
"sha256": "1cc43d181afefd63b53cb9c3626bb58195fad20187d6e534adc1ecbe65f75da5"
|
||||
},
|
||||
"targetFixture": {
|
||||
"path": "tests/golden/M12-01D/catalog-v2.json",
|
||||
"sha256": "38e5d1771b3d2d63ce5e10268eac4513f2043b0cbf71ab1799c1868bc2aa97fb"
|
||||
},
|
||||
"migrationReport": {
|
||||
"path": "tests/golden/M12-01D/migration-report.json",
|
||||
"sha256": "57b972c1bbb9ae326c3f264c225c21546682442ff3194fbc343e1c46a2cd410a"
|
||||
}
|
||||
},
|
||||
"nextTask": "M12-01E"
|
||||
}
|
||||
53
tests/golden/M12-01D/migration-report.json
Normal file
53
tests/golden/M12-01D/migration-report.json
Normal file
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"task": "M12-01D",
|
||||
"status": "MIGRATED",
|
||||
"sourceSchemaVersion": 1,
|
||||
"targetSchemaVersion": 2,
|
||||
"sourceRevision": 7,
|
||||
"targetRevision": 7,
|
||||
"sourceManifestSha256": "cbdc8b03dc9ce1f38b44bae1c7459eb273126cbd6c7dd33115d2b1966e975881",
|
||||
"targetManifestSha256": "0c08ed1af1dd0998c809f76d969fd21d8fdae28ad37cf5571bdc99b16bf3ec90",
|
||||
"catalogMappings": [
|
||||
{
|
||||
"legacyId": "44444444-4444-4444-8444-444444444444",
|
||||
"catalogId": "44444444-4444-4444-8444-444444444444",
|
||||
"path": "Animation"
|
||||
},
|
||||
{
|
||||
"legacyId": "catalog:heroes",
|
||||
"catalogId": "b4bb3608-9267-5aba-bb9e-9e97b006d5ab",
|
||||
"path": "Characters/Heroes"
|
||||
},
|
||||
{
|
||||
"legacyId": "catalog:root",
|
||||
"catalogId": "fe3ca14c-95d7-549f-a13b-1bb4c07b6074",
|
||||
"path": "Characters"
|
||||
}
|
||||
],
|
||||
"assetMappings": [
|
||||
{
|
||||
"legacyId": "legacy:hero",
|
||||
"assetId": "asset:bb91ef0dddf69bb7d87dd4217a01e7f549b50ad628db0367d3896c6840b3a326",
|
||||
"relativeAssetIdentifier": "Object/Legacy Hero"
|
||||
},
|
||||
{
|
||||
"legacyId": "legacy:walk",
|
||||
"assetId": "asset:ac88c6147ada877adccf4df8ca54d64e1a9cb50a13514212f6d19b1da1660f8d",
|
||||
"relativeAssetIdentifier": "Action/Legacy Walk"
|
||||
}
|
||||
],
|
||||
"defaultsApplied": {
|
||||
"dynamicMetadataFields": 4,
|
||||
"preferredImportMethods": 2,
|
||||
"customPropertyCollections": 2
|
||||
},
|
||||
"preserved": {
|
||||
"catalogs": 3,
|
||||
"assets": 2,
|
||||
"libraries": 2,
|
||||
"previews": 1,
|
||||
"sourceBindings": 2
|
||||
},
|
||||
"nextTask": "M12-01E"
|
||||
}
|
||||
255
tests/golden/M12-01E/compatibility-report.json
Normal file
255
tests/golden/M12-01E/compatibility-report.json
Normal file
@@ -0,0 +1,255 @@
|
||||
{
|
||||
"read": {
|
||||
"schemaVersion": 1,
|
||||
"task": "M12-01E",
|
||||
"readerSchemaVersion": 1,
|
||||
"documentSchemaVersion": 2,
|
||||
"operation": "READ",
|
||||
"status": "READ_ONLY",
|
||||
"code": "ASSET_SCHEMA_DOWNGRADE_BLOCKED",
|
||||
"recoverable": true,
|
||||
"sourceSha256": "0c08ed1af1dd0998c809f76d969fd21d8fdae28ad37cf5571bdc99b16bf3ec90",
|
||||
"snapshot": {
|
||||
"revision": 7,
|
||||
"catalogs": [
|
||||
{
|
||||
"catalogId": "44444444-4444-4444-8444-444444444444",
|
||||
"path": "Animation",
|
||||
"simpleName": "Animation"
|
||||
},
|
||||
{
|
||||
"catalogId": "fe3ca14c-95d7-549f-a13b-1bb4c07b6074",
|
||||
"path": "Characters",
|
||||
"simpleName": "Characters"
|
||||
},
|
||||
{
|
||||
"catalogId": "b4bb3608-9267-5aba-bb9e-9e97b006d5ab",
|
||||
"path": "Characters/Heroes",
|
||||
"simpleName": "Heroes"
|
||||
}
|
||||
],
|
||||
"assets": [
|
||||
{
|
||||
"assetId": "asset:ac88c6147ada877adccf4df8ca54d64e1a9cb50a13514212f6d19b1da1660f8d",
|
||||
"relativeAssetIdentifier": "Action/Legacy Walk",
|
||||
"idType": "ACTION",
|
||||
"name": "Legacy Walk",
|
||||
"catalogId": "44444444-4444-4444-8444-444444444444"
|
||||
},
|
||||
{
|
||||
"assetId": "asset:bb91ef0dddf69bb7d87dd4217a01e7f549b50ad628db0367d3896c6840b3a326",
|
||||
"relativeAssetIdentifier": "Object/Legacy Hero",
|
||||
"idType": "OBJECT",
|
||||
"name": "Legacy Hero",
|
||||
"catalogId": "b4bb3608-9267-5aba-bb9e-9e97b006d5ab"
|
||||
}
|
||||
],
|
||||
"libraries": [
|
||||
{
|
||||
"libraryId": "library:characters",
|
||||
"name": "Characters",
|
||||
"readOnly": true
|
||||
},
|
||||
{
|
||||
"libraryId": "library:materials",
|
||||
"name": "Materials",
|
||||
"readOnly": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"nextTask": "M12-01F"
|
||||
},
|
||||
"catalogWrite": {
|
||||
"schemaVersion": 1,
|
||||
"task": "M12-01E",
|
||||
"readerSchemaVersion": 1,
|
||||
"documentSchemaVersion": 2,
|
||||
"operation": "CATALOG_WRITE",
|
||||
"status": "BLOCKED",
|
||||
"code": "ASSET_SCHEMA_DOWNGRADE_BLOCKED",
|
||||
"recoverable": true,
|
||||
"sourceSha256": "0c08ed1af1dd0998c809f76d969fd21d8fdae28ad37cf5571bdc99b16bf3ec90",
|
||||
"snapshot": {
|
||||
"revision": 7,
|
||||
"catalogs": [
|
||||
{
|
||||
"catalogId": "44444444-4444-4444-8444-444444444444",
|
||||
"path": "Animation",
|
||||
"simpleName": "Animation"
|
||||
},
|
||||
{
|
||||
"catalogId": "fe3ca14c-95d7-549f-a13b-1bb4c07b6074",
|
||||
"path": "Characters",
|
||||
"simpleName": "Characters"
|
||||
},
|
||||
{
|
||||
"catalogId": "b4bb3608-9267-5aba-bb9e-9e97b006d5ab",
|
||||
"path": "Characters/Heroes",
|
||||
"simpleName": "Heroes"
|
||||
}
|
||||
],
|
||||
"assets": [
|
||||
{
|
||||
"assetId": "asset:ac88c6147ada877adccf4df8ca54d64e1a9cb50a13514212f6d19b1da1660f8d",
|
||||
"relativeAssetIdentifier": "Action/Legacy Walk",
|
||||
"idType": "ACTION",
|
||||
"name": "Legacy Walk",
|
||||
"catalogId": "44444444-4444-4444-8444-444444444444"
|
||||
},
|
||||
{
|
||||
"assetId": "asset:bb91ef0dddf69bb7d87dd4217a01e7f549b50ad628db0367d3896c6840b3a326",
|
||||
"relativeAssetIdentifier": "Object/Legacy Hero",
|
||||
"idType": "OBJECT",
|
||||
"name": "Legacy Hero",
|
||||
"catalogId": "b4bb3608-9267-5aba-bb9e-9e97b006d5ab"
|
||||
}
|
||||
],
|
||||
"libraries": [
|
||||
{
|
||||
"libraryId": "library:characters",
|
||||
"name": "Characters",
|
||||
"readOnly": true
|
||||
},
|
||||
{
|
||||
"libraryId": "library:materials",
|
||||
"name": "Materials",
|
||||
"readOnly": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"nextTask": "M12-01F"
|
||||
},
|
||||
"assetWrite": {
|
||||
"schemaVersion": 1,
|
||||
"task": "M12-01E",
|
||||
"readerSchemaVersion": 1,
|
||||
"documentSchemaVersion": 2,
|
||||
"operation": "ASSET_WRITE",
|
||||
"status": "BLOCKED",
|
||||
"code": "ASSET_SCHEMA_DOWNGRADE_BLOCKED",
|
||||
"recoverable": true,
|
||||
"sourceSha256": "0c08ed1af1dd0998c809f76d969fd21d8fdae28ad37cf5571bdc99b16bf3ec90",
|
||||
"snapshot": {
|
||||
"revision": 7,
|
||||
"catalogs": [
|
||||
{
|
||||
"catalogId": "44444444-4444-4444-8444-444444444444",
|
||||
"path": "Animation",
|
||||
"simpleName": "Animation"
|
||||
},
|
||||
{
|
||||
"catalogId": "fe3ca14c-95d7-549f-a13b-1bb4c07b6074",
|
||||
"path": "Characters",
|
||||
"simpleName": "Characters"
|
||||
},
|
||||
{
|
||||
"catalogId": "b4bb3608-9267-5aba-bb9e-9e97b006d5ab",
|
||||
"path": "Characters/Heroes",
|
||||
"simpleName": "Heroes"
|
||||
}
|
||||
],
|
||||
"assets": [
|
||||
{
|
||||
"assetId": "asset:ac88c6147ada877adccf4df8ca54d64e1a9cb50a13514212f6d19b1da1660f8d",
|
||||
"relativeAssetIdentifier": "Action/Legacy Walk",
|
||||
"idType": "ACTION",
|
||||
"name": "Legacy Walk",
|
||||
"catalogId": "44444444-4444-4444-8444-444444444444"
|
||||
},
|
||||
{
|
||||
"assetId": "asset:bb91ef0dddf69bb7d87dd4217a01e7f549b50ad628db0367d3896c6840b3a326",
|
||||
"relativeAssetIdentifier": "Object/Legacy Hero",
|
||||
"idType": "OBJECT",
|
||||
"name": "Legacy Hero",
|
||||
"catalogId": "b4bb3608-9267-5aba-bb9e-9e97b006d5ab"
|
||||
}
|
||||
],
|
||||
"libraries": [
|
||||
{
|
||||
"libraryId": "library:characters",
|
||||
"name": "Characters",
|
||||
"readOnly": true
|
||||
},
|
||||
{
|
||||
"libraryId": "library:materials",
|
||||
"name": "Materials",
|
||||
"readOnly": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"nextTask": "M12-01F"
|
||||
},
|
||||
"save": {
|
||||
"schemaVersion": 1,
|
||||
"task": "M12-01E",
|
||||
"readerSchemaVersion": 1,
|
||||
"documentSchemaVersion": 2,
|
||||
"operation": "SAVE",
|
||||
"status": "BLOCKED",
|
||||
"code": "ASSET_SCHEMA_DOWNGRADE_BLOCKED",
|
||||
"recoverable": true,
|
||||
"sourceSha256": "0c08ed1af1dd0998c809f76d969fd21d8fdae28ad37cf5571bdc99b16bf3ec90",
|
||||
"snapshot": {
|
||||
"revision": 7,
|
||||
"catalogs": [
|
||||
{
|
||||
"catalogId": "44444444-4444-4444-8444-444444444444",
|
||||
"path": "Animation",
|
||||
"simpleName": "Animation"
|
||||
},
|
||||
{
|
||||
"catalogId": "fe3ca14c-95d7-549f-a13b-1bb4c07b6074",
|
||||
"path": "Characters",
|
||||
"simpleName": "Characters"
|
||||
},
|
||||
{
|
||||
"catalogId": "b4bb3608-9267-5aba-bb9e-9e97b006d5ab",
|
||||
"path": "Characters/Heroes",
|
||||
"simpleName": "Heroes"
|
||||
}
|
||||
],
|
||||
"assets": [
|
||||
{
|
||||
"assetId": "asset:ac88c6147ada877adccf4df8ca54d64e1a9cb50a13514212f6d19b1da1660f8d",
|
||||
"relativeAssetIdentifier": "Action/Legacy Walk",
|
||||
"idType": "ACTION",
|
||||
"name": "Legacy Walk",
|
||||
"catalogId": "44444444-4444-4444-8444-444444444444"
|
||||
},
|
||||
{
|
||||
"assetId": "asset:bb91ef0dddf69bb7d87dd4217a01e7f549b50ad628db0367d3896c6840b3a326",
|
||||
"relativeAssetIdentifier": "Object/Legacy Hero",
|
||||
"idType": "OBJECT",
|
||||
"name": "Legacy Hero",
|
||||
"catalogId": "b4bb3608-9267-5aba-bb9e-9e97b006d5ab"
|
||||
}
|
||||
],
|
||||
"libraries": [
|
||||
{
|
||||
"libraryId": "library:characters",
|
||||
"name": "Characters",
|
||||
"readOnly": true
|
||||
},
|
||||
{
|
||||
"libraryId": "library:materials",
|
||||
"name": "Materials",
|
||||
"readOnly": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"nextTask": "M12-01F"
|
||||
},
|
||||
"future": {
|
||||
"schemaVersion": 1,
|
||||
"task": "M12-01E",
|
||||
"readerSchemaVersion": 1,
|
||||
"documentSchemaVersion": 3,
|
||||
"operation": "READ",
|
||||
"status": "BLOCKED",
|
||||
"code": "PROTOCOL_MISMATCH",
|
||||
"recoverable": false,
|
||||
"sourceSha256": "d6b7646f6ede911358f954a0ccae376703f1c0e8854a9953a4a77a968b600b80",
|
||||
"snapshot": null,
|
||||
"nextTask": "M12-01F"
|
||||
}
|
||||
}
|
||||
39
tests/golden/M12-01E/manifest.json
Normal file
39
tests/golden/M12-01E/manifest.json
Normal file
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"task": "M12-01E",
|
||||
"parentTask": "M12-01",
|
||||
"parityId": "N-023-ASSET-CATALOG-SCHEMA",
|
||||
"enablingTask": true,
|
||||
"parityStateChange": false,
|
||||
"implementationClass": "NOT_APPLICABLE",
|
||||
"policy": {
|
||||
"nativeV1": "READY",
|
||||
"validV2Read": "READ_ONLY",
|
||||
"validV2Write": "ASSET_SCHEMA_DOWNGRADE_BLOCKED",
|
||||
"futureSchema": "PROTOCOL_MISMATCH",
|
||||
"sourceMutation": "FORBIDDEN"
|
||||
},
|
||||
"artifacts": {
|
||||
"protocol": {
|
||||
"path": "web/protocol/asset-catalog-compatibility.ts",
|
||||
"sha256": "5d51029b232e03f08fc0778d4933910b2a54818b4d653fbcc185c8d764641349"
|
||||
},
|
||||
"errorContract": {
|
||||
"path": "web/protocol/error.ts",
|
||||
"sha256": "309ab84d5c755a69466ddb73e4b54e87caf62cbcac0f10f34d904e9f6f4a5f34"
|
||||
},
|
||||
"generator": {
|
||||
"path": "tools/web/generate-asset-catalog-compatibility.mjs",
|
||||
"sha256": "208f568db092e1712095d77ab28db92d5a1eb11fe62813b237ce466f9268fd4e"
|
||||
},
|
||||
"v2Fixture": {
|
||||
"path": "tests/golden/M12-01D/catalog-v2.json",
|
||||
"sha256": "38e5d1771b3d2d63ce5e10268eac4513f2043b0cbf71ab1799c1868bc2aa97fb"
|
||||
},
|
||||
"compatibilityReport": {
|
||||
"path": "tests/golden/M12-01E/compatibility-report.json",
|
||||
"sha256": "3c2f9241b56ad5bd742dc4152dec703bdc56eef4e6d8d60e6edf163c4571ad54"
|
||||
}
|
||||
},
|
||||
"nextTask": "M12-01F"
|
||||
}
|
||||
33
tests/golden/M12-01F/manifest.json
Normal file
33
tests/golden/M12-01F/manifest.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"task": "M12-01F",
|
||||
"parentTask": "M12-01",
|
||||
"parityId": "N-023-ASSET-CATALOG-SCHEMA",
|
||||
"enablingTask": true,
|
||||
"parityStateChange": false,
|
||||
"implementationClass": "NOT_APPLICABLE",
|
||||
"negativeCaseCount": 9,
|
||||
"artifacts": {
|
||||
"schemaProtocol": {
|
||||
"path": "web/protocol/asset-catalog-v2.ts",
|
||||
"sha256": "0949838efb2f69466368185b80bb1d2e851672d78311fcd5270eb0814576e5d7"
|
||||
},
|
||||
"migrationProtocol": {
|
||||
"path": "web/protocol/asset-catalog-migration.ts",
|
||||
"sha256": "6d74bc5605f94e05208eda82830192d38f88066dfc409058555a8876c785f226"
|
||||
},
|
||||
"v1Fixture": {
|
||||
"path": "tests/golden/M12-01D/catalog-v1.json",
|
||||
"sha256": "1cc43d181afefd63b53cb9c3626bb58195fad20187d6e534adc1ecbe65f75da5"
|
||||
},
|
||||
"v2Fixture": {
|
||||
"path": "tests/golden/M12-01D/catalog-v2.json",
|
||||
"sha256": "38e5d1771b3d2d63ce5e10268eac4513f2043b0cbf71ab1799c1868bc2aa97fb"
|
||||
},
|
||||
"negativeCases": {
|
||||
"path": "tests/golden/M12-01F/negative-cases.json",
|
||||
"sha256": "74672fa0aec248fd5d9a265743ef4dfe4d417fce8a24622b8b3d6be2f95c8874"
|
||||
}
|
||||
},
|
||||
"nextTask": "M12-01G"
|
||||
}
|
||||
17
tests/golden/M12-01F/negative-cases.json
Normal file
17
tests/golden/M12-01F/negative-cases.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"task": "M12-01F",
|
||||
"cases": [
|
||||
{ "id": "DUPLICATE_CATALOG_ID", "code": "ASSET_MANIFEST_INVALID" },
|
||||
{ "id": "DUPLICATE_ASSET_ID", "code": "ASSET_MANIFEST_INVALID" },
|
||||
{ "id": "LEGACY_PARENT_CYCLE", "code": "ASSET_MANIFEST_INVALID" },
|
||||
{ "id": "PARENT_PATH_MISMATCH", "code": "ASSET_MANIFEST_INVALID" },
|
||||
{ "id": "OVERLONG_SIMPLE_NAME_UTF8", "code": "ASSET_BUDGET_EXCEEDED" },
|
||||
{ "id": "OVERLONG_TAG_UTF8", "code": "ASSET_BUDGET_EXCEEDED" },
|
||||
{ "id": "V2_UNKNOWN_TOP_LEVEL", "code": "ASSET_MANIFEST_INVALID" },
|
||||
{ "id": "V2_UNKNOWN_ASSET_FIELD", "code": "ASSET_MANIFEST_INVALID" },
|
||||
{ "id": "V1_UNKNOWN_ASSET_FIELD", "code": "ASSET_MANIFEST_INVALID" }
|
||||
],
|
||||
"recovery": "VALID_V2_AFTER_EACH_NEGATIVE",
|
||||
"nextTask": "M12-01G"
|
||||
}
|
||||
40
tests/golden/M12-01G/manifest.json
Normal file
40
tests/golden/M12-01G/manifest.json
Normal file
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"task": "M12-01G",
|
||||
"parentTask": "M12-01",
|
||||
"parityId": "N-023-ASSET-CATALOG-SCHEMA",
|
||||
"enablingTask": true,
|
||||
"parityStateChange": false,
|
||||
"implementationClass": "NOT_APPLICABLE",
|
||||
"transaction": {
|
||||
"sourceRow": "asset-catalog:index:v1",
|
||||
"targetRow": "asset-catalog:index:v2",
|
||||
"receiptRow": "asset-catalog:migration:v1-to-v2",
|
||||
"faultPoints": ["AFTER_TARGET_PUT", "AFTER_SOURCE_DELETE"],
|
||||
"failureCode": "STORAGE_TRANSACTION",
|
||||
"rollbackPolicy": "PRESERVE_SOURCE_TARGET_RECEIPT_AND_UNRELATED_ROWS"
|
||||
},
|
||||
"artifacts": {
|
||||
"indexedDBMigration": {
|
||||
"path": "web/app/src/storage/asset-catalog-indexeddb.ts",
|
||||
"sha256": "283a08bccb5ea301bd2aef8f01d4496972e3962f75e340479336c50e3539ba57"
|
||||
},
|
||||
"browserSuite": {
|
||||
"path": "web/tests/e2e/asset-catalog-indexeddb-migration.spec.ts",
|
||||
"sha256": "82c40f38898277b4053a9654537ec9c21bc01a784d1091831e139361575a81e9"
|
||||
},
|
||||
"sourceFixture": {
|
||||
"path": "tests/golden/M12-01D/catalog-v1.json",
|
||||
"sha256": "1cc43d181afefd63b53cb9c3626bb58195fad20187d6e534adc1ecbe65f75da5"
|
||||
},
|
||||
"targetFixture": {
|
||||
"path": "tests/golden/M12-01D/catalog-v2.json",
|
||||
"sha256": "38e5d1771b3d2d63ce5e10268eac4513f2043b0cbf71ab1799c1868bc2aa97fb"
|
||||
},
|
||||
"migrationReport": {
|
||||
"path": "tests/golden/M12-01D/migration-report.json",
|
||||
"sha256": "57b972c1bbb9ae326c3f264c225c21546682442ff3194fbc343e1c46a2cd410a"
|
||||
}
|
||||
},
|
||||
"nextTask": "M12-01H"
|
||||
}
|
||||
43
tests/golden/M12-01H/manifest.json
Normal file
43
tests/golden/M12-01H/manifest.json
Normal file
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"task": "M12-01H",
|
||||
"parentTask": "M12-01",
|
||||
"parityId": "N-023-ASSET-CATALOG-SCHEMA",
|
||||
"enablingTask": true,
|
||||
"parityStateChange": false,
|
||||
"implementationClass": "NOT_APPLICABLE",
|
||||
"restart": {
|
||||
"contexts": ["INITIAL_PAGE", "RELOADED_PAGE", "WORKER_GENERATION_1", "WORKER_GENERATION_2"],
|
||||
"catalogCount": 3,
|
||||
"assetCount": 2,
|
||||
"manifestSha256": "0c08ed1af1dd0998c809f76d969fd21d8fdae28ad37cf5571bdc99b16bf3ec90",
|
||||
"catalogOrder": [
|
||||
"44444444-4444-4444-8444-444444444444",
|
||||
"fe3ca14c-95d7-549f-a13b-1bb4c07b6074",
|
||||
"b4bb3608-9267-5aba-bb9e-9e97b006d5ab"
|
||||
],
|
||||
"assetOrder": [
|
||||
"asset:ac88c6147ada877adccf4df8ca54d64e1a9cb50a13514212f6d19b1da1660f8d",
|
||||
"asset:bb91ef0dddf69bb7d87dd4217a01e7f549b50ad628db0367d3896c6840b3a326"
|
||||
]
|
||||
},
|
||||
"artifacts": {
|
||||
"indexedDBReader": {
|
||||
"path": "web/app/src/storage/asset-catalog-indexeddb.ts",
|
||||
"sha256": "283a08bccb5ea301bd2aef8f01d4496972e3962f75e340479336c50e3539ba57"
|
||||
},
|
||||
"restartWorker": {
|
||||
"path": "web/app/src/workers/asset-catalog-restart-test.worker.ts",
|
||||
"sha256": "e6007d352555be046b89d0becd719692652ee0418cc97087bf7599155f2a538e"
|
||||
},
|
||||
"browserSuite": {
|
||||
"path": "web/tests/e2e/asset-catalog-restart.spec.ts",
|
||||
"sha256": "7945ff38dd474a74c265594b66534a280eb69407509137e60d41f1bea3b3c19b"
|
||||
},
|
||||
"targetFixture": {
|
||||
"path": "tests/golden/M12-01D/catalog-v2.json",
|
||||
"sha256": "38e5d1771b3d2d63ce5e10268eac4513f2043b0cbf71ab1799c1868bc2aa97fb"
|
||||
}
|
||||
},
|
||||
"nextTask": "M12-01I"
|
||||
}
|
||||
98
tests/golden/M12-02A/asset-preview-inventory.json
Normal file
98
tests/golden/M12-02A/asset-preview-inventory.json
Normal file
@@ -0,0 +1,98 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"task": "M12-02A",
|
||||
"parentTask": "M12-02",
|
||||
"parityId": "N-023-ASSET-PREVIEW",
|
||||
"enablingTask": true,
|
||||
"parityStateChange": false,
|
||||
"blenderVersion": "5.2.0",
|
||||
"sources": [
|
||||
{ "role": "PREVIEW_DNA", "path": "blender-5.2.0/source/blender/makesdna/DNA_ID.h", "sha256": "27d0eac02df737f2d2298eeef45e66eae0d76da66542ccdf32400b3e9bd904a9" },
|
||||
{ "role": "PREVIEW_SLOT_ENUM", "path": "blender-5.2.0/source/blender/makesdna/DNA_ID_enums.h", "sha256": "9b97042ca408924c2efa92b49bc44a4f2f949b11768fb89b2c1b71c9c796cd61" },
|
||||
{ "role": "PREVIEW_API", "path": "blender-5.2.0/source/blender/blenkernel/BKE_preview_image.hh", "sha256": "25656ad3ef44bb6d00b6e6f9748d26fd04a358d7bb9f876da4194c05d1e5a9a1" },
|
||||
{ "role": "PREVIEW_IMPLEMENTATION", "path": "blender-5.2.0/source/blender/blenkernel/intern/preview_image.cc", "sha256": "886ae47b7f4950b7b3dc558ac1973c0b114a265d837aa130e4479dd8b438db86" },
|
||||
{ "role": "PREVIEW_RNA", "path": "blender-5.2.0/source/blender/makesrna/intern/rna_ID.cc", "sha256": "d97b3346a3d36e5198989087cbb6a24f43d9d6d535a6f795392a47f77ccdfc2c" },
|
||||
{ "role": "ASSET_PREVIEW_GENERATOR", "path": "blender-5.2.0/source/blender/editors/asset/intern/asset_ops.cc", "sha256": "94ae9ce907d1060b3f4dc7536ddd66590efb8e1680bf7d3dc72ce1ce53523732" },
|
||||
{ "role": "PREVIEW_BLEND_READER", "path": "blender-5.2.0/source/blender/blenloader/intern/readblenentry.cc", "sha256": "dd10bdbdd5f42a09722411e8b99ce34749186d256e91413b7cb5bdef422b8cde" },
|
||||
{ "role": "PREVIEW_SIZE_CONSTANTS", "path": "blender-5.2.0/source/blender/imbuf/IMB_thumbs.hh", "sha256": "c7d5ddf7c3fbbdd731eac7f0761cada3e8e0df024d8d2c7b48d777e517761be1" },
|
||||
{ "role": "ICON_SIZE_CONSTANTS", "path": "blender-5.2.0/source/blender/blenkernel/BKE_icons.hh", "sha256": "c16c52503a2fe2e5df9c2925b04d6ce64468cf2c5d98fd85e0e47423cfc9d2aa" },
|
||||
{ "role": "CURRENT_WEB_V1", "path": "web/protocol/asset-library-io.ts", "sha256": "5c62539db3833a2113007dfbdfd12ca06704d86e0cf0e57ad97f7fbf6acda7f4" },
|
||||
{ "role": "CURRENT_WEB_V2", "path": "web/protocol/asset-catalog-v2.ts", "sha256": "0949838efb2f69466368185b80bb1d2e851672d78311fcd5270eb0814576e5d7" }
|
||||
],
|
||||
"fixtures": [
|
||||
{ "role": "LOADED_RGBA8_PNG", "path": "tests/files/web/media/sequencer-frame.png", "sha256": "295b083db2299ab904947eb39c17173de70c211882b0d855537d8f3cc27698ed" },
|
||||
{ "role": "ASSET_NO_PREVIEW_BLEND", "path": "tests/files/web/m12_asset_catalog_v1/m12_asset_catalog_v1.blend", "sha256": "8facfe82e3ca0a0605c139ef5bdcbb82d27a6953a6add4e008b68af90bc1211f" }
|
||||
],
|
||||
"storage": {
|
||||
"slotCount": 2,
|
||||
"slots": [
|
||||
{ "index": 0, "name": "ICON", "defaultRenderMaxHeight": 32 },
|
||||
{ "index": 1, "name": "PREVIEW", "defaultRenderMaxHeight": 128, "customAssetMaxHeight": 256 }
|
||||
],
|
||||
"fields": [
|
||||
{ "name": "w", "cType": "unsigned int[2]", "persistence": "BLEND", "semantic": "PIXEL_WIDTH" },
|
||||
{ "name": "h", "cType": "unsigned int[2]", "persistence": "BLEND", "semantic": "PIXEL_HEIGHT" },
|
||||
{ "name": "flag", "cType": "short[2]", "persistence": "BLEND_WITH_RUNTIME_BITS_CLEARED", "semantic": "CHANGED_USER_EDITED_RENDERING" },
|
||||
{ "name": "changed_timestamp", "cType": "short[2]", "persistence": "BLEND", "semantic": "CHANGE_COUNTER" },
|
||||
{ "name": "rect", "cType": "unsigned int*[2]", "persistence": "BLEND_ARRAY", "semantic": "ONE_PACKED_RGBA32_VALUE_PER_PIXEL" },
|
||||
{ "name": "runtime", "cType": "PreviewImageRuntime*", "persistence": "RUNTIME_ONLY", "semantic": "GPU_DEFERRED_LOADING_AND_USER_COUNT" }
|
||||
],
|
||||
"pixelFormat": "RGBA8_PACKED_UINT32",
|
||||
"byteComponents": 4,
|
||||
"alphaMode": "PREMULTIPLIED",
|
||||
"colorSpace": null,
|
||||
"colorSpaceFinding": "NO_COLOR_SPACE_FIELD_OR_RNA_PROPERTY; BYTE_BUFFER_INTERPRETATION_DEPENDS_ON_PRODUCER_PIPELINE"
|
||||
},
|
||||
"rna": {
|
||||
"idProperty": { "identifier": "preview", "type": "POINTER", "readOnly": true, "nullWhenAbsent": true },
|
||||
"imagePreviewProperties": [
|
||||
{ "identifier": "is_image_custom", "type": "BOOLEAN", "readOnly": false, "arrayLength": 0 },
|
||||
{ "identifier": "image_size", "type": "INT", "readOnly": false, "arrayLength": 2 },
|
||||
{ "identifier": "image_pixels", "type": "INT", "readOnly": false, "arrayLength": 0 },
|
||||
{ "identifier": "image_pixels_float", "type": "FLOAT", "readOnly": false, "arrayLength": 0 },
|
||||
{ "identifier": "is_icon_custom", "type": "BOOLEAN", "readOnly": false, "arrayLength": 0 },
|
||||
{ "identifier": "icon_size", "type": "INT", "readOnly": false, "arrayLength": 2 },
|
||||
{ "identifier": "icon_pixels", "type": "INT", "readOnly": false, "arrayLength": 0 },
|
||||
{ "identifier": "icon_pixels_float", "type": "FLOAT", "readOnly": false, "arrayLength": 0 },
|
||||
{ "identifier": "icon_id", "type": "INT", "readOnly": true, "arrayLength": 0 }
|
||||
],
|
||||
"lengthRules": {
|
||||
"packedPixels": "width*height",
|
||||
"floatComponents": "width*height*4",
|
||||
"floatConversion": "BYTE_COMPONENT_DIVIDED_BY_255_WITHOUT_COLOR_TRANSFORM"
|
||||
}
|
||||
},
|
||||
"runtimeStates": {
|
||||
"noPreview": {
|
||||
"state": "ABSENT_POINTER",
|
||||
"assetCount": 3,
|
||||
"assetTypes": ["Object", "Material", "World"],
|
||||
"previewValues": [null, null, null]
|
||||
},
|
||||
"loadedPreview": {
|
||||
"sourceSize": [8, 8],
|
||||
"imageSize": [8, 8],
|
||||
"iconSize": [32, 32],
|
||||
"imagePackedPixelCount": 64,
|
||||
"imageFloatComponentCount": 256,
|
||||
"iconPackedPixelCount": 1024,
|
||||
"imageCustom": false,
|
||||
"iconCustom": false
|
||||
},
|
||||
"emptyBufferRule": "NULL_RECT_OR_ZERO_WIDTH_OR_ZERO_HEIGHT_IS_NO_IMAGE_FOR_THAT_SLOT",
|
||||
"invalidDeferredRule": "RUNTIME_TAG_ONLY_NOT_A_PERSISTED_COLOR_OR_CONTENT_IDENTITY"
|
||||
},
|
||||
"currentWeb": {
|
||||
"v1Fields": ["assetId", "sha256", "mimeType", "width", "height", "byteLength"],
|
||||
"v2Fields": ["sha256", "mimeType", "width", "height", "byteLength"],
|
||||
"noPreviewRepresentation": { "v1": "OPTIONAL_PROPERTY_ABSENT", "v2": "NULL" },
|
||||
"missingSemantics": ["slot", "colorSpace", "alphaMode", "sourceSha256", "generatorIdentity", "deferredOrInvalidState"]
|
||||
},
|
||||
"nonGoals": [
|
||||
"Preview content identity schema",
|
||||
"Decode budgets",
|
||||
"OPFS commit ordering",
|
||||
"Browser display parity"
|
||||
],
|
||||
"nextTask": "M12-02B"
|
||||
}
|
||||
28
tests/golden/M12-02B/identity.json
Normal file
28
tests/golden/M12-02B/identity.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"assetId": "asset:4e9e5a3755581b0f3b604748fc5c64f4f51f14b5c2efd485a7093edeae4dbabd",
|
||||
"slot": "PREVIEW",
|
||||
"source": {
|
||||
"mimeType": "image/png",
|
||||
"byteLength": 261,
|
||||
"sha256": "295b083db2299ab904947eb39c17173de70c211882b0d855537d8f3cc27698ed"
|
||||
},
|
||||
"content": {
|
||||
"mimeType": "image/png",
|
||||
"byteLength": 513,
|
||||
"sha256": "ff139c4a1c5d388c78c5168dfd3272e80ea0d1b15b3743e29a38cd908e8c70e7",
|
||||
"width": 8,
|
||||
"height": 8,
|
||||
"pixelFormat": "RGBA8",
|
||||
"colorSpace": "SRGB",
|
||||
"alphaMode": "STRAIGHT"
|
||||
},
|
||||
"generator": {
|
||||
"name": "BLENDER_ASSET_PREVIEW_IDENTITY",
|
||||
"version": "5.2.0",
|
||||
"executableSha256": "d4483926610484ef9c2ad9241aae1469f934d955ebe791f1920e263e0ba85b82",
|
||||
"scriptSha256": "753a26be90f7d97828737ebc3a4ab88275a7c655b4f9992406c7c35fba6535ce",
|
||||
"settingsSha256": "07ebc55d4b6cbb293badf0640874846912df3d23549997cf6f87f4c325280ca0"
|
||||
},
|
||||
"identitySha256": "3139b1df11615fd8c7caaf920a0ba2c93e8921d999374876a2c65d889d256e5f"
|
||||
}
|
||||
31
tests/golden/M12-02B/manifest.json
Normal file
31
tests/golden/M12-02B/manifest.json
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"task": "M12-02B",
|
||||
"parentTask": "M12-02",
|
||||
"parityId": "N-023-ASSET-PREVIEW",
|
||||
"enablingTask": true,
|
||||
"parityStateChange": false,
|
||||
"implementationClass": "NOT_APPLICABLE",
|
||||
"generatorSettings": {
|
||||
"schemaVersion": 1,
|
||||
"format": "PNG",
|
||||
"colorMode": "RGBA",
|
||||
"colorDepth": 8,
|
||||
"compression": 0,
|
||||
"displayDevice": "sRGB",
|
||||
"viewTransform": "Standard",
|
||||
"look": "None",
|
||||
"exposure": 0,
|
||||
"gamma": 1
|
||||
},
|
||||
"generatorSettingsSha256": "07ebc55d4b6cbb293badf0640874846912df3d23549997cf6f87f4c325280ca0",
|
||||
"artifacts": {
|
||||
"protocol": { "path": "web/protocol/asset-preview.ts", "sha256": "2a1d877af42424014097c669e9d44c21b27e3171be185320554693cbefcd7438" },
|
||||
"errorContract": { "path": "web/protocol/error.ts", "sha256": "6aec7b8a9d6903d83ae67718db3ae775d709cabb207cf2a02968d4ff7c5f26c0" },
|
||||
"generator": { "path": "tools/web/generate-asset-preview-identity.py", "sha256": "753a26be90f7d97828737ebc3a4ab88275a7c655b4f9992406c7c35fba6535ce" },
|
||||
"blender": { "path": "build_blender_5.2.0/bin/blender", "sha256": "d4483926610484ef9c2ad9241aae1469f934d955ebe791f1920e263e0ba85b82" },
|
||||
"source": { "path": "tests/files/web/media/sequencer-frame.png", "sha256": "295b083db2299ab904947eb39c17173de70c211882b0d855537d8f3cc27698ed" },
|
||||
"content": { "path": "tests/golden/M12-02B/preview.png", "sha256": "ff139c4a1c5d388c78c5168dfd3272e80ea0d1b15b3743e29a38cd908e8c70e7" }
|
||||
},
|
||||
"nextTask": "M12-02C"
|
||||
}
|
||||
BIN
tests/golden/M12-02B/preview.png
Normal file
BIN
tests/golden/M12-02B/preview.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 513 B |
34
tests/golden/M12-02C/manifest.json
Normal file
34
tests/golden/M12-02C/manifest.json
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"task": "M12-02C",
|
||||
"parentTask": "M12-02",
|
||||
"parityId": "N-023-ASSET-PREVIEW",
|
||||
"enablingTask": true,
|
||||
"parityStateChange": false,
|
||||
"implementationClass": "NOT_APPLICABLE",
|
||||
"budget": {
|
||||
"maxEncodedBytes": 16777216,
|
||||
"maxWidth": 4096,
|
||||
"maxHeight": 4096,
|
||||
"maxPixels": 16777216,
|
||||
"maxDecodedBytes": 67108864,
|
||||
"maxCompressionRatio": 100
|
||||
},
|
||||
"preDecodeOrder": [
|
||||
"IDENTITY_PARSE",
|
||||
"ENCODED_BYTE_BUDGET",
|
||||
"CONTENT_SHA256",
|
||||
"MIME_AND_HEADER",
|
||||
"DIMENSION_BINDING",
|
||||
"PIXEL_AND_DECODED_BYTE_BUDGET",
|
||||
"COMPRESSION_RATIO"
|
||||
],
|
||||
"negativeCaseCount": 8,
|
||||
"artifacts": {
|
||||
"decodeProtocol": { "path": "web/protocol/asset-preview-decode.ts", "sha256": "3d1c491af2503a7a6012541ed43400d680f8e666cc12a54db284aeb6ac18ff63" },
|
||||
"identityProtocol": { "path": "web/protocol/asset-preview.ts", "sha256": "2a1d877af42424014097c669e9d44c21b27e3171be185320554693cbefcd7438" },
|
||||
"identityGolden": { "path": "tests/golden/M12-02B/identity.json", "sha256": "1b1eb8416ce4aa4d83b0dc42b9cb375f1da965e3f0cb9650991289fc619a0a7b" },
|
||||
"contentPNG": { "path": "tests/golden/M12-02B/preview.png", "sha256": "ff139c4a1c5d388c78c5168dfd3272e80ea0d1b15b3743e29a38cd908e8c70e7" }
|
||||
},
|
||||
"nextTask": "M12-02D"
|
||||
}
|
||||
48
tests/golden/M12-02D/manifest.json
Normal file
48
tests/golden/M12-02D/manifest.json
Normal file
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"task": "M12-02D",
|
||||
"parentTask": "M12-02",
|
||||
"parityId": "N-023-ASSET-PREVIEW",
|
||||
"enablingTask": true,
|
||||
"parityStateChange": false,
|
||||
"implementationClass": "NOT_APPLICABLE",
|
||||
"commitOrder": [
|
||||
"PRE_DECODE_GATE",
|
||||
"OPFS_WRITE",
|
||||
"OPFS_READBACK",
|
||||
"CATALOG_TRANSACTION"
|
||||
],
|
||||
"faultPoints": [
|
||||
"BEFORE_OPFS_WRITE",
|
||||
"AFTER_OPFS_WRITE",
|
||||
"AFTER_CATALOG_PUT"
|
||||
],
|
||||
"failureCode": "STORAGE_TRANSACTION",
|
||||
"artifacts": {
|
||||
"commitCoordinator": {
|
||||
"path": "web/app/src/storage/asset-preview-opfs-commit.ts",
|
||||
"sha256": "36418e480b108eb885b5d8da6633174ed1face1fa62742a64372a45280e727a2"
|
||||
},
|
||||
"browserSuite": {
|
||||
"path": "web/tests/e2e/asset-preview-opfs-commit.spec.ts",
|
||||
"sha256": "043b9b06d4c0275938cc1d4f9837df95f58f281130f3c0f2b90c10540244592e"
|
||||
},
|
||||
"unitSuite": {
|
||||
"path": "web/tests/unit/asset-preview-opfs-commit.test.mjs",
|
||||
"sha256": "f489a1d9a4c5fb24a99bce6d31841534c7cf3981b53076a0d9e19d106e9d0d74"
|
||||
},
|
||||
"decodeProtocol": {
|
||||
"path": "web/protocol/asset-preview-decode.ts",
|
||||
"sha256": "3d1c491af2503a7a6012541ed43400d680f8e666cc12a54db284aeb6ac18ff63"
|
||||
},
|
||||
"identityGolden": {
|
||||
"path": "tests/golden/M12-02B/identity.json",
|
||||
"sha256": "1b1eb8416ce4aa4d83b0dc42b9cb375f1da965e3f0cb9650991289fc619a0a7b"
|
||||
},
|
||||
"contentPNG": {
|
||||
"path": "tests/golden/M12-02B/preview.png",
|
||||
"sha256": "ff139c4a1c5d388c78c5168dfd3272e80ea0d1b15b3743e29a38cd908e8c70e7"
|
||||
}
|
||||
},
|
||||
"nextTask": "M12-02E"
|
||||
}
|
||||
41
tests/golden/M12-02E/manifest.json
Normal file
41
tests/golden/M12-02E/manifest.json
Normal file
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"task": "M12-02E",
|
||||
"parentTask": "M12-02",
|
||||
"parityId": "N-023-ASSET-PREVIEW",
|
||||
"enablingTask": true,
|
||||
"parityStateChange": false,
|
||||
"implementationClass": "NOT_APPLICABLE",
|
||||
"claims": [
|
||||
"ONE_CONTENT_HASH_ONE_OPFS_PAYLOAD",
|
||||
"DISTINCT_ASSET_IDENTITY_PRESERVED",
|
||||
"DISTINCT_ASSET_METADATA_PRESERVED",
|
||||
"REOPENED_HEAD_VERIFIED"
|
||||
],
|
||||
"assetCount": 2,
|
||||
"physicalPayloadCount": 1,
|
||||
"contentSha256": "ff139c4a1c5d388c78c5168dfd3272e80ea0d1b15b3743e29a38cd908e8c70e7",
|
||||
"artifacts": {
|
||||
"commitCoordinator": {
|
||||
"path": "web/app/src/storage/asset-preview-opfs-commit.ts",
|
||||
"sha256": "36418e480b108eb885b5d8da6633174ed1face1fa62742a64372a45280e727a2"
|
||||
},
|
||||
"browserSuite": {
|
||||
"path": "web/tests/e2e/asset-preview-dedup.spec.ts",
|
||||
"sha256": "4e53b7a35eb109b152bd6dc0c3348dcb08879d17619968d37fc761c67aa38b40"
|
||||
},
|
||||
"unitSuite": {
|
||||
"path": "web/tests/unit/asset-preview-dedup.test.mjs",
|
||||
"sha256": "e023fdd33e0f40c1331dfdd73cd287dc684256b9d134971ab17db8cfe2a83835"
|
||||
},
|
||||
"catalogFixture": {
|
||||
"path": "tests/golden/M12-01D/catalog-v2.json",
|
||||
"sha256": "38e5d1771b3d2d63ce5e10268eac4513f2043b0cbf71ab1799c1868bc2aa97fb"
|
||||
},
|
||||
"contentPNG": {
|
||||
"path": "tests/golden/M12-02B/preview.png",
|
||||
"sha256": "ff139c4a1c5d388c78c5168dfd3272e80ea0d1b15b3743e29a38cd908e8c70e7"
|
||||
}
|
||||
},
|
||||
"nextTask": "M12-02F"
|
||||
}
|
||||
35
tests/golden/M12-02F/manifest.json
Normal file
35
tests/golden/M12-02F/manifest.json
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"task": "M12-02F",
|
||||
"parentTask": "M12-02",
|
||||
"parityId": "N-023-ASSET-PREVIEW",
|
||||
"enablingTask": true,
|
||||
"parityStateChange": false,
|
||||
"implementationClass": "NOT_APPLICABLE",
|
||||
"states": ["READY", "QUARANTINED", "REOPENED_QUARANTINED"],
|
||||
"failureCode": "ASSET_SOURCE_HASH_MISMATCH",
|
||||
"catalogMutationOnCorruption": false,
|
||||
"artifacts": {
|
||||
"quarantineProtocol": {
|
||||
"path": "web/app/src/storage/asset-preview-quarantine.ts",
|
||||
"sha256": "a5f3330dbd16d9f578d85d9504c980fffea85b834b77340a307ce21ce9b1cb04"
|
||||
},
|
||||
"browserSuite": {
|
||||
"path": "web/tests/e2e/asset-preview-quarantine.spec.ts",
|
||||
"sha256": "d8e1d0741765e8f8bfd47b45cf03213d8629efdbbf77dfd33846785a3ec80c27"
|
||||
},
|
||||
"unitSuite": {
|
||||
"path": "web/tests/unit/asset-preview-quarantine.test.mjs",
|
||||
"sha256": "79c836c7a3be4e9f305a962775444c766577cca87fa6d1698811e75eb21f2d9a"
|
||||
},
|
||||
"commitCoordinator": {
|
||||
"path": "web/app/src/storage/asset-preview-opfs-commit.ts",
|
||||
"sha256": "36418e480b108eb885b5d8da6633174ed1face1fa62742a64372a45280e727a2"
|
||||
},
|
||||
"contentPNG": {
|
||||
"path": "tests/golden/M12-02B/preview.png",
|
||||
"sha256": "ff139c4a1c5d388c78c5168dfd3272e80ea0d1b15b3743e29a38cd908e8c70e7"
|
||||
}
|
||||
},
|
||||
"nextTask": "M12-02G"
|
||||
}
|
||||
39
tests/golden/M12-02G/manifest.json
Normal file
39
tests/golden/M12-02G/manifest.json
Normal file
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"task": "M12-02G",
|
||||
"parentTask": "M12-02",
|
||||
"parityId": "N-023-ASSET-PREVIEW",
|
||||
"enablingTask": true,
|
||||
"parityStateChange": false,
|
||||
"implementationClass": "NOT_APPLICABLE",
|
||||
"sequence": [
|
||||
"REMOVE_FIRST_REFERENCE",
|
||||
"RETAIN_PAYLOAD",
|
||||
"REMOVE_FINAL_REFERENCE",
|
||||
"COLLECT_PAYLOAD"
|
||||
],
|
||||
"otherProjectPayload": "PRESERVED_READY",
|
||||
"artifacts": {
|
||||
"referenceGC": {
|
||||
"path": "web/app/src/storage/asset-preview-reference-gc.ts",
|
||||
"sha256": "76b9d8ccd3e7992356f33adc694001f9b0ff364788234389eacdb09198d1415e"
|
||||
},
|
||||
"browserSuite": {
|
||||
"path": "web/tests/e2e/asset-preview-reference-gc.spec.ts",
|
||||
"sha256": "2e9e9b47883997092c5afb0fb7859a484754764de19006e24a8ff2bdeb24f35a"
|
||||
},
|
||||
"unitSuite": {
|
||||
"path": "web/tests/unit/asset-preview-reference-gc.test.mjs",
|
||||
"sha256": "c5de3122b5d8aab17daf0179cb4f5bb0b5d299408818849cbe7b18e40a47706e"
|
||||
},
|
||||
"opfsStorage": {
|
||||
"path": "web/app/src/storage/opfs-files.ts",
|
||||
"sha256": "2e8ef9d72194bf803a7e823fb2b7de30b9f10ae6dd4b031b81b97ff82eee9cc6"
|
||||
},
|
||||
"contentPNG": {
|
||||
"path": "tests/golden/M12-02B/preview.png",
|
||||
"sha256": "ff139c4a1c5d388c78c5168dfd3272e80ea0d1b15b3743e29a38cd908e8c70e7"
|
||||
}
|
||||
},
|
||||
"nextTask": "M12-02H"
|
||||
}
|
||||
17
tests/golden/M12-02H/desktop-report.json
Normal file
17
tests/golden/M12-02H/desktop-report.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"task": "M12-02H",
|
||||
"generator": "BLENDER_ASSET_PREVIEW_IDENTITY",
|
||||
"blenderVersion": "5.2.0",
|
||||
"width": 8,
|
||||
"height": 8,
|
||||
"pixelCount": 64,
|
||||
"pixelFormat": "RGBA8",
|
||||
"colorSpace": "SRGB",
|
||||
"alphaMode": "STRAIGHT",
|
||||
"uniquePixelCount": 1,
|
||||
"referencePixel": [51, 102, 204, 255],
|
||||
"rgbaSha256": "c2ff81750b41193ce1a06d47d0ff18168a756136dbf0f2586cd0638d4bf0ef00",
|
||||
"sourceContentMaxFloatError": 0,
|
||||
"contentSha256": "ff139c4a1c5d388c78c5168dfd3272e80ea0d1b15b3743e29a38cd908e8c70e7"
|
||||
}
|
||||
64
tests/golden/M12-02H/manifest.json
Normal file
64
tests/golden/M12-02H/manifest.json
Normal file
@@ -0,0 +1,64 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"task": "M12-02H",
|
||||
"parentTask": "M12-02",
|
||||
"parityId": "N-023-ASSET-PREVIEW",
|
||||
"enablingTask": false,
|
||||
"parityStateChange": true,
|
||||
"implementationClass": "LOCAL_EXACT",
|
||||
"completedSlice": "A-preview-desktop-browser-main-offscreen-display",
|
||||
"backends": [
|
||||
"BLENDER_5_2_DESKTOP",
|
||||
"MAIN_THREAD_CANVAS_2D",
|
||||
"OFFSCREEN_CANVAS_2D"
|
||||
],
|
||||
"reference": {
|
||||
"width": 8,
|
||||
"height": 8,
|
||||
"pixelFormat": "RGBA8",
|
||||
"colorSpace": "SRGB",
|
||||
"alphaMode": "STRAIGHT",
|
||||
"rgbaSha256": "c2ff81750b41193ce1a06d47d0ff18168a756136dbf0f2586cd0638d4bf0ef00"
|
||||
},
|
||||
"thresholds": {
|
||||
"maxMeanAbsoluteError": 0,
|
||||
"maxRootMeanSquaredError": 0,
|
||||
"maxP95ChannelError": 0,
|
||||
"maxBadPixelRatio": 0,
|
||||
"badPixelChannelError": 0,
|
||||
"foregroundDeltaFromReferenceBackground": 1,
|
||||
"minForegroundIntersectionOverUnion": 1,
|
||||
"maxAlphaCoverageDeltaRatio": 0
|
||||
},
|
||||
"artifacts": {
|
||||
"displayRuntime": {
|
||||
"path": "web/app/src/assets/AssetPreviewDisplay.ts",
|
||||
"sha256": "e4a46a40291427afc185caf016b02120ea71639ab4ab4ac250a5c3de28ee8d28"
|
||||
},
|
||||
"displayWorker": {
|
||||
"path": "web/app/src/workers/asset-preview-display-test.worker.ts",
|
||||
"sha256": "6322562f54b608b7eaac4ca3bc0f8d861f47bb8e9b37b3c2ae3d61163108724f"
|
||||
},
|
||||
"desktopChecker": {
|
||||
"path": "tools/web/check-asset-preview-display.mjs",
|
||||
"sha256": "d03d766e69ea0547be2c247b5d7ef8fd5ceab6b399dc996eba711fa0cac0ca54"
|
||||
},
|
||||
"desktopReport": {
|
||||
"path": "tests/golden/M12-02H/desktop-report.json",
|
||||
"sha256": "cdf841f5d27362f6df380772ada8b7b1427d051a8b46b5c2c20aab0b597c25d0"
|
||||
},
|
||||
"browserSuite": {
|
||||
"path": "web/tests/e2e/asset-preview-display.spec.ts",
|
||||
"sha256": "340d386c6a854c44cd6f6207e3699f870b8ded2aada4747afd6ff1103c719520"
|
||||
},
|
||||
"unitSuite": {
|
||||
"path": "web/tests/unit/asset-preview-display.test.mjs",
|
||||
"sha256": "f10a05900c4d1282251f7a76e7c413a18602910f55fe8400f1713f548eba3873"
|
||||
},
|
||||
"contentPNG": {
|
||||
"path": "tests/golden/M12-02B/preview.png",
|
||||
"sha256": "ff139c4a1c5d388c78c5168dfd3272e80ea0d1b15b3743e29a38cd908e8c70e7"
|
||||
}
|
||||
},
|
||||
"nextTask": "M12-03A"
|
||||
}
|
||||
255
tests/golden/M12-03A/library-operation-inventory.json
Normal file
255
tests/golden/M12-03A/library-operation-inventory.json
Normal file
@@ -0,0 +1,255 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"task": "M12-03A",
|
||||
"enablingTask": true,
|
||||
"parityStateChange": false,
|
||||
"blenderVersion": "5.2.0",
|
||||
"sources": [
|
||||
{
|
||||
"role": "LINK_APPEND_CONTEXT_API",
|
||||
"path": "blender-5.2.0/source/blender/blenkernel/BKE_blendfile_link_append.hh",
|
||||
"sha256": "3676283eca608a12fa6485c438374dfbe07a787efe22fd7719aadb8ccb8c81f9"
|
||||
},
|
||||
{
|
||||
"role": "LINK_APPEND_CLOSURE",
|
||||
"path": "blender-5.2.0/source/blender/blenkernel/intern/blendfile_link_append.cc",
|
||||
"sha256": "b6e7f839a934eabd708c5465fdcf00e138e1f84837ae6690198b14a5069b6d23"
|
||||
},
|
||||
{
|
||||
"role": "LINK_APPEND_OPERATORS",
|
||||
"path": "blender-5.2.0/source/blender/windowmanager/intern/wm_files_link.cc",
|
||||
"sha256": "d67d555f8534e6794ba7e520d95faef5a0a83017415517c8821690fa4accfb8d"
|
||||
},
|
||||
{
|
||||
"role": "PYTHON_LIBRARY_LOAD",
|
||||
"path": "blender-5.2.0/source/blender/python/intern/bpy_library_load.cc",
|
||||
"sha256": "41b80637140582c3c75fabe234a8c33e9749c75cfc5c8f9c748401c18b253cab"
|
||||
},
|
||||
{
|
||||
"role": "BLEND_LIBRARY_READER",
|
||||
"path": "blender-5.2.0/source/blender/blenloader/BLO_readfile.hh",
|
||||
"sha256": "24e65071b94e7fbc754e12563b9698f2d92a5aab06b89763950d2d4f7d66c623"
|
||||
},
|
||||
{
|
||||
"role": "OVERRIDE_STORAGE",
|
||||
"path": "blender-5.2.0/source/blender/makesdna/DNA_ID.h",
|
||||
"sha256": "27d0eac02df737f2d2298eeef45e66eae0d76da66542ccdf32400b3e9bd904a9"
|
||||
},
|
||||
{
|
||||
"role": "OVERRIDE_API",
|
||||
"path": "blender-5.2.0/source/blender/blenkernel/BKE_lib_override.hh",
|
||||
"sha256": "78545dad30c66a61e1f9754ba8017422b8d7fd3a4b41e522299e27f951762fde"
|
||||
},
|
||||
{
|
||||
"role": "OVERRIDE_CLOSURE",
|
||||
"path": "blender-5.2.0/source/blender/blenkernel/intern/lib_override.cc",
|
||||
"sha256": "34ed9f0edd6549c77e73dc958ec189717bb6eb7dda3531109161546b8c532808"
|
||||
},
|
||||
{
|
||||
"role": "CURRENT_WEB_LIBRARY_GATE",
|
||||
"path": "web/protocol/asset-library-io.ts",
|
||||
"sha256": "5c62539db3833a2113007dfbdfd12ca06704d86e0cf0e57ad97f7fbf6acda7f4"
|
||||
}
|
||||
],
|
||||
"runtimeFixture": {
|
||||
"path": "tests/files/web/image_resource_matrix.blend",
|
||||
"sha256": "21e16821b62dad84539560bfa53ac755f097dbcd33ca2d614c07123461cdd74e",
|
||||
"nestedLibraries": [
|
||||
{
|
||||
"filepath": "//resources/image_resource_library.blend",
|
||||
"isArchive": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"dataBlockSurface": {
|
||||
"discoverableCollections": [
|
||||
"actions",
|
||||
"annotations",
|
||||
"armatures",
|
||||
"brushes",
|
||||
"cache_files",
|
||||
"cameras",
|
||||
"collections",
|
||||
"curves",
|
||||
"fonts",
|
||||
"grease_pencils",
|
||||
"hair_curves",
|
||||
"images",
|
||||
"lattices",
|
||||
"lightprobes",
|
||||
"lights",
|
||||
"linestyles",
|
||||
"masks",
|
||||
"materials",
|
||||
"meshes",
|
||||
"metaballs",
|
||||
"movieclips",
|
||||
"node_groups",
|
||||
"objects",
|
||||
"paint_curves",
|
||||
"palettes",
|
||||
"particles",
|
||||
"pointclouds",
|
||||
"scenes",
|
||||
"screens",
|
||||
"sounds",
|
||||
"speakers",
|
||||
"texts",
|
||||
"textures",
|
||||
"volumes",
|
||||
"workspaces",
|
||||
"worlds"
|
||||
],
|
||||
"onlyAppendableCollections": ["screens", "workspaces"],
|
||||
"nonRootRoles": [
|
||||
{"collection": "libraries", "role": "SOURCE_AND_TRANSITIVE_LIBRARY_METADATA"},
|
||||
{"collection": "shape_keys", "role": "EMBEDDED_OWNER_DEPENDENCY"},
|
||||
{"collection": "window_managers", "role": "CURRENT_FILE_RUNTIME_ONLY"},
|
||||
{"collection": "all_ids", "role": "AGGREGATE_QUERY_NOT_LIBRARY_SECTION"}
|
||||
]
|
||||
},
|
||||
"operatorSurface": {
|
||||
"appendProperties": [
|
||||
"filepath", "directory", "filename", "files", "check_existing", "filter_blender",
|
||||
"filter_backup", "filter_image", "filter_movie", "filter_python", "filter_font",
|
||||
"filter_sound", "filter_text", "filter_archive", "filter_btx", "filter_alembic",
|
||||
"filter_usd", "filter_obj", "filter_volume", "filter_folder", "filter_blenlib",
|
||||
"filemode", "display_type", "sort_method", "link", "do_reuse_local_id",
|
||||
"clear_asset_data", "autoselect", "active_collection", "instance_collections",
|
||||
"instance_object_data", "set_fake", "use_recursive"
|
||||
],
|
||||
"linkProperties": [
|
||||
"filepath", "directory", "filename", "files", "check_existing", "filter_blender",
|
||||
"filter_backup", "filter_image", "filter_movie", "filter_python", "filter_font",
|
||||
"filter_sound", "filter_text", "filter_archive", "filter_btx", "filter_alembic",
|
||||
"filter_usd", "filter_obj", "filter_volume", "filter_folder", "filter_blenlib",
|
||||
"filemode", "relative_path", "display_type", "sort_method", "link",
|
||||
"do_reuse_local_id", "clear_asset_data", "autoselect", "active_collection",
|
||||
"instance_collections", "instance_object_data"
|
||||
],
|
||||
"pythonLoadArguments": [
|
||||
"filepath", "link", "pack", "relative", "set_fake", "recursive", "reuse_local_id",
|
||||
"assets_only", "clear_asset_data", "create_liboverrides", "reuse_liboverrides",
|
||||
"create_liboverrides_runtime"
|
||||
]
|
||||
},
|
||||
"operations": [
|
||||
{
|
||||
"operation": "APPEND",
|
||||
"entryPoints": ["WM_OT_append", "bpy.data.libraries.load(link=False)"],
|
||||
"rootCollections": "ALL_DISCOVERABLE",
|
||||
"dataBlockRoles": [
|
||||
"SOURCE_LIBRARY",
|
||||
"DIRECT_SELECTED_ROOT",
|
||||
"INDIRECT_ID_DEPENDENCY",
|
||||
"TRANSITIVE_LIBRARY_DEPENDENCY",
|
||||
"EMBEDDED_OWNER_DEPENDENCY",
|
||||
"LIBOVERRIDE_DEPENDENCY",
|
||||
"REUSABLE_LOCAL_ID",
|
||||
"INSTANTIATED_COLLECTION_OBJECT_OR_OBJECT_DATA"
|
||||
],
|
||||
"closure": {
|
||||
"walker": "BKE_library_foreach_ID_link",
|
||||
"walkFlags": ["IDWALK_NOP"],
|
||||
"ignoredAsIndependentEdges": [
|
||||
"IDWALK_CB_EMBEDDED",
|
||||
"IDWALK_CB_EMBEDDED_NOT_OWNING",
|
||||
"IDWALK_CB_INTERNAL",
|
||||
"IDWALK_CB_LOOPBACK"
|
||||
],
|
||||
"selectedRootTag": "DIRECT",
|
||||
"discoveredDependencyTag": "INDIRECT",
|
||||
"overrideDependencyTags": ["LIBOVERRIDE_DEPENDENCY", "LIBOVERRIDE_DEPENDENCY_ONLY"],
|
||||
"terminalActions": ["KEEP_LINKED", "REUSE_LOCAL", "MAKE_LOCAL", "COPY_LOCAL"],
|
||||
"recursiveCrossLibraryLocalization": "CONTROLLED_BY_USE_RECURSIVE"
|
||||
}
|
||||
},
|
||||
{
|
||||
"operation": "LINK",
|
||||
"entryPoints": ["WM_OT_link", "bpy.data.libraries.load(link=True)"],
|
||||
"rootCollections": "DISCOVERABLE_EXCEPT_ONLY_APPENDABLE",
|
||||
"dataBlockRoles": [
|
||||
"SOURCE_LIBRARY",
|
||||
"DIRECT_LINKED_ROOT",
|
||||
"INDIRECT_LINKED_ID_DEPENDENCY",
|
||||
"TRANSITIVE_LIBRARY_DEPENDENCY",
|
||||
"EMBEDDED_OWNER_DEPENDENCY",
|
||||
"INSTANTIATED_COLLECTION_OBJECT_OR_OBJECT_DATA"
|
||||
],
|
||||
"closure": {
|
||||
"readerStages": ["BLO_library_link_begin", "BLO_library_link_named_part", "BLO_library_link_end"],
|
||||
"selectedRootTag": "ID_TAG_EXTERN",
|
||||
"discoveredDependencyTag": "ID_TAG_INDIRECT",
|
||||
"sourceOwnership": "ID.lib",
|
||||
"transitiveLibraryOwnership": "Library.runtime.parent",
|
||||
"localization": "NONE"
|
||||
}
|
||||
},
|
||||
{
|
||||
"operation": "LIBRARY_OVERRIDE",
|
||||
"entryPoints": ["bpy.data.libraries.load(link=True, create_liboverrides=True)", "BKE_blendfile_override"],
|
||||
"rootCollections": "LINK_ROOT_SURFACE_SUBJECT_TO_OVERRIDE_HIERARCHY",
|
||||
"dataBlockRoles": [
|
||||
"LINKED_REFERENCE_ROOT",
|
||||
"LINKED_REFERENCE_DEPENDENCY",
|
||||
"LOCAL_OVERRIDE_ID",
|
||||
"LOCAL_HIERARCHY_ROOT",
|
||||
"SYSTEM_OVERRIDE_DEPENDENCY",
|
||||
"OVERRIDE_PROPERTY",
|
||||
"OVERRIDE_PROPERTY_OPERATION",
|
||||
"REFERENCE_AND_LOCAL_SUBITEM"
|
||||
],
|
||||
"closure": {
|
||||
"referencePointer": "IDOverrideLibrary.reference",
|
||||
"hierarchyRootPointer": "IDOverrideLibrary.hierarchy_root",
|
||||
"propertyPath": "IDOverrideLibraryProperty.rna_path",
|
||||
"operationCollection": "IDOverrideLibraryProperty.operations",
|
||||
"creation": "BKE_lib_override_library_create",
|
||||
"resync": "BKE_lib_override_library_resync",
|
||||
"operationDiff": "BKE_lib_override_library_operations_create",
|
||||
"dependencyRule": "REFERENCE_POINTER_GRAPH_WITH_LOCAL_OVERRIDE_REMAP",
|
||||
"linkedReferencePreserved": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"overrideRNA": {
|
||||
"IDOverrideLibrary": [
|
||||
{"identifier": "reference", "type": "POINTER", "isReadonly": true},
|
||||
{"identifier": "hierarchy_root", "type": "POINTER", "isReadonly": true},
|
||||
{"identifier": "is_in_hierarchy", "type": "BOOLEAN", "isReadonly": false},
|
||||
{"identifier": "is_system_override", "type": "BOOLEAN", "isReadonly": false},
|
||||
{"identifier": "properties", "type": "COLLECTION", "isReadonly": true}
|
||||
],
|
||||
"IDOverrideLibraryProperty": [
|
||||
{"identifier": "rna_path", "type": "STRING", "isReadonly": true},
|
||||
{"identifier": "operations", "type": "COLLECTION", "isReadonly": true}
|
||||
],
|
||||
"IDOverrideLibraryPropertyOperation": [
|
||||
{"identifier": "operation", "type": "ENUM", "isReadonly": true},
|
||||
{"identifier": "flag", "type": "ENUM", "isReadonly": true},
|
||||
{"identifier": "subitem_reference_name", "type": "STRING", "isReadonly": true},
|
||||
{"identifier": "subitem_local_name", "type": "STRING", "isReadonly": true},
|
||||
{"identifier": "subitem_reference_id", "type": "POINTER", "isReadonly": true},
|
||||
{"identifier": "subitem_local_id", "type": "POINTER", "isReadonly": true},
|
||||
{"identifier": "subitem_reference_index", "type": "INT", "isReadonly": true},
|
||||
{"identifier": "subitem_local_index", "type": "INT", "isReadonly": true},
|
||||
{"identifier": "label", "type": "STRING", "isReadonly": true},
|
||||
{"identifier": "tooltip", "type": "STRING", "isReadonly": true}
|
||||
]
|
||||
},
|
||||
"currentWebGap": {
|
||||
"represented": ["LIBRARY_SOURCE", "LIBRARY_DEPENDENCY_IDS", "READ_ONLY_FLAG"],
|
||||
"missing": [
|
||||
"PER_ID_SOURCE_LIBRARY",
|
||||
"DIRECT_INDIRECT_ID_CLASSIFICATION",
|
||||
"APPEND_LOCAL_OWNERSHIP",
|
||||
"APPEND_REUSE_WEAK_REFERENCE",
|
||||
"OVERRIDE_REFERENCE",
|
||||
"OVERRIDE_HIERARCHY_ROOT",
|
||||
"OVERRIDE_PROPERTY_OPERATIONS",
|
||||
"INVALIDATION_TOKEN"
|
||||
],
|
||||
"mutationGate": "LIBRARY_MUTATION_UNAVAILABLE"
|
||||
},
|
||||
"nextTask": "M12-03B"
|
||||
}
|
||||
80
tests/golden/M12-03B/library-operation-bindings.json
Normal file
80
tests/golden/M12-03B/library-operation-bindings.json
Normal file
@@ -0,0 +1,80 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"task": "M12-03B",
|
||||
"source": {
|
||||
"schemaVersion": 1,
|
||||
"sourceLibraryId": "library:f35430c19c85351788053ceb44ba4dc01ec0a190a985197e60826d9d52c55a94",
|
||||
"sourceLocator": "project-assets/libraries/m12-library-source.blend",
|
||||
"sourceSha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
||||
},
|
||||
"bindings": [
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"operation": "APPEND",
|
||||
"source": {
|
||||
"schemaVersion": 1,
|
||||
"sourceLibraryId": "library:f35430c19c85351788053ceb44ba4dc01ec0a190a985197e60826d9d52c55a94",
|
||||
"sourceLocator": "project-assets/libraries/m12-library-source.blend",
|
||||
"sourceSha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
||||
},
|
||||
"sourceDataBlockId": "Object/M12Asset",
|
||||
"owner": {
|
||||
"kind": "LOCAL_MAIN",
|
||||
"projectId": "project:m12",
|
||||
"localDataBlockId": "Object/M12Asset"
|
||||
},
|
||||
"readOnly": false,
|
||||
"referenceReadOnly": false,
|
||||
"sourceGeneration": 3,
|
||||
"sourceRevision": 7,
|
||||
"dependencyClosureSha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
|
||||
"invalidationToken": "libtoken:c650807d1afb1c64d1e9deba45120b2bff5336dfdff52ce61c5d3645be923e6a"
|
||||
},
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"operation": "LINK",
|
||||
"source": {
|
||||
"schemaVersion": 1,
|
||||
"sourceLibraryId": "library:f35430c19c85351788053ceb44ba4dc01ec0a190a985197e60826d9d52c55a94",
|
||||
"sourceLocator": "project-assets/libraries/m12-library-source.blend",
|
||||
"sourceSha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
||||
},
|
||||
"sourceDataBlockId": "Object/M12Asset",
|
||||
"owner": {
|
||||
"kind": "SOURCE_LIBRARY",
|
||||
"sourceLibraryId": "library:f35430c19c85351788053ceb44ba4dc01ec0a190a985197e60826d9d52c55a94",
|
||||
"sourceDataBlockId": "Object/M12Asset"
|
||||
},
|
||||
"readOnly": true,
|
||||
"referenceReadOnly": true,
|
||||
"sourceGeneration": 3,
|
||||
"sourceRevision": 7,
|
||||
"dependencyClosureSha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
|
||||
"invalidationToken": "libtoken:028c1336cb8f27596daa8754654be461ab10af66caa35f706eac3f4b9480cfa4"
|
||||
},
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"operation": "LIBRARY_OVERRIDE",
|
||||
"source": {
|
||||
"schemaVersion": 1,
|
||||
"sourceLibraryId": "library:f35430c19c85351788053ceb44ba4dc01ec0a190a985197e60826d9d52c55a94",
|
||||
"sourceLocator": "project-assets/libraries/m12-library-source.blend",
|
||||
"sourceSha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
||||
},
|
||||
"sourceDataBlockId": "Object/M12Asset",
|
||||
"owner": {
|
||||
"kind": "LOCAL_OVERRIDE",
|
||||
"projectId": "project:m12",
|
||||
"localDataBlockId": "Object/M12AssetOverride",
|
||||
"referenceSourceDataBlockId": "Object/M12Asset",
|
||||
"hierarchyRootDataBlockId": "Collection/M12Root"
|
||||
},
|
||||
"readOnly": false,
|
||||
"referenceReadOnly": true,
|
||||
"sourceGeneration": 3,
|
||||
"sourceRevision": 7,
|
||||
"dependencyClosureSha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
|
||||
"invalidationToken": "libtoken:a308528792e6056af25bebb3a349ac7e1ff5c4e40189d2cb7aad453a8a8299fe"
|
||||
}
|
||||
]
|
||||
}
|
||||
42
tests/golden/M12-03B/manifest.json
Normal file
42
tests/golden/M12-03B/manifest.json
Normal file
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"task": "M12-03B",
|
||||
"parentTask": "M12-03",
|
||||
"enablingTask": true,
|
||||
"parityStateChange": false,
|
||||
"operations": ["APPEND", "LINK", "LIBRARY_OVERRIDE"],
|
||||
"ownershipSemantics": {
|
||||
"APPEND": {"owner": "LOCAL_MAIN", "readOnly": false, "referenceReadOnly": false},
|
||||
"LINK": {"owner": "SOURCE_LIBRARY", "readOnly": true, "referenceReadOnly": true},
|
||||
"LIBRARY_OVERRIDE": {"owner": "LOCAL_OVERRIDE", "readOnly": false, "referenceReadOnly": true}
|
||||
},
|
||||
"invalidationFields": [
|
||||
"operation",
|
||||
"sourceLibraryId",
|
||||
"sourceSha256",
|
||||
"sourceDataBlockId",
|
||||
"owner",
|
||||
"sourceGeneration",
|
||||
"sourceRevision",
|
||||
"dependencyClosureSha256"
|
||||
],
|
||||
"artifacts": {
|
||||
"parentInventory": {
|
||||
"path": "tests/golden/M12-03A/library-operation-inventory.json",
|
||||
"sha256": "0dcb16233b52b3c0be2373f489d6bf3c771c9d6564993ff98b5d63dcd322fef7"
|
||||
},
|
||||
"protocol": {
|
||||
"path": "web/protocol/library-operation-identity.ts",
|
||||
"sha256": "5371133bd98e05935cd75bab582abedec32a36559af8d212f0bc8a534ef19451"
|
||||
},
|
||||
"golden": {
|
||||
"path": "tests/golden/M12-03B/library-operation-bindings.json",
|
||||
"sha256": "b39c0f26697ac688c1203ae4e3e27d8a02686c7c9da9afc438c89a0b4d69370f"
|
||||
},
|
||||
"unitSuite": {
|
||||
"path": "web/tests/unit/library-operation-identity.test.mjs",
|
||||
"sha256": "0e5b2c82d481180eb8eb22de16ad8cdef5e58fa7423c26011faaffbac788e547"
|
||||
}
|
||||
},
|
||||
"nextTask": "M12-03C"
|
||||
}
|
||||
202
tests/golden/M12-03C/desktop-append-report.json
Normal file
202
tests/golden/M12-03C/desktop-append-report.json
Normal file
@@ -0,0 +1,202 @@
|
||||
{
|
||||
"appendedGraph": {
|
||||
"edges": [
|
||||
{
|
||||
"from": "Object/M12 Append Object",
|
||||
"relation": "OBJECT_DATA",
|
||||
"to": "Mesh/M12 Append Mesh"
|
||||
},
|
||||
{
|
||||
"from": "Mesh/M12 Append Mesh",
|
||||
"relation": "MATERIAL_SLOT[0]",
|
||||
"to": "Material/M12 Append Material"
|
||||
},
|
||||
{
|
||||
"from": "Material/M12 Append Material",
|
||||
"relation": "NODE_IMAGE[M12 Append Image Node]",
|
||||
"to": "Image/M12 Append Image"
|
||||
}
|
||||
],
|
||||
"geometry": {
|
||||
"edges": 4,
|
||||
"loops": 4,
|
||||
"materialSlots": [
|
||||
"M12 Append Material"
|
||||
],
|
||||
"polygons": 1,
|
||||
"uvLayers": [
|
||||
"UVMap"
|
||||
],
|
||||
"vertices": 4
|
||||
},
|
||||
"ids": {
|
||||
"IMAGE": {
|
||||
"idType": "IMAGE",
|
||||
"isLibraryOverride": false,
|
||||
"library": null,
|
||||
"name": "M12 Append Image",
|
||||
"nameFull": "M12 Append Image"
|
||||
},
|
||||
"MATERIAL": {
|
||||
"idType": "MATERIAL",
|
||||
"isLibraryOverride": false,
|
||||
"library": null,
|
||||
"name": "M12 Append Material",
|
||||
"nameFull": "M12 Append Material"
|
||||
},
|
||||
"MESH": {
|
||||
"idType": "MESH",
|
||||
"isLibraryOverride": false,
|
||||
"library": null,
|
||||
"name": "M12 Append Mesh",
|
||||
"nameFull": "M12 Append Mesh"
|
||||
},
|
||||
"OBJECT": {
|
||||
"idType": "OBJECT",
|
||||
"isLibraryOverride": false,
|
||||
"library": null,
|
||||
"name": "M12 Append Object",
|
||||
"nameFull": "M12 Append Object"
|
||||
}
|
||||
},
|
||||
"image": {
|
||||
"channels": 4,
|
||||
"colorspace": "sRGB",
|
||||
"packed": true,
|
||||
"pixelFloat32Sha256": "6f0f8c231d65149e69e6ed12d370bcfa095ef90adc202065492ea8c8ef17e45a",
|
||||
"size": [
|
||||
2,
|
||||
2
|
||||
]
|
||||
},
|
||||
"root": {
|
||||
"idType": "OBJECT",
|
||||
"isLibraryOverride": false,
|
||||
"library": null,
|
||||
"name": "M12 Append Object",
|
||||
"nameFull": "M12 Append Object"
|
||||
},
|
||||
"sourceMarker": "M12-03C"
|
||||
},
|
||||
"blenderVersion": "5.2.0",
|
||||
"nextTask": "M12-03D",
|
||||
"operation": "APPEND",
|
||||
"schemaVersion": 1,
|
||||
"selectedRoots": [
|
||||
"Object/M12 Append Object"
|
||||
],
|
||||
"source": {
|
||||
"file": "m12_append_source.blend",
|
||||
"sha256": "5b60d02926efd588a6ca300ba31414cdf37dbc48786c17b383a70319057c0606"
|
||||
},
|
||||
"sourceGraph": {
|
||||
"edges": [
|
||||
{
|
||||
"from": "Object/M12 Append Object",
|
||||
"relation": "OBJECT_DATA",
|
||||
"to": "Mesh/M12 Append Mesh"
|
||||
},
|
||||
{
|
||||
"from": "Mesh/M12 Append Mesh",
|
||||
"relation": "MATERIAL_SLOT[0]",
|
||||
"to": "Material/M12 Append Material"
|
||||
},
|
||||
{
|
||||
"from": "Material/M12 Append Material",
|
||||
"relation": "NODE_IMAGE[M12 Append Image Node]",
|
||||
"to": "Image/M12 Append Image"
|
||||
}
|
||||
],
|
||||
"geometry": {
|
||||
"edges": 4,
|
||||
"loops": 4,
|
||||
"materialSlots": [
|
||||
"M12 Append Material"
|
||||
],
|
||||
"polygons": 1,
|
||||
"uvLayers": [
|
||||
"UVMap"
|
||||
],
|
||||
"vertices": 4
|
||||
},
|
||||
"ids": {
|
||||
"IMAGE": {
|
||||
"idType": "IMAGE",
|
||||
"isLibraryOverride": false,
|
||||
"library": null,
|
||||
"name": "M12 Append Image",
|
||||
"nameFull": "M12 Append Image"
|
||||
},
|
||||
"MATERIAL": {
|
||||
"idType": "MATERIAL",
|
||||
"isLibraryOverride": false,
|
||||
"library": null,
|
||||
"name": "M12 Append Material",
|
||||
"nameFull": "M12 Append Material"
|
||||
},
|
||||
"MESH": {
|
||||
"idType": "MESH",
|
||||
"isLibraryOverride": false,
|
||||
"library": null,
|
||||
"name": "M12 Append Mesh",
|
||||
"nameFull": "M12 Append Mesh"
|
||||
},
|
||||
"OBJECT": {
|
||||
"idType": "OBJECT",
|
||||
"isLibraryOverride": false,
|
||||
"library": null,
|
||||
"name": "M12 Append Object",
|
||||
"nameFull": "M12 Append Object"
|
||||
}
|
||||
},
|
||||
"image": {
|
||||
"channels": 4,
|
||||
"colorspace": "sRGB",
|
||||
"packed": true,
|
||||
"pixelFloat32Sha256": "6f0f8c231d65149e69e6ed12d370bcfa095ef90adc202065492ea8c8ef17e45a",
|
||||
"size": [
|
||||
2,
|
||||
2
|
||||
]
|
||||
},
|
||||
"root": {
|
||||
"idType": "OBJECT",
|
||||
"isLibraryOverride": false,
|
||||
"library": null,
|
||||
"name": "M12 Append Object",
|
||||
"nameFull": "M12 Append Object"
|
||||
},
|
||||
"sourceMarker": "M12-03C"
|
||||
},
|
||||
"stableMapping": [
|
||||
{
|
||||
"local": "Object/M12 Append Object",
|
||||
"owner": "LOCAL_MAIN",
|
||||
"readOnly": false,
|
||||
"source": "Object/M12 Append Object"
|
||||
},
|
||||
{
|
||||
"local": "Mesh/M12 Append Mesh",
|
||||
"owner": "LOCAL_MAIN",
|
||||
"readOnly": false,
|
||||
"source": "Mesh/M12 Append Mesh"
|
||||
},
|
||||
{
|
||||
"local": "Material/M12 Append Material",
|
||||
"owner": "LOCAL_MAIN",
|
||||
"readOnly": false,
|
||||
"source": "Material/M12 Append Material"
|
||||
},
|
||||
{
|
||||
"local": "Image/M12 Append Image",
|
||||
"owner": "LOCAL_MAIN",
|
||||
"readOnly": false,
|
||||
"source": "Image/M12 Append Image"
|
||||
}
|
||||
],
|
||||
"target": {
|
||||
"file": "m12_append_target.blend",
|
||||
"sha256": "5cde4927f47e33f0338de38184ce5ba971825fcefd1d8dc6feb48d41a6cdb60c"
|
||||
},
|
||||
"task": "M12-03C"
|
||||
}
|
||||
39
tests/golden/M12-03C/manifest.json
Normal file
39
tests/golden/M12-03C/manifest.json
Normal file
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"task": "M12-03C",
|
||||
"parentTask": "M12-03",
|
||||
"enablingTask": true,
|
||||
"parityStateChange": false,
|
||||
"runtime": "BLENDER_5_2_DESKTOP",
|
||||
"operation": "APPEND",
|
||||
"selectedRoots": ["Object/M12 Append Object"],
|
||||
"dependencyTypes": ["OBJECT", "MESH", "MATERIAL", "IMAGE"],
|
||||
"containerDeterminism": "SESSION_BOUND_SEMANTIC_REPORT_EXACT",
|
||||
"artifacts": {
|
||||
"parentContract": {
|
||||
"path": "tests/golden/M12-03B/manifest.json",
|
||||
"sha256": "f388aedb9c0901932cf90c058f2f2b83259d9797cd6b144566a25e37a348c64b"
|
||||
},
|
||||
"generator": {
|
||||
"path": "tools/web/generate-library-append-fixture.py",
|
||||
"sha256": "b9d8e3bd24966b0d7d419382d7ef0b35ae8def9ee2c2236dfd3ad1a90aebe66f"
|
||||
},
|
||||
"checker": {
|
||||
"path": "tools/web/check-library-append-fixture.mjs",
|
||||
"sha256": "6f8cc0d51be19423cf6fb7203c3c16f8d27e0fbb3b5b357bad5dec6c5ceb432d"
|
||||
},
|
||||
"sourceBlend": {
|
||||
"path": "tests/files/web/m12_library_append_v1/m12_append_source.blend",
|
||||
"sha256": "5b60d02926efd588a6ca300ba31414cdf37dbc48786c17b383a70319057c0606"
|
||||
},
|
||||
"targetBlend": {
|
||||
"path": "tests/files/web/m12_library_append_v1/m12_append_target.blend",
|
||||
"sha256": "5cde4927f47e33f0338de38184ce5ba971825fcefd1d8dc6feb48d41a6cdb60c"
|
||||
},
|
||||
"desktopReport": {
|
||||
"path": "tests/golden/M12-03C/desktop-append-report.json",
|
||||
"sha256": "b1d7b8b9e832d18d69081f63981062800e0f00f0c9aec025b18274510ed76e2a"
|
||||
}
|
||||
},
|
||||
"nextTask": "M12-03D"
|
||||
}
|
||||
136
tools/web/check-asset-catalog-field-inventory.mjs
Normal file
136
tools/web/check-asset-catalog-field-inventory.mjs
Normal file
@@ -0,0 +1,136 @@
|
||||
import assert from "node:assert/strict";
|
||||
import crypto from "node:crypto";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import ts from "../../web/node_modules/typescript/lib/typescript.js";
|
||||
|
||||
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
||||
const manifestPath = path.join(root, "tests/golden/M12-01A/asset-catalog-field-inventory.json");
|
||||
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
|
||||
const sourceByRole = new Map();
|
||||
const sha256 = (bytes) => crypto.createHash("sha256").update(bytes).digest("hex");
|
||||
|
||||
assert.equal(manifest.schemaVersion, 1);
|
||||
assert.equal(manifest.task, "M12-01A");
|
||||
assert.equal(manifest.enablingTask, true);
|
||||
assert.equal(manifest.parityStateChange, false);
|
||||
assert.equal(manifest.blenderVersion, "5.2.0");
|
||||
assert.equal(manifest.nextTask, "M12-01B");
|
||||
|
||||
const expectedRoles = [
|
||||
"DNA_ASSET_STORAGE",
|
||||
"RNA_ASSET_API",
|
||||
"ASSET_BLEND_IO",
|
||||
"CATALOG_MODEL",
|
||||
"CATALOG_PATH",
|
||||
"CATALOG_FILE_API",
|
||||
"CATALOG_FILE_FORMAT",
|
||||
"CURRENT_WEB_V1_SNAPSHOT",
|
||||
];
|
||||
assert.deepEqual(manifest.sources.map((source) => source.role), expectedRoles);
|
||||
for (const source of manifest.sources) {
|
||||
const absolutePath = path.join(root, source.path);
|
||||
const bytes = fs.readFileSync(absolutePath);
|
||||
assert.equal(sha256(bytes), source.sha256, `${source.role} source hash drifted`);
|
||||
sourceByRole.set(source.role, bytes.toString("utf8"));
|
||||
}
|
||||
|
||||
const expectedTagFields = ["next", "prev", "name"];
|
||||
const expectedDNAFields = [
|
||||
"local_type_info",
|
||||
"properties",
|
||||
"catalog_id",
|
||||
"catalog_simple_name",
|
||||
"author",
|
||||
"description",
|
||||
"copyright",
|
||||
"license",
|
||||
"tags",
|
||||
"active_tag",
|
||||
"tot_tags",
|
||||
"flag",
|
||||
"preferred_import_method",
|
||||
"_pad",
|
||||
];
|
||||
assert.deepEqual(manifest.assetTag.fields.map((field) => field.name), expectedTagFields);
|
||||
assert.deepEqual(manifest.assetMetaData.dnaFields.map((field) => field.name), expectedDNAFields);
|
||||
assert.equal(new Set(expectedDNAFields).size, manifest.assetMetaData.dnaFields.length);
|
||||
|
||||
const dna = sourceByRole.get("DNA_ASSET_STORAGE");
|
||||
for (const field of [...manifest.assetTag.fields, ...manifest.assetMetaData.dnaFields]) {
|
||||
assert.ok(dna.includes(field.sourceToken), `DNA token for ${field.name} is missing`);
|
||||
}
|
||||
assert.equal(manifest.assetTag.fields.find((field) => field.name === "name").maximumStorageBytesIncludingNull, 64);
|
||||
assert.equal(manifest.assetMetaData.dnaFields.find((field) => field.name === "properties").semantic, "CUSTOM_METADATA_NO_ID_POINTERS");
|
||||
assert.equal(manifest.assetMetaData.dnaFields.find((field) => field.name === "catalog_simple_name").semantic, "RECOVERY_ONLY_NOT_AUTHORITY");
|
||||
|
||||
const rna = sourceByRole.get("RNA_ASSET_API");
|
||||
const assetDataDefinition = rna.slice(rna.indexOf("static void rna_def_asset_data"), rna.indexOf("static void rna_def_asset_representation"));
|
||||
const sourceRNAProperties = [...assetDataDefinition.matchAll(/RNA_def_property\(srna, "([^"]+)"/g)].map((match) => match[1]);
|
||||
assert.deepEqual(sourceRNAProperties, manifest.assetMetaData.rnaProperties.map((property) => property.identifier));
|
||||
assert.match(rna, /RNA_def_property_string_maxlength\(prop, MAX_NAME\)/);
|
||||
assert.match(rna, /RNA_def_function\(srna, "new", "rna_AssetMetaData_tag_new"\)/);
|
||||
assert.match(rna, /RNA_def_function\(srna, "remove", "rna_AssetMetaData_tag_remove"\)/);
|
||||
|
||||
const blender = process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender");
|
||||
assert.match(execFileSync(blender, ["--version"], { encoding: "utf8" }), /^Blender 5\.2\.0 LTS/m);
|
||||
const python = [
|
||||
"import bpy,json",
|
||||
"props=[{'identifier':p.identifier,'type':p.type,'isReadonly':p.is_readonly,'isRuntime':p.is_runtime} for p in bpy.types.AssetMetaData.bl_rna.properties if p.identifier != 'rna_type']",
|
||||
"print('M12_RNA='+json.dumps(props,separators=(',',':')))",
|
||||
].join(";");
|
||||
const runtimeOutput = execFileSync(blender, ["--factory-startup", "--background", "--python-expr", python], { encoding: "utf8" });
|
||||
const runtimeLine = runtimeOutput.split(/\r?\n/).find((line) => line.startsWith("M12_RNA="));
|
||||
assert.ok(runtimeLine, "Blender AssetMetaData RNA report is missing");
|
||||
assert.deepEqual(JSON.parse(runtimeLine.slice("M12_RNA=".length)), manifest.assetMetaData.rnaProperties);
|
||||
|
||||
const blendIO = sourceByRole.get("ASSET_BLEND_IO");
|
||||
for (const value of ["properties", "author", "description", "copyright", "license", "tags"]) {
|
||||
assert.match(blendIO, new RegExp(`asset_data->${value}`), `${value} is not bound to Blend IO`);
|
||||
}
|
||||
assert.match(blendIO, /asset_data->tags\.count\(\) == asset_data->tot_tags/);
|
||||
|
||||
const catalog = sourceByRole.get("CATALOG_MODEL");
|
||||
for (const field of [...manifest.catalog.semanticFields, ...manifest.catalog.runtimeFlags]) {
|
||||
assert.ok(catalog.includes(field.sourceToken), `catalog token for ${field.name} is missing`);
|
||||
}
|
||||
assert.deepEqual(manifest.catalog.definitionFile.recordFields, ["catalog_id", "path", "simple_name"]);
|
||||
assert.deepEqual(manifest.catalog.definitionFile.writeOrder, ["path", "is_first_loaded", "catalog_id"]);
|
||||
assert.deepEqual(manifest.catalog.identityRules.duplicatePathSelectionOrder, ["is_first_loaded", "catalog_id"]);
|
||||
|
||||
const catalogFile = sourceByRole.get("CATALOG_FILE_FORMAT");
|
||||
assert.match(catalogFile, /SUPPORTED_VERSION = 1/);
|
||||
assert.match(catalogFile, /VERSION_MARKER = "VERSION "/);
|
||||
assert.match(catalogFile, /catalog->catalog_id << ":" << catalog->path << ":" << catalog->simple_name/);
|
||||
const catalogPath = sourceByRole.get("CATALOG_PATH");
|
||||
for (const statement of [
|
||||
"Only slashes are used as path component separators",
|
||||
"Paths are stored as byte sequences, and assumed to be UTF8",
|
||||
"Empty components (caused by double slashes or leading/trailing slashes) are removed",
|
||||
]) assert.ok(catalogPath.includes(statement));
|
||||
|
||||
const webPath = path.join(root, manifest.sources.find((source) => source.role === "CURRENT_WEB_V1_SNAPSHOT").path);
|
||||
const webSource = ts.createSourceFile(webPath, fs.readFileSync(webPath, "utf8"), ts.ScriptTarget.Latest, true);
|
||||
function interfaceFields(name) {
|
||||
const declaration = webSource.statements.find((statement) => ts.isInterfaceDeclaration(statement) && statement.name.text === name);
|
||||
assert.ok(declaration, `missing Web interface ${name}`);
|
||||
return declaration.members.filter(ts.isPropertySignature).map((member) => member.name.getText(webSource));
|
||||
}
|
||||
assert.deepEqual(interfaceFields("AssetCatalogIR"), manifest.currentWebSnapshot.assetCatalogIRFields);
|
||||
assert.deepEqual(interfaceFields("AssetEntryIR"), manifest.currentWebSnapshot.assetEntryIRFields);
|
||||
assert.deepEqual(manifest.currentWebSnapshot.missingMetadata, [
|
||||
"properties",
|
||||
"description",
|
||||
"copyright",
|
||||
"catalog_simple_name",
|
||||
"active_tag",
|
||||
"use_preferred_import_method",
|
||||
"preferred_import_method",
|
||||
]);
|
||||
assert.equal(manifest.currentWebSnapshot.semanticDrift.length, 5);
|
||||
|
||||
process.stdout.write(
|
||||
`asset-catalog-field-inventory-ok dna=${expectedDNAFields.length} rna=${sourceRNAProperties.length} tag=${expectedTagFields.length} catalog=${manifest.catalog.semanticFields.length} gaps=${manifest.currentWebSnapshot.missingMetadata.length} next=${manifest.nextTask}\n`,
|
||||
);
|
||||
67
tools/web/check-asset-catalog-m12-evidence.mjs
Normal file
67
tools/web/check-asset-catalog-m12-evidence.mjs
Normal file
@@ -0,0 +1,67 @@
|
||||
import assert from "node:assert/strict";
|
||||
import crypto from "node:crypto";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
||||
const read = (file) => JSON.parse(fs.readFileSync(path.join(root, file), "utf8"));
|
||||
const sha256File = (file) => crypto.createHash("sha256").update(fs.readFileSync(path.join(root, file))).digest("hex");
|
||||
const record = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
const stableJSON = (value) => {
|
||||
if (Array.isArray(value)) return `[${value.map(stableJSON).join(",")}]`;
|
||||
if (record(value)) return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableJSON(value[key])}`).join(",")}}`;
|
||||
return JSON.stringify(value);
|
||||
};
|
||||
const sha256Value = (value) => crypto.createHash("sha256").update(stableJSON(value)).digest("hex");
|
||||
const checkArtifact = (artifact) => assert.equal(sha256File(artifact.path), artifact.sha256, artifact.path);
|
||||
|
||||
const evidence = read("tests/golden/M12-01/evidence.json");
|
||||
assert.equal(evidence.task, "M12-01I");
|
||||
assert.equal(evidence.status, "READY");
|
||||
assert.equal(evidence.enablingTask, true);
|
||||
assert.equal(evidence.parityStateChange, false);
|
||||
assert.equal(evidence.nextTask, "M12-02A");
|
||||
assert.equal(evidence.subtasks.length, 8);
|
||||
|
||||
const taskNames = evidence.subtasks.map((entry) => entry.task);
|
||||
assert.deepEqual(taskNames, ["M12-01A", "M12-01B", "M12-01C", "M12-01D", "M12-01E", "M12-01F", "M12-01G", "M12-01H"]);
|
||||
evidence.subtasks.forEach((entry, index) => {
|
||||
checkArtifact(entry);
|
||||
const manifest = read(entry.path);
|
||||
assert.equal(manifest.task, entry.task);
|
||||
assert.equal(manifest.enablingTask, true);
|
||||
assert.equal(manifest.parityStateChange, false);
|
||||
assert.equal(manifest.nextTask, index === evidence.subtasks.length - 1 ? "M12-01I" : evidence.subtasks[index + 1].task);
|
||||
if (Array.isArray(manifest.sources)) manifest.sources.forEach(checkArtifact);
|
||||
if (record(manifest.artifacts)) Object.values(manifest.artifacts).forEach(checkArtifact);
|
||||
});
|
||||
|
||||
Object.values(evidence.schema).forEach(checkArtifact);
|
||||
for (const key of ["sourceFixture", "targetFixture", "report", "productionProtocol"]) checkArtifact(evidence.migration[key]);
|
||||
for (const key of ["indexedDB", "restartWorker"]) checkArtifact(evidence.runtime[key]);
|
||||
|
||||
const source = read(evidence.migration.sourceFixture.path);
|
||||
const target = read(evidence.migration.targetFixture.path);
|
||||
const report = read(evidence.migration.report.path);
|
||||
assert.equal(source.schemaVersion, 1);
|
||||
assert.equal(target.schemaVersion, 2);
|
||||
assert.equal(source.revision, evidence.migration.revision);
|
||||
assert.equal(target.revision, evidence.migration.revision);
|
||||
assert.equal(sha256Value(source), evidence.migration.sourceManifestSha256);
|
||||
assert.equal(sha256Value(target), evidence.migration.targetManifestSha256);
|
||||
assert.equal(report.sourceManifestSha256, evidence.migration.sourceManifestSha256);
|
||||
assert.equal(report.targetManifestSha256, evidence.migration.targetManifestSha256);
|
||||
assert.equal(report.sourceRevision, evidence.migration.revision);
|
||||
assert.equal(report.targetRevision, evidence.migration.revision);
|
||||
assert.equal(report.preserved.catalogs, evidence.runtime.catalogCount);
|
||||
assert.equal(report.preserved.assets, evidence.runtime.assetCount);
|
||||
|
||||
const restart = read("tests/golden/M12-01H/manifest.json").restart;
|
||||
assert.deepEqual(restart.catalogOrder, target.catalogs.map((catalog) => catalog.catalogId));
|
||||
assert.deepEqual(restart.assetOrder, target.assets.map((asset) => asset.assetId));
|
||||
assert.equal(restart.manifestSha256, evidence.migration.targetManifestSha256);
|
||||
assert.deepEqual(restart.contexts, evidence.runtime.restartContexts);
|
||||
assert.deepEqual(read("tests/golden/M12-01G/manifest.json").transaction.faultPoints, evidence.runtime.faultPoints);
|
||||
|
||||
process.stdout.write(`asset-catalog-m12-evidence-ok subtasks=${evidence.subtasks.length} catalogs=${evidence.runtime.catalogCount} assets=${evidence.runtime.assetCount} next=${evidence.nextTask}\n`);
|
||||
80
tools/web/check-asset-catalog-v1-fixture.mjs
Normal file
80
tools/web/check-asset-catalog-v1-fixture.mjs
Normal file
@@ -0,0 +1,80 @@
|
||||
import assert from "node:assert/strict";
|
||||
import crypto from "node:crypto";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
||||
const manifestPath = path.join(root, "tests/golden/M12-01B/manifest.json");
|
||||
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
|
||||
const blender = process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender");
|
||||
const sha256 = (file) => crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex");
|
||||
const artifact = (name) => path.join(root, manifest.artifacts[name].path);
|
||||
|
||||
assert.equal(manifest.schemaVersion, 1);
|
||||
assert.equal(manifest.task, "M12-01B");
|
||||
assert.equal(manifest.enablingTask, true);
|
||||
assert.equal(manifest.parityStateChange, false);
|
||||
assert.equal(manifest.nextTask, "M12-01C");
|
||||
assert.match(execFileSync(blender, ["--version"], { encoding: "utf8" }), /^Blender 5\.2\.0 LTS/m);
|
||||
for (const name of ["generator", "exporter", "checker", "fixture", "catalogDefinition", "canonicalReport"]) {
|
||||
assert.equal(sha256(artifact(name)), manifest.artifacts[name].sha256, `${name} hash drifted`);
|
||||
}
|
||||
|
||||
const expected = JSON.parse(fs.readFileSync(artifact("canonicalReport"), "utf8"));
|
||||
assert.equal(expected.schemaVersion, 1);
|
||||
assert.equal(expected.blenderVersion, "5.2.0");
|
||||
assert.equal(expected.catalogDefinition.version, 1);
|
||||
assert.equal(expected.catalogDefinition.records.length, 3);
|
||||
assert.deepEqual(expected.catalogDefinition.records.map((item) => item.path), ["Characters", "Characters/Heroes", "Materials/Metal"]);
|
||||
assert.deepEqual(expected.catalogDefinition.records.map((item) => item.parentPath), [null, "Characters", "Materials"]);
|
||||
assert.equal(expected.assets.length, 3);
|
||||
assert.deepEqual(expected.assets.map((item) => `${item.idType}:${item.name}`), [
|
||||
"MATERIAL:M12 Brushed Metal",
|
||||
"OBJECT:M12 Hero",
|
||||
"WORLD:M12 Uncataloged World",
|
||||
]);
|
||||
const hero = expected.assets.find((item) => item.name === "M12 Hero");
|
||||
assert.deepEqual(hero.tags, ["character", "hero", "rig-ready"]);
|
||||
assert.equal(hero.activeTag, 1);
|
||||
assert.equal(hero.catalogId, "22222222-2222-4222-8222-222222222222");
|
||||
assert.equal(hero.catalogSimpleName, "");
|
||||
assert.equal(hero.usePreferredImportMethod, true);
|
||||
assert.equal(hero.preferredImportMethod, "APPEND");
|
||||
assert.deepEqual(hero.customProperties.map((item) => item.name), ["approved", "dimensions", "rating", "source"]);
|
||||
assert.deepEqual(hero.customProperties.find((item) => item.name === "dimensions").value, [2, 2, 0]);
|
||||
const material = expected.assets.find((item) => item.name === "M12 Brushed Metal");
|
||||
assert.equal(material.author, "");
|
||||
assert.equal(material.license, "");
|
||||
const world = expected.assets.find((item) => item.name === "M12 Uncataloged World");
|
||||
assert.equal(world.catalogId, "00000000-0000-0000-0000-000000000000");
|
||||
|
||||
const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "m12-asset-catalog-v1-"));
|
||||
try {
|
||||
execFileSync(blender, [
|
||||
"--background", "--factory-startup", "--python", artifact("generator"), "--", temporary,
|
||||
], { cwd: root, stdio: "pipe" });
|
||||
const generatedFixture = path.join(temporary, "m12_asset_catalog_v1.blend");
|
||||
const generatedCatalog = path.join(temporary, "blender_assets.cats.txt");
|
||||
const generatedReport = path.join(temporary, "canonical.json");
|
||||
assert.ok(fs.statSync(generatedFixture).size > 0);
|
||||
assert.equal(fs.readFileSync(generatedCatalog, "utf8"), fs.readFileSync(artifact("catalogDefinition"), "utf8"));
|
||||
execFileSync(blender, [
|
||||
"--background", generatedFixture, "--python", artifact("exporter"), "--", generatedCatalog, generatedReport,
|
||||
], { cwd: root, stdio: "pipe" });
|
||||
assert.deepEqual(JSON.parse(fs.readFileSync(generatedReport, "utf8")), expected);
|
||||
|
||||
const reopenedReport = path.join(temporary, "checked-in-canonical.json");
|
||||
execFileSync(blender, [
|
||||
"--background", artifact("fixture"), "--python", artifact("exporter"), "--",
|
||||
artifact("catalogDefinition"), reopenedReport,
|
||||
], { cwd: root, stdio: "pipe" });
|
||||
assert.deepEqual(JSON.parse(fs.readFileSync(reopenedReport, "utf8")), expected);
|
||||
}
|
||||
finally {
|
||||
fs.rmSync(temporary, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
process.stdout.write(`asset-catalog-v1-fixture-ok catalogs=3 assets=3 metadata=complete next=${manifest.nextTask}\n`);
|
||||
48
tools/web/check-asset-preview-display.mjs
Normal file
48
tools/web/check-asset-preview-display.mjs
Normal file
@@ -0,0 +1,48 @@
|
||||
import assert from "node:assert/strict";
|
||||
import crypto from "node:crypto";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
||||
const manifest = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M12-02H/manifest.json"), "utf8"));
|
||||
const report = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M12-02H/desktop-report.json"), "utf8"));
|
||||
const content = path.join(root, "tests/golden/M12-02B/preview.png");
|
||||
const generator = path.join(root, "tools/web/generate-asset-preview-identity.py");
|
||||
const source = path.join(root, "tests/files/web/media/sequencer-frame.png");
|
||||
const blender = process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender");
|
||||
const sha256 = (value) => crypto.createHash("sha256").update(value).digest("hex");
|
||||
const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "asset-preview-display-"));
|
||||
|
||||
try {
|
||||
const regenerated = path.join(temporary, "preview.png");
|
||||
const raw = path.join(temporary, "preview.rgba8");
|
||||
const output = execFileSync(blender, ["--background", "--factory-startup", "--python", generator, "--", source, regenerated], {
|
||||
cwd: root,
|
||||
encoding: "utf8",
|
||||
});
|
||||
assert.match(output, /asset-preview-generated width=8 height=8/);
|
||||
assert.match(execFileSync(blender, ["--version"], { encoding: "utf8" }), /^Blender 5\.2\.0 LTS/m);
|
||||
assert.equal(sha256(fs.readFileSync(regenerated)), report.contentSha256);
|
||||
execFileSync("magick", [regenerated, "-depth", "8", `RGBA:${raw}`], { cwd: root, stdio: "pipe" });
|
||||
const pixels = fs.readFileSync(raw);
|
||||
assert.equal(pixels.byteLength, report.pixelCount * 4);
|
||||
assert.equal(sha256(pixels), report.rgbaSha256);
|
||||
const unique = new Set();
|
||||
for (let offset = 0; offset < pixels.byteLength; offset += 4) {
|
||||
const pixel = Array.from(pixels.subarray(offset, offset + 4));
|
||||
unique.add(pixel.join(","));
|
||||
assert.deepEqual(pixel, report.referencePixel);
|
||||
}
|
||||
assert.equal(unique.size, report.uniquePixelCount);
|
||||
for (const artifact of Object.values(manifest.artifacts)) {
|
||||
assert.equal(sha256(fs.readFileSync(path.join(root, artifact.path))), artifact.sha256, artifact.path);
|
||||
}
|
||||
}
|
||||
finally {
|
||||
fs.rmSync(temporary, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
process.stdout.write(`asset-preview-display-desktop-ok size=${report.width}x${report.height} rgba=${report.rgbaSha256} next=${manifest.nextTask}\n`);
|
||||
50
tools/web/check-asset-preview-identity.mjs
Normal file
50
tools/web/check-asset-preview-identity.mjs
Normal file
@@ -0,0 +1,50 @@
|
||||
import assert from "node:assert/strict";
|
||||
import crypto from "node:crypto";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
||||
const manifest = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M12-02B/manifest.json"), "utf8"));
|
||||
const identity = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M12-02B/identity.json"), "utf8"));
|
||||
const sha256 = (file) => crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex");
|
||||
const blender = process.env.BLENDER_BIN ?? path.join(root, manifest.artifacts.blender.path);
|
||||
const source = path.join(root, manifest.artifacts.source.path);
|
||||
const generator = path.join(root, manifest.artifacts.generator.path);
|
||||
const golden = path.join(root, manifest.artifacts.content.path);
|
||||
|
||||
assert.match(execFileSync(blender, ["--version"], { encoding: "utf8" }), /^Blender 5\.2\.0 LTS/m);
|
||||
assert.equal(sha256(blender), identity.generator.executableSha256);
|
||||
assert.equal(sha256(generator), identity.generator.scriptSha256);
|
||||
assert.equal(sha256(source), identity.source.sha256);
|
||||
assert.equal(fs.statSync(source).size, identity.source.byteLength);
|
||||
assert.equal(sha256(golden), identity.content.sha256);
|
||||
assert.equal(fs.statSync(golden).size, identity.content.byteLength);
|
||||
|
||||
const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "m12-asset-preview-identity-"));
|
||||
try {
|
||||
const first = path.join(temporary, "first.png");
|
||||
const second = path.join(temporary, "second.png");
|
||||
for (const output of [first, second]) {
|
||||
execFileSync(blender, ["--background", "--factory-startup", "--python", generator, "--", source, output], { cwd: root, stdio: "pipe" });
|
||||
assert.equal(sha256(output), identity.content.sha256);
|
||||
assert.equal(fs.statSync(output).size, identity.content.byteLength);
|
||||
}
|
||||
const probe = [
|
||||
"import bpy,json",
|
||||
`a=bpy.data.images.load(${JSON.stringify(source)},check_existing=False)`,
|
||||
`b=bpy.data.images.load(${JSON.stringify(first)},check_existing=False)`,
|
||||
"print('M12_PREVIEW_COMPARE='+json.dumps({'source':list(a.size),'content':list(b.size),'max':max(abs(x-y) for x,y in zip(a.pixels[:],b.pixels[:]))},separators=(',',':')))",
|
||||
].join(";");
|
||||
const output = execFileSync(blender, ["--background", "--factory-startup", "--python-expr", probe], { cwd: root, encoding: "utf8" });
|
||||
const line = output.split(/\r?\n/).find((item) => item.startsWith("M12_PREVIEW_COMPARE="));
|
||||
assert.ok(line);
|
||||
assert.deepEqual(JSON.parse(line.slice("M12_PREVIEW_COMPARE=".length)), { source: [8, 8], content: [8, 8], max: 0 });
|
||||
}
|
||||
finally {
|
||||
fs.rmSync(temporary, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
process.stdout.write(`asset-preview-identity-ok source=${identity.source.sha256} content=${identity.content.sha256} size=${identity.content.width}x${identity.content.height} next=${manifest.nextTask}\n`);
|
||||
98
tools/web/check-asset-preview-inventory.mjs
Normal file
98
tools/web/check-asset-preview-inventory.mjs
Normal file
@@ -0,0 +1,98 @@
|
||||
import assert from "node:assert/strict";
|
||||
import crypto from "node:crypto";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import ts from "../../web/node_modules/typescript/lib/typescript.js";
|
||||
|
||||
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
||||
const inventory = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M12-02A/asset-preview-inventory.json"), "utf8"));
|
||||
const sha256 = (file) => crypto.createHash("sha256").update(fs.readFileSync(path.join(root, file))).digest("hex");
|
||||
|
||||
assert.equal(inventory.task, "M12-02A");
|
||||
assert.equal(inventory.enablingTask, true);
|
||||
assert.equal(inventory.parityStateChange, false);
|
||||
assert.equal(inventory.blenderVersion, "5.2.0");
|
||||
assert.equal(inventory.nextTask, "M12-02B");
|
||||
for (const item of [...inventory.sources, ...inventory.fixtures]) assert.equal(sha256(item.path), item.sha256, item.path);
|
||||
|
||||
const source = (role) => fs.readFileSync(path.join(root, inventory.sources.find((item) => item.role === role).path), "utf8");
|
||||
const dna = source("PREVIEW_DNA");
|
||||
for (const token of ["unsigned int w[2]", "unsigned int h[2]", "short flag[2]", "short changed_timestamp[2]", "unsigned int *rect[2]", "PreviewImageRuntime *runtime"]) {
|
||||
assert.ok(dna.includes(token), `PreviewImage DNA token is missing: ${token}`);
|
||||
}
|
||||
const slots = source("PREVIEW_SLOT_ENUM");
|
||||
assert.match(slots, /ICON_SIZE_ICON = 0/);
|
||||
assert.match(slots, /ICON_SIZE_PREVIEW = 1/);
|
||||
assert.equal(inventory.storage.slotCount, 2);
|
||||
|
||||
const implementation = source("PREVIEW_IMPLEMENTATION");
|
||||
assert.match(implementation, /PreviewImage assumes pre-multiplied alpha/);
|
||||
assert.match(implementation, /writer->write_uint32_array\(prv_copy\.w\[0\] \* prv_copy\.h\[0\]/);
|
||||
assert.match(implementation, /writer->write_uint32_array\(prv_copy\.w\[1\] \* prv_copy\.h\[1\]/);
|
||||
assert.match(implementation, /prv->flag\[i\] &= ~PRV_RENDERING/);
|
||||
assert.equal(inventory.storage.colorSpace, null);
|
||||
assert.ok(!inventory.storage.fields.some((field) => /color/i.test(field.name)));
|
||||
|
||||
const rnaSource = source("PREVIEW_RNA");
|
||||
assert.match(rnaSource, /Image pixels, as bytes \(always 32-bit RGBA\)/);
|
||||
assert.match(rnaSource, /length\[0\] = prv_img->w\[size\] \* prv_img->h\[size\] \* 4/);
|
||||
assert.match(rnaSource, /values\[i\] = data\[i\] \* \(1\.0f \/ 255\.0f\)/);
|
||||
assert.doesNotMatch(rnaSource.slice(rnaSource.indexOf("static void rna_def_image_preview"), rnaSource.indexOf("static void rna_def_image_user")), /color.?space/i);
|
||||
|
||||
const parseInterface = (file, name) => {
|
||||
const absolute = path.join(root, file);
|
||||
const parsed = ts.createSourceFile(absolute, fs.readFileSync(absolute, "utf8"), ts.ScriptTarget.Latest, true);
|
||||
const declaration = parsed.statements.find((statement) => ts.isInterfaceDeclaration(statement) && statement.name.text === name);
|
||||
assert.ok(declaration, `${name} is missing`);
|
||||
return declaration.members.filter(ts.isPropertySignature).map((member) => member.name.getText(parsed));
|
||||
};
|
||||
assert.deepEqual(parseInterface("web/protocol/asset-library-io.ts", "AssetPreviewIR"), inventory.currentWeb.v1Fields);
|
||||
assert.deepEqual(parseInterface("web/protocol/asset-catalog-v2.ts", "AssetCatalogV2PreviewIR"), inventory.currentWeb.v2Fields);
|
||||
|
||||
const blender = process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender");
|
||||
assert.match(execFileSync(blender, ["--version"], { encoding: "utf8" }), /^Blender 5\.2\.0 LTS/m);
|
||||
const rnaProbe = [
|
||||
"import bpy,json",
|
||||
"props=[{'identifier':p.identifier,'type':p.type,'readOnly':p.is_readonly,'arrayLength':p.array_length} for p in bpy.types.ImagePreview.bl_rna.properties if p.identifier != 'rna_type']",
|
||||
"idp=bpy.types.ID.bl_rna.properties['preview']",
|
||||
"print('M12_PREVIEW_RNA='+json.dumps({'idProperty':{'identifier':idp.identifier,'type':idp.type,'readOnly':idp.is_readonly,'nullWhenAbsent':True},'imagePreviewProperties':props},separators=(',',':')))",
|
||||
].join(";");
|
||||
const rnaOutput = execFileSync(blender, ["--factory-startup", "--background", "--python-expr", rnaProbe], { encoding: "utf8" });
|
||||
const rnaLine = rnaOutput.split(/\r?\n/).find((line) => line.startsWith("M12_PREVIEW_RNA="));
|
||||
assert.ok(rnaLine);
|
||||
assert.deepEqual(JSON.parse(rnaLine.slice("M12_PREVIEW_RNA=".length)), {
|
||||
idProperty: inventory.rna.idProperty,
|
||||
imagePreviewProperties: inventory.rna.imagePreviewProperties,
|
||||
});
|
||||
|
||||
const noPreviewScript = [
|
||||
"import bpy,json",
|
||||
"items=[]",
|
||||
"groups=(bpy.data.objects,bpy.data.materials,bpy.data.worlds)",
|
||||
"[items.append({'type':item.bl_rna.identifier,'preview':None if item.preview is None else list(item.preview.image_size)}) for group in groups for item in group if item.asset_data]",
|
||||
"print('M12_NO_PREVIEW='+json.dumps(items,separators=(',',':')))",
|
||||
].join(";");
|
||||
const noPreviewOutput = execFileSync(blender, ["--factory-startup", "--background", path.join(root, inventory.fixtures[1].path), "--python-expr", noPreviewScript], { encoding: "utf8" });
|
||||
const noPreviewLine = noPreviewOutput.split(/\r?\n/).find((line) => line.startsWith("M12_NO_PREVIEW="));
|
||||
assert.ok(noPreviewLine);
|
||||
const noPreview = JSON.parse(noPreviewLine.slice("M12_NO_PREVIEW=".length));
|
||||
assert.deepEqual(noPreview.map((item) => item.type), inventory.runtimeStates.noPreview.assetTypes);
|
||||
assert.deepEqual(noPreview.map((item) => item.preview), inventory.runtimeStates.noPreview.previewValues);
|
||||
|
||||
const png = path.join(root, inventory.fixtures[0].path);
|
||||
const loadedScript = [
|
||||
"import bpy,json,bpy.utils.previews",
|
||||
"collection=bpy.utils.previews.new()",
|
||||
`preview=collection.load('m12-preview',${JSON.stringify(png)},'IMAGE',True)`,
|
||||
"result={'sourceSize':[8,8],'imageSize':list(preview.image_size),'iconSize':list(preview.icon_size),'imagePackedPixelCount':len(preview.image_pixels),'imageFloatComponentCount':len(preview.image_pixels_float),'iconPackedPixelCount':len(preview.icon_pixels),'imageCustom':preview.is_image_custom,'iconCustom':preview.is_icon_custom}",
|
||||
"print('M12_LOADED_PREVIEW='+json.dumps(result,separators=(',',':')))",
|
||||
"bpy.utils.previews.remove(collection)",
|
||||
].join(";");
|
||||
const loadedOutput = execFileSync(blender, ["--factory-startup", "--background", "--python-expr", loadedScript], { encoding: "utf8" });
|
||||
const loadedLine = loadedOutput.split(/\r?\n/).find((line) => line.startsWith("M12_LOADED_PREVIEW="));
|
||||
assert.ok(loadedLine);
|
||||
assert.deepEqual(JSON.parse(loadedLine.slice("M12_LOADED_PREVIEW=".length)), inventory.runtimeStates.loadedPreview);
|
||||
|
||||
process.stdout.write(`asset-preview-inventory-ok slots=${inventory.storage.slotCount} rna=${inventory.rna.imagePreviewProperties.length} absent=${noPreview.length} loaded=${inventory.runtimeStates.loadedPreview.imageSize.join("x")} next=${inventory.nextTask}\n`);
|
||||
100
tools/web/check-library-append-fixture.mjs
Normal file
100
tools/web/check-library-append-fixture.mjs
Normal file
@@ -0,0 +1,100 @@
|
||||
import assert from "node:assert/strict";
|
||||
import crypto from "node:crypto";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
||||
const reportPath = path.join(root, "tests/golden/M12-03C/desktop-append-report.json");
|
||||
const report = JSON.parse(fs.readFileSync(reportPath, "utf8"));
|
||||
const manifest = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M12-03C/manifest.json"), "utf8"));
|
||||
const fixtureRoot = path.join(root, "tests/files/web/m12_library_append_v1");
|
||||
const generator = path.join(root, "tools/web/generate-library-append-fixture.py");
|
||||
const blender = process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender");
|
||||
const sha256 = (value) => crypto.createHash("sha256").update(value).digest("hex");
|
||||
|
||||
assert.equal(manifest.task, "M12-03C");
|
||||
assert.equal(manifest.nextTask, "M12-03D");
|
||||
for (const artifact of Object.values(manifest.artifacts)) {
|
||||
assert.equal(sha256(fs.readFileSync(path.join(root, artifact.path))), artifact.sha256, artifact.path);
|
||||
}
|
||||
assert.equal(report.schemaVersion, 1);
|
||||
assert.equal(report.task, "M12-03C");
|
||||
assert.equal(report.operation, "APPEND");
|
||||
assert.equal(report.blenderVersion, "5.2.0");
|
||||
assert.equal(report.nextTask, "M12-03D");
|
||||
assert.deepEqual(report.selectedRoots, ["Object/M12 Append Object"]);
|
||||
assert.equal(sha256(fs.readFileSync(path.join(fixtureRoot, report.source.file))), report.source.sha256);
|
||||
assert.equal(sha256(fs.readFileSync(path.join(fixtureRoot, report.target.file))), report.target.sha256);
|
||||
assert.deepEqual(report.sourceGraph, report.appendedGraph);
|
||||
assert.deepEqual(report.appendedGraph.edges, [
|
||||
{ from: "Object/M12 Append Object", relation: "OBJECT_DATA", to: "Mesh/M12 Append Mesh" },
|
||||
{ from: "Mesh/M12 Append Mesh", relation: "MATERIAL_SLOT[0]", to: "Material/M12 Append Material" },
|
||||
{ from: "Material/M12 Append Material", relation: "NODE_IMAGE[M12 Append Image Node]", to: "Image/M12 Append Image" },
|
||||
]);
|
||||
assert.deepEqual(Object.keys(report.appendedGraph.ids).sort(), ["IMAGE", "MATERIAL", "MESH", "OBJECT"]);
|
||||
for (const value of Object.values(report.appendedGraph.ids)) {
|
||||
assert.equal(value.library, null);
|
||||
assert.equal(value.isLibraryOverride, false);
|
||||
}
|
||||
assert.deepEqual(report.appendedGraph.geometry, {
|
||||
edges: 4,
|
||||
loops: 4,
|
||||
materialSlots: ["M12 Append Material"],
|
||||
polygons: 1,
|
||||
uvLayers: ["UVMap"],
|
||||
vertices: 4,
|
||||
});
|
||||
assert.deepEqual(report.appendedGraph.image.size, [2, 2]);
|
||||
assert.equal(report.appendedGraph.image.channels, 4);
|
||||
assert.equal(report.appendedGraph.image.colorspace, "sRGB");
|
||||
assert.equal(report.appendedGraph.image.packed, true);
|
||||
assert.match(report.appendedGraph.image.pixelFloat32Sha256, /^[a-f0-9]{64}$/);
|
||||
assert.deepEqual(report.stableMapping.map((item) => [item.owner, item.readOnly]), [
|
||||
["LOCAL_MAIN", false],
|
||||
["LOCAL_MAIN", false],
|
||||
["LOCAL_MAIN", false],
|
||||
["LOCAL_MAIN", false],
|
||||
]);
|
||||
assert.deepEqual(report.stableMapping.map((item) => item.source), [
|
||||
"Object/M12 Append Object",
|
||||
"Mesh/M12 Append Mesh",
|
||||
"Material/M12 Append Material",
|
||||
"Image/M12 Append Image",
|
||||
]);
|
||||
assert.deepEqual(report.stableMapping.map((item) => item.local), report.stableMapping.map((item) => item.source));
|
||||
|
||||
const normalizeContainerHashes = (value) => ({
|
||||
...value,
|
||||
source: { ...value.source, sha256: "<SESSION_BOUND_BLEND_CONTAINER>" },
|
||||
target: { ...value.target, sha256: "<SESSION_BOUND_BLEND_CONTAINER>" },
|
||||
});
|
||||
const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "m12-library-append-"));
|
||||
try {
|
||||
const generatedFixtureRoot = path.join(temporary, "files");
|
||||
const generatedReportPath = path.join(temporary, "report.json");
|
||||
const output = execFileSync(blender, [
|
||||
"--background",
|
||||
"--factory-startup",
|
||||
"--python",
|
||||
generator,
|
||||
"--",
|
||||
generatedFixtureRoot,
|
||||
generatedReportPath,
|
||||
], { cwd: root, encoding: "utf8" });
|
||||
assert.match(output, /library-append-fixture-ok roots=1 mapping=4/);
|
||||
assert.match(output, /next=M12-03D/);
|
||||
const generated = JSON.parse(fs.readFileSync(generatedReportPath, "utf8"));
|
||||
assert.deepEqual(normalizeContainerHashes(generated), normalizeContainerHashes(report));
|
||||
assert.equal(sha256(fs.readFileSync(path.join(generatedFixtureRoot, generated.source.file))), generated.source.sha256);
|
||||
assert.equal(sha256(fs.readFileSync(path.join(generatedFixtureRoot, generated.target.file))), generated.target.sha256);
|
||||
}
|
||||
finally {
|
||||
fs.rmSync(temporary, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
process.stdout.write(
|
||||
`library-append-fixture-check-ok roots=${report.selectedRoots.length} mapping=${report.stableMapping.length} edges=${report.appendedGraph.edges.length} pixels=${report.appendedGraph.image.pixelFloat32Sha256} next=${report.nextTask}\n`,
|
||||
);
|
||||
153
tools/web/check-library-operation-inventory.mjs
Normal file
153
tools/web/check-library-operation-inventory.mjs
Normal file
@@ -0,0 +1,153 @@
|
||||
import assert from "node:assert/strict";
|
||||
import crypto from "node:crypto";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
||||
const inventory = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M12-03A/library-operation-inventory.json"), "utf8"));
|
||||
const sha256 = (bytes) => crypto.createHash("sha256").update(bytes).digest("hex");
|
||||
const sources = new Map();
|
||||
|
||||
assert.equal(inventory.schemaVersion, 1);
|
||||
assert.equal(inventory.task, "M12-03A");
|
||||
assert.equal(inventory.enablingTask, true);
|
||||
assert.equal(inventory.parityStateChange, false);
|
||||
assert.equal(inventory.blenderVersion, "5.2.0");
|
||||
assert.equal(inventory.nextTask, "M12-03B");
|
||||
assert.deepEqual(inventory.sources.map((source) => source.role), [
|
||||
"LINK_APPEND_CONTEXT_API",
|
||||
"LINK_APPEND_CLOSURE",
|
||||
"LINK_APPEND_OPERATORS",
|
||||
"PYTHON_LIBRARY_LOAD",
|
||||
"BLEND_LIBRARY_READER",
|
||||
"OVERRIDE_STORAGE",
|
||||
"OVERRIDE_API",
|
||||
"OVERRIDE_CLOSURE",
|
||||
"CURRENT_WEB_LIBRARY_GATE",
|
||||
]);
|
||||
for (const source of inventory.sources) {
|
||||
const bytes = fs.readFileSync(path.join(root, source.path));
|
||||
assert.equal(sha256(bytes), source.sha256, `${source.role} source hash drifted`);
|
||||
sources.set(source.role, bytes.toString("utf8"));
|
||||
}
|
||||
assert.equal(sha256(fs.readFileSync(path.join(root, inventory.runtimeFixture.path))), inventory.runtimeFixture.sha256);
|
||||
|
||||
assert.equal(inventory.dataBlockSurface.discoverableCollections.length, 36);
|
||||
assert.equal(new Set(inventory.dataBlockSurface.discoverableCollections).size, 36);
|
||||
assert.deepEqual(inventory.dataBlockSurface.onlyAppendableCollections, ["screens", "workspaces"]);
|
||||
assert.deepEqual(inventory.operations.map((operation) => operation.operation), ["APPEND", "LINK", "LIBRARY_OVERRIDE"]);
|
||||
for (const operation of inventory.operations) {
|
||||
assert.ok(operation.dataBlockRoles.length >= 6, `${operation.operation} has an incomplete role inventory`);
|
||||
assert.ok(operation.closure && Object.keys(operation.closure).length >= 5, `${operation.operation} has an incomplete closure inventory`);
|
||||
}
|
||||
|
||||
const context = sources.get("LINK_APPEND_CONTEXT_API");
|
||||
for (const token of [
|
||||
"LINK_APPEND_ACT_KEEP_LINKED",
|
||||
"LINK_APPEND_ACT_REUSE_LOCAL",
|
||||
"LINK_APPEND_ACT_MAKE_LOCAL",
|
||||
"LINK_APPEND_ACT_COPY_LOCAL",
|
||||
"LINK_APPEND_TAG_INDIRECT",
|
||||
"LINK_APPEND_TAG_LIBOVERRIDE_DEPENDENCY",
|
||||
"LINK_APPEND_TAG_LIBOVERRIDE_DEPENDENCY_ONLY",
|
||||
"ID *new_id",
|
||||
"Library *source_library",
|
||||
"ID *liboverride_id",
|
||||
"ID *reusable_local_id",
|
||||
"enum class ProcessStage",
|
||||
]) assert.ok(context.includes(token), `link/append context token is missing: ${token}`);
|
||||
|
||||
const closure = sources.get("LINK_APPEND_CLOSURE");
|
||||
for (const token of [
|
||||
"BKE_library_foreach_ID_link",
|
||||
"IDWALK_CB_EMBEDDED",
|
||||
"IDWALK_CB_EMBEDDED_NOT_OWNING",
|
||||
"IDWALK_CB_INTERNAL",
|
||||
"IDWALK_CB_LOOPBACK",
|
||||
"LINK_APPEND_TAG_LIBOVERRIDE_DEPENDENCY_ONLY",
|
||||
"new_id->lib->runtime->parent",
|
||||
]) assert.ok(closure.includes(token), `dependency closure token is missing: ${token}`);
|
||||
assert.match(closure, /BKE_blendfile_append\(/);
|
||||
assert.match(closure, /BKE_blendfile_override\(/);
|
||||
|
||||
const operators = sources.get("LINK_APPEND_OPERATORS");
|
||||
for (const token of [
|
||||
"BKE_idtype_idcode_is_linkable(idcode)",
|
||||
"BKE_idtype_idcode_is_only_appendable(idcode)",
|
||||
"BLO_LIBLINK_APPEND_RECURSIVE",
|
||||
"BLO_LIBLINK_APPEND_LOCAL_ID_REUSE",
|
||||
"void WM_OT_link",
|
||||
"void WM_OT_append",
|
||||
]) assert.ok(operators.includes(token), `operator inventory token is missing: ${token}`);
|
||||
|
||||
const pythonLoad = sources.get("PYTHON_LIBRARY_LOAD");
|
||||
for (const argument of inventory.operatorSurface.pythonLoadArguments) {
|
||||
assert.ok(pythonLoad.includes(`\"${argument}\"`), `Python load argument is missing: ${argument}`);
|
||||
}
|
||||
for (const token of [
|
||||
"if (!is_library && !BKE_idtype_idcode_is_linkable(code))",
|
||||
"if (!BKE_idtype_idcode_is_linkable(idcode) || (idcode == ID_WS && !do_append))",
|
||||
"create_liboverrides",
|
||||
"BKE_blendfile_link(lapp_context",
|
||||
"BKE_blendfile_append(lapp_context",
|
||||
"BKE_blendfile_override(lapp_context",
|
||||
]) assert.ok(pythonLoad.includes(token), `Python library closure token is missing: ${token}`);
|
||||
|
||||
const reader = sources.get("BLEND_LIBRARY_READER");
|
||||
for (const token of ["BLO_library_link_begin", "BLO_library_link_named_part", "BLO_library_link_end"]) {
|
||||
assert.ok(reader.includes(token), `reader stage is missing: ${token}`);
|
||||
}
|
||||
|
||||
const overrideStorage = sources.get("OVERRIDE_STORAGE");
|
||||
for (const token of [
|
||||
"struct IDOverrideLibraryPropertyOperation",
|
||||
"struct IDOverrideLibraryProperty",
|
||||
"struct IDOverrideLibrary",
|
||||
"ID *reference",
|
||||
"ID *hierarchy_root",
|
||||
"ListBaseT<IDOverrideLibraryProperty> properties",
|
||||
]) assert.ok(overrideStorage.includes(token), `override storage token is missing: ${token}`);
|
||||
const overrideApi = sources.get("OVERRIDE_API");
|
||||
for (const token of [
|
||||
"BKE_lib_override_library_create(",
|
||||
"BKE_lib_override_library_resync(",
|
||||
"BKE_lib_override_library_operations_create(",
|
||||
]) assert.ok(overrideApi.includes(token), `override API token is missing: ${token}`);
|
||||
assert.match(sources.get("OVERRIDE_CLOSURE"), /BKE_library_foreach_ID_link/);
|
||||
|
||||
const web = sources.get("CURRENT_WEB_LIBRARY_GATE");
|
||||
assert.match(web, /gateLibraryMutation\(operation:/);
|
||||
assert.match(web, /LIBRARY_MUTATION_UNAVAILABLE/);
|
||||
for (const gap of inventory.currentWebGap.missing) assert.ok(!inventory.currentWebGap.represented.includes(gap));
|
||||
|
||||
const blender = process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender");
|
||||
assert.match(execFileSync(blender, ["--version"], { encoding: "utf8" }), /^Blender 5\.2\.0 LTS/m);
|
||||
const script = [
|
||||
"import bpy,json",
|
||||
`fixture=${JSON.stringify(path.join(root, inventory.runtimeFixture.path))}`,
|
||||
"result={}",
|
||||
"result['appendProperties']=[p.identifier for p in bpy.ops.wm.append.get_rna_type().properties if p.identifier != 'rna_type']",
|
||||
"result['linkProperties']=[p.identifier for p in bpy.ops.wm.link.get_rna_type().properties if p.identifier != 'rna_type']",
|
||||
"ctx=bpy.data.libraries.load(fixture,link=True)",
|
||||
"source,target=ctx.__enter__()",
|
||||
"result['collections']=sorted([name for name in dir(source) if not name.startswith('_') and name not in {'libraries','version'}])",
|
||||
"result['libraries']=[{'filepath':item.filepath,'isArchive':item.is_archive} for item in source.libraries]",
|
||||
"ctx.__exit__(None,None,None)",
|
||||
"result['overrideRNA']={name:[{'identifier':p.identifier,'type':p.type,'isReadonly':p.is_readonly} for p in getattr(bpy.types,name).bl_rna.properties if p.identifier != 'rna_type'] for name in ['IDOverrideLibrary','IDOverrideLibraryProperty','IDOverrideLibraryPropertyOperation']}",
|
||||
"print('M12_03A='+json.dumps(result,separators=(',',':'),sort_keys=True))",
|
||||
].join(";");
|
||||
const runtimeOutput = execFileSync(blender, ["--background", "--factory-startup", "--python-expr", script], { cwd: root, encoding: "utf8" });
|
||||
const runtimeLine = runtimeOutput.split(/\r?\n/).find((line) => line.startsWith("M12_03A="));
|
||||
assert.ok(runtimeLine, "Blender link/append/override runtime inventory is missing");
|
||||
const runtime = JSON.parse(runtimeLine.slice("M12_03A=".length));
|
||||
assert.deepEqual(runtime.appendProperties, inventory.operatorSurface.appendProperties);
|
||||
assert.deepEqual(runtime.linkProperties, inventory.operatorSurface.linkProperties);
|
||||
assert.deepEqual(runtime.collections, inventory.dataBlockSurface.discoverableCollections);
|
||||
assert.deepEqual(runtime.libraries, inventory.runtimeFixture.nestedLibraries);
|
||||
assert.deepEqual(runtime.overrideRNA, inventory.overrideRNA);
|
||||
|
||||
process.stdout.write(
|
||||
`library-operation-inventory-ok roots=${runtime.collections.length} appendOnly=${inventory.dataBlockSurface.onlyAppendableCollections.length} operations=${inventory.operations.length} overrideFields=${Object.values(runtime.overrideRNA).reduce((sum, values) => sum + values.length, 0)} gaps=${inventory.currentWebGap.missing.length} next=${inventory.nextTask}\n`,
|
||||
);
|
||||
105
tools/web/export-asset-catalog-v1.py
Normal file
105
tools/web/export-asset-catalog-v1.py
Normal file
@@ -0,0 +1,105 @@
|
||||
import json
|
||||
import pathlib
|
||||
import sys
|
||||
|
||||
import bpy
|
||||
|
||||
|
||||
NIL_UUID = "00000000-0000-0000-0000-000000000000"
|
||||
|
||||
|
||||
def catalog_records(path):
|
||||
version = None
|
||||
records = []
|
||||
for line_number, raw_line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1):
|
||||
line = raw_line.strip()
|
||||
if not line or line.startswith("#"):
|
||||
continue
|
||||
if version is None:
|
||||
if not line.startswith("VERSION "):
|
||||
raise RuntimeError(f"catalog definition line {line_number} has no version marker")
|
||||
version = int(line.removeprefix("VERSION "))
|
||||
continue
|
||||
catalog_id, catalog_path, simple_name = line.split(":", 2)
|
||||
records.append({
|
||||
"catalogId": catalog_id,
|
||||
"path": catalog_path,
|
||||
"simpleName": simple_name.strip(),
|
||||
"parentPath": catalog_path.rpartition("/")[0] or None,
|
||||
})
|
||||
return version, records
|
||||
|
||||
|
||||
def custom_property_value(value):
|
||||
if isinstance(value, (bool, int, float, str)):
|
||||
return value
|
||||
if hasattr(value, "to_list"):
|
||||
return value.to_list()
|
||||
if isinstance(value, (list, tuple)):
|
||||
return list(value)
|
||||
raise RuntimeError(f"unsupported asset custom property type {type(value).__name__}")
|
||||
|
||||
|
||||
def asset_record(id_type, asset):
|
||||
metadata = asset.asset_data
|
||||
custom_properties = [
|
||||
{
|
||||
"name": name,
|
||||
"type": type(metadata[name]).__name__.upper(),
|
||||
"value": custom_property_value(metadata[name]),
|
||||
}
|
||||
for name in sorted(metadata.keys())
|
||||
]
|
||||
return {
|
||||
"idType": id_type,
|
||||
"name": asset.name,
|
||||
"catalogId": metadata.catalog_id or NIL_UUID,
|
||||
"catalogSimpleName": metadata.catalog_simple_name,
|
||||
"author": metadata.author,
|
||||
"description": metadata.description,
|
||||
"copyright": metadata.copyright,
|
||||
"license": metadata.license,
|
||||
"tags": [tag.name for tag in metadata.tags],
|
||||
"activeTag": metadata.active_tag,
|
||||
"usePreferredImportMethod": metadata.use_preferred_import_method,
|
||||
"preferredImportMethod": metadata.preferred_import_method,
|
||||
"customProperties": custom_properties,
|
||||
}
|
||||
|
||||
|
||||
def main(catalog_file, output_file):
|
||||
catalog_path = pathlib.Path(catalog_file).resolve()
|
||||
output_path = pathlib.Path(output_file).resolve()
|
||||
version, catalogs = catalog_records(catalog_path)
|
||||
assets = []
|
||||
for id_type, collection in (
|
||||
("MATERIAL", bpy.data.materials),
|
||||
("OBJECT", bpy.data.objects),
|
||||
("WORLD", bpy.data.worlds),
|
||||
):
|
||||
assets.extend(asset_record(id_type, asset) for asset in collection if asset.asset_data is not None)
|
||||
assets.sort(key=lambda item: (item["idType"], item["name"]))
|
||||
report = {
|
||||
"schemaVersion": 1,
|
||||
"task": "M12-01B",
|
||||
"enablingTask": True,
|
||||
"parityStateChange": False,
|
||||
"blenderVersion": ".".join(str(value) for value in bpy.app.version),
|
||||
"catalogDefinition": {
|
||||
"fileName": catalog_path.name,
|
||||
"version": version,
|
||||
"records": catalogs,
|
||||
},
|
||||
"assets": assets,
|
||||
"nextTask": "M12-01C",
|
||||
}
|
||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
output_path.write_text(json.dumps(report, indent=2, ensure_ascii=True) + "\n", encoding="utf-8", newline="\n")
|
||||
print(f"m12-asset-catalog-v1-canonical catalogs={len(catalogs)} assets={len(assets)} output={output_path}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
arguments = sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else []
|
||||
if len(arguments) != 2:
|
||||
raise SystemExit("usage: blender --background FIXTURE --python export-asset-catalog-v1.py -- CATALOG_FILE OUTPUT_JSON")
|
||||
main(*arguments)
|
||||
39
tools/web/generate-asset-catalog-compatibility.mjs
Normal file
39
tools/web/generate-asset-catalog-compatibility.mjs
Normal file
@@ -0,0 +1,39 @@
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import ts from "../../web/node_modules/typescript/lib/typescript.js";
|
||||
|
||||
const [sourceArg, outputArg] = process.argv.slice(2);
|
||||
if (!sourceArg || !outputArg) throw new Error("usage: node generate-asset-catalog-compatibility.mjs V2_JSON OUTPUT_JSON");
|
||||
const root = path.resolve(import.meta.dirname, "../..");
|
||||
const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "asset-catalog-compatibility-generator-"));
|
||||
const sources = ["asset-path.ts", "capability-gates.ts", "asset-library-io.ts", "asset-catalog-v2.ts", "asset-catalog-compatibility.ts"];
|
||||
try {
|
||||
for (const sourceName of sources) {
|
||||
const sourcePath = path.join(root, "web/protocol", sourceName);
|
||||
const result = ts.transpileModule(fs.readFileSync(sourcePath, "utf8"), {
|
||||
compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 },
|
||||
fileName: sourcePath,
|
||||
reportDiagnostics: true,
|
||||
});
|
||||
assert.deepEqual(result.diagnostics, []);
|
||||
fs.writeFileSync(path.join(temporary, sourceName.replace(/\.ts$/, ".mjs")), result.outputText.replaceAll(/from "\.\/([a-z0-9-]+)"/g, 'from "./$1.mjs"'));
|
||||
}
|
||||
const compatibility = await import(pathToFileURL(path.join(temporary, "asset-catalog-compatibility.mjs")));
|
||||
const value = JSON.parse(fs.readFileSync(path.resolve(sourceArg), "utf8"));
|
||||
const output = {
|
||||
read: await compatibility.inspectAssetCatalogForLegacyReader(value, "READ"),
|
||||
catalogWrite: await compatibility.inspectAssetCatalogForLegacyReader(value, "CATALOG_WRITE"),
|
||||
assetWrite: await compatibility.inspectAssetCatalogForLegacyReader(value, "ASSET_WRITE"),
|
||||
save: await compatibility.inspectAssetCatalogForLegacyReader(value, "SAVE"),
|
||||
future: await compatibility.inspectAssetCatalogForLegacyReader({ schemaVersion: 3 }, "READ"),
|
||||
};
|
||||
fs.mkdirSync(path.dirname(path.resolve(outputArg)), { recursive: true });
|
||||
fs.writeFileSync(path.resolve(outputArg), `${JSON.stringify(output, null, 2)}\n`);
|
||||
process.stdout.write("asset-catalog-compatibility-generated read=READ_ONLY writes=3/BLOCKED future=BLOCKED\n");
|
||||
}
|
||||
finally {
|
||||
fs.rmSync(temporary, { recursive: true, force: true });
|
||||
}
|
||||
35
tools/web/generate-asset-catalog-migration.mjs
Normal file
35
tools/web/generate-asset-catalog-migration.mjs
Normal file
@@ -0,0 +1,35 @@
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import ts from "../../web/node_modules/typescript/lib/typescript.js";
|
||||
|
||||
const [sourceArg, targetArg, reportArg] = process.argv.slice(2);
|
||||
if (!sourceArg || !targetArg || !reportArg) throw new Error("usage: node generate-asset-catalog-migration.mjs V1_JSON V2_JSON REPORT_JSON");
|
||||
const root = path.resolve(import.meta.dirname, "../..");
|
||||
const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "asset-catalog-migration-generator-"));
|
||||
const sources = ["asset-path.ts", "capability-gates.ts", "asset-library-io.ts", "asset-catalog-v2.ts", "asset-catalog-migration.ts"];
|
||||
try {
|
||||
for (const sourceName of sources) {
|
||||
const sourcePath = path.join(root, "web/protocol", sourceName);
|
||||
const result = ts.transpileModule(fs.readFileSync(sourcePath, "utf8"), {
|
||||
compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 },
|
||||
fileName: sourcePath,
|
||||
reportDiagnostics: true,
|
||||
});
|
||||
assert.deepEqual(result.diagnostics, []);
|
||||
const output = result.outputText.replaceAll(/from "\.\/([a-z0-9-]+)"/g, 'from "./$1.mjs"');
|
||||
fs.writeFileSync(path.join(temporary, sourceName.replace(/\.ts$/, ".mjs")), output);
|
||||
}
|
||||
const migration = await import(pathToFileURL(path.join(temporary, "asset-catalog-migration.mjs")));
|
||||
const value = JSON.parse(fs.readFileSync(path.resolve(sourceArg), "utf8"));
|
||||
const result = await migration.migrateAssetCatalogV1ToV2(value);
|
||||
fs.mkdirSync(path.dirname(path.resolve(targetArg)), { recursive: true });
|
||||
fs.writeFileSync(path.resolve(targetArg), `${JSON.stringify(result.manifest, null, 2)}\n`);
|
||||
fs.writeFileSync(path.resolve(reportArg), `${JSON.stringify(result.report, null, 2)}\n`);
|
||||
process.stdout.write(`asset-catalog-migration-generated catalogs=${result.manifest.catalogs.length} assets=${result.manifest.assets.length} libraries=${result.manifest.libraries.length}\n`);
|
||||
}
|
||||
finally {
|
||||
fs.rmSync(temporary, { recursive: true, force: true });
|
||||
}
|
||||
111
tools/web/generate-asset-catalog-v1.py
Normal file
111
tools/web/generate-asset-catalog-v1.py
Normal file
@@ -0,0 +1,111 @@
|
||||
import pathlib
|
||||
import sys
|
||||
|
||||
import bpy
|
||||
|
||||
|
||||
CATALOGS = (
|
||||
("11111111-1111-4111-8111-111111111111", "Characters", "Characters"),
|
||||
("22222222-2222-4222-8222-222222222222", "Characters/Heroes", "Heroes"),
|
||||
("33333333-3333-4333-8333-333333333333", "Materials/Metal", "Metal"),
|
||||
)
|
||||
|
||||
|
||||
def set_metadata(asset, *, catalog_id, author, description, copyright_text, license_text,
|
||||
tags, active_tag, preferred_import_method, properties):
|
||||
metadata = asset.asset_data
|
||||
metadata.catalog_id = catalog_id
|
||||
metadata.author = author
|
||||
metadata.description = description
|
||||
metadata.copyright = copyright_text
|
||||
metadata.license = license_text
|
||||
for tag in tags:
|
||||
metadata.tags.new(tag, skip_if_exists=False)
|
||||
metadata.active_tag = active_tag
|
||||
metadata.use_preferred_import_method = preferred_import_method is not None
|
||||
if preferred_import_method is not None:
|
||||
metadata.preferred_import_method = preferred_import_method
|
||||
for name, value in properties.items():
|
||||
metadata[name] = value
|
||||
|
||||
|
||||
def write_catalog_definition(path):
|
||||
lines = [
|
||||
"# This is an Asset Catalog Definition file for Blender.",
|
||||
"#",
|
||||
"# Generated by M12-01B from a locked Blender 5.2 runtime.",
|
||||
"",
|
||||
"VERSION 1",
|
||||
"",
|
||||
]
|
||||
lines.extend(f"{catalog_id}:{catalog_path}:{simple_name}" for catalog_id, catalog_path, simple_name in CATALOGS)
|
||||
path.write_text("\n".join(lines) + "\n", encoding="utf-8", newline="\n")
|
||||
|
||||
|
||||
def main(output_directory):
|
||||
output = pathlib.Path(output_directory).resolve()
|
||||
output.mkdir(parents=True, exist_ok=True)
|
||||
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||
|
||||
mesh = bpy.data.meshes.new("M12 Hero Mesh")
|
||||
mesh.from_pydata(((-1.0, -1.0, 0.0), (1.0, -1.0, 0.0), (0.0, 1.0, 0.0)), (), ((0, 1, 2),))
|
||||
hero = bpy.data.objects.new("M12 Hero", mesh)
|
||||
bpy.context.scene.collection.objects.link(hero)
|
||||
hero.asset_mark()
|
||||
set_metadata(
|
||||
hero,
|
||||
catalog_id=CATALOGS[1][0],
|
||||
author="M12 Artist",
|
||||
description="Desktop catalog v1 object fixture",
|
||||
copyright_text="Copyright 2026 M12 Fixture Authors",
|
||||
license_text="CC0-1.0",
|
||||
tags=("character", "hero", "rig-ready"),
|
||||
active_tag=1,
|
||||
preferred_import_method="APPEND",
|
||||
properties={"approved": True, "rating": 5, "source": "M12-01B"},
|
||||
)
|
||||
|
||||
material = bpy.data.materials.new("M12 Brushed Metal")
|
||||
material.diffuse_color = (0.25, 0.3, 0.35, 1.0)
|
||||
material.asset_mark()
|
||||
set_metadata(
|
||||
material,
|
||||
catalog_id=CATALOGS[2][0],
|
||||
author="",
|
||||
description="",
|
||||
copyright_text="",
|
||||
license_text="",
|
||||
tags=("metal",),
|
||||
active_tag=0,
|
||||
preferred_import_method=None,
|
||||
properties={"roughness": 0.35},
|
||||
)
|
||||
|
||||
world = bpy.data.worlds.new("M12 Uncataloged World")
|
||||
world.color = (0.02, 0.03, 0.04)
|
||||
world.asset_mark()
|
||||
set_metadata(
|
||||
world,
|
||||
catalog_id="",
|
||||
author="M12 Artist",
|
||||
description="Asset without a catalog assignment",
|
||||
copyright_text="",
|
||||
license_text="CC0-1.0",
|
||||
tags=(),
|
||||
active_tag=0,
|
||||
preferred_import_method="LINK",
|
||||
properties={},
|
||||
)
|
||||
|
||||
catalog_path = output / "blender_assets.cats.txt"
|
||||
fixture_path = output / "m12_asset_catalog_v1.blend"
|
||||
write_catalog_definition(catalog_path)
|
||||
bpy.ops.wm.save_as_mainfile(filepath=str(fixture_path), compress=True)
|
||||
print(f"m12-asset-catalog-v1 fixture={fixture_path} catalogs={len(CATALOGS)} assets=3")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
arguments = sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else []
|
||||
if len(arguments) != 1:
|
||||
raise SystemExit("usage: blender --background --factory-startup --python generate-asset-catalog-v1.py -- OUTPUT_DIRECTORY")
|
||||
main(arguments[0])
|
||||
29
tools/web/generate-asset-catalog-v2.mjs
Normal file
29
tools/web/generate-asset-catalog-v2.mjs
Normal file
@@ -0,0 +1,29 @@
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import ts from "../../web/node_modules/typescript/lib/typescript.js";
|
||||
|
||||
const [sourceArg, outputArg] = process.argv.slice(2);
|
||||
if (!sourceArg || !outputArg) throw new Error("usage: node generate-asset-catalog-v2.mjs SOURCE_CANONICAL OUTPUT_JSON");
|
||||
const root = path.resolve(import.meta.dirname, "../..");
|
||||
const sourcePath = path.join(root, "web/protocol/asset-catalog-v2.ts");
|
||||
const temporaryPath = path.resolve(outputArg, "../asset-catalog-v2.generated.mjs");
|
||||
const transpiled = ts.transpileModule(fs.readFileSync(sourcePath, "utf8"), {
|
||||
compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 },
|
||||
fileName: sourcePath,
|
||||
reportDiagnostics: true,
|
||||
});
|
||||
assert.deepEqual(transpiled.diagnostics, []);
|
||||
fs.mkdirSync(path.dirname(outputArg), { recursive: true });
|
||||
fs.writeFileSync(temporaryPath, transpiled.outputText);
|
||||
try {
|
||||
const protocol = await import(`${pathToFileURL(temporaryPath)}?v=${Date.now()}`);
|
||||
const desktop = JSON.parse(fs.readFileSync(path.resolve(sourceArg), "utf8"));
|
||||
const manifest = await protocol.createAssetCatalogManifestV2FromDesktop(desktop, 1);
|
||||
fs.writeFileSync(path.resolve(outputArg), `${JSON.stringify(manifest, null, 2)}\n`);
|
||||
process.stdout.write(`asset-catalog-v2-generated catalogs=${manifest.catalogs.length} assets=${manifest.assets.length}\n`);
|
||||
}
|
||||
finally {
|
||||
fs.rmSync(temporaryPath, { force: true });
|
||||
}
|
||||
33
tools/web/generate-asset-preview-identity.py
Normal file
33
tools/web/generate-asset-preview-identity.py
Normal file
@@ -0,0 +1,33 @@
|
||||
import pathlib
|
||||
import sys
|
||||
|
||||
import bpy
|
||||
|
||||
|
||||
def main() -> None:
|
||||
if "--" not in sys.argv or len(sys.argv[sys.argv.index("--") + 1 :]) != 2:
|
||||
raise SystemExit("usage: blender --background --factory-startup --python generate-asset-preview-identity.py -- INPUT OUTPUT")
|
||||
source_arg, output_arg = sys.argv[sys.argv.index("--") + 1 :]
|
||||
source = pathlib.Path(source_arg).resolve()
|
||||
output = pathlib.Path(output_arg).resolve()
|
||||
output.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
image = bpy.data.images.load(str(source), check_existing=False)
|
||||
width, height = image.size[:]
|
||||
scene = bpy.context.scene
|
||||
scene.render.image_settings.file_format = "PNG"
|
||||
scene.render.image_settings.color_mode = "RGBA"
|
||||
scene.render.image_settings.color_depth = "8"
|
||||
scene.render.image_settings.compression = 0
|
||||
scene.display_settings.display_device = "sRGB"
|
||||
scene.view_settings.view_transform = "Standard"
|
||||
scene.view_settings.look = "None"
|
||||
scene.view_settings.exposure = 0
|
||||
scene.view_settings.gamma = 1
|
||||
image.save_render(str(output), scene=scene)
|
||||
if not output.is_file() or output.stat().st_size == 0:
|
||||
raise RuntimeError("asset preview output was not written")
|
||||
print(f"asset-preview-generated width={width} height={height} output={output}")
|
||||
|
||||
|
||||
main()
|
||||
181
tools/web/generate-library-append-fixture.py
Normal file
181
tools/web/generate-library-append-fixture.py
Normal file
@@ -0,0 +1,181 @@
|
||||
import hashlib
|
||||
import json
|
||||
import pathlib
|
||||
import struct
|
||||
import sys
|
||||
|
||||
import bpy
|
||||
|
||||
|
||||
ROOT_OBJECT = "M12 Append Object"
|
||||
MESH_NAME = "M12 Append Mesh"
|
||||
MATERIAL_NAME = "M12 Append Material"
|
||||
IMAGE_NAME = "M12 Append Image"
|
||||
IMAGE_NODE_NAME = "M12 Append Image Node"
|
||||
|
||||
|
||||
def sha256_file(path: pathlib.Path) -> str:
|
||||
return hashlib.sha256(path.read_bytes()).hexdigest()
|
||||
|
||||
|
||||
def pixel_sha256(image: bpy.types.Image) -> str:
|
||||
values = list(image.pixels)
|
||||
return hashlib.sha256(struct.pack(f"<{len(values)}f", *values)).hexdigest()
|
||||
|
||||
|
||||
def reset() -> None:
|
||||
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||
|
||||
|
||||
def create_source(path: pathlib.Path) -> None:
|
||||
reset()
|
||||
image = bpy.data.images.new(IMAGE_NAME, width=2, height=2, alpha=True, float_buffer=False)
|
||||
image.colorspace_settings.name = "sRGB"
|
||||
image.pixels = [
|
||||
1.0, 0.0, 0.0, 1.0,
|
||||
0.0, 1.0, 0.0, 1.0,
|
||||
0.0, 0.0, 1.0, 1.0,
|
||||
1.0, 1.0, 1.0, 0.5,
|
||||
]
|
||||
image.pack()
|
||||
|
||||
material = bpy.data.materials.new(MATERIAL_NAME)
|
||||
material.use_nodes = True
|
||||
node_tree = material.node_tree
|
||||
principled = node_tree.nodes.get("Principled BSDF")
|
||||
image_node = node_tree.nodes.new("ShaderNodeTexImage")
|
||||
image_node.name = IMAGE_NODE_NAME
|
||||
image_node.label = IMAGE_NODE_NAME
|
||||
image_node.image = image
|
||||
image_node.interpolation = "Closest"
|
||||
image_node.extension = "REPEAT"
|
||||
node_tree.links.new(image_node.outputs["Color"], principled.inputs["Base Color"])
|
||||
|
||||
mesh = bpy.data.meshes.new(MESH_NAME)
|
||||
mesh.from_pydata(
|
||||
[(-1.0, -1.0, 0.0), (1.0, -1.0, 0.0), (1.0, 1.0, 0.0), (-1.0, 1.0, 0.0)],
|
||||
[],
|
||||
[(0, 1, 2, 3)],
|
||||
)
|
||||
mesh.materials.append(material)
|
||||
uv_layer = mesh.uv_layers.new(name="UVMap")
|
||||
for loop, uv in zip(uv_layer.data, [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)]):
|
||||
loop.uv = uv
|
||||
mesh.update()
|
||||
|
||||
obj = bpy.data.objects.new(ROOT_OBJECT, mesh)
|
||||
obj["m12_source_marker"] = "M12-03C"
|
||||
bpy.context.scene.collection.objects.link(obj)
|
||||
bpy.ops.wm.save_as_mainfile(filepath=str(path), check_existing=False)
|
||||
|
||||
|
||||
def data_block(id_type: str, value: object) -> dict:
|
||||
return {
|
||||
"idType": id_type,
|
||||
"name": value.name,
|
||||
"nameFull": value.name_full,
|
||||
"library": None if value.library is None else value.library.filepath,
|
||||
"isLibraryOverride": value.override_library is not None,
|
||||
}
|
||||
|
||||
|
||||
def inspect_local_graph() -> dict:
|
||||
obj = bpy.data.objects[ROOT_OBJECT]
|
||||
mesh = obj.data
|
||||
material = mesh.materials[0]
|
||||
image_node = material.node_tree.nodes[IMAGE_NODE_NAME]
|
||||
image = image_node.image
|
||||
ids = {
|
||||
"OBJECT": data_block("OBJECT", obj),
|
||||
"MESH": data_block("MESH", mesh),
|
||||
"MATERIAL": data_block("MATERIAL", material),
|
||||
"IMAGE": data_block("IMAGE", image),
|
||||
}
|
||||
return {
|
||||
"root": ids["OBJECT"],
|
||||
"ids": ids,
|
||||
"edges": [
|
||||
{"from": "Object/M12 Append Object", "relation": "OBJECT_DATA", "to": "Mesh/M12 Append Mesh"},
|
||||
{"from": "Mesh/M12 Append Mesh", "relation": "MATERIAL_SLOT[0]", "to": "Material/M12 Append Material"},
|
||||
{"from": "Material/M12 Append Material", "relation": f"NODE_IMAGE[{IMAGE_NODE_NAME}]", "to": "Image/M12 Append Image"},
|
||||
],
|
||||
"geometry": {
|
||||
"vertices": len(mesh.vertices),
|
||||
"edges": len(mesh.edges),
|
||||
"polygons": len(mesh.polygons),
|
||||
"loops": len(mesh.loops),
|
||||
"uvLayers": [layer.name for layer in mesh.uv_layers],
|
||||
"materialSlots": [item.name for item in mesh.materials],
|
||||
},
|
||||
"image": {
|
||||
"size": list(image.size),
|
||||
"channels": image.channels,
|
||||
"colorspace": image.colorspace_settings.name,
|
||||
"packed": image.packed_file is not None,
|
||||
"pixelFloat32Sha256": pixel_sha256(image),
|
||||
},
|
||||
"sourceMarker": obj["m12_source_marker"],
|
||||
}
|
||||
|
||||
|
||||
def append_object(source: pathlib.Path, target: pathlib.Path) -> dict:
|
||||
reset()
|
||||
with bpy.data.libraries.load(str(source), link=False) as (data_from, data_to):
|
||||
if ROOT_OBJECT not in data_from.objects:
|
||||
raise RuntimeError("source root object is missing")
|
||||
data_to.objects = [ROOT_OBJECT]
|
||||
if len(data_to.objects) != 1 or data_to.objects[0] is None:
|
||||
raise RuntimeError("desktop append did not return one object")
|
||||
bpy.context.scene.collection.objects.link(data_to.objects[0])
|
||||
before_save = inspect_local_graph()
|
||||
if any(item["library"] is not None or item["isLibraryOverride"] for item in before_save["ids"].values()):
|
||||
raise RuntimeError("appended dependency closure is not fully local")
|
||||
bpy.ops.wm.save_as_mainfile(filepath=str(target), check_existing=False)
|
||||
bpy.ops.wm.open_mainfile(filepath=str(target), load_ui=False)
|
||||
reopened = inspect_local_graph()
|
||||
if reopened != before_save:
|
||||
raise RuntimeError("appended dependency mapping drifted after save/reopen")
|
||||
return reopened
|
||||
|
||||
|
||||
def main() -> None:
|
||||
if "--" not in sys.argv or len(sys.argv[sys.argv.index("--") + 1 :]) != 2:
|
||||
raise SystemExit("usage: blender --background --factory-startup --python generate-library-append-fixture.py -- OUTPUT_DIR REPORT")
|
||||
output_arg, report_arg = sys.argv[sys.argv.index("--") + 1 :]
|
||||
output_dir = pathlib.Path(output_arg).resolve()
|
||||
report_path = pathlib.Path(report_arg).resolve()
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
report_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
source = output_dir / "m12_append_source.blend"
|
||||
target = output_dir / "m12_append_target.blend"
|
||||
|
||||
create_source(source)
|
||||
source_graph = inspect_local_graph()
|
||||
appended_graph = append_object(source, target)
|
||||
report = {
|
||||
"schemaVersion": 1,
|
||||
"task": "M12-03C",
|
||||
"operation": "APPEND",
|
||||
"blenderVersion": "5.2.0",
|
||||
"source": {"file": source.name, "sha256": sha256_file(source)},
|
||||
"target": {"file": target.name, "sha256": sha256_file(target)},
|
||||
"selectedRoots": ["Object/M12 Append Object"],
|
||||
"sourceGraph": source_graph,
|
||||
"appendedGraph": appended_graph,
|
||||
"stableMapping": [
|
||||
{"source": "Object/M12 Append Object", "local": "Object/M12 Append Object", "owner": "LOCAL_MAIN", "readOnly": False},
|
||||
{"source": "Mesh/M12 Append Mesh", "local": "Mesh/M12 Append Mesh", "owner": "LOCAL_MAIN", "readOnly": False},
|
||||
{"source": "Material/M12 Append Material", "local": "Material/M12 Append Material", "owner": "LOCAL_MAIN", "readOnly": False},
|
||||
{"source": "Image/M12 Append Image", "local": "Image/M12 Append Image", "owner": "LOCAL_MAIN", "readOnly": False},
|
||||
],
|
||||
"nextTask": "M12-03D",
|
||||
}
|
||||
report_path.write_text(json.dumps(report, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
||||
print(
|
||||
f"library-append-fixture-ok roots={len(report['selectedRoots'])} "
|
||||
f"mapping={len(report['stableMapping'])} source={report['source']['sha256']} "
|
||||
f"target={report['target']['sha256']} next={report['nextTask']}"
|
||||
)
|
||||
|
||||
|
||||
main()
|
||||
2
web/app/public/vendor/blender/web_engine.js
vendored
2
web/app/public/vendor/blender/web_engine.js
vendored
File diff suppressed because one or more lines are too long
BIN
web/app/public/vendor/blender/web_engine.wasm
vendored
BIN
web/app/public/vendor/blender/web_engine.wasm
vendored
Binary file not shown.
87
web/app/src/assets/AssetPreviewDisplay.ts
Normal file
87
web/app/src/assets/AssetPreviewDisplay.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import { planAssetPreviewDecode } from "../../../protocol/asset-preview-decode";
|
||||
import type { AssetPreviewIdentityIR } from "../../../protocol/asset-preview";
|
||||
|
||||
export type AssetPreviewDisplayBackend = "MAIN_THREAD_CANVAS_2D" | "OFFSCREEN_CANVAS_2D";
|
||||
|
||||
export interface AssetPreviewDisplayReceiptIR {
|
||||
schemaVersion: 1;
|
||||
status: "READY";
|
||||
backend: AssetPreviewDisplayBackend;
|
||||
identitySha256: string;
|
||||
contentSha256: string;
|
||||
width: number;
|
||||
height: number;
|
||||
pixelByteLength: number;
|
||||
pixelSha256: string;
|
||||
nonTransparentPixels: number;
|
||||
bitmapClosed: true;
|
||||
}
|
||||
|
||||
export interface AssetPreviewDisplayResultIR {
|
||||
receipt: AssetPreviewDisplayReceiptIR;
|
||||
pixels: Uint8Array;
|
||||
}
|
||||
|
||||
type PreviewCanvas = HTMLCanvasElement | OffscreenCanvas;
|
||||
|
||||
async function sha256Bytes(value: Uint8Array): Promise<string> {
|
||||
const digest = await crypto.subtle.digest("SHA-256", Uint8Array.from(value).buffer);
|
||||
return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("");
|
||||
}
|
||||
|
||||
export async function displayAssetPreview(
|
||||
canvas: PreviewCanvas,
|
||||
backend: AssetPreviewDisplayBackend,
|
||||
identityValue: unknown,
|
||||
encodedBytes: ArrayBuffer,
|
||||
): Promise<AssetPreviewDisplayResultIR> {
|
||||
const plan = await planAssetPreviewDecode(identityValue, encodedBytes);
|
||||
if (typeof createImageBitmap !== "function") throw new Error("CAPABILITY_MISSING: createImageBitmap is unavailable");
|
||||
const identity = identityValue as AssetPreviewIdentityIR;
|
||||
const bitmap = await createImageBitmap(new Blob([encodedBytes], { type: plan.mimeType }), {
|
||||
colorSpaceConversion: "none",
|
||||
premultiplyAlpha: "none",
|
||||
imageOrientation: "none",
|
||||
});
|
||||
try {
|
||||
if (bitmap.width !== plan.width || bitmap.height !== plan.height) {
|
||||
throw new Error("ASSET_PREVIEW_IDENTITY_MISMATCH: decoded preview dimensions changed");
|
||||
}
|
||||
canvas.width = plan.width;
|
||||
canvas.height = plan.height;
|
||||
const context = canvas.getContext("2d", { alpha: true, willReadFrequently: true }) as
|
||||
| CanvasRenderingContext2D
|
||||
| OffscreenCanvasRenderingContext2D
|
||||
| null;
|
||||
if (!context) throw new Error("CAPABILITY_MISSING: preview Canvas2D context is unavailable");
|
||||
context.clearRect(0, 0, plan.width, plan.height);
|
||||
context.imageSmoothingEnabled = false;
|
||||
context.globalCompositeOperation = "copy";
|
||||
context.drawImage(bitmap, 0, 0, plan.width, plan.height);
|
||||
const pixels = new Uint8Array(context.getImageData(0, 0, plan.width, plan.height).data);
|
||||
let nonTransparentPixels = 0;
|
||||
for (let offset = 3; offset < pixels.byteLength; offset += 4) if (pixels[offset] !== 0) nonTransparentPixels++;
|
||||
const receipt: AssetPreviewDisplayReceiptIR = {
|
||||
schemaVersion: 1,
|
||||
status: "READY",
|
||||
backend,
|
||||
identitySha256: identity.identitySha256,
|
||||
contentSha256: identity.content.sha256,
|
||||
width: plan.width,
|
||||
height: plan.height,
|
||||
pixelByteLength: pixels.byteLength,
|
||||
pixelSha256: await sha256Bytes(pixels),
|
||||
nonTransparentPixels,
|
||||
bitmapClosed: true,
|
||||
};
|
||||
if (typeof HTMLCanvasElement !== "undefined" && canvas instanceof HTMLCanvasElement) {
|
||||
canvas.dataset.assetPreviewStatus = "ready";
|
||||
canvas.dataset.assetPreviewBackend = backend;
|
||||
canvas.dataset.assetPreviewPixelSha256 = receipt.pixelSha256;
|
||||
}
|
||||
return { receipt, pixels };
|
||||
}
|
||||
finally {
|
||||
bitmap.close();
|
||||
}
|
||||
}
|
||||
@@ -30,6 +30,7 @@ import type {
|
||||
PaintStrokeSessionReceiptIR,
|
||||
} from "../../../protocol/paint-stroke-session";
|
||||
import type { PaintPBVHCapabilityRequest } from "../../../protocol/paint-pbvh-capability";
|
||||
import type { LibraryMainAppendReceiptIR, LibraryMainAppendRequestIR } from "../../../protocol/library-main-append";
|
||||
|
||||
interface PendingRequest {
|
||||
resolve: (result: WebEngineResult) => void;
|
||||
@@ -122,6 +123,28 @@ export class WebEngineClient {
|
||||
};
|
||||
}
|
||||
|
||||
async appendLibraryObject(
|
||||
source: ArrayBuffer,
|
||||
request: LibraryMainAppendRequestIR,
|
||||
): Promise<BlendOpenResult & { delta: SceneDelta; receipt: LibraryMainAppendReceiptIR }> {
|
||||
const result = await this.request({ type: "appendLibraryObject", request, source }, [source]);
|
||||
if (!result.snapshot || !result.delta || !result.libraryAppend) {
|
||||
throw this.report("ASSET_MANIFEST_INVALID", "WebEngine did not return the committed library append", true);
|
||||
}
|
||||
this.geometryBuffers = result.geometryDelta
|
||||
? applyMeshGeometryDelta(this.geometryBuffers, result.geometryDelta)
|
||||
: result.geometryBuffers ?? this.geometryBuffers;
|
||||
this.nonMeshGeometryBuffers = result.nonMeshGeometryBuffers ?? this.nonMeshGeometryBuffers;
|
||||
return {
|
||||
status: result.status,
|
||||
snapshot: result.snapshot,
|
||||
geometryBuffers: [...this.geometryBuffers],
|
||||
nonMeshGeometryBuffers: [...this.nonMeshGeometryBuffers],
|
||||
delta: result.delta,
|
||||
receipt: result.libraryAppend,
|
||||
};
|
||||
}
|
||||
|
||||
async beginPaintStroke(session: PaintStrokeSessionBeginIR): Promise<PaintStrokeSessionReceiptIR> {
|
||||
const result = await this.request({ type: "beginPaintStroke", session });
|
||||
if (!result.paintStrokeSession) throw this.report("PAINT_SCHEMA_INVALID", "WebEngine did not open the paint pointer session", true);
|
||||
|
||||
@@ -42,7 +42,13 @@ async function sha256(data: ArrayBuffer): Promise<string> {
|
||||
return Array.from(new Uint8Array(result), (value) => value.toString(16).padStart(2, "0")).join("");
|
||||
}
|
||||
|
||||
function waitForLoadedFrame(video: HTMLVideoElement, timeoutMs: number): Promise<void> {
|
||||
function cancelled(signal: AbortSignal | undefined): void {
|
||||
if (signal?.aborted) {
|
||||
throw new SequencerMediaCacheValidationError("SEQUENCER_CANCELLED", "Movie proxy generation was cancelled");
|
||||
}
|
||||
}
|
||||
|
||||
function waitForLoadedFrame(video: HTMLVideoElement, timeoutMs: number, signal?: AbortSignal): Promise<void> {
|
||||
if (video.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA) return Promise.resolve();
|
||||
return new Promise((resolve, reject) => {
|
||||
const timeout = window.setTimeout(() => finish(new Error("timeout")), timeoutMs);
|
||||
@@ -50,12 +56,16 @@ function waitForLoadedFrame(video: HTMLVideoElement, timeoutMs: number): Promise
|
||||
window.clearTimeout(timeout);
|
||||
video.removeEventListener("loadeddata", ready);
|
||||
video.removeEventListener("error", failed);
|
||||
signal?.removeEventListener("abort", aborted);
|
||||
if (error) reject(error); else resolve();
|
||||
};
|
||||
const ready = (): void => finish();
|
||||
const failed = (): void => finish(new Error("decode"));
|
||||
const aborted = (): void => finish(new SequencerMediaCacheValidationError("SEQUENCER_CANCELLED", "Movie proxy generation was cancelled"));
|
||||
video.addEventListener("loadeddata", ready, { once: true });
|
||||
video.addEventListener("error", failed, { once: true });
|
||||
signal?.addEventListener("abort", aborted, { once: true });
|
||||
if (signal?.aborted) aborted();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -64,7 +74,9 @@ export async function generateInitialSequencerMovieProxyFrame(
|
||||
capabilityValue: unknown,
|
||||
profileValue: unknown,
|
||||
sourceData: ArrayBuffer,
|
||||
signal?: AbortSignal,
|
||||
): Promise<SequencerGeneratedProxyFrameIR> {
|
||||
cancelled(signal);
|
||||
const source = parseSequencerCodecProbeRequest(sourceValue);
|
||||
const capability = parseSequencerCodecProbeResult(capabilityValue);
|
||||
const profile = parseSequencerMediaProxyProfile(profileValue);
|
||||
@@ -75,6 +87,7 @@ export async function generateInitialSequencerMovieProxyFrame(
|
||||
if (!(sourceData instanceof ArrayBuffer) || sourceData.byteLength !== source.byteLength || await sha256(sourceData) !== source.sourceSha256) {
|
||||
throw new SequencerMediaCacheValidationError("SEQUENCER_CACHE_SOURCE_MISMATCH", "Movie proxy source bytes failed identity verification");
|
||||
}
|
||||
cancelled(signal);
|
||||
if (typeof document === "undefined" || typeof HTMLMediaElement === "undefined") {
|
||||
throw new SequencerMediaCacheValidationError("SEQUENCER_CODEC_UNSUPPORTED", "HTML media proxy generation is unavailable");
|
||||
}
|
||||
@@ -87,7 +100,8 @@ export async function generateInitialSequencerMovieProxyFrame(
|
||||
try {
|
||||
video.src = url;
|
||||
video.load();
|
||||
await waitForLoadedFrame(video, 10_000);
|
||||
await waitForLoadedFrame(video, 10_000, signal);
|
||||
cancelled(signal);
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = profile.width;
|
||||
canvas.height = profile.height;
|
||||
@@ -97,6 +111,7 @@ export async function generateInitialSequencerMovieProxyFrame(
|
||||
}
|
||||
context.clearRect(0, 0, profile.width, profile.height);
|
||||
context.drawImage(video, 0, 0, profile.width, profile.height);
|
||||
cancelled(signal);
|
||||
const pixels = context.getImageData(0, 0, profile.width, profile.height).data;
|
||||
const data = pixels.buffer.slice(pixels.byteOffset, pixels.byteOffset + pixels.byteLength);
|
||||
return {
|
||||
|
||||
236
web/app/src/storage/asset-catalog-indexeddb.ts
Normal file
236
web/app/src/storage/asset-catalog-indexeddb.ts
Normal file
@@ -0,0 +1,236 @@
|
||||
import {
|
||||
migrateAssetCatalogV1ToV2,
|
||||
type AssetCatalogMigrationReportIR,
|
||||
} from "../../../protocol/asset-catalog-migration";
|
||||
import {
|
||||
parseAssetCatalogManifestV2,
|
||||
type AssetCatalogManifestV2IR,
|
||||
} from "../../../protocol/asset-catalog-v2";
|
||||
import type { ErrorCode } from "../../../protocol/error";
|
||||
|
||||
export const ASSET_CATALOG_INDEX_V1_ID = "asset-catalog:index:v1" as const;
|
||||
export const ASSET_CATALOG_INDEX_V2_ID = "asset-catalog:index:v2" as const;
|
||||
export const ASSET_CATALOG_MIGRATION_ID = "asset-catalog:migration:v1-to-v2" as const;
|
||||
|
||||
export type AssetCatalogMigrationFault = "AFTER_TARGET_PUT" | "AFTER_SOURCE_DELETE";
|
||||
|
||||
export interface AssetCatalogIndexRowV1 {
|
||||
id: typeof ASSET_CATALOG_INDEX_V1_ID;
|
||||
value: unknown;
|
||||
}
|
||||
|
||||
export interface AssetCatalogIndexRowV2 {
|
||||
id: typeof ASSET_CATALOG_INDEX_V2_ID;
|
||||
value: AssetCatalogManifestV2IR;
|
||||
}
|
||||
|
||||
export interface AssetCatalogIndexedDBMigrationReceiptIR {
|
||||
schemaVersion: 1;
|
||||
id: typeof ASSET_CATALOG_MIGRATION_ID;
|
||||
task: "M12-01G";
|
||||
status: "MIGRATED";
|
||||
sourceRevision: number;
|
||||
targetRevision: number;
|
||||
sourceManifestSha256: string;
|
||||
targetManifestSha256: string;
|
||||
}
|
||||
|
||||
export interface AssetCatalogIndexedDBMigrationResultIR {
|
||||
status: "MIGRATED" | "ALREADY_MIGRATED";
|
||||
manifest: AssetCatalogManifestV2IR;
|
||||
receipt: AssetCatalogIndexedDBMigrationReceiptIR;
|
||||
migrationReport: AssetCatalogMigrationReportIR | null;
|
||||
}
|
||||
|
||||
export interface AssetCatalogIndexedDBSnapshotIR {
|
||||
schemaVersion: 1;
|
||||
revision: number;
|
||||
manifestSha256: string;
|
||||
catalogOrder: Array<{ catalogId: string; path: string }>;
|
||||
assetOrder: string[];
|
||||
assetIdentities: Array<{
|
||||
assetId: string;
|
||||
assetLibraryIdentifier: string | null;
|
||||
relativeAssetIdentifier: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export class AssetCatalogIndexedDBMigrationError extends Error {
|
||||
readonly code: ErrorCode;
|
||||
|
||||
constructor(code: ErrorCode, message: string, options?: ErrorOptions) {
|
||||
super(`${code}: ${message}`, options);
|
||||
this.name = "AssetCatalogIndexedDBMigrationError";
|
||||
this.code = code;
|
||||
}
|
||||
}
|
||||
|
||||
interface StoredRow {
|
||||
id: string;
|
||||
value?: unknown;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
function record(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function stableJSON(value: unknown): string {
|
||||
if (Array.isArray(value)) return `[${value.map(stableJSON).join(",")}]`;
|
||||
if (record(value)) {
|
||||
return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableJSON(value[key])}`).join(",")}}`;
|
||||
}
|
||||
return JSON.stringify(value);
|
||||
}
|
||||
|
||||
async function sha256(value: unknown): Promise<string> {
|
||||
const bytes = new TextEncoder().encode(stableJSON(value));
|
||||
const digest = await crypto.subtle.digest("SHA-256", bytes);
|
||||
return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("");
|
||||
}
|
||||
|
||||
function transactionComplete(transaction: IDBTransaction, failure: () => Error | undefined): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
transaction.oncomplete = () => resolve();
|
||||
transaction.onerror = () => reject(failure() ?? transaction.error ?? new AssetCatalogIndexedDBMigrationError("STORAGE_TRANSACTION", "Catalog migration transaction failed"));
|
||||
transaction.onabort = () => reject(failure() ?? transaction.error ?? new AssetCatalogIndexedDBMigrationError("STORAGE_TRANSACTION", "Catalog migration transaction aborted"));
|
||||
});
|
||||
}
|
||||
|
||||
function readRow(store: IDBObjectStore, id: string): Promise<StoredRow | undefined> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = store.get(id);
|
||||
request.onsuccess = () => resolve(request.result as StoredRow | undefined);
|
||||
request.onerror = () => reject(request.error ?? new AssetCatalogIndexedDBMigrationError("STORAGE_TRANSACTION", `Catalog index lookup failed for ${id}`));
|
||||
});
|
||||
}
|
||||
|
||||
function assertStores(database: IDBDatabase, settingStore: string, migrationStore: string): void {
|
||||
if (!database.objectStoreNames.contains(settingStore) || !database.objectStoreNames.contains(migrationStore)) {
|
||||
throw new AssetCatalogIndexedDBMigrationError("STORAGE_TRANSACTION", "Catalog migration stores are unavailable");
|
||||
}
|
||||
}
|
||||
|
||||
function parseReceipt(value: unknown): AssetCatalogIndexedDBMigrationReceiptIR {
|
||||
const digest = /^[a-f0-9]{64}$/;
|
||||
if (!record(value) || value.schemaVersion !== 1 || value.id !== ASSET_CATALOG_MIGRATION_ID || value.task !== "M12-01G" || value.status !== "MIGRATED" ||
|
||||
!Number.isSafeInteger(value.sourceRevision) || Number(value.sourceRevision) < 0 ||
|
||||
!Number.isSafeInteger(value.targetRevision) || Number(value.targetRevision) < 0 ||
|
||||
typeof value.sourceManifestSha256 !== "string" || !digest.test(value.sourceManifestSha256) ||
|
||||
typeof value.targetManifestSha256 !== "string" || !digest.test(value.targetManifestSha256)) {
|
||||
throw new AssetCatalogIndexedDBMigrationError("ASSET_MANIFEST_INVALID", "Stored catalog migration receipt is invalid");
|
||||
}
|
||||
return value as unknown as AssetCatalogIndexedDBMigrationReceiptIR;
|
||||
}
|
||||
|
||||
export async function readAssetCatalogIndexedDBIndex(
|
||||
database: IDBDatabase,
|
||||
settingStore = "setting",
|
||||
migrationStore = "migration",
|
||||
): Promise<{ source: StoredRow | undefined; target: StoredRow | undefined; receipt: StoredRow | undefined }> {
|
||||
assertStores(database, settingStore, migrationStore);
|
||||
const transaction = database.transaction([settingStore, migrationStore], "readonly");
|
||||
const sourcePromise = readRow(transaction.objectStore(settingStore), ASSET_CATALOG_INDEX_V1_ID);
|
||||
const targetPromise = readRow(transaction.objectStore(settingStore), ASSET_CATALOG_INDEX_V2_ID);
|
||||
const receiptPromise = readRow(transaction.objectStore(migrationStore), ASSET_CATALOG_MIGRATION_ID);
|
||||
const result = await Promise.all([sourcePromise, targetPromise, receiptPromise]);
|
||||
await transactionComplete(transaction, () => undefined);
|
||||
return { source: result[0], target: result[1], receipt: result[2] };
|
||||
}
|
||||
|
||||
export async function migrateAssetCatalogIndexedDB(
|
||||
database: IDBDatabase,
|
||||
options: {
|
||||
settingStore?: string;
|
||||
migrationStore?: string;
|
||||
faultAt?: AssetCatalogMigrationFault;
|
||||
} = {},
|
||||
): Promise<AssetCatalogIndexedDBMigrationResultIR> {
|
||||
const settingStore = options.settingStore ?? "setting";
|
||||
const migrationStore = options.migrationStore ?? "migration";
|
||||
assertStores(database, settingStore, migrationStore);
|
||||
|
||||
const before = await readAssetCatalogIndexedDBIndex(database, settingStore, migrationStore);
|
||||
if (!before.source) {
|
||||
if (!before.target || !before.receipt) {
|
||||
throw new AssetCatalogIndexedDBMigrationError("ASSET_MANIFEST_INVALID", "Catalog index has neither a complete v1 source nor a complete v2 migration");
|
||||
}
|
||||
const manifest = await parseAssetCatalogManifestV2(before.target.value);
|
||||
const receipt = parseReceipt(before.receipt);
|
||||
if (receipt.targetRevision !== manifest.revision || await sha256(manifest) !== receipt.targetManifestSha256) {
|
||||
throw new AssetCatalogIndexedDBMigrationError("ASSET_MANIFEST_INVALID", "Catalog migration receipt does not match the stored v2 index");
|
||||
}
|
||||
return { status: "ALREADY_MIGRATED", manifest, receipt, migrationReport: null };
|
||||
}
|
||||
|
||||
if (before.target || before.receipt?.id === ASSET_CATALOG_MIGRATION_ID) {
|
||||
throw new AssetCatalogIndexedDBMigrationError("ASSET_MANIFEST_INVALID", "Catalog index contains a partial v1 to v2 migration");
|
||||
}
|
||||
|
||||
const prepared = await migrateAssetCatalogV1ToV2(before.source.value);
|
||||
const receipt: AssetCatalogIndexedDBMigrationReceiptIR = {
|
||||
schemaVersion: 1,
|
||||
id: ASSET_CATALOG_MIGRATION_ID,
|
||||
task: "M12-01G",
|
||||
status: "MIGRATED",
|
||||
sourceRevision: prepared.report.sourceRevision,
|
||||
targetRevision: prepared.report.targetRevision,
|
||||
sourceManifestSha256: prepared.report.sourceManifestSha256,
|
||||
targetManifestSha256: prepared.report.targetManifestSha256,
|
||||
};
|
||||
|
||||
let migrationFailure: Error | undefined;
|
||||
const transaction = database.transaction([settingStore, migrationStore], "readwrite");
|
||||
const completion = transactionComplete(transaction, () => migrationFailure);
|
||||
const settings = transaction.objectStore(settingStore);
|
||||
const migrations = transaction.objectStore(migrationStore);
|
||||
const sourceRequest = settings.get(ASSET_CATALOG_INDEX_V1_ID);
|
||||
sourceRequest.onerror = () => {
|
||||
migrationFailure = sourceRequest.error ?? new AssetCatalogIndexedDBMigrationError("STORAGE_TRANSACTION", "Catalog source recheck failed");
|
||||
};
|
||||
sourceRequest.onsuccess = () => {
|
||||
const current = sourceRequest.result as StoredRow | undefined;
|
||||
if (!current || stableJSON(current.value) !== stableJSON(before.source?.value)) {
|
||||
migrationFailure = new AssetCatalogIndexedDBMigrationError("REVISION_CONFLICT", "Catalog v1 index changed while migration was prepared");
|
||||
transaction.abort();
|
||||
return;
|
||||
}
|
||||
settings.put({ id: ASSET_CATALOG_INDEX_V2_ID, value: prepared.manifest } satisfies AssetCatalogIndexRowV2);
|
||||
if (options.faultAt === "AFTER_TARGET_PUT") {
|
||||
migrationFailure = new AssetCatalogIndexedDBMigrationError("STORAGE_TRANSACTION", "Injected catalog migration failure after target write");
|
||||
transaction.abort();
|
||||
return;
|
||||
}
|
||||
migrations.put(receipt);
|
||||
settings.delete(ASSET_CATALOG_INDEX_V1_ID);
|
||||
if (options.faultAt === "AFTER_SOURCE_DELETE") {
|
||||
migrationFailure = new AssetCatalogIndexedDBMigrationError("STORAGE_TRANSACTION", "Injected catalog migration failure after source delete");
|
||||
transaction.abort();
|
||||
}
|
||||
};
|
||||
await completion;
|
||||
return { status: "MIGRATED", manifest: prepared.manifest, receipt, migrationReport: prepared.report };
|
||||
}
|
||||
|
||||
export async function loadAssetCatalogIndexedDBSnapshot(
|
||||
database: IDBDatabase,
|
||||
options: { settingStore?: string; migrationStore?: string } = {},
|
||||
): Promise<AssetCatalogIndexedDBSnapshotIR> {
|
||||
const loaded = await migrateAssetCatalogIndexedDB(database, options);
|
||||
const manifestSha256 = await sha256(loaded.manifest);
|
||||
if (manifestSha256 !== loaded.receipt.targetManifestSha256) {
|
||||
throw new AssetCatalogIndexedDBMigrationError("ASSET_MANIFEST_INVALID", "Loaded catalog index does not match its migration receipt");
|
||||
}
|
||||
return {
|
||||
schemaVersion: 1,
|
||||
revision: loaded.manifest.revision,
|
||||
manifestSha256,
|
||||
catalogOrder: loaded.manifest.catalogs.map(({ catalogId, path }) => ({ catalogId, path })),
|
||||
assetOrder: loaded.manifest.assets.map(({ assetId }) => assetId),
|
||||
assetIdentities: loaded.manifest.assets.map(({ assetId, assetLibraryIdentifier, relativeAssetIdentifier }) => ({
|
||||
assetId,
|
||||
assetLibraryIdentifier,
|
||||
relativeAssetIdentifier,
|
||||
})),
|
||||
};
|
||||
}
|
||||
262
web/app/src/storage/asset-preview-opfs-commit.ts
Normal file
262
web/app/src/storage/asset-preview-opfs-commit.ts
Normal file
@@ -0,0 +1,262 @@
|
||||
import {
|
||||
parseAssetCatalogManifestV2,
|
||||
type AssetCatalogManifestV2IR,
|
||||
type AssetCatalogV2PreviewIR,
|
||||
} from "../../../protocol/asset-catalog-v2";
|
||||
import { planAssetPreviewDecode } from "../../../protocol/asset-preview-decode";
|
||||
import {
|
||||
createAssetPreviewIdentity,
|
||||
parseAssetPreviewIdentity,
|
||||
type AssetPreviewIdentityIR,
|
||||
} from "../../../protocol/asset-preview";
|
||||
import type { ErrorCode } from "../../../protocol/error";
|
||||
import {
|
||||
ASSET_CATALOG_INDEX_V2_ID,
|
||||
type AssetCatalogIndexRowV2,
|
||||
} from "./asset-catalog-indexeddb";
|
||||
import { readContentAsset, writeContentAsset } from "./opfs-files";
|
||||
|
||||
export const ASSET_PREVIEW_CATALOG_HEAD_ID = "asset-catalog:preview-head:v1" as const;
|
||||
export { createAssetPreviewIdentity, parseAssetCatalogManifestV2 };
|
||||
|
||||
export type AssetPreviewCatalogCommitFault =
|
||||
| "BEFORE_OPFS_WRITE"
|
||||
| "AFTER_OPFS_WRITE"
|
||||
| "AFTER_CATALOG_PUT";
|
||||
|
||||
export interface AssetPreviewCatalogCommitReceiptIR {
|
||||
schemaVersion: 1;
|
||||
id: typeof ASSET_PREVIEW_CATALOG_HEAD_ID;
|
||||
task: "M12-02D";
|
||||
status: "COMMITTED";
|
||||
projectId: string;
|
||||
assetId: string;
|
||||
baseRevision: number;
|
||||
committedRevision: number;
|
||||
baseManifestSha256: string;
|
||||
committedManifestSha256: string;
|
||||
previewIdentitySha256: string;
|
||||
contentSha256: string;
|
||||
contentByteLength: number;
|
||||
opfsPath: string;
|
||||
}
|
||||
|
||||
export interface AssetPreviewCatalogCommitResultIR {
|
||||
manifest: AssetCatalogManifestV2IR;
|
||||
receipt: AssetPreviewCatalogCommitReceiptIR;
|
||||
preview: AssetCatalogV2PreviewIR;
|
||||
deduplicated: boolean;
|
||||
}
|
||||
|
||||
export class AssetPreviewCatalogCommitError extends Error {
|
||||
readonly code: ErrorCode;
|
||||
|
||||
constructor(code: ErrorCode, message: string, options?: ErrorOptions) {
|
||||
super(`${code}: ${message}`, options);
|
||||
this.name = "AssetPreviewCatalogCommitError";
|
||||
this.code = code;
|
||||
}
|
||||
}
|
||||
|
||||
interface StoredRow {
|
||||
id: string;
|
||||
value?: unknown;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
function record(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function stableJSON(value: unknown): string {
|
||||
if (Array.isArray(value)) return `[${value.map(stableJSON).join(",")}]`;
|
||||
if (record(value)) {
|
||||
return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableJSON(value[key])}`).join(",")}}`;
|
||||
}
|
||||
return JSON.stringify(value);
|
||||
}
|
||||
|
||||
async function sha256(value: unknown): Promise<string> {
|
||||
const bytes = new TextEncoder().encode(stableJSON(value));
|
||||
const digest = await crypto.subtle.digest("SHA-256", bytes);
|
||||
return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("");
|
||||
}
|
||||
|
||||
async function sha256Bytes(value: ArrayBuffer): Promise<string> {
|
||||
const digest = await crypto.subtle.digest("SHA-256", value);
|
||||
return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("");
|
||||
}
|
||||
|
||||
function readRow(store: IDBObjectStore, id: string): Promise<StoredRow | undefined> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = store.get(id);
|
||||
request.onsuccess = () => resolve(request.result as StoredRow | undefined);
|
||||
request.onerror = () => reject(request.error ?? new AssetPreviewCatalogCommitError("STORAGE_TRANSACTION", `Catalog row lookup failed for ${id}`));
|
||||
});
|
||||
}
|
||||
|
||||
function transactionComplete(transaction: IDBTransaction, failure: () => Error | undefined): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
transaction.oncomplete = () => resolve();
|
||||
transaction.onerror = () => reject(failure() ?? transaction.error ?? new AssetPreviewCatalogCommitError("STORAGE_TRANSACTION", "Preview catalog transaction failed"));
|
||||
transaction.onabort = () => reject(failure() ?? transaction.error ?? new AssetPreviewCatalogCommitError("STORAGE_TRANSACTION", "Preview catalog transaction aborted"));
|
||||
});
|
||||
}
|
||||
|
||||
async function readCatalog(database: IDBDatabase, settingStore: string): Promise<{ row: StoredRow; manifest: AssetCatalogManifestV2IR }> {
|
||||
if (!database.objectStoreNames.contains(settingStore)) {
|
||||
throw new AssetPreviewCatalogCommitError("STORAGE_TRANSACTION", "Catalog setting store is unavailable");
|
||||
}
|
||||
const transaction = database.transaction(settingStore, "readonly");
|
||||
const row = await readRow(transaction.objectStore(settingStore), ASSET_CATALOG_INDEX_V2_ID);
|
||||
await transactionComplete(transaction, () => undefined);
|
||||
if (!row) throw new AssetPreviewCatalogCommitError("ASSET_MANIFEST_INVALID", "Catalog v2 index is missing");
|
||||
return { row, manifest: await parseAssetCatalogManifestV2(row.value) };
|
||||
}
|
||||
|
||||
function parseReceipt(value: unknown): AssetPreviewCatalogCommitReceiptIR {
|
||||
const digest = /^[a-f0-9]{64}$/;
|
||||
if (!record(value) || value.schemaVersion !== 1 || value.id !== ASSET_PREVIEW_CATALOG_HEAD_ID ||
|
||||
value.task !== "M12-02D" || value.status !== "COMMITTED" || typeof value.projectId !== "string" ||
|
||||
typeof value.assetId !== "string" || !Number.isSafeInteger(value.baseRevision) || Number(value.baseRevision) < 0 ||
|
||||
!Number.isSafeInteger(value.committedRevision) || Number(value.committedRevision) !== Number(value.baseRevision) + 1 ||
|
||||
typeof value.baseManifestSha256 !== "string" || !digest.test(value.baseManifestSha256) ||
|
||||
typeof value.committedManifestSha256 !== "string" || !digest.test(value.committedManifestSha256) ||
|
||||
typeof value.previewIdentitySha256 !== "string" || !digest.test(value.previewIdentitySha256) ||
|
||||
typeof value.contentSha256 !== "string" || !digest.test(value.contentSha256) ||
|
||||
!Number.isSafeInteger(value.contentByteLength) || Number(value.contentByteLength) <= 0 ||
|
||||
typeof value.opfsPath !== "string" || value.opfsPath.length === 0) {
|
||||
throw new AssetPreviewCatalogCommitError("ASSET_MANIFEST_INVALID", "Preview catalog commit receipt is invalid");
|
||||
}
|
||||
return value as unknown as AssetPreviewCatalogCommitReceiptIR;
|
||||
}
|
||||
|
||||
function previewFromIdentity(identity: AssetPreviewIdentityIR): AssetCatalogV2PreviewIR {
|
||||
return {
|
||||
sha256: identity.content.sha256,
|
||||
mimeType: identity.content.mimeType,
|
||||
width: identity.content.width,
|
||||
height: identity.content.height,
|
||||
byteLength: identity.content.byteLength,
|
||||
};
|
||||
}
|
||||
|
||||
export async function commitAssetPreviewToOPFS(
|
||||
database: IDBDatabase,
|
||||
projectId: string,
|
||||
expectedRevision: number,
|
||||
identityValue: unknown,
|
||||
encodedBytes: ArrayBuffer,
|
||||
options: {
|
||||
settingStore?: string;
|
||||
storage?: StorageManager;
|
||||
faultAt?: AssetPreviewCatalogCommitFault;
|
||||
} = {},
|
||||
): Promise<AssetPreviewCatalogCommitResultIR> {
|
||||
const settingStore = options.settingStore ?? "setting";
|
||||
const identity = await parseAssetPreviewIdentity(identityValue);
|
||||
await planAssetPreviewDecode(identity, encodedBytes);
|
||||
const before = await readCatalog(database, settingStore);
|
||||
if (before.manifest.revision !== expectedRevision) {
|
||||
throw new AssetPreviewCatalogCommitError("REVISION_CONFLICT", "Catalog revision changed before preview storage");
|
||||
}
|
||||
const assetIndex = before.manifest.assets.findIndex((asset) => asset.assetId === identity.assetId);
|
||||
if (assetIndex === -1) {
|
||||
throw new AssetPreviewCatalogCommitError("ASSET_MANIFEST_INVALID", "Preview identity does not belong to a catalog asset");
|
||||
}
|
||||
if (expectedRevision === Number.MAX_SAFE_INTEGER) {
|
||||
throw new AssetPreviewCatalogCommitError("ASSET_BUDGET_EXCEEDED", "Catalog revision cannot advance safely");
|
||||
}
|
||||
const preview = previewFromIdentity(identity);
|
||||
const candidate = await parseAssetCatalogManifestV2({
|
||||
...before.manifest,
|
||||
revision: expectedRevision + 1,
|
||||
assets: before.manifest.assets.map((asset, index) => index === assetIndex ? { ...asset, preview } : asset),
|
||||
});
|
||||
const baseManifestSha256 = await sha256(before.manifest);
|
||||
const committedManifestSha256 = await sha256(candidate);
|
||||
|
||||
if (options.faultAt === "BEFORE_OPFS_WRITE") {
|
||||
throw new AssetPreviewCatalogCommitError("STORAGE_TRANSACTION", "Injected failure before preview OPFS write");
|
||||
}
|
||||
let written: Awaited<ReturnType<typeof writeContentAsset>>;
|
||||
try {
|
||||
written = await writeContentAsset(projectId, identity.content.sha256, encodedBytes, options.storage);
|
||||
const persisted = await readContentAsset(projectId, identity.content.sha256, options.storage);
|
||||
await planAssetPreviewDecode(identity, persisted);
|
||||
}
|
||||
catch (error) {
|
||||
if (error instanceof AssetPreviewCatalogCommitError || (error instanceof Error && "code" in error)) throw error;
|
||||
throw new AssetPreviewCatalogCommitError("STORAGE_TRANSACTION", "Preview OPFS write or readback verification failed", { cause: error });
|
||||
}
|
||||
if (options.faultAt === "AFTER_OPFS_WRITE") {
|
||||
throw new AssetPreviewCatalogCommitError("STORAGE_TRANSACTION", "Injected failure after preview OPFS write");
|
||||
}
|
||||
|
||||
const receipt: AssetPreviewCatalogCommitReceiptIR = {
|
||||
schemaVersion: 1,
|
||||
id: ASSET_PREVIEW_CATALOG_HEAD_ID,
|
||||
task: "M12-02D",
|
||||
status: "COMMITTED",
|
||||
projectId,
|
||||
assetId: identity.assetId,
|
||||
baseRevision: expectedRevision,
|
||||
committedRevision: candidate.revision,
|
||||
baseManifestSha256,
|
||||
committedManifestSha256,
|
||||
previewIdentitySha256: identity.identitySha256,
|
||||
contentSha256: identity.content.sha256,
|
||||
contentByteLength: identity.content.byteLength,
|
||||
opfsPath: written.path,
|
||||
};
|
||||
|
||||
let commitFailure: Error | undefined;
|
||||
const transaction = database.transaction(settingStore, "readwrite");
|
||||
const completion = transactionComplete(transaction, () => commitFailure);
|
||||
const store = transaction.objectStore(settingStore);
|
||||
const request = store.get(ASSET_CATALOG_INDEX_V2_ID);
|
||||
request.onerror = () => {
|
||||
commitFailure = request.error ?? new AssetPreviewCatalogCommitError("STORAGE_TRANSACTION", "Catalog source recheck failed");
|
||||
};
|
||||
request.onsuccess = () => {
|
||||
const current = request.result as StoredRow | undefined;
|
||||
if (!current || stableJSON(current.value) !== stableJSON(before.row.value)) {
|
||||
commitFailure = new AssetPreviewCatalogCommitError("REVISION_CONFLICT", "Catalog index changed while preview storage was prepared");
|
||||
transaction.abort();
|
||||
return;
|
||||
}
|
||||
store.put({ id: ASSET_CATALOG_INDEX_V2_ID, value: candidate } satisfies AssetCatalogIndexRowV2);
|
||||
store.put(receipt);
|
||||
if (options.faultAt === "AFTER_CATALOG_PUT") {
|
||||
commitFailure = new AssetPreviewCatalogCommitError("STORAGE_TRANSACTION", "Injected failure after preview catalog writes");
|
||||
transaction.abort();
|
||||
}
|
||||
};
|
||||
await completion;
|
||||
return { manifest: candidate, receipt, preview, deduplicated: written.deduplicated };
|
||||
}
|
||||
|
||||
export async function loadCommittedAssetPreviewCatalog(
|
||||
database: IDBDatabase,
|
||||
options: { settingStore?: string; storage?: StorageManager } = {},
|
||||
): Promise<AssetPreviewCatalogCommitResultIR> {
|
||||
const settingStore = options.settingStore ?? "setting";
|
||||
const catalog = await readCatalog(database, settingStore);
|
||||
const transaction = database.transaction(settingStore, "readonly");
|
||||
const row = await readRow(transaction.objectStore(settingStore), ASSET_PREVIEW_CATALOG_HEAD_ID);
|
||||
await transactionComplete(transaction, () => undefined);
|
||||
if (!row) throw new AssetPreviewCatalogCommitError("ASSET_MANIFEST_INVALID", "Preview catalog commit receipt is missing");
|
||||
const receipt = parseReceipt(row);
|
||||
if (catalog.manifest.revision !== receipt.committedRevision || await sha256(catalog.manifest) !== receipt.committedManifestSha256) {
|
||||
throw new AssetPreviewCatalogCommitError("ASSET_MANIFEST_INVALID", "Preview catalog receipt does not match the current catalog");
|
||||
}
|
||||
const asset = catalog.manifest.assets.find((candidate) => candidate.assetId === receipt.assetId);
|
||||
if (!asset?.preview || asset.preview.sha256 !== receipt.contentSha256 || asset.preview.byteLength !== receipt.contentByteLength) {
|
||||
throw new AssetPreviewCatalogCommitError("ASSET_MANIFEST_INVALID", "Committed catalog preview does not match its receipt");
|
||||
}
|
||||
const persisted = await readContentAsset(receipt.projectId, receipt.contentSha256, options.storage);
|
||||
if (persisted.byteLength !== receipt.contentByteLength || await sha256Bytes(persisted) !== receipt.contentSha256) {
|
||||
throw new AssetPreviewCatalogCommitError("ASSET_SOURCE_HASH_MISMATCH", "Committed preview payload identity changed");
|
||||
}
|
||||
return { manifest: catalog.manifest, receipt, preview: asset.preview, deduplicated: false };
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user