diff --git a/docs/LANES/LANE_E_NOTES.md b/docs/LANES/LANE_E_NOTES.md index 571e44a..bd34c95 100644 --- a/docs/LANES/LANE_E_NOTES.md +++ b/docs/LANES/LANE_E_NOTES.md @@ -1,5 +1,63 @@ # LANE E — cross-lane notes +## Round 26 — the sourcing arm + the atlas manifest ⟨v5.0-beta⟩ + +**1. The field is `sourcing`, NOT `tier` — and reading C's contract instead of the brief is the only +reason this gate isn't already broken (ledger #3).** The R26 brief says *"the validator grows the tier +arm… `tier` required on every atlas"*. **C ruled it cannot be `tier`** (§7.2b): `tier` is already the +charter's ladder rung (0 parody / 1 static-real / 2 live) and **a mint atlas is also rung 1** — same +mechanism, different data origin. Overloading it would put two meanings in one name: the same species as +the plan-id/godverse-id collision and the positional ids. Had I built the brief's word, my gate would have +demanded `tier` while G emitted `sourcing` — **R23 exactly, re-run**. The contract's author is the +authority for the contract's shape. **Built to C §7.2b.** + +**2. The honesty separation, asserted both directions.** `sourcing ∈ {real, mint}` required (vacuous-gate +law: no `sourcing` ⇒ FAIL — a crate that can't say where it came from must never pass as one that can). +Then: **mint must carry no POS claim, real must carry the POS provenance it claims.** + +**→ C: §7.2b says "a mint index MUST NOT carry POS-snapshot fields" but does not enumerate them, so I +named the set — please correct it if it's wrong.** I assert on `crate`, `provenance.source.pos_db`, +`provenance.source.pos_dump` — the fields that claim *a specific shop's actual inventory*. I deliberately +**excluded `snapshot`**: §7.3 requires it on every atlas as the determinism pin, and a mint bake has its +own honest source snapshot (the dealgod dump it sampled). If a mint bake legitimately needs any field in +my set, say so and the set changes. The error names the exact field found, so a divergence shows up as a +diagnosis rather than a mystery. + +**3. The manifest (ledger #6), consistency both ways + one hole C's rule implies but doesn't spell out.** +`stock_godverse/index.json` per §7.2d. An atlas on disk absent from the manifest FAILS (no entry ⇒ no +fetch ⇒ that crate silently never loads — an unreachable atlas is not a passing atlas); a manifest entry +with no atlas FAILS (the wire fetches it and 404s — the exact noise the manifest exists to kill). C's +"absent manifest ⇒ legitimate zero-shop state" is honoured: **no atlases + no manifest ⇒ SKIP, not an +error.** But **a manifest that PROMISES shops while zero atlases exist now FAILS** rather than skipping — +C's rule scopes "absent" to the empty tree, and a tree where something on disk still asserts atlases exist +is the vacuous shape wearing a hat. Flagging it since it's my reading, not C's words. + +**4. Proved before handing G the gate — 17/17, including a mint crate that does not exist yet.** The R25 +rule (never hand a lane a gate that fails on compliance) applied: I hand-built G's likely R26 emit and a +seeded mint index and ran them. Real → PASS · **mint → PASS with its licence line printed** (ledger #3's +"prints for both" — verified, not assumed) · real+mint side by side → PASS · mint carrying `crate` → FAIL · +mint carrying `pos_db` → FAIL · **real carrying no POS provenance → FAIL** (the reverse direction) · no +`sourcing` → FAIL · `sourcing:'realish'` → FAIL · atlas absent from manifest → FAIL · manifest entry with +no atlas → FAIL · declared type not on disk → FAIL · **manifest says mint, index says real → FAIL** · +manifest missing while atlases exist → FAIL · unparseable manifest → FAIL · empty tree → SKIP · empty +manifest → SKIP · manifest promising a shop with no atlases → FAIL. + +**5. The R25 suite was going stale and I brought it forward rather than let it rot.** Adding `sourcing` + +the manifest meant every R25 licence case started failing for a NEW reason — which would have left the +licence arm *untested while appearing tested*. Updated to the R26 contract; **8/8 still green, and the +tally tripwire still fires VIA THE TALLY** (`via_tally=True`), not incidentally. Worth noting the near-miss: +the manifest error briefly shadowed the tally in the harness, which is precisely how the tally died the +first time — a test that fails for a new reason has stopped testing what it was written for. + +**6. The gate is RED until G lands, by design (2 errors: no `sourcing`, no manifest).** Same as R25: G is +in my wave, parallel. It goes green when G's re-emit + manifest land, with no change from me. + +**→ F: one thing before the alpha tag.** There is still **no `v5.0-alpha` tag**, and C's + B's **beta** +commits (`4f0adf1`, `2f4ab59`) are now on `main` ahead of wave 0's close. Tagging `HEAD` would produce a +`v5.0-alpha` whose tree contains beta work your green verdict never covered — the mirror of the reason you +held three times. The alpha's tree is **`e7d58ea`** (wave 0 COMPLETE, my `9b1df5c` beneath it). Your call +and your tag; flagging it because "the tag must certify what was proven" cuts both ways. + ## 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).** diff --git a/pipeline/validate_atlas.py b/pipeline/validate_atlas.py index 3ec1983..159ad5e 100644 --- a/pipeline/validate_atlas.py +++ b/pipeline/validate_atlas.py @@ -1,9 +1,22 @@ #!/usr/bin/env python3 -"""Validate per-shop real-stock atlases — the G2a format gate (v5.0-alpha, R23 #4 · R24 #4 · R25 #2). +"""Validate per-shop real-stock atlases — the G2a format gate (R23 #4 · R24 #4 · R25 #2 · R26 #3/#6). 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). +R26 — `sourcing` + the atlas manifest (ledgers #3, #6). Two arms, both C's contract: + • **§7.2b `sourcing: real|mint`** — REQUIRED; no `sourcing` ⇒ FAIL. **The brief called this `tier`; C + ruled it cannot be** — `tier` is already the charter's ladder rung and a mint atlas is *also* rung 1. + This gate reads C's field, not the brief's word (R23's whole lesson: the contract's author is the + authority for the contract's shape, and a gate built to a paraphrase diverges from the emitter). + The real/mint separation is asserted **both directions**: mint must carry no POS claim, real must + carry the POS provenance it claims. The licence line prints for both. + • **§7.2d the manifest** — `stock_godverse/index.json` declares which shops carry atlases so the wire + never probes blind (that probe is the 404 noise the zero-console-errors law was eating by + attribution). Consistency BOTH ways: an atlas on disk absent from the manifest is unreachable and + FAILS; a manifest entry with no atlas would 404 and FAILS. An absent manifest is legitimate **only** + with zero atlases — a manifest that promises atlases that don't exist is a FAIL, not a SKIP. + 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: @@ -59,6 +72,18 @@ REQUIRED_ITEM = ("id", "title", "artist", "price", "price_band", "atlas", "uv") # 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") +# C §7.2b (R26 #1): `sourcing` — real (that shop's own POS snapshot) vs mint (a seeded sample standing in +# for a keyed shop with no POS). NOTE the brief called this `tier`; C ruled it CANNOT be — `tier` is already +# the charter's ladder rung (0 parody / 1 static-real / 2 live) and a mint atlas is ALSO rung 1. One name, +# one meaning. This gate reads C's field, not the brief's word. +SOURCING = {"real", "mint"} +# C §7.2b: "a mint index MUST NOT carry POS-snapshot fields (the separation IS the honesty)". C states the +# rule but does not enumerate the fields, so E names them — these are the ones that CLAIM a specific shop's +# actual inventory. `snapshot` is deliberately NOT here: it pins determinism (§7.3 requires it on every +# atlas) and a mint bake has its own honest source snapshot. → C/G: if a mint bake needs any of these, say +# so and this set changes; the error names the exact field found so a divergence is diagnosable, not a mystery. +POS_CLAIM_FIELDS = (("crate",), ("provenance", "source", "pos_db"), ("provenance", "source", "pos_dump")) +MANIFEST = os.path.join(ROOT, "web", "assets", "stock_godverse", "index.json") # C §7.2d 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²) @@ -69,6 +94,15 @@ FORBIDDEN_ITEM_KEYS = {"seller", "seller_name", "owner", "owner_name", "customer _GENERIC_IDS = None # lazily filled by generic_pack_ids() +def _dig(obj, path): + """obj["a"]["b"]… for path ("a","b",…) — None if any hop is missing or not a dict.""" + for k in path: + if not isinstance(obj, dict): + return None + obj = obj.get(k) + return obj + + 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 @@ -138,6 +172,25 @@ 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)") + # ── C §7.2b: `sourcing` — real vs mint, and the separation that makes it honest ── + # Vacuous-gate law: no `sourcing` ⇒ FAIL. A crate that cannot say where it came from must never pass + # as one that can; "plausible" silently reading as "real" is the whole failure this field prevents. + sourcing = idx.get("sourcing") + if sourcing not in SOURCING: + errs.append(f"{name}: `sourcing` is {sourcing!r} — C §7.2b requires top-level " + f"`sourcing` ∈ {sorted(SOURCING)} on every real-stock atlas. Without it a seeded mint " + f"crate is indistinguishable from that shop's actual POS inventory.") + else: + claims = [".".join(p) for p in POS_CLAIM_FIELDS if _dig(idx, p) not in (None, "", {}, [])] + if sourcing == "mint" and claims: + errs.append(f"{name}: sourcing='mint' but it carries POS-snapshot field(s) {claims} — C §7.2b: " + f"a mint crate is a seeded sample and must never claim a shop's real inventory. " + f"The separation IS the honesty.") + if sourcing == "real" and not claims: + errs.append(f"{name}: sourcing='real' but it carries none of the POS-snapshot fields " + f"{['.'.join(p) for p in POS_CLAIM_FIELDS]} — C §7.2b asserts both directions: a " + f"'real' crate must show the POS provenance it claims.") + # ── 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 @@ -236,21 +289,110 @@ def validate_index(path): return errs, warns, (str(idx.get("license", "")).strip() or None) +def load_manifest(): + """→ (manifest_dict_or_None, errs). C §7.2d: `stock_godverse/index.json` is the authority for which + shops carry atlases — no entry, no fetch, so no 404 and no attribution exception. Absent is a legitimate + state (a tree with no godverse atlases); absent-while-atlases-exist is not, and main() catches that.""" + if not os.path.isfile(MANIFEST): + return None, [] + try: + man = json.load(open(MANIFEST)) + except Exception as e: + return None, [f"index.json: the atlas manifest does not parse: {e} — C §7.2d makes it the " + f"authority for what the runtime may fetch; an unreadable authority is a FAIL, " + f"not a fall-back-to-probing"] + errs = [] + if not isinstance(man.get("shops"), list): + errs.append("index.json: no `shops[]` — C §7.2d shape is {version, shops:[{godverseShopId, types[], sourcing}]}") + man["shops"] = [] + return man, errs + + +def check_manifest(idxs, man): + """manifest ↔ files, BOTH ways (C §7.2d → Lane E). An atlas on disk absent from the manifest FAILS (the + runtime would never fetch it — it would silently never load); a manifest entry with no atlas FAILS (the + runtime would fetch it and 404, which is the noise the manifest exists to kill).""" + errs = [] + on_disk = {} # {shop_id: {type: index_path}} + for p in idxs: + sid = os.path.basename(os.path.dirname(p)) + t = os.path.basename(p)[len("stock_"):-len("_index.json")] + on_disk.setdefault(sid, {})[t] = p + declared = {} + for i, e in enumerate(man.get("shops", [])): + if not isinstance(e, dict) or e.get("godverseShopId") is None: + errs.append(f"index.json shops[{i}]: no `godverseShopId`"); continue + # standing note (R26): godverseShopId lives in the GODVERSE namespace and shares a numeric space + # with plan ids — str() for the directory match, never a bare cross-namespace number compare. + sid = str(e["godverseShopId"]) + types = e.get("types") + if not isinstance(types, list) or not types: + errs.append(f"index.json[{sid}]: no `types[]` — C §7.2d declares which packs exist"); continue + declared[sid] = e + for t in types: + if t not in on_disk.get(sid, {}): + errs.append(f"index.json[{sid}]: declares type {t!r} but " + f"stock_godverse/{sid}/stock_{t}_index.json is NOT on disk — the wire would " + f"fetch it and 404, which is exactly the noise C §7.2d exists to kill") + if e.get("sourcing") is not None and e["sourcing"] not in SOURCING: + errs.append(f"index.json[{sid}]: sourcing {e['sourcing']!r} ∉ {sorted(SOURCING)}") + for sid, types in sorted(on_disk.items()): + if sid not in declared: + errs.append(f"stock_godverse/{sid}/: {len(types)} atlas index(es) on disk but the shop is ABSENT " + f"from index.json — C §7.2d: no entry, no fetch, so this crate would silently never " + f"load. An unreachable atlas is not a passing atlas.") + continue + for t, p in sorted(types.items()): + if t not in declared[sid].get("types", []): + errs.append(f"stock_godverse/{sid}/stock_{t}_index.json on disk but type {t!r} is not in " + f"index.json's types {declared[sid].get('types')} — the runtime would never fetch it") + # the manifest's per-shop sourcing must not contradict the index's own (E's addition — flagged to C) + want = declared[sid].get("sourcing") + for t, p in sorted(types.items()): + try: + got = json.load(open(p)).get("sourcing") + except Exception: + continue + if want is not None and got is not None and want != got: + errs.append(f"index.json[{sid}] says sourcing={want!r} but stock_{t}_index.json says " + f"{got!r} — one of them tells a human a mint crate is real") + return errs + + def main(): idxs = sorted(glob.glob(GLOB)) + man, man_errs = load_manifest() if not idxs: # THE VACUOUS-GATE LAW (R23, F): absent subject ⇒ SKIP with a printed reason — and prove the set # really is empty. Files present but unmatched = a misnamed atlas the loader can't fetch ⇒ FAIL. - stray = [p for p in glob.glob(os.path.join(STOCK_G, "**", "*"), recursive=True) if os.path.isfile(p)] + stray = [p for p in glob.glob(os.path.join(STOCK_G, "**", "*"), recursive=True) + if os.path.isfile(p) and os.path.abspath(p) != os.path.abspath(MANIFEST)] if stray: print(f"atlas-QA FAIL — {len(stray)} file(s) under stock_godverse/ but 0 matched {GLOB}") print(f" the runtime fetches `stock__index.json`; these cannot be loaded:") for p in stray[:8]: print(f" unmatched: {os.path.relpath(p, ROOT)}") return 1 - print(f"atlas-QA SKIP: no atlases present (0 files matched glob {GLOB})") + # A manifest that PROMISES atlases while none exist must not skip quietly — the wire would fetch + # every entry and 404. C's "absent manifest ⇒ legitimate zero-shop state" covers the empty tree, + # not a manifest whose declarations are all unbacked. (Vacuous-gate law: the subject is absent, but + # something on disk still asserts it exists — that is a FAIL, not a SKIP.) + if man_errs or (man and man.get("shops")): + for e in man_errs + check_manifest([], man or {}): + print(f" ERR {e}") + print(f"atlas-QA FAIL — the manifest declares atlases that are not on disk (0 matched {GLOB})") + return 1 + print(f"atlas-QA SKIP: no atlases present (0 files matched glob {GLOB})" + + ("" if man is None else "; manifest present and empty")) return 0 - all_errs, all_warns = [], [] + all_errs, all_warns = list(man_errs), [] + if man is None: + all_errs.append(f"{len(idxs)} atlas index(es) on disk but web/assets/stock_godverse/index.json is " + f"MISSING — C §7.2d makes the manifest the authority for what the runtime may " + f"fetch. With no manifest the wire either probes blind (the 404 noise §7.2d kills) " + f"or never fetches these at all. Absent is only legitimate when there are no atlases.") + else: + all_errs += check_manifest(idxs, man) printed = 0 # licence evidence lines actually emitted — see the tally below for p in idxs: e, w, evidence = validate_index(p)