Lane E S15 gate 1.2: re-bake sail_post top_anchor 0.90 -> 1.00 per the gate 1.1 ruling; factory emits the per-type anchor ratings manifest (anchor_ratings.gen.js) from the re-imported GLBs, ambiguity-aware, --only-guarded; asset_report carries per-asset anchor extras

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
type-two 2026-07-20 17:49:38 +10:00
parent c182fc14cb
commit e46ff18648
4 changed files with 399 additions and 3 deletions

View File

@ -14,6 +14,7 @@
"ref_capsule",
"ref_capsule_mesh"
],
"anchors": [],
"status": "PASS",
"problems": []
},
@ -36,6 +37,23 @@
"tree_gum_01",
"trunk"
],
"anchors": [
{
"node": "branch_anchor_01",
"anchor_type": "tree",
"rating_hint": 1.0
},
{
"node": "branch_anchor_02",
"anchor_type": "tree",
"rating_hint": 0.88
},
{
"node": "branch_anchor_03",
"anchor_type": "tree",
"rating_hint": 0.76
}
],
"status": "PASS",
"problems": []
},
@ -56,6 +74,18 @@
"tree_gum_02",
"trunk"
],
"anchors": [
{
"node": "branch_anchor_01",
"anchor_type": "tree",
"rating_hint": 1.0
},
{
"node": "branch_anchor_02",
"anchor_type": "tree",
"rating_hint": 0.88
}
],
"status": "PASS",
"problems": []
},
@ -79,6 +109,23 @@
"tree_jacaranda_01",
"trunk"
],
"anchors": [
{
"node": "branch_anchor_01",
"anchor_type": "tree",
"rating_hint": 0.95
},
{
"node": "branch_anchor_02",
"anchor_type": "tree",
"rating_hint": 0.52
},
{
"node": "branch_anchor_03",
"anchor_type": "tree",
"rating_hint": 0.4
}
],
"status": "PASS",
"problems": []
},
@ -94,6 +141,7 @@
"fence_post",
"post"
],
"anchors": [],
"status": "PASS",
"problems": []
},
@ -110,6 +158,7 @@
"palings",
"rails"
],
"anchors": [],
"status": "PASS",
"problems": []
},
@ -128,6 +177,7 @@
"hinge_axis",
"hinges"
],
"anchors": [],
"status": "PASS",
"problems": []
},
@ -153,6 +203,30 @@
"window_glow",
"window_light_anchor"
],
"anchors": [
{
"node": "fascia_anchor_01",
"anchor_type": "house",
"rating_hint": 0.35,
"collateral": "gutter"
},
{
"node": "fascia_anchor_02",
"anchor_type": "house",
"rating_hint": 0.35,
"collateral": "gutter"
},
{
"node": "fascia_anchor_03",
"anchor_type": "house",
"rating_hint": 0.35,
"collateral": "gutter"
},
{
"node": "window_light_anchor",
"tie_off": false
}
],
"status": "PASS",
"problems": []
},
@ -178,6 +252,12 @@
"window_glow",
"window_light_anchor"
],
"anchors": [
{
"node": "window_light_anchor",
"tie_off": false
}
],
"status": "PASS",
"problems": []
},
@ -200,6 +280,32 @@
"posts",
"roof"
],
"anchors": [
{
"node": "beam_anchor_01",
"anchor_type": "carport",
"rating_hint": 0.22,
"collateral": "carport"
},
{
"node": "beam_anchor_02",
"anchor_type": "carport",
"rating_hint": 0.22,
"collateral": "carport"
},
{
"node": "post_anchor_01",
"anchor_type": "carport_post",
"rating_hint": 0.3,
"collateral": "carport"
},
{
"node": "post_anchor_02",
"anchor_type": "carport_post",
"rating_hint": 0.3,
"collateral": "carport"
}
],
"status": "PASS",
"problems": []
},
@ -218,6 +324,7 @@
"posts",
"roof_down"
],
"anchors": [],
"status": "PASS",
"problems": []
},
@ -237,6 +344,24 @@
"swing_set_01",
"swings"
],
"anchors": [
{
"node": "crossbar",
"tie_off": false
},
{
"node": "frame_anchor_01",
"anchor_type": "swing_frame",
"rating_hint": 0.45,
"collateral": "swing_set"
},
{
"node": "frame_anchor_02",
"anchor_type": "swing_frame",
"rating_hint": 0.45,
"collateral": "swing_set"
}
],
"status": "PASS",
"problems": []
},
@ -254,6 +379,12 @@
"swing_set_01_wrecked",
"swings"
],
"anchors": [
{
"node": "crossbar",
"tie_off": false
}
],
"status": "PASS",
"problems": []
},
@ -272,6 +403,12 @@
"shed_01",
"shell"
],
"anchors": [
{
"node": "door_anchor",
"tie_off": false
}
],
"status": "PASS",
"problems": []
},
@ -289,6 +426,12 @@
"table_frame",
"table_top"
],
"anchors": [
{
"node": "pickup_anchor",
"tie_off": false
}
],
"status": "PASS",
"problems": []
},
@ -308,6 +451,7 @@
"plants_tattered",
"soil"
],
"anchors": [],
"status": "PASS",
"problems": []
},
@ -327,6 +471,13 @@
"sail_post",
"top_anchor"
],
"anchors": [
{
"node": "top_anchor",
"anchor_type": "post",
"rating_hint": 1.0
}
],
"status": "PASS",
"problems": []
},
@ -344,6 +495,7 @@
"ladder_base",
"ladder_top"
],
"anchors": [],
"status": "PASS",
"problems": []
},
@ -360,6 +512,7 @@
"pin",
"shackle"
],
"anchors": [],
"status": "PASS",
"problems": []
},
@ -376,6 +529,7 @@
"carabiner",
"gate"
],
"anchors": [],
"status": "PASS",
"problems": []
},
@ -393,6 +547,7 @@
"eye_b",
"turnbuckle"
],
"anchors": [],
"status": "PASS",
"problems": []
},
@ -411,6 +566,7 @@
"rim",
"tramp_01"
],
"anchors": [],
"status": "PASS",
"problems": []
},
@ -429,6 +585,7 @@
"wheelie_bin_01",
"wheels"
],
"anchors": [],
"status": "PASS",
"problems": []
},
@ -446,6 +603,7 @@
"mast",
"washing_line_01"
],
"anchors": [],
"status": "PASS",
"problems": []
},
@ -461,6 +619,7 @@
"garden_gnome_01",
"gnome"
],
"anchors": [],
"status": "PASS",
"problems": []
},
@ -479,6 +638,7 @@
"wheel_front",
"wheel_rear"
],
"anchors": [],
"status": "PASS",
"problems": []
},
@ -497,6 +657,7 @@
"shards",
"stump"
],
"anchors": [],
"status": "PASS",
"problems": []
},
@ -512,6 +673,7 @@
"hail_stone_01",
"stone"
],
"anchors": [],
"status": "PASS",
"problems": []
},
@ -531,6 +693,12 @@
"head",
"poke_tip"
],
"anchors": [
{
"node": "grip_anchor",
"tie_off": false
}
],
"status": "PASS",
"problems": []
},
@ -548,6 +716,7 @@
"palings",
"rails"
],
"anchors": [],
"status": "PASS",
"problems": []
}

