#!/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": "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 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 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")) 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" inventory = read_json(CONTROL_ROOT / "dynamic-inventory.json") batch = select_batch(inventory) contract = batch_contract(module_task, batch) 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"] 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", "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())