Files
workinf_Blender_Wasm/my-tools/authorize_next_wbv2_task.py
mes123456 96f66e8ce7
Some checks failed
M6 deployable RC / quick (push) Has been cancelled
M6 deployable RC / chromium (push) Has been cancelled
M6 deployable RC / release (push) Has been cancelled
feat(web): authorize native object transform apply batches
2026-09-04 15:47:43 +08:00

1137 lines
67 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""Authorize the next source-backed Blender module batch.
Each successor is a bounded capability batch. The queue advances only after a
single shared Blender Main/WASM/Worker/Chromium chain produces an aggregate
receipt for every selected source unit.
"""
from __future__ import annotations
import argparse
import hashlib
import json
import os
import re
import subprocess
from datetime import datetime, timezone
from pathlib import Path
from wbv2_runtime import CONTROL_ROOT, INDEX, QUEUE, ROOT, read_json, terminal_pointer
AUTO_PLANNER_STATUS = CONTROL_ROOT / "auto-planner.json"
PREFERRED_BATCHES = [
{
"moduleId": "OBJECT_TRANSFORM_APPLY_OPERATOR_BATCH",
"title": "verify object transform apply operator through native Main",
"owner": "OBJECT",
"identifiers": ["OBJECT_OT_transform_apply"],
"rna": [],
"fixture": "tests/files/web/basic_scene.blend",
"test": "web/tests/e2e/wbv2-object-transform-apply-batch-production.spec.ts",
"production": "tests/golden/{task}/object-transform-apply-production.json",
"types": [],
},
{
"moduleId": "OBJECT_VISIBILITY_CLEAR_OPERATOR_BATCH",
"title": "verify object visibility clear operator through native Main",
"owner": "OBJECT",
"identifiers": ["OBJECT_OT_hide_view_clear"],
"rna": [],
"fixture": "tests/files/web/generated/WBV2-P2-002-OBJECT.TRANSFORM_HIERARCHY.blend",
"test": "web/tests/e2e/wbv2-object-visibility-clear-batch-production.spec.ts",
"production": "tests/golden/{task}/object-visibility-clear-production.json",
"types": [],
},
{
"moduleId": "OBJECT_MOVE_COLLECTION_OPERATOR_BATCH",
"title": "verify object move collection operator through native Main",
"owner": "OBJECT",
"identifiers": ["OBJECT_OT_move_to_collection"],
"rna": [],
"fixture": "tests/files/web/generated/WBV2-P24-001-OBJECT_OPERATIONS.MOVE_COLLECTION.blend",
"test": "web/tests/e2e/wbv2-object-move-batch-production.spec.ts",
"production": "tests/golden/{task}/object-move-production.json",
"types": [],
},
{
"moduleId": "MESH_PRIMITIVE_CUBE_OPERATOR_BATCH",
"title": "verify primitive cube operator through native Main",
"owner": "MESH",
"identifiers": ["MESH_OT_primitive_cube_add"],
"rna": [],
"fixture": "tests/files/web/basic_scene.blend",
"test": "web/tests/e2e/wbv2-primitive-cube-batch-production.spec.ts",
"production": "tests/golden/{task}/primitive-cube-production.json",
"types": [],
},
{
"moduleId": "OBJECT_PARENT_OPERATOR_BATCH",
"title": "verify object parent operator through native Main",
"owner": "OBJECT",
"identifiers": ["OBJECT_OT_parent_set"],
"rna": [],
"fixture": "tests/files/web/basic_scene.blend",
"test": "web/tests/e2e/wbv2-object-parent-production.spec.ts",
"production": "tests/golden/{task}/object-parent-production.json",
"types": [],
},
{
"moduleId": "OBJECT_COLLECTION_CREATE_OPERATOR_BATCH",
"title": "verify collection create operator through native Main",
"owner": "OBJECT",
"identifiers": ["OBJECT_OT_collection_add"],
"rna": [],
"fixture": "tests/files/web/basic_scene.blend",
"test": "web/tests/e2e/wbv2-collection-create-batch-production.spec.ts",
"production": "tests/golden/{task}/collection-create-production.json",
"types": [],
},
{
"moduleId": "OBJECT_VISIBILITY_OPERATOR_BATCH",
"title": "verify object visibility operator through native Main",
"owner": "OBJECT",
"identifiers": ["OBJECT_OT_hide_view_set"],
"rna": [],
"fixture": "tests/files/web/generated/WBV2-P2-002-OBJECT.TRANSFORM_HIERARCHY.blend",
"test": "web/tests/e2e/wbv2-object-visibility-batch-production.spec.ts",
"production": "tests/golden/{task}/object-visibility-production.json",
"types": [],
},
{
"moduleId": "OBJECT_DELETE_OPERATOR_BATCH",
"title": "verify object delete operator through native Main",
"owner": "OBJECT",
"identifiers": ["OBJECT_OT_delete"],
"rna": [],
"fixture": "tests/files/web/basic_scene.blend",
"test": "web/tests/e2e/wbv2-object-delete-batch-production.spec.ts",
"production": "tests/golden/{task}/object-delete-production.json",
"types": [],
},
{
"moduleId": "OBJECT_DUPLICATE_OPERATOR_BATCH",
"title": "verify object duplicate operator through native Main",
"owner": "OBJECT",
"identifiers": ["OBJECT_OT_duplicate"],
"rna": [],
"fixture": "tests/files/web/basic_scene.blend",
"test": "web/tests/e2e/wbv2-object-duplicate-production.spec.ts",
"production": "tests/golden/{task}/object-duplicate-production.json",
"types": [],
},
{
"moduleId": "ANIMATION_FRAME_OPERATOR_BATCH",
"title": "verify animation frame operator through native Main",
"owner": "ANIMATION",
"identifiers": ["ANIM_OT_change_frame"],
"rna": [],
"fixture": "tests/files/web/generated/M16-GAP-00161-action-fcurve-samples.blend",
"test": "web/tests/e2e/wbv2-fcurve-sample-production.spec.ts",
"production": "tests/golden/{task}/fcurve-sample-production.json",
"types": [],
},
{
"moduleId": "COMPOSITOR_NATIVE_NODE_BATCH",
"title": "verify native compositor transform blur alpha reader",
"owner": "MEDIA",
"identifiers": ["CompositorNodeTransform", "CompositorNodeBlur", "CompositorNodeAlphaOver", "CompositorNodeViewer"],
"rna": [],
"fixture": "tests/files/web/compositor_node_batch_scene.blend",
"test": "web/tests/e2e/compositor-main-reader-production.spec.ts",
"production": "tests/golden/{task}/compositor-main-reader-production.json",
"types": ["CompositorNodeTransform", "CompositorNodeBlur", "CompositorNodeAlphaOver", "VIEWER"],
},
{
"moduleId": "COMPOSITOR_NODE_READER_BATCH",
"title": "verify compositor node reader batch",
"owner": "MEDIA",
"identifiers": [],
"rna": ["CompositorNodeGlare", "CompositorNodeViewer"],
"fixture": "tests/files/web/compositor_scene.blend",
"test": "web/tests/e2e/compositor-main-reader-production.spec.ts",
"production": "tests/golden/{task}/compositor-main-reader-production.json",
"types": ["CompositorNodeGlare", "VIEWER"],
},
{
"moduleId": "MODIFIER_READER_BATCH",
"title": "verify modifier reader batch",
"owner": "MODIFIER",
"identifiers": ["MOD_datatransfer", "MOD_dynamicpaint", "MOD_explode", "MOD_fluid"],
"rna": ["DataTransferModifier", "DynamicPaintModifier", "ExplodeModifier", "FluidModifier", "FluidDomainSettings"],
"fixture": "tests/files/web/modifier_reader_batch_scene.blend",
"test": "web/tests/e2e/wbv2-modifier-reader-batch-production.spec.ts",
"production": "tests/golden/{task}/modifier-reader-batch-production.json",
"types": ["DATA_TRANSFER", "DYNAMIC_PAINT", "EXPLODE", "FLUID"],
},
{
"moduleId": "MODIFIER_READER_BATCH_TWO",
"title": "verify modifier reader batch two",
"owner": "MODIFIER",
"identifiers": ["MOD_laplaciandeform", "MOD_laplaciansmooth", "MOD_mask", "MOD_shrinkwrap"],
"rna": ["LaplacianDeformModifier", "LaplacianSmoothModifier", "MaskModifier", "ShrinkwrapModifier"],
"fixture": "tests/files/web/modifier_reader_batch_two_scene.blend",
"test": "web/tests/e2e/wbv2-modifier-reader-batch-two-production.spec.ts",
"production": "tests/golden/{task}/modifier-reader-batch-production.json",
"types": ["LAPLACIANDEFORM", "LAPLACIANSMOOTH", "MASK", "SHRINKWRAP"],
},
{
"moduleId": "MODIFIER_READER_BATCH_THREE",
"title": "verify modifier reader batch three",
"owner": "MODIFIER",
"identifiers": ["MOD_remesh", "MOD_skin", "MOD_weighted_normal", "MOD_warp"],
"rna": ["RemeshModifier", "SkinModifier", "WeightedNormalModifier", "WarpModifier"],
"fixture": "tests/files/web/modifier_reader_batch_three_scene.blend",
"test": "web/tests/e2e/wbv2-modifier-reader-batch-two-production.spec.ts",
"production": "tests/golden/{task}/modifier-reader-batch-production.json",
"types": ["REMESH", "SKIN", "WEIGHTED_NORMAL", "WARP"],
},
{
"moduleId": "MODIFIER_READER_BATCH_FOUR",
"title": "verify modifier reader batch four",
"owner": "MODIFIER",
"identifiers": ["MOD_uvproject", "MOD_uvwarp", "MOD_normal_edit", "MOD_volume_to_mesh"],
"rna": ["UVProjectModifier", "UVWarpModifier", "NormalEditModifier", "VolumeToMeshModifier"],
"fixture": "tests/files/web/modifier_reader_batch_four_scene.blend",
"test": "web/tests/e2e/wbv2-modifier-reader-batch-two-production.spec.ts",
"production": "tests/golden/{task}/modifier-reader-batch-production.json",
"types": ["UV_PROJECT", "UV_WARP", "NORMAL_EDIT", "VOLUME_TO_MESH"],
},
{
"moduleId": "MODIFIER_READER_BATCH_FIVE",
"title": "verify modifier reader batch five",
"owner": "MODIFIER",
"identifiers": ["MOD_meshcache", "MOD_meshsequencecache", "MOD_multires", "MOD_ocean"],
"rna": ["MeshCacheModifier", "MeshSequenceCacheModifier", "MultiresModifier", "OceanModifier"],
"fixture": "tests/files/web/modifier_reader_batch_five_scene.blend",
"test": "web/tests/e2e/wbv2-modifier-reader-batch-two-production.spec.ts",
"production": "tests/golden/{task}/modifier-reader-batch-production.json",
"types": ["MESH_CACHE", "MESH_SEQUENCE_CACHE", "MULTIRES", "OCEAN"],
},
{
"moduleId": "MODIFIER_READER_BATCH_SIX",
"title": "verify modifier reader batch six",
"owner": "MODIFIER",
"identifiers": ["MOD_mesh_to_volume", "MOD_particleinstance", "MOD_surface", "MOD_weightvgedit", "MOD_weightvgmix", "MOD_weightvgproximity"],
"rna": ["MeshToVolumeModifier", "ParticleInstanceModifier", "SurfaceModifier", "VertexWeightEditModifier", "VertexWeightMixModifier", "VertexWeightProximityModifier"],
"fixtures": [
"tests/files/web/generated/M16-GAP-00081-modifier-MESH_TO_VOLUME.blend",
"tests/files/web/generated/M16-GAP-00087-modifier-PARTICLE_INSTANCE.blend",
"tests/files/web/generated/M16-GAP-00098-modifier-SURFACE.blend",
"tests/files/web/generated/M16-GAP-00103-modifier-VERTEX_WEIGHT_EDIT.blend",
"tests/files/web/generated/M16-GAP-00104-modifier-VERTEX_WEIGHT_MIX.blend",
"tests/files/web/generated/M16-GAP-00105-modifier-VERTEX_WEIGHT_PROXIMITY.blend",
],
"test": "web/tests/e2e/wbv2-modifier-reader-multi-fixture-production.spec.ts",
"production": "tests/golden/{task}/modifier-reader-batch-production.json",
"types": ["MESH_TO_VOLUME", "PARTICLE_INSTANCE", "SURFACE", "VERTEX_WEIGHT_EDIT", "VERTEX_WEIGHT_MIX", "VERTEX_WEIGHT_PROXIMITY"],
},
{
"moduleId": "MODIFIER_RNA_READER_BATCH",
"title": "verify modifier RNA reader batch",
"owner": "MODIFIER",
"identifiers": ["MOD_lineart", "MOD_particlesystem", "MOD_decimate"],
"rna": ["GreasePencilLineartModifier", "ParticleSystemModifier", "DecimateModifier"],
"fixtures": [
"tests/files/web/modifier_grease_pencil_scene.blend",
"tests/files/web/generated/M16-GAP-00088-modifier-PARTICLE_SYSTEM.blend",
"tests/files/web/generated/M16-GAP-00041-modifier-DECIMATE.blend",
],
"test": "web/tests/e2e/wbv2-modifier-reader-multi-fixture-production.spec.ts",
"production": "tests/golden/{task}/modifier-reader-batch-production.json",
"types": ["LINEART", "PARTICLE_SYSTEM", "DECIMATE"],
},
{
"moduleId": "MODIFIER_VOLUME_DISPLACE_BATCH",
"title": "verify volume displace modifier reader",
"owner": "MODIFIER",
"identifiers": ["MOD_volume_displace"],
"rna": ["VolumeDisplaceModifier"],
"fixture": "tests/files/web/generated/M16-GAP-00106-modifier-VOLUME_DISPLACE.blend",
"test": "web/tests/e2e/wbv2-modifier-reader-multi-fixture-production.spec.ts",
"production": "tests/golden/{task}/modifier-reader-batch-production.json",
"types": ["VOLUME_DISPLACE"],
},
{
"moduleId": "SHAPE_KEY_POINT_READER_BATCH",
"title": "verify shape key point reader",
"owner": "ANIMATION",
"identifiers": [],
"rna": ["ShapeKeyPoint"],
"fixture": "tests/files/web/rigged_shape_scene.blend",
"test": "web/tests/e2e/wbv2-shape-key-reader-production.spec.ts",
"production": "tests/golden/{task}/shape-key-reader-production.json",
"types": [],
},
{
"moduleId": "MODIFIER_SHAPEKEY_READER_BATCH",
"title": "verify shape key modifier reader dependency",
"owner": "MODIFIER",
"identifiers": ["MOD_shapekey"],
"rna": ["ShapeKeyPoint"],
"fixture": "tests/files/web/rigged_shape_scene.blend",
"test": "web/tests/e2e/wbv2-shape-key-reader-production.spec.ts",
"production": "tests/golden/{task}/shape-key-reader-production.json",
"types": [],
},
{
"moduleId": "LINE_STYLE_READER_BATCH",
"title": "verify line style reader batch",
"owner": "DRAWING",
"identifiers": [],
"rna": ["FreestyleLineStyle", "LineStyleModifier", "LineStyleAlphaModifier", "LineStyleColorModifier", "LineStyleGeometryModifier", "LineStyleThicknessModifier"],
"fixture": "tests/files/web/generated/M16-GAP-00008-datablock-FreestyleLineStyle.blend",
"test": "web/tests/e2e/wbv2-line-style-reader-production.spec.ts",
"production": "tests/golden/{task}/line-style-reader-production.json",
"types": [],
},
{
"moduleId": "LINE_STYLE_COLLECTION_READER_BATCH",
"title": "verify line style collection reader batch",
"owner": "DRAWING",
"identifiers": [],
"rna": ["BlendDataLineStyles", "LineStyleColorModifiers", "LineStyleAlphaModifiers", "LineStyleThicknessModifiers", "LineStyleGeometryModifiers"],
"fixture": "tests/files/web/line_style_modifier_batch_scene.blend",
"test": "web/tests/e2e/wbv2-line-style-reader-production.spec.ts",
"production": "tests/golden/{task}/line-style-reader-production.json",
"types": [],
},
{
"moduleId": "LINE_STYLE_SUBTYPE_READER_BATCH",
"title": "verify line style modifier subtype reader batch",
"owner": "DRAWING",
"identifiers": [],
"rna": [
"LineStyleAlphaModifier_AlongStroke", "LineStyleAlphaModifier_CreaseAngle", "LineStyleAlphaModifier_Curvature_3D", "LineStyleAlphaModifier_DistanceFromCamera", "LineStyleAlphaModifier_DistanceFromObject", "LineStyleAlphaModifier_Material", "LineStyleAlphaModifier_Noise", "LineStyleAlphaModifier_Tangent",
"LineStyleColorModifier_AlongStroke", "LineStyleColorModifier_CreaseAngle", "LineStyleColorModifier_Curvature_3D", "LineStyleColorModifier_DistanceFromCamera", "LineStyleColorModifier_DistanceFromObject", "LineStyleColorModifier_Material", "LineStyleColorModifier_Noise", "LineStyleColorModifier_Tangent",
"LineStyleGeometryModifier_2DOffset", "LineStyleGeometryModifier_2DTransform", "LineStyleGeometryModifier_BackboneStretcher", "LineStyleGeometryModifier_BezierCurve", "LineStyleGeometryModifier_Blueprint", "LineStyleGeometryModifier_GuidingLines", "LineStyleGeometryModifier_PerlinNoise1D", "LineStyleGeometryModifier_PerlinNoise2D", "LineStyleGeometryModifier_Polygonalization", "LineStyleGeometryModifier_Sampling", "LineStyleGeometryModifier_Simplification", "LineStyleGeometryModifier_SinusDisplacement", "LineStyleGeometryModifier_SpatialNoise", "LineStyleGeometryModifier_TipRemover",
"LineStyleThicknessModifier_AlongStroke", "LineStyleThicknessModifier_Calligraphy", "LineStyleThicknessModifier_CreaseAngle", "LineStyleThicknessModifier_Curvature_3D", "LineStyleThicknessModifier_DistanceFromCamera", "LineStyleThicknessModifier_DistanceFromObject", "LineStyleThicknessModifier_Material", "LineStyleThicknessModifier_Noise", "LineStyleThicknessModifier_Tangent",
],
"fixture": "tests/files/web/line_style_subtype_batch_scene.blend",
"test": "web/tests/e2e/wbv2-line-style-reader-production.spec.ts",
"production": "tests/golden/{task}/line-style-reader-production.json",
"types": [],
},
{
"moduleId": "FCURVE_MODIFIER_SUBTYPE_READER_BATCH",
"title": "verify FCurve modifier subtype reader batch",
"owner": "ANIMATION",
"identifiers": [],
"rna": ["FModifierEnvelope", "FModifierGenerator", "FModifierLimits", "FModifierNoise", "FModifierSmooth", "FModifierStepped"],
"fixture": "tests/files/web/fcurve_modifier_subtype_batch_scene.blend",
"test": "web/tests/e2e/wbv2-fcurve-modifier-production.spec.ts",
"production": "tests/golden/{task}/fcurve-modifier-production.json",
"types": [],
},
{
"moduleId": "MODIFIER_SOURCE_DEPENDENCY_REUSE",
"title": "reuse verified modifier receipts for shared source utilities",
"owner": "MODIFIER",
"identifiers": ["MOD_grease_pencil_util", "MOD_meshcache_mdd", "MOD_meshcache_pc2", "MOD_meshcache_util", "MOD_weightvg_util"],
"rna": [],
"test": "tools/web/reuse-wbv2-source-dependencies.mjs",
"production": "tests/golden/{task}/modifier-reader-batch-production.json",
"types": [],
"reuseReceipts": [
{"manifest": "tests/golden/WBV2-P1150-001/manifest.json", "report": "tests/golden/WBV2-P1150-001/module-receipt.json"},
{"manifest": "tests/golden/WBV2-P1151-001/manifest.json", "report": "tests/golden/WBV2-P1151-001/module-receipt.json"},
{"manifest": "tests/golden/WBV2-P1152-001/manifest.json", "report": "tests/golden/WBV2-P1152-001/module-receipt.json"},
],
"dependencyReuse": [
{"unitId": "modifier-source:MOD_grease_pencil_util", "representativeUnitId": "modifier-source:MOD_grease_pencil_array", "proofSource": "blender-5.2.0/source/blender/modifiers/intern/MOD_grease_pencil_array.cc", "marker": "MOD_grease_pencil_util.hh", "productionEvidence": "tests/golden/WBV2-P1151-001/modifier-reader-batch-production.json"},
{"unitId": "modifier-source:MOD_meshcache_mdd", "representativeUnitId": "modifier-source:MOD_meshcache", "proofSource": "blender-5.2.0/source/blender/modifiers/intern/MOD_meshcache.cc", "marker": "MOD_meshcache_read_mdd_times", "productionEvidence": "tests/golden/WBV2-P1150-001/modifier-reader-batch-production.json"},
{"unitId": "modifier-source:MOD_meshcache_pc2", "representativeUnitId": "modifier-source:MOD_meshcache", "proofSource": "blender-5.2.0/source/blender/modifiers/intern/MOD_meshcache.cc", "marker": "MOD_meshcache_read_pc2_times", "productionEvidence": "tests/golden/WBV2-P1150-001/modifier-reader-batch-production.json"},
{"unitId": "modifier-source:MOD_meshcache_util", "representativeUnitId": "modifier-source:MOD_meshcache", "proofSource": "blender-5.2.0/source/blender/modifiers/intern/MOD_meshcache.cc", "marker": "MOD_meshcache_util.hh", "productionEvidence": "tests/golden/WBV2-P1150-001/modifier-reader-batch-production.json"},
{"unitId": "modifier-source:MOD_weightvg_util", "representativeUnitId": "modifier-source:MOD_weightvgedit", "proofSource": "blender-5.2.0/source/blender/modifiers/intern/MOD_weightvgedit.cc", "marker": "MOD_weightvg_util.hh", "productionEvidence": "tests/golden/WBV2-P1152-001/modifier-reader-batch-production.json"},
],
},
{
"moduleId": "MODIFIER_SOLIDIFY_HELPER_BATCH",
"title": "verify solidify split helper sources",
"owner": "MODIFIER",
"identifiers": ["MOD_solidify_extrude", "MOD_solidify_nonmanifold"],
"rna": [],
"fixture": "tests/files/web/modifier_generate_scene.blend",
"test": "web/tests/e2e/wbv2-modifier-reader-multi-fixture-production.spec.ts",
"production": "tests/golden/{task}/modifier-reader-batch-production.json",
"types": ["SOLIDIFY"],
},
{
"moduleId": "GREASE_PENCIL_MODIFIER_READER_BATCH",
"title": "verify Grease Pencil modifier reader batch",
"owner": "DRAWING",
"identifiers": [
"MOD_grease_pencil_armature", "MOD_grease_pencil_array", "MOD_grease_pencil_build",
"MOD_grease_pencil_color", "MOD_grease_pencil_dash", "MOD_grease_pencil_envelope",
"MOD_grease_pencil_hook", "MOD_grease_pencil_lattice", "MOD_grease_pencil_length",
"MOD_grease_pencil_mirror", "MOD_grease_pencil_multiply", "MOD_grease_pencil_noise",
"MOD_grease_pencil_offset", "MOD_grease_pencil_opacity", "MOD_grease_pencil_outline",
"MOD_grease_pencil_shrinkwrap", "MOD_grease_pencil_simplify", "MOD_grease_pencil_smooth",
"MOD_grease_pencil_subdiv", "MOD_grease_pencil_texture", "MOD_grease_pencil_thickness",
"MOD_grease_pencil_time", "MOD_grease_pencil_tint", "MOD_grease_pencil_weight_angle",
"MOD_grease_pencil_weight_proximity",
],
"rna": [
"GreasePencilArmatureModifier", "GreasePencilArrayModifier", "GreasePencilBuildModifier",
"GreasePencilColorModifier", "GreasePencilEnvelopeModifier", "GreasePencilHookModifier",
"GreasePencilLatticeModifier", "GreasePencilLengthModifier", "GreasePencilMirrorModifier",
"GreasePencilMultiplyModifier", "GreasePencilNoiseModifier", "GreasePencilOffsetModifier",
"GreasePencilOpacityModifier", "GreasePencilOutlineModifier", "GreasePencilShrinkwrapModifier",
"GreasePencilSimplifyModifier", "GreasePencilSmoothModifier", "GreasePencilSubdivModifier",
"GreasePencilTextureModifier", "GreasePencilTimeModifier", "GreasePencilTintModifier",
"GreasePencilWeightAngleModifier", "GreasePencilWeightProximityModifier",
],
"fixture": "tests/files/web/modifier_grease_pencil_scene.blend",
"test": "web/tests/e2e/wbv2-modifier-reader-batch-two-production.spec.ts",
"production": "tests/golden/{task}/modifier-reader-batch-production.json",
"types": [
"GREASE_PENCIL_ARMATURE", "GREASE_PENCIL_ARRAY", "GREASE_PENCIL_BUILD", "GREASE_PENCIL_COLOR",
"GREASE_PENCIL_DASH", "GREASE_PENCIL_ENVELOPE", "GREASE_PENCIL_HOOK", "GREASE_PENCIL_LATTICE",
"GREASE_PENCIL_LENGTH", "GREASE_PENCIL_MIRROR", "GREASE_PENCIL_MULTIPLY", "GREASE_PENCIL_NOISE",
"GREASE_PENCIL_OFFSET", "GREASE_PENCIL_OPACITY", "GREASE_PENCIL_OUTLINE", "GREASE_PENCIL_SHRINKWRAP",
"GREASE_PENCIL_SIMPLIFY", "GREASE_PENCIL_SMOOTH", "GREASE_PENCIL_SUBDIV", "GREASE_PENCIL_TEXTURE",
"GREASE_PENCIL_THICKNESS", "GREASE_PENCIL_TIME", "GREASE_PENCIL_TINT", "GREASE_PENCIL_VERTEX_WEIGHT_ANGLE",
"GREASE_PENCIL_VERTEX_WEIGHT_PROXIMITY",
],
},
]
def sha(path: Path) -> str:
return hashlib.sha256(path.read_bytes()).hexdigest()
def write_json(path: Path, value: object) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(json.dumps(value, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
def write_text(path: Path, value: str) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(value, encoding="utf-8")
def write_planner_status(payload: dict) -> None:
"""Persist a deduplicated planner heartbeat for unattended observability."""
value = {"schemaVersion": 1, **payload}
try:
current = read_json(AUTO_PLANNER_STATUS)
stable_current = {key: item for key, item in current.items() if key != "updatedAt"}
stable_value = {key: item for key, item in value.items() if key != "updatedAt"}
if stable_current == stable_value:
return
except (OSError, AttributeError):
pass
value["updatedAt"] = datetime.now(timezone.utc).isoformat()
AUTO_PLANNER_STATUS.parent.mkdir(parents=True, exist_ok=True)
temporary = AUTO_PLANNER_STATUS.with_suffix(AUTO_PLANNER_STATUS.suffix + f".tmp-{os.getpid()}")
temporary.write_text(json.dumps(value, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
temporary.replace(AUTO_PLANNER_STATUS)
def rel(path: Path) -> str:
return path.relative_to(ROOT).as_posix()
def batch_contract(task: str, batch: dict) -> dict:
selected = [unit["id"] for unit in batch.get("units", [])]
digest = hashlib.sha256(json.dumps(selected, ensure_ascii=False, separators=(",", ":")).encode()).hexdigest()[:16]
return {"batchId": batch.get("batchId") or f"{task}:{batch['moduleId']}:{digest}", "selectedRecordIds": selected}
def native_compositor_batch(inventory: dict) -> dict | None:
"""Select a bounded node-reader batch that can generate its own fixture."""
candidates = [
unit for unit in inventory.get("units", [])
if isinstance(unit, dict)
and unit.get("coverageStatus") != "VERIFIED"
and unit.get("kind") == "node"
and isinstance(unit.get("identifier"), str)
and unit["identifier"].startswith("CompositorNode")
]
if not candidates:
return None
# Native reader fixtures remain compact: some compositor node storage
# layouts interact when more than five heterogeneous nodes share one
# group. Keep the upper bound at the empirically stable shape; the
# adaptive history may still reduce it after a focused gate failure.
max_size = 5
module_id = "AUTO_COMPOSITOR_NATIVE_NODE_READER"
history = []
for receipt_path in sorted(ROOT.glob("tests/golden/WBV2-P*-001/module-receipt.json"), key=lambda item: item.parent.name):
receipt = read_json(receipt_path)
if receipt.get("moduleId") != module_id:
continue
duration = receipt.get("efficiency", {}).get("totalDurationMs")
if not isinstance(duration, (int, float)):
duration = sum(item.get("durationMs", 0) for item in receipt.get("commands", []) if isinstance(item, dict))
history.append({
"durationMs": duration,
"commands": len(receipt.get("commands", [])),
"timeout": any("timeout" in str(item.get("output", "")).lower() for item in receipt.get("commands", []) if isinstance(item, dict)),
"focusedGateFailure": receipt.get("status") != "PASS",
})
recent = history[-3:]
current_size = min(len(candidates), max_size)
if recent and any(item["timeout"] or item["durationMs"] > 900_000 or item["commands"] > 48 for item in recent):
next_size = max(1, current_size // 2)
elif recent and all(item["durationMs"] < 300_000 and item["commands"] < 24 and not item["focusedGateFailure"] for item in recent):
next_size = min(max_size, current_size + 4)
else:
next_size = current_size
units = candidates[:next_size]
return {
"moduleId": module_id,
"title": "verify auto-generated native compositor reader batch",
"owner": "NODES",
"identifiers": [unit["identifier"] for unit in units],
"rna": [],
"test": "web/tests/e2e/compositor-main-reader-production.spec.ts",
"production": "tests/golden/{task}/compositor-main-reader-production.json",
"types": [unit["identifier"] for unit in units],
"units": units,
"candidateUnits": candidates,
"nativeCompositorFixture": True,
"adaptiveKey": f"{module_id}:LOCAL_EXACT:NODES",
"adaptiveHistory": recent,
"adaptiveCurrentSize": current_size,
"adaptiveNextSize": next_size,
"adaptiveMinSize": 1,
"adaptiveMaxSize": max_size,
}
def ensure_native_compositor_fixture(batch: dict, contract: dict) -> None:
"""Materialize a selected native node batch before its manifest references it."""
if not batch.get("nativeCompositorFixture"):
return
digest = contract["batchId"].rsplit(":", 1)[-1]
fixture = Path(f"tests/files/web/generated/wbv2-compositor-{digest}.blend")
batch["fixture"] = fixture.as_posix()
if (ROOT / fixture).is_file():
return
command = ["python3", "my-tools/generate_wbv2_compositor_fixture.py", "--output", fixture.as_posix()]
for unit in batch["units"]:
command.extend(["--node", unit["identifier"]])
result = subprocess.run(command, cwd=ROOT, text=True, capture_output=True, check=False)
if result.returncode:
raise SystemExit(f"native fixture preparation failed for {batch['moduleId']}:\n{result.stdout}{result.stderr}")
def ensure_native_geometry_fixture(batch: dict, contract: dict) -> None:
if not batch.get("nativeGeometryFixture"):
return
digest = contract["batchId"].rsplit(":", 1)[-1]
fixture = Path(f"tests/files/web/generated/wbv2-geometry-{digest}.blend")
batch["fixture"] = fixture.as_posix()
if fixture.is_file() or (ROOT / fixture).is_file():
return
command = ["python3", "my-tools/generate_wbv2_geometry_fixture.py", "--output", fixture.as_posix()]
for unit in batch["units"]:
command.extend(["--node", unit["identifier"]])
result = subprocess.run(command, cwd=ROOT, text=True, capture_output=True, check=False)
if result.returncode:
raise SystemExit(f"native geometry fixture preparation failed for {batch['moduleId']}:\n{result.stdout}{result.stderr}")
def ensure_native_shader_fixture(batch: dict, contract: dict) -> None:
if not batch.get("nativeShaderFixture"):
return
digest = contract["batchId"].rsplit(":", 1)[-1]
fixture = Path(f"tests/files/web/generated/wbv2-shader-{digest}.blend")
batch["fixture"] = fixture.as_posix()
if (ROOT / fixture).is_file():
return
command = ["python3", "my-tools/generate_wbv2_shader_fixture.py", "--output", fixture.as_posix()]
for unit in batch["units"]:
command.extend(["--node", unit["identifier"]])
result = subprocess.run(command, cwd=ROOT, text=True, capture_output=True, check=False)
if result.returncode:
raise SystemExit(f"native shader fixture preparation failed for {batch['moduleId']}:\n{result.stdout}{result.stderr}")
def native_geometry_batch(inventory: dict) -> dict | None:
candidates = [
unit for unit in inventory.get("units", [])
if isinstance(unit, dict)
and unit.get("coverageStatus") != "VERIFIED"
and unit.get("kind") == "node"
and isinstance(unit.get("identifier"), str)
and (unit["identifier"].startswith("GeometryNode") or unit["identifier"].startswith("FunctionNode") or unit["identifier"].startswith("Node"))
]
if not candidates:
return None
max_size = 5
units = candidates[:max_size]
return {
"moduleId": "AUTO_GEOMETRY_NATIVE_NODE_READER",
"title": "verify auto-generated native geometry node reader batch",
"owner": "NODES",
"identifiers": [unit["identifier"] for unit in units],
"rna": [],
"types": [unit["identifier"] for unit in units],
"units": units,
"candidateUnits": candidates,
"nativeGeometryFixture": True,
"test": "web/tests/e2e/geometry-node-main-reader.spec.ts",
"production": "tests/golden/{task}/geometry-node-reader-production.json",
"adaptiveKey": "AUTO_GEOMETRY_NATIVE_NODE_READER:LOCAL_EXACT:NODES",
"adaptiveHistory": [],
"adaptiveCurrentSize": len(units),
"adaptiveNextSize": len(units),
"adaptiveMinSize": 1,
"adaptiveMaxSize": max_size,
}
def native_shader_batch(inventory: dict) -> dict | None:
candidates = [unit for unit in inventory.get("units", []) if isinstance(unit, dict) and unit.get("coverageStatus") != "VERIFIED" and unit.get("kind") == "node" and isinstance(unit.get("identifier"), str) and unit["identifier"].startswith("ShaderNode")]
if not candidates:
return None
units = candidates[:5]
return {"moduleId": "AUTO_SHADER_NATIVE_NODE_READER", "title": "verify auto-generated native shader node reader batch", "owner": "NODES", "identifiers": [u["identifier"] for u in units], "rna": [], "types": [u["identifier"] for u in units], "units": units, "candidateUnits": candidates, "nativeShaderFixture": True, "test": "web/tests/e2e/shader-main-reader-production.spec.ts", "production": "tests/golden/{task}/shader-main-reader-production.json", "adaptiveKey": "AUTO_SHADER_NATIVE_NODE_READER:LOCAL_EXACT:NODES", "adaptiveHistory": [], "adaptiveCurrentSize": len(units), "adaptiveNextSize": len(units), "adaptiveMinSize": 1, "adaptiveMaxSize": 5}
def prepare_native_fixture(batch: dict, task: str, contract: dict) -> dict:
"""Generate a native fixture, shrinking only when Blender rejects a mix."""
if not (batch.get("nativeCompositorFixture") or batch.get("nativeGeometryFixture") or batch.get("nativeShaderFixture")):
return contract
while True:
try:
if batch.get("nativeCompositorFixture"):
ensure_native_compositor_fixture(batch, contract)
elif batch.get("nativeGeometryFixture"):
ensure_native_geometry_fixture(batch, contract)
else:
ensure_native_shader_fixture(batch, contract)
return contract
except SystemExit:
units = list(batch.get("units", []))
if len(units) <= 1:
raise
units = units[: max(1, len(units) // 2)]
batch["units"] = units
batch["identifiers"] = [unit["identifier"] for unit in units]
batch["types"] = [unit["identifier"] for unit in units]
batch.pop("fixture", None)
contract = batch_contract(task, batch)
def recover_blocked_native_batch(current: str, index: dict, plan: dict, inventory: dict) -> bool:
"""Shrink one failed auto-native batch to the last proven fixture shape.
This only applies to the active task's recorded BLOCKED receipt. The
failed receipt remains immutable evidence; the task contract is repaired
in place so the scheduler can retry the same bounded increment.
"""
record = next((item for item in plan.get("tasks", []) if item.get("id") == current), None)
module = record.get("module") if isinstance(record, dict) else None
receipt_path = ROOT / "tests/golden" / current / "module-receipt.json"
receipt = read_json(receipt_path) if receipt_path.is_file() else {}
if not isinstance(module, dict) or not str(module.get("moduleId", "")).startswith(("AUTO_COMPOSITOR_NATIVE_NODE_READER", "AUTO_SHADER_NATIVE_NODE_READER")):
return False
if receipt.get("status") != "BLOCKED":
return False
by_id = {unit.get("id"): unit for unit in inventory.get("units", []) if isinstance(unit, dict)}
selected_ids = list(receipt.get("selectedRecordIds", []))[:5]
units = [by_id[unit_id] for unit_id in selected_ids if unit_id in by_id]
if len(units) != len(selected_ids):
return False
is_shader = str(module["moduleId"]).startswith("AUTO_SHADER")
if not is_shader and len(receipt.get("selectedRecordIds", [])) <= 5:
return False
batch = {
"moduleId": str(module["moduleId"]),
"title": "recover native compositor node reader batch",
"owner": "NODES",
"identifiers": [unit["identifier"] for unit in units],
"rna": [],
"types": [unit["identifier"] for unit in units],
"units": units,
"nativeCompositorFixture": True,
"test": "web/tests/e2e/shader-main-reader-production.spec.ts" if is_shader else "web/tests/e2e/compositor-main-reader-production.spec.ts",
"production": "tests/golden/{task}/shader-main-reader-production.json" if is_shader else "tests/golden/{task}/compositor-main-reader-production.json",
"nativeShaderFixture": is_shader,
"nativeCompositorFixture": not is_shader,
}
contract = batch_contract(current, batch)
if is_shader:
ensure_native_shader_fixture(batch, contract)
else:
ensure_native_compositor_fixture(batch, contract)
fixture = batch["fixture"]
source_paths = [unit["locations"][0]["path"] for unit in units]
source_paths += [
"blender-5.2.0/source/blender/web_engine/web_engine_blend_reader.cpp",
batch["test"],
fixture,
"web/app/src/vendor/blender/web_engine.wasm",
]
types = json.dumps(batch["types"], separators=(",", ":"))
ids = json.dumps(selected_ids, separators=(",", ":"))
production = f"tests/golden/{current}/shader-main-reader-production.json" if is_shader else f"tests/golden/{current}/compositor-main-reader-production.json"
command = (
f"WBV2_BATCH_TASK={current} WBV2_BATCH_PRODUCTION={production} "
f"WBV2_BATCH_FIXTURE={fixture} WBV2_BATCH_TYPES='{types}' WBV2_BATCH_UNITS='{ids}' "
f"WEB_TEST_PORT=5371 npm --prefix web run test:e2e -- --workers=1 {batch['test']}"
)
module.update({
"batchId": contract["batchId"],
"selectedRecordIds": selected_ids,
"sourcePaths": source_paths,
"positive": [f"{name} type is read from Blender Main" for name in batch["identifiers"]]
+ ["shared RNA metadata is preserved", "save/reopen preserves the batch metadata"],
"commands": [
"source tools/web/emscripten-env.sh && cmake --build build_web_blender6 --target web_engine -- -j8 && cp build_web_blender6/bin/web_engine.wasm web/app/src/vendor/blender/web_engine.wasm && cp build_web_blender6/bin/web_engine.wasm web/app/public/vendor/blender/web_engine.wasm && node tools/web/split-wbv2-wasm.mjs --input web/app/public/vendor/blender/web_engine.wasm --output-dir web/app/public/vendor/blender/modules/single --split-funcs web_engine_get_live_handles && node tools/web/split-wbv2-wasm.mjs --input web/app/public/vendor/blender/pthread/web_engine.wasm --output-dir web/app/public/vendor/blender/modules/pthread --split-funcs web_engine_get_live_handles",
"npm --prefix web run typecheck",
"npm --prefix web run build",
command,
],
"coverageBindings": selected_ids,
})
record["batchId"] = contract["batchId"]
record["selectedRecordIds"] = selected_ids
write_json(ROOT / index["source"]["path"], plan)
run_node("tools/web/generate-v2-task-index.mjs", "--plan-path", index["source"]["path"])
card_path = ROOT / "docs/tasks" / f"{current}.md"
write_text(card_path, make_card(current, record.get("dependsOn", [""])[-1], "in_progress", batch, f"node tools/web/run-wbv2-module-task.mjs --task {current} --write", contract))
run_node("tools/web/check-task-context.mjs", "--task", current, "--write")
print(f"wbv2-blocked-batch-recovered task={current} batchId={contract['batchId']} selected={len(selected_ids)}")
return True
def discover_existing_evidence_batch(inventory: dict) -> dict | None:
"""Find exact Chromium receipts that list currently-unbound records.
This is an evidence-only discovery path: it never infers coverage from a
neighboring source file and never schedules a browser rerun. A new batch
receives a copied, task-bound evidence record through the module runner.
"""
unresolved = {unit["id"]: unit for unit in inventory.get("units", []) if isinstance(unit, dict) and unit.get("coverageStatus") != "VERIFIED"}
if not unresolved:
return None
operation_tests = (
("FCURVE", "web/tests/e2e/wbv2-fcurve-modifier-production.spec.ts"),
("LINE_STYLE", "web/tests/e2e/wbv2-line-style-reader-production.spec.ts"),
("SHAPE_KEY", "web/tests/e2e/wbv2-shape-key-reader-production.spec.ts"),
("MODIFIER", "web/tests/e2e/wbv2-modifier-reader-batch-two-production.spec.ts"),
)
for evidence_path in sorted(ROOT.glob("tests/golden/WBV2-P*-001/*.json"), key=lambda item: item.as_posix()):
try:
evidence = json.loads(evidence_path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError):
continue
if evidence.get("browser") != "chromium" or evidence.get("queueMutation") is not False:
continue
source_units = evidence.get("sourceUnits")
if not isinstance(source_units, list) or not source_units or not all(isinstance(unit, str) for unit in source_units):
continue
if not set(source_units).issubset(unresolved):
continue
operation = str(evidence.get("operation", ""))
test = next((path for marker, path in operation_tests if marker in operation), None)
fixture_paths = evidence.get("fixtures") if isinstance(evidence.get("fixtures"), list) else [evidence.get("fixture")]
fixture_paths = [fixture for fixture in fixture_paths if isinstance(fixture, str) and (ROOT / fixture).is_file()]
if not test or not (ROOT / test).is_file() or not fixture_paths:
continue
identifiers = [unit.split(":", 1)[1] for unit in source_units if unit.startswith("modifier-source:")]
rna = [unit.split(":", 1)[1] for unit in source_units if unit.startswith("rna-struct:")]
expected_types = evidence.get("expectedTypes") if isinstance(evidence.get("expectedTypes"), list) else []
return {
"moduleId": f"AUTO_EVIDENCE_REUSE_{evidence_path.parent.name}",
"title": f"reuse exact Chromium evidence from {evidence_path.parent.name}",
"owner": str(next((unresolved[unit].get("family") for unit in source_units if unresolved[unit].get("family")), "PROGRAM")),
"identifiers": identifiers,
"rna": rna,
"fixtures": fixture_paths,
"test": test,
"production": "tests/golden/{task}/auto-reused-production.json",
"types": expected_types,
"units": [unresolved[unit] for unit in source_units],
"candidateUnits": [unresolved[unit] for unit in source_units],
"reuseExistingEvidence": rel(evidence_path),
"reuseEvidenceSource": rel(evidence_path),
}
def pending_summary(inventory: dict) -> dict:
pending = [unit for unit in inventory.get("units", []) if isinstance(unit, dict) and unit.get("coverageStatus") != "VERIFIED"]
by_kind: dict[str, int] = {}
by_family: dict[str, int] = {}
for unit in pending:
by_kind[unit.get("kind", "UNKNOWN")] = by_kind.get(unit.get("kind", "UNKNOWN"), 0) + 1
by_family[unit.get("family", "UNKNOWN")] = by_family.get(unit.get("family", "UNKNOWN"), 0) + 1
return {"count": len(pending), "byKind": by_kind, "byFamily": by_family, "sample": [unit.get("id") for unit in pending[:12]]}
def run_node(*args: str) -> None:
result = subprocess.run(["node", *args], cwd=ROOT, text=True, capture_output=True, check=False)
output = result.stdout or result.stderr
if output:
print(output, end="" if output.endswith("\n") else "\n")
if result.returncode:
raise SystemExit(result.returncode)
def current_task() -> str:
match = re.search(r"\| 当前任务 \| `(WBV2-P(\d+)-(\d{3}))` \|", QUEUE.read_text(encoding="utf-8"))
if not match:
raise SystemExit("queue pointer is not a WBV2 task")
return match.group(1)
def queue_parent() -> str:
match = re.search(r"\| parent manifest \| `([^`]+)` \|", QUEUE.read_text(encoding="utf-8"), re.IGNORECASE)
if not match:
raise SystemExit("queue parent manifest is missing")
return match.group(1)
def snapshot(text: str, task: str, parent: str, next_task: str) -> str:
tick = "`"
substitutions = [
(r"\| Current task when this file was written \| `[^`]+` \|", f"| Current task when this file was written | {tick}{task}{tick} |"),
(r"\| Current task state \| `[^`]+` \|", f"| Current task state | {tick}in_progress{tick} |"),
(r"\| Parent manifest \| `[^`]+` \|", f"| Parent manifest | {tick}tests/golden/{parent}/manifest.json{tick} |"),
(r"\| Current task card \| `[^`]+` \|", f"| Current task card | {tick}docs/tasks/{task}.md{tick} |"),
(r"\| Current task-context nextTask \| `[^`]+` \|", f"| Current task-context nextTask | {tick}{next_task}{tick} |"),
(r"\| Focused command \| `[^`]+` \|", f"| Focused command | {tick}node tools/web/run-wbv2-module-task.mjs --task {task} --write{tick} |"),
]
for pattern, replacement in substitutions:
text = re.sub(pattern, replacement, text, count=1)
return text
def make_card(task: str, parent: str, status: str, batch: dict, entrypoint: str, contract: dict) -> str:
fixture_paths = batch.get("fixtures") or ([batch["fixture"]] if batch.get("fixture") else [])
fixture_text = ", ".join(f"`{fixture}`" for fixture in fixture_paths)
if not fixture_text:
fixture_text = "existing Chromium receipts"
if task.endswith("-000"):
return f"""# {task}: authorize module batch
- task: {task}
- parent: {parent}
- status: {status}
- gap: capability:{batch['moduleId']}
- ownerFamily: {batch['owner']}
- targetImplementationClass: QUEUE_ONLY
- batchId: {contract['batchId']}
- selectedRecordIds: {json.dumps(contract['selectedRecordIds'], ensure_ascii=False, separators=(',', ':'))}
- commandBudget: max=48; read/exploration max=16
- Implementation entrypoint: `{entrypoint}`
## 目标
授权一个共享 Blender Main/WASM/Chromium 功能批次:{batch['moduleId']},共 {len(batch['identifiers']) + len(batch['rna'])} 个源码/RNA 单元。
## 输入与范围
- Blender source: `blender-5.2.0/source/blender`
- Fixtures/test: {fixture_text}, `{batch['test']}`
## 验收
- prior evidence and dynamic inventory remain hash-bound and immutable
- one shared function chain validates the complete selected batch
- missing source, unsupported data, hash drift or false completion remains blocked
- `node tools/web/check-task-context.mjs --task {task} --write`
## 交付与回滚
- Manifest: `tests/golden/{task}/manifest.json`
- Status: `docs/status/{task}.md`
- Rollback: restore {parent} as queue parent.
"""
if len(batch["identifiers"]) + len(batch["rna"]) > 16:
return f"""# {task}: {batch['title']}
- task: {task}
- parent: {parent}
- status: {status}
- gap: capability:{batch['moduleId']}
- ownerFamily: {batch['owner']}
- targetImplementationClass: LOCAL_EXACT
- batchId: {contract['batchId']}
- selectedRecordIds: {json.dumps(contract['selectedRecordIds'], ensure_ascii=False, separators=(',', ':'))}
- commandBudget: max=48; read/exploration max=16
- Implementation entrypoint: `{entrypoint}`
## 目标
使用一个共享 Blender Main/WASM/Worker/Chromium 链验证 {len(batch['identifiers'])} 个 modifier source 与 {len(batch['rna'])} 个 RNA unit,形成一个聚合 receipt。
## 输入与范围
- Blender source family: `blender-5.2.0/source/blender`
- Fixtures: {fixture_text}
- Chromium test: `{batch['test']}`
- Unit selection: `{len(batch['identifiers']) + len(batch['rna'])} units`(完整列表只在 module plan/manifest)
## 验收
- one shared function chain covers every selected unit exactly once
- malformed input, unsupported data and hash drift fail closed without Main mutation
- save/reopen, deterministic second read and Chromium evidence are exact
- `node tools/web/check-task-context.mjs --task {task} --write`
## 失败边界
- partial batch success never advances or claims omitted units
- existing source/receipt hashes are reused; historical inventory remains read-only
## 交付与回滚
- Aggregate receipt: `tests/golden/{task}/module-receipt.json`
- Production evidence: `tests/golden/{task}/modifier-reader-batch-production.json`
- Manifest: `tests/golden/{task}/manifest.json`
- Rollback: restore {parent} as queue parent.
"""
names = ", ".join(batch["identifiers"]) or "RNA-only reader units"
units = ", ".join(f"`modifier-source:{name}`" for name in batch["identifiers"]) or "none"
rna_units = ", ".join(f"`rna-struct:{name}`" for name in batch["rna"]) or "none (source-only dependency batch)"
return f"""# {task}: {batch['title']}
- task: {task}
- parent: {parent}
- status: {status}
- gap: capability:{batch['moduleId']}
- ownerFamily: {batch['owner']}
- targetImplementationClass: LOCAL_EXACT
- batchId: {contract['batchId']}
- selectedRecordIds: {json.dumps(contract['selectedRecordIds'], ensure_ascii=False, separators=(',', ':'))}
- commandBudget: max=48; read/exploration max=16
- Implementation entrypoint: `{entrypoint}`
## 目标
以一个共享 Blender Main reader、WASM build、Worker 路由和 Chromium receipt 批量验证 {names} 的真实 modifier/RNA 元数据。
## 输入与范围
- Blender modifier sources: `blender-5.2.0/source/blender/modifiers/intern/`
- RNA source: `blender-5.2.0/source/blender/makesrna/intern/rna_modifier.cc`
- Native reader: `blender-5.2.0/source/blender/web_engine/web_engine_blend_reader.cpp`
- Fixtures: {fixture_text}
- Chromium test: `{batch['test']}`
- Selected modifier units: {units}
- Selected RNA units: {rna_units}
## 验收
- one shared Main/WASM/Worker/Chromium chain observes every selected type
- malformed input is rejected without Main mutation
- save/reopen and deterministic second read are exact
- only Chromium is used; JS simulation and per-API duplicate workers are forbidden
- each selected source unit receives an explicit hash-bound coverage binding
- `node tools/web/check-task-context.mjs --task {task} --write`
## 失败边界
- any missing source, unsupported type, hash drift, malformed data or non-exact recovery keeps the batch `in_progress`/`blocked`
- partial batch success never claims the omitted family; historical inventory remains read-only
## 交付与回滚
- Receipt: `tests/golden/{task}/module-receipt.json`
- Aggregate production evidence: `tests/golden/{task}/modifier-reader-batch-production.json`
- Manifest: `tests/golden/{task}/manifest.json`
- Status: `docs/status/{task}.md`
- Rollback: restore {parent} as queue parent without rewriting prior evidence.
"""
def select_batch(inventory: dict) -> dict:
units = {unit.get("identifier"): unit for unit in inventory.get("units", []) if isinstance(unit, dict)}
for candidate in PREFERRED_BATCHES:
selected = [units.get(name) for name in candidate["identifiers"] + candidate["rna"]]
if all(selected):
unresolved = [unit for unit in selected if unit.get("coverageStatus") != "VERIFIED"]
if unresolved:
key = f"{candidate['moduleId']}:LOCAL_EXACT:{candidate['owner']}"
history = []
for receipt_path in sorted(ROOT.glob("tests/golden/WBV2-P*-001/module-receipt.json"), key=lambda item: item.parent.name):
receipt = read_json(receipt_path)
if receipt.get("moduleId") != candidate["moduleId"]:
continue
duration = receipt.get("efficiency", {}).get("totalDurationMs")
if not isinstance(duration, (int, float)):
duration = sum(item.get("durationMs", 0) for item in receipt.get("commands", []) if isinstance(item, dict))
history.append({
"durationMs": duration,
"commands": len(receipt.get("commands", [])),
"timeout": any("timeout" in str(item.get("output", "")).lower() for item in receipt.get("commands", []) if isinstance(item, dict)),
"focusedGateFailure": receipt.get("status") != "PASS",
})
recent = history[-3:]
min_size = 1
max_size = 8 if len(unresolved) > 16 or len(candidate.get("fixtures", [])) > 1 else 16
current_size = min(len(unresolved), max_size)
if recent and any(item["timeout"] or item["durationMs"] > 900_000 or item["commands"] > 48 for item in recent):
next_size = max(min_size, current_size // 2)
elif recent and all(item["durationMs"] < 300_000 and item["commands"] < 24 and not item["focusedGateFailure"] for item in recent):
next_size = min(max_size, current_size + 4)
else:
next_size = current_size
chosen = unresolved[:max(min_size, min(max_size, len(unresolved), next_size))]
return {**candidate, "units": chosen, "candidateUnits": selected, "adaptiveKey": key, "adaptiveHistory": recent, "adaptiveCurrentSize": current_size, "adaptiveNextSize": len(chosen), "adaptiveMinSize": min_size, "adaptiveMaxSize": max_size}
discovered = discover_existing_evidence_batch(inventory)
if discovered:
return discovered
native = native_compositor_batch(inventory)
if native:
return native
geometry = native_geometry_batch(inventory)
if geometry:
return geometry
shader = native_shader_batch(inventory)
if shader:
return shader
summary = pending_summary(inventory)
write_planner_status({
"status": "waiting_for_exact_evidence",
"reason": "no_configured_unresolved_source_batch",
"pending": summary,
"nextAction": "add a real fixture/adapter or exact Chromium receipt; do not fabricate coverage",
})
raise SystemExit(f"no configured unresolved Blender source batch remains; pending={summary['count']} sample={summary['sample']}")
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--write", action="store_true")
args = parser.parse_args()
current = current_task()
parent_path = queue_parent()
current_manifest_path = ROOT / "tests/golden" / current / "manifest.json"
current_manifest = read_json(current_manifest_path)
index = read_json(INDEX)
plan_path = ROOT / index["source"]["path"]
plan = json.loads(plan_path.read_text(encoding="utf-8"))
inventory = read_json(CONTROL_ROOT / "dynamic-inventory.json")
if recover_blocked_native_batch(current, index, plan, inventory):
return 0
terminal = current_manifest.get("status") == "done" and current_manifest.get("nextTask") is None and index.get("activeTask") is None
if not terminal and not current_manifest.get("nextTask"):
raise SystemExit(f"current task is not terminal: {current}")
phase = max(int(item["id"].split("-")[1][1:]) for item in plan["tasks"]) + 1
root_task, module_task = f"WBV2-P{phase}-000", f"WBV2-P{phase}-001"
batch = select_batch(inventory)
contract = batch_contract(module_task, batch)
batch["batchId"] = contract["batchId"]
if batch.get("nativeCompositorFixture"):
digest = contract["batchId"].rsplit(":", 1)[-1]
batch["fixture"] = f"tests/files/web/generated/wbv2-compositor-{digest}.blend"
if args.write:
contract = prepare_native_fixture(batch, module_task, contract)
if batch.get("nativeGeometryFixture"):
digest = contract["batchId"].rsplit(":", 1)[-1]
batch["fixture"] = f"tests/files/web/generated/wbv2-geometry-{digest}.blend"
if args.write:
contract = prepare_native_fixture(batch, module_task, contract)
if batch.get("nativeShaderFixture"):
digest = contract["batchId"].rsplit(":", 1)[-1]
batch["fixture"] = f"tests/files/web/generated/wbv2-shader-{digest}.blend"
if args.write:
ensure_native_shader_fixture(batch, contract)
batch["batchId"] = contract["batchId"]
write_planner_status({
"status": "ready",
"reason": "source_backed_batch_selected",
"task": module_task,
"moduleId": batch["moduleId"],
"batchId": contract["batchId"],
"selectedRecordIds": contract["selectedRecordIds"],
"nextAction": "scheduler runs the focused module gate",
})
if not args.write:
print(json.dumps({"status": "ready", "rootTask": root_task, "nextTask": module_task, "moduleId": batch["moduleId"], "batchId": contract["batchId"], "batchSize": len(batch["units"]), "selectedRecordIds": contract["selectedRecordIds"], "sourceUnits": [unit["id"] for unit in batch["units"]], "adaptive": {"key": batch.get("adaptiveKey"), "history": batch.get("adaptiveHistory", []), "nextSize": batch.get("adaptiveNextSize", len(batch["units"]))}}, ensure_ascii=False))
return 0
current_status = ROOT / "docs/status" / f"{current}.md"
if current_status.is_file():
status_text = re.sub(r"- nextTask: `[^`]+`", f"- nextTask: `{root_task}`", current_status.read_text(encoding="utf-8"))
write_text(current_status, status_text)
current_manifest["nextTask"] = root_task
if isinstance(current_manifest.get("artifacts"), dict) and isinstance(current_manifest["artifacts"].get("status"), dict):
current_manifest["artifacts"]["status"]["sha256"] = sha(current_status)
write_json(current_manifest_path, current_manifest)
source_paths = [unit["locations"][0]["path"] for unit in batch["units"]]
fixture_paths = batch.get("fixtures") or ([batch["fixture"]] if batch.get("fixture") else [])
if batch.get("dependencyReuse"):
source_paths += [dependency["proofSource"] for dependency in batch["dependencyReuse"]]
source_paths += [dependency["productionEvidence"] for dependency in batch["dependencyReuse"]]
source_paths += [batch["test"]]
else:
source_paths += [
"blender-5.2.0/source/blender/web_engine/web_engine_blend_reader.cpp",
batch["test"],
*fixture_paths,
"web/app/src/vendor/blender/web_engine.wasm",
"web/app/public/vendor/blender/web_engine.wasm",
"web/app/public/wasm-module-manifest.json",
"web/app/public/vendor/blender/modules/single/web_engine.secondary.wasm",
"web/app/public/vendor/blender/modules/pthread/web_engine.secondary.wasm",
]
batch_types = json.dumps(batch["types"], separators=(',', ':'))
batch_units = json.dumps([unit["id"] for unit in batch["units"]], separators=(',', ':'))
if batch.get("dependencyReuse"):
fixture_env = ""
elif batch.get("fixtures"):
fixture_env = f"WBV2_BATCH_FIXTURES='{json.dumps(fixture_paths, separators=(',', ':'))}'"
else:
fixture_env = f"WBV2_BATCH_FIXTURE={batch['fixture']}"
production_env = f"WBV2_BATCH_PRODUCTION={batch['production'].format(task=module_task)}"
if batch.get("dependencyReuse"):
commands = [f"node tools/web/reuse-wbv2-source-dependencies.mjs --task {module_task} --write"]
else:
commands = [
"source tools/web/emscripten-env.sh && cmake --build build_web_blender6 --target web_engine -- -j8 && cp build_web_blender6/bin/web_engine.wasm web/app/src/vendor/blender/web_engine.wasm && cp build_web_blender6/bin/web_engine.wasm web/app/public/vendor/blender/web_engine.wasm && node tools/web/split-wbv2-wasm.mjs --input web/app/public/vendor/blender/web_engine.wasm --output-dir web/app/public/vendor/blender/modules/single --split-funcs web_engine_get_live_handles && node tools/web/split-wbv2-wasm.mjs --input web/app/public/vendor/blender/pthread/web_engine.wasm --output-dir web/app/public/vendor/blender/modules/pthread --split-funcs web_engine_get_live_handles",
"npm --prefix web run typecheck",
"npm --prefix web run build",
f"WBV2_BATCH_TASK={module_task} {production_env} {fixture_env} WBV2_BATCH_TYPES='{batch_types}' WBV2_BATCH_UNITS='{batch_units}' WEB_TEST_PORT=5371 npm --prefix web run test:e2e -- --workers=1 tests/e2e/{Path(batch['test']).name}",
]
root_record = {"id": root_task, "phase": str(phase), "title": "authorize_modifier_reader_batch", "kind": "enabling", "owner": "PROGRAM", "dependsOn": [current], "parityId": None, "implementationClass": "QUEUE_ONLY", "parallelGroup": f"module-root-{phase}", "gate": "MODULE", "card": f"docs/tasks/{root_task}.md", "behavior": "Authorize one shared source-backed modifier reader batch.", "outputs": ["successor authorization", "module plan", "manifest", "status"], "acceptance": ["prior evidence remains hash-bound", "batch source mapping is explicit", "inventory remains source-derived"], "commands": ["node tools/web/check-execution-control-plane.mjs"], "status": "done", "ready": False, "queueMutationAllowed": True}
module_record = {"id": module_task, "phase": str(phase), "title": batch["title"], "kind": "module", "owner": batch["owner"], "dependsOn": [root_task], "parityId": batch["moduleId"], "implementationClass": "LOCAL_EXACT", "parallelGroup": f"module-{batch['moduleId'].lower()}", "gate": "MODULE", "card": f"docs/tasks/{module_task}.md", "behavior": f"Verify the {batch['moduleId']} batch through Blender Main, WASM and Chromium recovery.", "outputs": ["aggregate module receipt", "coverage bindings", "manifest", "status"], "acceptance": ["source and protocol hashes are bound", "every selected Main reader type is proven", "negative no-mutation and recovery are proven", "Chromium and save/reopen evidence are exact"], "status": "in_progress", "ready": True, "queueMutationAllowed": True, "batchId": contract["batchId"], "selectedRecordIds": contract["selectedRecordIds"], "module": {"moduleId": batch["moduleId"], "batchId": contract["batchId"], "selectedRecordIds": contract["selectedRecordIds"], "sourceBacked": True, "capabilities": [f"CAP-{batch['moduleId']}-READER", f"CAP-{batch['moduleId']}-RECOVERY", f"CAP-{batch['moduleId']}-FAIL-CLOSED"], "sourcePaths": source_paths, "reuseReceipts": batch.get("reuseReceipts", [{"manifest": "tests/golden/WBV2-P1107-000/manifest.json", "report": "tests/golden/WBV2-P1107-000/module-receipt.json"}]), "dependencyReuse": batch.get("dependencyReuse", []), "positive": [f"{name} type is read from Blender Main" for name in batch["identifiers"]] + ["shared RNA metadata is preserved", "save/reopen preserves the batch metadata"], "negative": ["malformed blend is rejected without Main mutation", "unsupported batch data fails closed", "non-finite metadata is not synthesized"], "recovery": ["save/reopen", "worker termination", "deterministic second read"], "commands": commands, "fullFamilyParity": False, "evidencePaths": [batch["production"].format(task=module_task)], "coverageBindings": contract["selectedRecordIds"]}}
module_record["module"]["commandBudget"] = {"maxCommands": 48, "maxReadCommands": 16}
if batch.get("reuseExistingEvidence"):
module_record["module"]["reuseExistingEvidence"] = batch["reuseExistingEvidence"]
root_out = CONTROL_ROOT.parent / root_task
root_plan_path = root_out / "module-execution-plan.json"
root_plan = {**plan, "planId": f"BLENDER_WEB_MODULE_EXECUTION_P{phase}", "status": "ACTIVE_MODULE_QUEUE", "tasks": [*plan["tasks"], root_record, module_record], "modules": [*plan.get("modules", []), {"id": batch["moduleId"], "task": module_task, "owner": batch["owner"], "sourceBacked": True, "batchId": contract["batchId"], "selectedRecordIds": contract["selectedRecordIds"], "capabilities": module_record["module"]["capabilities"], "sourcePaths": source_paths, "reuseReceipts": module_record["module"]["reuseReceipts"], "fullFamilyParity": False}]}
write_json(root_plan_path, root_plan)
root_card_path, module_card_path = ROOT / "docs/tasks" / f"{root_task}.md", ROOT / "docs/tasks" / f"{module_task}.md"
write_text(root_card_path, make_card(root_task, current, "done", batch, f"python3 my-tools/authorize_next_wbv2_task.py --write", contract))
write_text(module_card_path, make_card(module_task, root_task, "in_progress", batch, f"node tools/web/run-wbv2-module-task.mjs --task {module_task} --write", contract))
root_report_path, root_status_path, root_manifest_path = root_out / "successor-authorization-report.json", ROOT / "docs/status" / f"{root_task}.md", root_out / "manifest.json"
write_json(root_report_path, {"schemaVersion": 1, "task": root_task, "operation": "AUTHORIZE_SOURCE_BACKED_MODULE_BATCH", "taskClass": "QUEUE_ENABLEMENT", "status": "PASS", "parent": {"task": current, "manifest": rel(current_manifest_path), "manifestSha256": sha(current_manifest_path)}, "batchId": contract["batchId"], "selectedRecordIds": contract["selectedRecordIds"], "module": {"moduleId": batch["moduleId"], "task": module_task, "batchSize": len(batch["units"]), "candidateBatchSize": len(batch.get("candidateUnits", batch["units"])), "sourceUnits": contract["selectedRecordIds"], "sourceBacked": True}, "inventory": {"unitCount": inventory["summary"]["unitCount"], "verifiedUnits": inventory["coverage"]["verifiedUnits"], "unverifiedUnits": inventory["coverage"]["unverifiedUnits"], "sourceTreeSha256": inventory["authority"]["sourceTreeSha256"], "boundUnitDelta": 0}, "efficiency": {"existingBindingsReused": len(batch.get("candidateUnits", batch["units"])) - len(batch["units"]), "selectionPolicy": "all-candidate-units-then-unresolved-only", "adaptiveKey": batch.get("adaptiveKey"), "adaptiveHistory": batch.get("adaptiveHistory", []), "adaptiveNextSize": batch.get("adaptiveNextSize", len(batch["units"]))}, "nextTask": module_task})
write_text(root_status_path, f"# {root_task} status\n\n- task: `{root_task}`; parent: `{current}`; status: `done`\n- taskClass: `QUEUE_ENABLEMENT`; no modifier-family parity completion claimed\n- source-backed batch authorized: `{batch['moduleId']}` ({len(batch['units'])} units)\n- dynamic inventory: `{inventory['summary']['unitCount']}` units; verified `{inventory['coverage']['verifiedUnits']}`; unverified `{inventory['coverage']['unverifiedUnits']}`; bound delta `0`\n- nextTask: `{module_task}`\n")
write_json(root_manifest_path, {"schemaVersion": 1, "task": root_task, "parentTask": current, "operation": "AUTHORIZE_SOURCE_BACKED_MODULE_BATCH", "status": "done", "enablingTask": True, "parityStateChange": False, "runtime": "MODULE_EXECUTION_QUEUE_BRIDGE", "batchId": contract["batchId"], "selectedRecordIds": contract["selectedRecordIds"], "artifacts": {"parentManifest": {"path": rel(current_manifest_path), "sha256": sha(current_manifest_path)}, "plan": {"path": rel(root_plan_path), "sha256": sha(root_plan_path)}, "report": {"path": rel(root_report_path), "sha256": sha(root_report_path)}, "status": {"path": rel(root_status_path), "sha256": sha(root_status_path)}}, "assertions": {"sourceBackedSuccessor": True, "moduleId": batch["moduleId"], "batchSize": len(batch["units"]), "inventoryTotal": 6900, "effectiveCompleted": 290, "effectivePending": 6609, "dynamicUnitCount": inventory["summary"]["unitCount"], "dynamicVerifiedUnits": inventory["coverage"]["verifiedUnits"], "dynamicUnverifiedUnits": inventory["coverage"]["unverifiedUnits"], "boundUnitDelta": 0, "fullHistoricalReproduction": False}, "nextTask": module_task})
run_node("tools/web/generate-v2-task-index.mjs", "--plan-path", rel(root_plan_path))
queue_text = QUEUE.read_text(encoding="utf-8")
queue_text = re.sub(r"\| 当前任务 \| `[^`]+` \|", f"| 当前任务 | `{module_task}` |", queue_text, count=1)
queue_text = re.sub(r"\| parent manifest \| `[^`]+` \|", f"| parent manifest | `tests/golden/{root_task}/manifest.json` |", queue_text, count=1, flags=re.IGNORECASE)
queue_text = re.sub(r"\| 任务卡 \| \[[^\]]+\]\([^\)]+\) \|", f"| 任务卡 | [`tasks/{module_task}.md`](tasks/{module_task}.md) |", queue_text, count=1)
write_text(QUEUE, queue_text)
next_path = ROOT / "nextTask.md"
if next_path.is_file():
write_text(next_path, snapshot(next_path.read_text(encoding="utf-8"), module_task, root_task, "NONE"))
# Do not rerun the legacy hard-coded reconciliation here: it would discard
# newer explicit bindings produced by completed module batches. The active
# module's binder owns the incremental inventory update.
# The root is a completed queue-enable record. Preparing its context again
# wastes budget and can fail solely because the parent manifest grew; only
# the active implementation batch needs a packet.
print(f"wbv2-successor-authorized root={root_task} next={module_task} module={batch['moduleId']} batchSize={len(batch['units'])} reused={len(batch.get('candidateUnits', batch['units'])) - len(batch['units'])} inventoryDelta=0")
return 0
if __name__ == "__main__":
raise SystemExit(main())