View File

@ -70,6 +70,16 @@ DEBRIS_DIR = os.path.join(MODELS_DIR, "debris")
TEXTURES_DIR = os.path.join(MODELS_DIR, "textures")
CONTACT_SHEET = os.path.join(SCRIPT_DIR, "contact_sheet.png")
REPORT_JSON = os.path.join(SCRIPT_DIR, "asset_report.json")
# SPRINT15 gate 1.2 — the ratings manifest the RUNTIME reads. A baked
# rating_hint used to reach the sim only through adoptAnchor, which only runs
# for anchors that name a GLB `node` — every plain-JSON post defaulted to
# `?? 1` no matter what this factory baked, and factory and sim silently
# disagreed for fourteen sprints. This module is the missing wire: per
# anchor_type, the factory's baked extras, generated HERE so it cannot be
# hand-edited into a second source of truth. world.js resolves a node-less
# JSON anchor's type through it; e.test.js pins runtime === manifest === GLB.
RATINGS_GEN_JS = os.path.join(REPO_ROOT, "web", "world", "js",
"anchor_ratings.gen.js")
# The 3D-STORE library on this box. PLAN3D §2 lists ~/Documents/3D-STORE (that
# path is from the M1 Ultra); on the M3 Ultra the library lives here. Checked in
@ -1684,7 +1694,16 @@ def build_sail_post(name):
e = add_empty("top_anchor", (0, 0, H - 0.02), size=0.2)
e["anchor_type"] = "post"
e["rating_hint"] = 0.9
# SPRINT15 gate 1.1 RULING (THREADS [A] 2026-07-18): the honest post is
# 1.00 — the shipped value becomes canon. The 0.90 baked here from Sprint 1
# never reached the sim: every shipped post is a plain JSON entry with no
# `node`, so adoptAnchor never ran and `?? 1` won. Every measured number in
# the repo (separation pins, winnable-lines tables, night 5's whisker) was
# measured at effective 1.00; 0.90 was design intent that never shipped.
# A weaker honest post later is a DESIGN CHANGE proposed with evidence,
# wearing its own name — not a bugfix. Do not lower this without
# re-opening that ruling.
e["rating_hint"] = 1.0
above.append(e)
for o in above:
parent_keep_transform(o, rake)
@ -3137,6 +3156,104 @@ def add_label(cam, text):
return t
def extract_anchor_meta(objs):
"""The anchor-family custom props (`anchor_type`, `rating_hint`, `tie_off`,
`collateral`), read off the RE-IMPORTED GLB rather than the build graph
so the report describes what actually shipped through the exporter, not
what this script intended. Same provenance rule as the dims/nodes checks.
"""
out = []
for o in objs:
keys = set(o.keys())
if "anchor_type" not in keys and "tie_off" not in keys:
continue
m = {"node": o.name}
if "anchor_type" in keys:
m["anchor_type"] = str(o["anchor_type"])
if "rating_hint" in keys:
m["rating_hint"] = round(float(o["rating_hint"]), 4)
if "tie_off" in keys:
m["tie_off"] = bool(o["tie_off"])
if "collateral" in keys:
m["collateral"] = str(o["collateral"])
out.append(m)
return sorted(out, key=lambda m: m["node"])
def build_ratings_manifest(report):
"""Collapse the per-asset anchor meta into per-TYPE ratings the runtime can
resolve. The contract:
· `tie_off: false` nodes are DENIALS, not anchors excluded.
· a type where every rated node in the palette agrees on
(rating_hint, collateral) is UNAMBIGUOUS and lands in `types` a
node-less JSON anchor of that type takes these extras at runtime.
· a type whose nodes disagree (tree: 1.0/0.88/0.76 down the trunk) is
AMBIGUOUS listed with every variant so the runtime can refuse to
guess. A node-less anchor of an ambiguous type is a site bug; e.test
pins that no shipped site declares one.
· a node that carries `anchor_type` but no `rating_hint` and no denial
violates RULE 1 (no silent anchors) and fails the build loudly here,
before the GLB can ship.
"""
per_type = {}
for a in report:
for m in a.get("anchors", []):
if m.get("tie_off") is False:
continue # explicit denial: not an anchor
ty = m.get("anchor_type")
if ty is None:
continue
if "rating_hint" not in m:
raise RuntimeError(
f"{a['name']}/{m['node']} carries anchor_type={ty!r} with "
f"no rating_hint and no tie_off denial — RULE 1 (no silent "
f"anchors). Rate it or not_a_tie_off() it.")
key = (m["rating_hint"], m.get("collateral"))
per_type.setdefault(ty, {}).setdefault(key, []).append(
f"{a['name']}/{m['node']}")
types, ambiguous = {}, {}
for ty in sorted(per_type):
variants = per_type[ty]
if len(variants) == 1:
(hint, coll), sources = next(iter(variants.items()))
entry = {"rating_hint": hint, "sources": sorted(sources)}
if coll is not None:
entry["collateral"] = coll
types[ty] = entry
else:
ambiguous[ty] = {"variants": [
dict(rating_hint=h, sources=sorted(srcs),
**({"collateral": c} if c is not None else {}))
for (h, c), srcs in sorted(variants.items(),
key=lambda kv: (-kv[0][0],
kv[0][1] or ""))
]}
return {"types": types, "ambiguous": ambiguous}
def write_ratings_module(manifest):
body = json.dumps(manifest, indent=2, sort_keys=True)
header = (
"// GENERATED by tools/blender/build_yard_assets.py — do NOT hand-edit.\n"
"// Regenerate: blender -b -P tools/blender/build_yard_assets.py\n"
"//\n"
"// SPRINT15 gate 1.2 — the wire between the factory and the sim.\n"
"// adoptAnchor only carries a baked rating_hint to anchors that name a\n"
"// GLB `node`; a plain-JSON anchor (every shipped sail post) defaulted\n"
"// to `?? 1` regardless of the bake, and nobody could see the\n"
"// disagreement. This module is the factory's bakes, resolvable by\n"
"// anchor TYPE at runtime: `types` are unambiguous (every rated node\n"
"// of that type agrees), `ambiguous` lists the disagreements so the\n"
"// runtime can refuse to guess. ratings.js is the reader; e.test.js\n"
"// pins runtime === this module === the GLBs, so none of the three\n"
"// can drift from the others silently again.\n"
)
with open(RATINGS_GEN_JS, "w") as f:
f.write(header + "export const FACTORY_ANCHOR_RATINGS = "
+ "Object.freeze(" + body + ");\n")
def verify_all(only=None):
todo = [a for a in ASSETS if only is None or a["name"] in only]
print(f"\n--- VERIFY {len(todo)} GLBs (re-import + assert + render) ---\n")
@ -3202,8 +3319,8 @@ def verify_all(only=None):
for p in problems:
print(f" -> {p}")
report.append(dict(name=name, dims=dims, tris=tris,
nodes=sorted(names), status=status,
problems=problems))
nodes=sorted(names), anchors=extract_anchor_meta(objs),
status=status, problems=problems))
return report, failures, thumbs
@ -3293,6 +3410,16 @@ def main():
json.dump(dict(blender=bpy.app.version_string, assets=report,
debris=debris), f, indent=2)
print(f" report -> {REPORT_JSON}")
# The runtime manifest is only honest when built from the WHOLE
# palette — an --only run would collapse "every node of this type
# agrees" to a sample of one and could flip an ambiguous type to
# unambiguous. So partial runs leave it untouched, loudly.
if only is None:
write_ratings_module(build_ratings_manifest(report))
print(f" ratings -> {RATINGS_GEN_JS}")
else:
print(" ratings -> NOT rewritten (--only run is a partial "
"palette; run the full factory to regenerate)")
print("\n" + "=" * 72)
if failures:

