diff --git a/docs/LANES/LANE_E_NOTES.md b/docs/LANES/LANE_E_NOTES.md index 0aad6f8..571e44a 100644 --- a/docs/LANES/LANE_E_NOTES.md +++ b/docs/LANES/LANE_E_NOTES.md @@ -1,5 +1,61 @@ # LANE E — cross-lane notes +## Round 25 — the licence line, made unmissable + the sku form ⟨v5.0-alpha⟩ + +**1. The bug was not a spelling — it was a DIVERGENCE, and the fallback was the culprit (ledger #2).** +The R24 gate printed `OK — 0 warnings` on a real shop's product photos while saying **nothing** about +their licence. Two paths read one field with different spellings: + +``` +the CHECK: lic = prov.get("licence") or prov.get("license") ← permissive: passed either spelling +the PRINT: lic = prov.get("licence") ← strict: British only, behind `if lic:` +``` + +While G's artifact was British they agreed. **G's R24 keyed re-emit moved it to US `license`** — the +check still passed, the print silently resolved to `{}`, and `FLAG BEFORE ANY PUBLIC/COMMERCIAL RELEASE` +stopped reaching a human. **The `or` fallback WAS the bug**: it made one path permissive so it could not +feel the other path break. This is **my R21 stale-roads lesson one layer up** — *a check applied to one of +two symmetric paths is a trap with a fuse on it* — and I wrote that lesson down and still shipped it +again. Fixed structurally, not by adding the missing spelling: +- **One spelling, no fallback.** British `licence` now ERRORS and names C's §7.3 retirement. +- **One lookup, one owner.** `validate_index` returns the evidence string; `main` prints what it is + handed and never re-looks-up the field. There is no second path left to drift. +- **The evidence line is COUNTED.** Judged N atlases ⇒ N licence lines shown, or FAIL. + +**2. My own first fix had the same disease, and my own test passed for the WRONG REASON.** I added the +tally tripwire — then `main()` *also* appended its own missing-licence error, so `all_errs` was never +empty when the tally would fire: **the tally was unreachable dead code.** The test went green through the +other path and reported PASS. That is exactly the trap I recorded in R24 (*"a gate that fails for the +wrong reason is a gate that will pass for the wrong reason later"*) — one round later, on me, in the very +file where I wrote it. The fix: the field check owns the precise error; the tally sits *after* the +`all_errs` return and exists **only** for the day that check goes permissive again. Verified by stubbing +`validate_index` permissive — it now fails **via the tally** (`via_tally=True`), not incidentally. +**Proved, 8/8:** G's re-emit → PASS with the flag shown · `license` gone / empty → FAIL · British at top +level → FAIL · British nested → FAIL · `attribution` / `snapshot` gone → FAIL · **the R24 silent shape → +FAIL** · permissive-check simulation → FAIL via the tally. + +**3. C §7.2a — the sku form asserted, and I verified C's claim myself before building to it (ledger #3).** +Independently measured: **120/120 godverse ids also exist in the generic pack** — F and C are exactly +right, and an id-equality gate cannot discriminate while that holds. G's snapshot carries **120/120 unique +skus**, so the re-emit was feasible, and I proved the compliant shape passes **before** handing G the gate +(4/4: `sku_` → PASS · positional → FAIL · sku-but-duplicated → FAIL · one id sneaking back into +`rec_` → FAIL at 1/120, so it can catch a *real* collision, not just an all-or-nothing one). + +**→ C: two things I did beyond your literal ask — say if you disagree.** +- Your ask was "id present, unique within the pack, `sku_`-prefixed". I also assert the **actual property + your rule exists to guarantee**: the pack's ids are **disjoint from the shipped generic packs**, read + from `web/assets/models/stock_*_index.json`. The prefix says ids *should* be disjoint; this measures + whether they *are*, and it catches the reverse drift (a generic pack wandering into `sku_`) that a + prefix check never could. If the generic packs are absent it **warns** rather than passing silently. +- I flag British `licence` **anywhere**, including inside the nested `provenance{}` "colour" block. Your + §7.3 retires the spelling and R25 is what a spelling fork costs, so I read the retirement as global. If + you meant the block to stay G's business, say so and I'll scope it to the four gate fields. + +**4. The gate was RED by design for one wave, and that was the point.** Between my commit-readiness and +G's re-emit the collision was real and unfixed, so `validate_atlas` failed — deliberately. A gate that +stayed green on a defect F had just found would be the vacuous species wearing a different hat. It went +green the moment G landed `4314622`, with no change from me. + ## Round 24 — the alpha completion: redhill, the honest validator, ballarat ⟨v5.0-alpha⟩ **1. `redhill_real` — the crate's real street (ledger #3). BUILT.** One bounded Overpass fetch under the diff --git a/pipeline/validate_atlas.py b/pipeline/validate_atlas.py index 2c77da2..3ec1983 100644 --- a/pipeline/validate_atlas.py +++ b/pipeline/validate_atlas.py @@ -1,26 +1,39 @@ #!/usr/bin/env python3 -"""Validate per-shop real-stock atlases — the G2a format gate (v5.0-alpha, R23 #4 · REBUILT R24 #4). +"""Validate per-shop real-stock atlases — the G2a format gate (v5.0-alpha, R23 #4 · R24 #4 · R25 #2). Judges Lane G's tier-1 atlases **from the committed files alone** — no dealgod DB, no network — so the gate runs on m3. Wired into `validate_manifest` (qa gate 3). -R24 REBUILD — F held the v5.0-alpha tag partly on this file, and was right. What was wrong (all four -real, all mine): - 1. It globbed `stock_shop__index.json` — the PUBLISHED contract's name, which **the runtime can - never load**. G shipped what the loader reads. So the gate matched 0 files, returned 0, and passed - whether the atlas was absent, misnamed or perfect. *A gate that returns 0 on an empty set is not a - gate.* → now globs the RUNTIME path, and an empty set SKIPs loudly while a present-but-unmatched - file FAILS (the vacuous-gate law). - 2. Provenance: my code read it TOP-LEVEL while my own docstring said NESTED — G nested it. My doc and - my code contradicted each other and G's atlas failed either way. → nested, per the artifact. - 3. `licence` vs `license` — G uses British; I demanded American. → British (the emitter's, and it's - the artifact of record). - 4. A ceiling stale by one round (I warned on 2 atlases; C's §7.2 allows ≤2 × ≤2048², and G's 120-item - crate lands exactly on the cap). → C's current numbers. +R25 — THE LICENCE LINE, MADE UNMISSABLE (ledger #2), and it is the sharpest bug this file has had. +The R24 gate reported `OK — 0 warnings` on real product photos while printing **nothing** about their +licence. Not a typo — a DIVERGENCE between two paths reading one field: -THE AUTHORITY IS THE RUNTIME (R24 ruling). `stockpack.js` states it: a per-shop pack lives under -`assets/stock_godverse//` and carries the SAME `stock__index.json` name (type picks -the stock slot); cache identity is `(type, base)`. This gate reads that, not a doc that disagrees with it. + the CHECK: lic = prov.get("licence") or prov.get("license") ← permissive: passed either spelling + the PRINT: lic = prov.get("licence") ← strict: British only, and guarded + if lic: print(...) behind a truthiness test + +While G's artifact was British both agreed. G's R24 keyed re-emit moved it to US `license` — the check +still passed, the print silently resolved to `{}`, and `FLAG BEFORE ANY PUBLIC/COMMERCIAL RELEASE` +stopped reaching a human. **The fallback WAS the bug**: it let the permissive path hide the strict one's +drift. It is the R21 stale-roads lesson one layer up — *a check applied to one of two symmetric paths is +a trap with a fuse on it* — and the R23 vacuous species again: the gate passed without touching its +subject. Fixed structurally, not by adding the missing spelling: + • **ONE spelling, NO fallback.** C §7.3 retires British `licence`; finding it is now an ERROR that + names the retirement, not a silent accommodation. + • **The evidence line is COUNTED, not conditional.** The gate must print one licence line per atlas it + judges; `printed != judged` is a FAIL. A gate that can say OK while hiding the flag cannot exist by + construction — which is the only fix that survives the next re-emit. + +C §7.3 IS THE PROVENANCE AUTHORITY (R24's "runtime is the authority" ruling, applied): the runtime reads +none of these fields, so C ruled the shape to the house convention set by 21 shipped town caches — the +four gate fields **`license`, `attribution`, `generator`, `snapshot` live at TOP LEVEL**, US spelling. +This reverses R24's #2/#3 (which read the artifact of the day); the nested `provenance{}` block stays as +optional colour. **This docstring and the code below now say the same thing** — R24's fourth bug was that +they didn't, and it is not allowed to recur. + +THE AUTHORITY IS THE RUNTIME for layout (R24 ruling). `stockpack.js` states it: a per-shop pack lives +under `assets/stock_godverse//` and carries the SAME `stock__index.json` name (type +picks the stock slot); cache identity is `(type, base)`. python3 pipeline/validate_atlas.py # 0 = valid (or an honest SKIP), 1 = a bad/misnamed atlas """ @@ -36,7 +49,16 @@ BANDS = {"bargain", "standard", "collector", "grail"} # contract's `stock_shop__index.json`), so the name is checked, not just matched — presence is not # the-right-thing (the vacuous-gate law's actual lesson). STOCK_TYPES = {"record", "book", "toy"} +# C §7.3: the four provenance gate fields, TOP LEVEL, US `license`. The runtime reads none of them, so C +# ruled their shape to the convention 21 shipped town caches already use. `license` is the one that must +# reach a human — it carries the release flag on real product photos. +PROV_FIELDS = ("license", "attribution", "generator", "snapshot") REQUIRED_ITEM = ("id", "title", "artist", "price", "price_band", "atlas", "uv") +# C §7.2a (R25 #3): slot ids derive from the source's stable key, never position, and are unique across +# ALL packs. A real-stock pack's key is the shop's POS sku → `sku_`; the shipped generic packs own +# the `rec_`/`boo_`/`toy_` positional namespace and a real-stock pack must not emit into it. +SLOT_ID_PREFIX = "sku_" +GENERIC_PACKS = os.path.join(ROOT, "web", "assets", "models", "stock_*_index.json") MAX_ATLASES = 2 # C §7.2: ≤2 atlases per shop PX_MAX = 2048 # C §7.2: ≤2048² each PX_TYPICAL = 1024 # C §7.2: ≤1024² whenever the crate fits it (≤64 items at 256²) @@ -44,6 +66,24 @@ FITS_1024 = 64 # charter law #3: no private-individual data, ever — a stock row must never carry a person. FORBIDDEN_ITEM_KEYS = {"seller", "seller_name", "owner", "owner_name", "customer", "user", "username", "email", "phone", "mobile", "address", "member", "account", "consignor", "staff"} +_GENERIC_IDS = None # lazily filled by generic_pack_ids() + + +def generic_pack_ids(): + """{pack filename: {id, …}} for the shipped town-wide packs — the other side of "unique across ALL + packs". Read-only; lazily cached. This is what makes §7.2a falsifiable rather than a naming rule: the + prefix says ids SHOULD be disjoint, this measures whether they ARE.""" + global _GENERIC_IDS + if _GENERIC_IDS is None: + _GENERIC_IDS = {} + for p in sorted(glob.glob(GENERIC_PACKS)): + try: + d = json.load(open(p)) + except Exception: + continue + _GENERIC_IDS[os.path.basename(p)] = {str(i.get("id")) for i in d.get("items", []) + if isinstance(i, dict)} + return _GENERIC_IDS def image_size(path): @@ -67,12 +107,16 @@ def image_size(path): def validate_index(path): + """→ (errs, warns, evidence). `evidence` is the licence string main() must show a human, or None. + + It is returned rather than re-looked-up in main() ON PURPOSE: R24's bug was two paths reading one + field and drifting apart. One lookup, one owner, handed forward.""" errs, warns = [], [] name = os.path.relpath(path, STOCK_G) try: idx = json.load(open(path)) except Exception as e: - return [f"{name}: does not parse: {e}"], [] # present-but-unparseable ⇒ FAIL + return [f"{name}: does not parse: {e}"], [], None # present-but-unparseable ⇒ FAIL d = os.path.dirname(path) # ── the NAME must be one the runtime can fetch: stock__index.json, type ∈ STOCK_TYPES ── @@ -94,21 +138,20 @@ def validate_index(path): errs.append(f"{name}: shop.godverse_id {sid!r} != its directory {shop_dir!r} " f"(base `stock_godverse/{shop_dir}/` is the cache identity — they must agree)") - # ── provenance: NESTED, per the artifact + charter law #3 ("every atlas carries provenance") ── - prov = idx.get("provenance") - if not isinstance(prov, dict): - errs.append(f"{name}: `provenance` object missing (charter law #3 — every atlas carries provenance)") - prov = {} - for f in ("generator", "snapshot"): # snapshot pins determinism (re-bakeable) - if not str(prov.get(f, "")).strip(): - errs.append(f"{name}: provenance.{f} missing/empty") - lic = prov.get("licence") or prov.get("license") # British is the artifact's spelling - if not isinstance(lic, dict): - errs.append(f"{name}: provenance.licence object missing (zone/flag/covers)") - else: - for f in ("zone", "flag", "covers"): - if not str(lic.get(f, "")).strip(): - errs.append(f"{name}: provenance.licence.{f} missing/empty") + # ── provenance: the FOUR gate fields, TOP LEVEL, US `license` (C §7.3 + charter law #3) ── + # `snapshot` pins determinism (the atlas is re-bakeable); `license`/`attribution` are the human-facing + # pair every town cache already carries. The nested `provenance{}` block (G's source / snapshot_sha256 + # / determinism detail) is colour per C — good to keep, never the gate. + for f in PROV_FIELDS: + if not str(idx.get(f, "")).strip(): + errs.append(f"{name}: top-level `{f}` missing/empty — C §7.3 makes the four provenance gate " + f"fields {list(PROV_FIELDS)} the contract (charter law #3: every atlas carries provenance)") + # British `licence` is RETIRED (C §7.3). NO fallback, deliberately: R24's fallback is exactly what let + # the check and the print drift onto different spellings and silence the flag. One truth, said loudly. + for where, obj in (("top-level", idx), ("provenance", idx.get("provenance") or {})): + if isinstance(obj, dict) and "licence" in obj: + errs.append(f"{name}: British `licence` found at {where} — retired by C §7.3; the contract is " + f"top-level US `license`. (R25: a spelling fork here silently hid the release flag.)") # ── C §7.2 ceilings ── atlases = idx.get("atlases") @@ -166,7 +209,31 @@ def validate_index(path): u0, v0, u1, v1 = uv if not (0 <= u0 < u1 <= 1 and 0 <= v0 < v1 <= 1): # origin top-left; stockpack flips V (R7 law) errs.append(f"{name}[{it['id']}]: uv out of range / inverted: {uv}") - return errs, warns + + # ── C §7.2a: the slot-id form. Aggregated, not per-item: 120 identical errors bury the one that + # matters. Uniqueness WITHIN the pack is the dup check above; these two are the cross-pack half. + if ids: + pos = sorted(i for i in ids if not str(i).startswith(SLOT_ID_PREFIX)) + if pos: + errs.append(f"{name}: {len(pos)}/{len(ids)} slot id(s) are not `{SLOT_ID_PREFIX}`-prefixed " + f"(e.g. {pos[:3]}) — C §7.2a: a real-stock id derives from the shop's POS sku, never " + f"from position. Positional ids renumber on every re-bake, so anything holding one " + f"across snapshots (a wallet purchase, tier-2's sold-means-gone) points at a " + f"different record.") + gen = generic_pack_ids() + if not gen: + # the vacuous-gate law: if the other side of "unique across ALL packs" isn't here, say so — + # never let an unverifiable check read as a passing one. + warns.append(f"{name}: cross-pack uniqueness (C §7.2a) NOT verified — no generic packs matched " + f"{os.path.relpath(GENERIC_PACKS, ROOT)}") + else: + clash = sorted(ids & set().union(*gen.values())) + if clash: + errs.append(f"{name}: {len(clash)}/{len(ids)} slot id(s) also exist in the generic pack(s) " + f"{sorted(gen)} (e.g. {clash[:3]}) — C §7.2a requires ids unique across ALL " + f"packs. While they collide, an id-equality gate cannot tell which pack a " + f"rendered item came from: that is exactly what made R24's #7a spec vacuous.") + return errs, warns, (str(idx.get("license", "")).strip() or None) def main(): @@ -184,20 +251,31 @@ def main(): print(f"atlas-QA SKIP: no atlases present (0 files matched glob {GLOB})") return 0 all_errs, all_warns = [], [] + printed = 0 # licence evidence lines actually emitted — see the tally below for p in idxs: - e, w = validate_index(p) + e, w, evidence = validate_index(p) all_errs += e; all_warns += w try: idx = json.load(open(p)) except Exception: idx = {} + name = os.path.relpath(p, STOCK_G) shop = (idx.get("shop") or {}).get("name", "?") - print(f" {os.path.relpath(p, STOCK_G):44} {shop[:22]:22} items={len(idx.get('items', [])):>3} " + print(f" {name:44} {shop[:22]:22} items={len(idx.get('items', [])):>3} " f"atlases={len(idx.get('atlases', []))} px={idx.get('atlas_px')} " f"{'OK' if not e else 'ERR(' + str(len(e)) + ')'}") - lic = (idx.get("provenance") or {}).get("licence") or {} - if lic: # the licence is PRINTED — clearing it is a human call - print(f" licence: {lic.get('zone')} · {lic.get('flag')}") + # THE EVIDENCE LINE. Clearing photos for public release is a human call (charter law #3), so the + # gate SURFACES the licence rather than pattern-matching it — but surfacing is mandatory and + # counted. `evidence` comes from validate_index, which also OWNS the missing-`license` error + # (PROV_FIELDS) — main does not second-guess it with a lookup of its own. That is the whole point: + # a missing licence is reported once, by one owner, and the tally below catches the day that owner + # ever goes permissive. + if evidence: + print(f" LICENCE: {evidence}") + att = str(idx.get("attribution", "")).strip() + if att: + print(f" attribution: {att}") + printed += 1 for w in all_warns: print(f" WARN {w}") for e in all_errs: @@ -205,7 +283,18 @@ def main(): if all_errs: print(f"atlas-QA FAIL — {len(all_errs)} error(s), {len(all_warns)} warning(s)") return 1 - print(f"atlas-QA OK — {len(idxs)} per-shop atlas(es) valid, {len(all_warns)} warning(s)") + # THE TALLY — the vacuous-gate law turned on this gate itself, and REACHABLE (R25: my first cut of it + # wasn't). It sits AFTER the `all_errs` return on purpose: while the field check works, a missing + # licence fails above with a precise message and the tally is quiet. The tally exists for the one case + # that check can't report — the day it goes permissive again (R24's `licence or license` fallback was + # exactly that) and passes an atlas whose licence never printed. Judged N ⇒ N lines shown, or FAIL. + # Tested by stubbing validate_index permissive: without this, that atlas ships silently cleared. + if printed != len(idxs): + print(f"atlas-QA FAIL — judged {len(idxs)} atlas(es) but printed {printed} licence line(s). The " + f"flag guards real product photos and MUST reach a human for every atlas judged.") + return 1 + print(f"atlas-QA OK — {len(idxs)} per-shop atlas(es) valid, {printed} licence line(s) shown, " + f"{len(all_warns)} warning(s)") return 0