View File

@ -0,0 +1,100 @@
// GENERATED by tools/blender/build_yard_assets.py — do NOT hand-edit.
// Regenerate: blender -b -P tools/blender/build_yard_assets.py
//
// SPRINT15 gate 1.2 — the wire between the factory and the sim.
// adoptAnchor only carries a baked rating_hint to anchors that name a
// GLB `node`; a plain-JSON anchor (every shipped sail post) defaulted
// to `?? 1` regardless of the bake, and nobody could see the
// disagreement. This module is the factory's bakes, resolvable by
// anchor TYPE at runtime: `types` are unambiguous (every rated node
// of that type agrees), `ambiguous` lists the disagreements so the
// runtime can refuse to guess. ratings.js is the reader; e.test.js
// pins runtime === this module === the GLBs, so none of the three
// can drift from the others silently again.
export const FACTORY_ANCHOR_RATINGS = Object.freeze({
"ambiguous": {
"tree": {
"variants": [
{
"rating_hint": 1.0,
"sources": [
"tree_gum_01/branch_anchor_01",
"tree_gum_02/branch_anchor_01"
]
},
{
"rating_hint": 0.95,
"sources": [
"tree_jacaranda_01/branch_anchor_01"
]
},
{
"rating_hint": 0.88,
"sources": [
"tree_gum_01/branch_anchor_02",
"tree_gum_02/branch_anchor_02"
]
},
{
"rating_hint": 0.76,
"sources": [
"tree_gum_01/branch_anchor_03"
]
},
{
"rating_hint": 0.52,
"sources": [
"tree_jacaranda_01/branch_anchor_02"
]
},
{
"rating_hint": 0.4,
"sources": [
"tree_jacaranda_01/branch_anchor_03"
]
}
]
}
},
"types": {
"carport": {
"collateral": "carport",
"rating_hint": 0.22,
"sources": [
"carport_01/beam_anchor_01",
"carport_01/beam_anchor_02"
]
},
"carport_post": {
"collateral": "carport",
"rating_hint": 0.3,
"sources": [
"carport_01/post_anchor_01",
"carport_01/post_anchor_02"
]
},
"house": {
"collateral": "gutter",
"rating_hint": 0.35,
"sources": [
"house_yardside/fascia_anchor_01",
"house_yardside/fascia_anchor_02",
"house_yardside/fascia_anchor_03"
]
},
"post": {
"rating_hint": 1.0,
"sources": [
"sail_post/top_anchor"
]
},
"swing_frame": {
"collateral": "swing_set",
"rating_hint": 0.45,
"sources": [
"swing_set_01/frame_anchor_01",
"swing_set_01/frame_anchor_02"
]
}
}
});

Binary file not shown.