#!/usr/bin/env python3 """Q13 fix pack - staged apply script for the 25 verified live-page clauses. Generated by Merlin, 2026-10-10, from the verified clause table in docs/q13-fix-pack.md. NOT RUN AS AN APPLY: every clause was exercised in CHECK mode only during the 2026-10-10 run (see the pack's landing evidence). Nothing in that run wrote to blocks.json. Scope, inherited from Q24 and Q13: this script edits the build source /root/stackchain-legends-build/blocks.json only. It does not rebuild, does not publish, does not contact a third party, and does not touch decisions/. python3 apply_fix_pack.py --list python3 apply_fix_pack.py --check # default: verify, write nothing python3 apply_fix_pack.py --apply # write blocks.json A clause marked undecided-for-Alexander refuses to apply unless BOTH --force and --decision are given, so a staged fix cannot be published without the approval being named on the command line. The one decided clause (D1, magazine-091) applies on its decision record alone - but see section 5 of docs/q13-fix-pack.md: its decided note conflicts with its proposed_action, so applying D1 publishes a live page Alexander's own note asked to hold. Fails closed: every delta must resolve exactly once inside its entry's own field span, otherwise nothing is written. Reverting: reverse the printed deltas, or restore blocks.json from a backup taken before --apply. """ from __future__ import annotations import json import pathlib import re import sys BASE = pathlib.Path(__file__).resolve().parents[3] BLOCKS = BASE / "stackchain-legends-build" / "blocks.json" CLAUSES = json.loads(r"""[ { "id": "A1", "slug": "block-25", "kind": "excerpt-url", "panel": "From the sources", "authority": "recoveries/evidence-tier-audit.md §6.1 (2026-10-10 ~09:3xZ) + recoveries/excerpt-binding-audit.md finding 1 (2026-10-10T13:26:16Z)", "decided": "undecided-for-Alexander", "ops": [ { "field": "excerpts[0].url", "old": "\"https://www.stackchainmagazine.net\"", "new": "\"https://www.stackchainmagazine.net/a-conversation-with-burnchainers/\"" } ], "locator": "\"title\": \"Wanting block 25, and being forked twice\"" }, { "id": "A2", "slug": "block-88", "kind": "excerpt-url", "panel": "From the sources", "authority": "recoveries/evidence-tier-audit.md §6.1 (2026-10-10 ~09:3xZ) + recoveries/excerpt-binding-audit.md finding 1 (2026-10-10T13:26:16Z)", "decided": "undecided-for-Alexander", "ops": [ { "field": "excerpts[0].url", "old": "\"https://www.stackchainmagazine.net\"", "new": "\"https://www.stackchainmagazine.net/a-conversation-with-burnchainers/\"" } ], "locator": "\"title\": \"Block 88, bought to secure the longer chain\"" }, { "id": "A3", "slug": "block-89", "kind": "excerpt-url", "panel": "From the sources", "authority": "recoveries/evidence-tier-audit.md §6.1 (2026-10-10 ~09:3xZ) + recoveries/excerpt-binding-audit.md finding 1 (2026-10-10T13:26:16Z)", "decided": "undecided-for-Alexander", "ops": [ { "field": "excerpts[0].url", "old": "\"https://www.stackchainmagazine.net\"", "new": "\"https://www.stackchainmagazine.net/a-conversation-with-burnchainers/\"" } ], "locator": "\"title\": \"Block 89, part of the 88-90 run\"" }, { "id": "A4", "slug": "block-90", "kind": "excerpt-url", "panel": "From the sources", "authority": "recoveries/evidence-tier-audit.md §6.1 (2026-10-10 ~09:3xZ) + recoveries/excerpt-binding-audit.md finding 1 (2026-10-10T13:26:16Z)", "decided": "undecided-for-Alexander", "ops": [ { "field": "excerpts[0].url", "old": "\"https://www.stackchainmagazine.net\"", "new": "\"https://www.stackchainmagazine.net/a-conversation-with-burnchainers/\"" } ], "locator": "\"title\": \"Block 90, and the chain held\"" }, { "id": "B1", "slug": "block-25", "kind": "direction-word", "panel": "Provenance", "authority": "WORK-QUEUE.md §Q21 verification addendum, control plane, 2026-10-10 (offsets quoted there)", "decided": "undecided-for-Alexander", "ops": [ { "field": "body_provenance", "old": "quoted below", "new": "quoted above" } ], "locator": "\"title\": \"Wanting block 25, and being forked twice\"" }, { "id": "B2", "slug": "block-88", "kind": "direction-word", "panel": "Provenance", "authority": "WORK-QUEUE.md §Q21 verification addendum, control plane, 2026-10-10 (offsets quoted there)", "decided": "undecided-for-Alexander", "ops": [ { "field": "body_provenance", "old": "quoted below", "new": "quoted above" } ], "locator": "\"title\": \"Block 88, bought to secure the longer chain\"" }, { "id": "B3", "slug": "block-89", "kind": "direction-word", "panel": "Provenance", "authority": "WORK-QUEUE.md §Q21 verification addendum, control plane, 2026-10-10 (offsets quoted there)", "decided": "undecided-for-Alexander", "ops": [ { "field": "body_provenance", "old": "quoted below", "new": "quoted above" } ], "locator": "\"title\": \"Block 89, part of the 88-90 run\"" }, { "id": "B4", "slug": "block-90", "kind": "direction-word", "panel": "Provenance", "authority": "WORK-QUEUE.md §Q21 verification addendum, control plane, 2026-10-10 (offsets quoted there)", "decided": "undecided-for-Alexander", "ops": [ { "field": "body_provenance", "old": "quoted below", "new": "quoted above" } ], "locator": "\"title\": \"Block 90, and the chain held\"" }, { "id": "C1", "slug": "event-2023-05-00-siggy-reaches-nostr", "kind": "excerpt-url", "panel": "From the sources", "authority": "recoveries/excerpt-binding-audit.md fetch note (301 observed) + recoveries/summary-fidelity-nonmagazine.md ev-017 row 1 + recoveries/ledger-audit-pass2.md row 19; 301 re-observed this run", "decided": "undecided-for-Alexander", "ops": [ { "field": "excerpts[0].url", "old": "\"https://github.com/plebiolira/stackchainsiggy_nostr\"", "new": "\"https://github.com/flav1000/stackchainsiggy_nostr\"" } ], "locator": "\"title\": \"Siggy reaches Nostr\"" }, { "id": "D1", "slug": "magazine-091", "kind": "body-clause", "panel": "What happened + Uncertainty", "authority": "decisions/2026-09-26.md item 13, approved 2026-09-26T14:51:11.019Z; staged record /root/stackchain-legends-build/EDITS/2026-09-26-magazine-091-correction.md", "decided": "DECIDED (approved) — decided note conflicts with proposed_action; see §5", "ops": [ { "field": "body", "old": "Comparing Strategy, Twenty One, Nakamoto, Metaplanet, Coinsilium and Smarter Web returns and drawdowns against Bitcoin, they find none consistently outperform,", "new": "Comparing Twenty One, Nakamoto, Metaplanet, Coinsilium and Smarter Web against Bitcoin — with earlier pioneers such as Strategy set aside as not a fair comparison — they find none consistently outperform," }, { "field": "uncertainty", "old": "Archive summary of a published article, not the article itself. Quotes are short verbatim excerpts; read the original for full context.", "new": "Archive summary of a published article, not the article itself. Quotes are short verbatim excerpts; read the original for full context. Correction 2026-09-26: this summary previously listed Strategy among the firms compared. The article sets Strategy aside (“not a fair comp for these newcomers”) and excludes it from the comparison; the listing was the archive’s error, not the article’s. Strategy has been removed from the compared set." } ], "locator": "\"title\": \"Fact Check: Bitcoin vs. Treasury Stocks.Who performs better?\"" }, { "id": "E1", "slug": "event-2022-08-18-the-bitcoin-beach-campaign-closes-and-hodlon", "kind": "uncertainty-append", "panel": "Uncertainty", "authority": "recoveries/named-source-liveness-unlinked.md finding 1 (2026-10-10T17:49:31Z) + recoveries/named-source-liveness.md row 6 (2026-10-10T15:39:39Z)", "decided": "undecided-for-Alexander", "ops": [ { "field": "uncertainty", "old": "Event status as recorded by the history research: sourced. The date basis is the reported event date, not necessarily a verified post timestamp.", "new": "Event status as recorded by the history research: sourced. The date basis is the reported event date, not necessarily a verified post timestamp. Note 2026-10-10: the named warrant “StackchainQuant” returns HTTP 404 at https://x.com/StackchainQuant (X: “User Profile Not Found”). The report it attributes is separately cited and live; the account itself is unreachable as of this date." } ], "locator": "\"title\": \"The Bitcoin Beach campaign closes and Hodlonaut support appears\"" }, { "id": "E2", "slug": "event-2022-08-28-the-fork-of-1492", "kind": "uncertainty-append", "panel": "Uncertainty", "authority": "recoveries/named-source-liveness-unlinked.md finding 1 (2026-10-10T17:49:31Z) + recoveries/named-source-liveness.md row 6 (2026-10-10T15:39:39Z)", "decided": "undecided-for-Alexander", "ops": [ { "field": "uncertainty", "old": "Event status as recorded by the history research: event corroborated; tactics from participant retrospective. The date basis is the reported event date, not necessarily a verified post timestamp.", "new": "Event status as recorded by the history research: event corroborated; tactics from participant retrospective. The date basis is the reported event date, not necessarily a verified post timestamp. Note 2026-10-10: the named warrant “StackchainQuant” returns HTTP 404 at https://x.com/StackchainQuant (X: “User Profile Not Found”). The report it attributes is separately cited and live; the account itself is unreachable as of this date." } ], "locator": "\"title\": \"The fork of 1492\"" }, { "id": "E3", "slug": "event-2022-10-05-the-community-documents-its-scale-and-charit", "kind": "uncertainty-append", "panel": "Uncertainty", "authority": "recoveries/named-source-liveness-unlinked.md finding 1 (2026-10-10T17:49:31Z) + recoveries/named-source-liveness.md row 6 (2026-10-10T15:39:39Z)", "decided": "undecided-for-Alexander", "ops": [ { "field": "uncertainty", "old": "Event status as recorded by the history research: sourced. The date basis is the reported event date, not necessarily a verified post timestamp.", "new": "Event status as recorded by the history research: sourced. The date basis is the reported event date, not necessarily a verified post timestamp. Note 2026-10-10: the named warrant “StackchainQuant” returns HTTP 404 at https://x.com/StackchainQuant (X: “User Profile Not Found”). The report it attributes is separately cited and live; the account itself is unreachable as of this date." } ], "locator": "\"title\": \"The community documents its scale and charitable turn\"" }, { "id": "F1", "slug": "magazine-020", "kind": "uncertainty-append", "panel": "Uncertainty", "authority": "recoveries/named-source-liveness.md row 1 (2026-10-10T15:39:17Z)", "decided": "undecided-for-Alexander", "ops": [ { "field": "uncertainty", "old": "Archive summary of a published article, not the article itself. Quotes are short verbatim excerpts; read the original for full context.", "new": "Archive summary of a published article, not the article itself. Quotes are short verbatim excerpts; read the original for full context. Note 2026-10-10: the byline handle @_overflow__ returns HTTP 404 at https://x.com/_overflow__ (X: “User Profile Not Found”). The name is retained as printed and is not currently linkable." } ], "locator": "\"title\": \"The coming Occupy Wall St\"" }, { "id": "F2", "slug": "magazine-051", "kind": "uncertainty-append", "panel": "Uncertainty", "authority": "recoveries/named-source-liveness.md row 2 (2026-10-10T15:39:33Z)", "decided": "undecided-for-Alexander", "ops": [ { "field": "uncertainty", "old": "Archive summary of a published article, not the article itself. Quotes are short verbatim excerpts; read the original for full context.", "new": "Archive summary of a published article, not the article itself. Quotes are short verbatim excerpts; read the original for full context. Note 2026-10-10: the byline handle @humblebitcoiner returns HTTP 404 at https://x.com/humblebitcoiner (X: “User Profile Not Found”). The name is retained as printed and is not currently linkable." } ], "locator": "\"title\": \"The Obstacle Is The Way – Finding Hope In Bitcoin\"" }, { "id": "G1", "slug": "magazine-035", "kind": "contributor-anchor", "panel": "Contributor", "authority": "recoveries/named-source-liveness.md row 3 (2026-10-10T15:39:21Z)", "decided": "undecided-for-Alexander", "ops": [ { "field": "contributor.display_name", "old": "\"@BTC_Freeborn and @AnthonyDessauer\"", "new": "\"BTC_Freeborn and Anthony Dessauer\"" }, { "field": "contributor.handle", "old": "null", "new": "\"BTC_Freeborn\"" } ], "locator": "\"title\": \"WAHT Stackchain Done Dided: The King of StackChain™\"" }, { "id": "G2", "slug": "magazine-056", "kind": "contributor-anchor", "panel": "Contributor", "authority": "recoveries/named-source-liveness.md row 4 (2026-10-10T15:39:33Z)", "decided": "undecided-for-Alexander", "ops": [ { "field": "contributor.display_name", "old": "\"Red Tail Hawk (@RedTailHawk1923)\"", "new": "\"Red Tail Hawk\"" }, { "field": "contributor.handle", "old": "null", "new": "\"RedTailHawk1923\"" } ], "locator": "\"title\": \"Culture is Bitcoin\"" }, { "id": "G3", "slug": "magazine-039", "kind": "contributor-anchor", "panel": "Contributor + Uncertainty", "authority": "recoveries/named-source-liveness.md row 5 (2026-10-10T15:39:22Z)", "decided": "undecided-for-Alexander", "ops": [ { "field": "contributor.handle", "old": "null", "new": "\"captainsiddh\"" }, { "field": "uncertainty", "old": "Archive summary of a published article, not the article itself. Quotes are short verbatim excerpts; read the original for full context.", "new": "Archive summary of a published article, not the article itself. Quotes are short verbatim excerpts; read the original for full context. Note 2026-10-10: the handle @captainsiddh resolves, but its own profile description reads “Now at @captainsidd” and https://x.com/captainsidd resolves; the byline above is one handle behind the account’s current home." } ], "locator": "\"title\": \"Traveling on a Bitcoin Standard\"" }, { "id": "H1", "slug": "event-2022-07-18-the-5-invitation-becomes-a-chain", "kind": "prose-handle-form", "panel": "What happened + In plain English", "authority": "recoveries/named-source-liveness-unlinked.md finding 2 (2026-10-10T17:49:31Z); https://x.com/AriZonanHODL 200 / 130,908 B", "decided": "undecided-for-Alexander", "ops": [ { "field": "body", "old": "AriZonanHODL posted a $5 bitcoin purchase", "new": "@AriZonanHODL posted a $5 bitcoin purchase" }, { "field": "plain_summary.text", "old": "AriZonanHODL posted a $5 bitcoin purchase", "new": "@AriZonanHODL posted a $5 bitcoin purchase" } ], "locator": "\"title\": \"The $5 invitation becomes a chain\"" }, { "id": "H2", "slug": "event-2022-07-18-the-5-invitation-becomes-a-chain", "kind": "prose-handle-form", "panel": "What happened", "authority": "WORK-QUEUE.md §Q24 addendum 2026-10-10 18:13Z (Satskeeper resolves as @satskeeper_, already linked on block-7)", "decided": "undecided-for-Alexander", "ops": [ { "field": "body", "old": "and Satskeeper with $7", "new": "and @satskeeper_ with $7" } ], "locator": "\"title\": \"The $5 invitation becomes a chain\"" }, { "id": "H3", "slug": "magazine-002", "kind": "prose-handle-form", "panel": "What happened", "authority": "WORK-QUEUE.md §Q23 verification correction (rule 3b yields 8 bare-and-unlinked mentions across 6 entries) — CONDITIONAL on step 5’s filter", "decided": "undecided-for-Alexander (conditional row)", "ops": [ { "field": "body", "old": "ArizonanHODL's $5", "new": "@AriZonanHODL's $5" } ], "locator": "\"title\": \"The Art of GFY by Nobody (aka Seven)\"" }, { "id": "H4", "slug": "magazine-005", "kind": "prose-handle-form", "panel": "What happened", "authority": "WORK-QUEUE.md §Q23 verification correction (rule 3b) — CONDITIONAL on step 5’s filter", "decided": "undecided-for-Alexander (conditional row)", "ops": [ { "field": "body", "old": "ArizonanHodl's July 18, 2022 tweet", "new": "@AriZonanHODL's July 18, 2022 tweet" } ], "locator": "\"title\": \"A Brief History of Stackchain.\"" }, { "id": "I1", "slug": "magazine-026", "kind": "contributor-anchor", "panel": "Contributor", "authority": "recoveries/evidence-tier-audit.md §6.3 (2026-10-10) — OUTSIDE the Q24 population rule, which names findings 1–2; included and flagged", "decided": "undecided-for-Alexander", "ops": [ { "field": "contributor.handle", "old": "null", "new": "\"a_koby\"" } ], "locator": "\"title\": \"Stackchain for Newcomers: How I Infiltrated the Stackchain.\"" }, { "id": "I2", "slug": "magazine-070", "kind": "contributor-anchor", "panel": "Contributor", "authority": "WORK-QUEUE.md §Q23 verification correction (rule 3a admits MidyReyes on magazine-070) — OUTSIDE the Q24 population rule; included and flagged", "decided": "undecided-for-Alexander", "ops": [ { "field": "contributor.display_name", "old": "\"@MidyReyes\"", "new": "\"MidyReyes\"" }, { "field": "contributor.handle", "old": "null", "new": "\"MidyReyes\"" } ], "locator": "\"title\": \"From Struggles to Strength: My Health & Fitness Story\"" }, { "id": "J1", "slug": "magazine-024", "kind": "uncertainty-append", "panel": "Uncertainty", "authority": "recoveries/named-source-liveness-unlinked.md finding 3 (2026-10-10T17:49:31Z)", "decided": "undecided-for-Alexander", "ops": [ { "field": "uncertainty", "old": "Archive summary of a published article, not the article itself. Quotes are short verbatim excerpts; read the original for full context.", "new": "Archive summary of a published article, not the article itself. Quotes are short verbatim excerpts; read the original for full context. Note 2026-10-10: the tipping address bitcoinbekka@getalby.com resolves at its canonical LNURL-pay endpoint https://getalby.com/.well-known/lnurlp/bitcoinbekka (HTTP 200); the vanity web path https://getalby.com/bitcoinbekka returns 404. It is not linked here because the ledger build emits anchors only for contributor handles, body sources and excerpt URLs." } ], "locator": "\"title\": \"Bekka’s Sammie Corner: Schnitzel\"" } ]""") def slug_for(b): raw = b.get("slug") or f"block-{b['n']}" return re.sub(r"[^a-z0-9-]+", "-", str(raw).lower()).strip("-") def region_of(text, slug): """(start, end, region_text, locator) for one entry object, found by its unique title.""" by_slug = {slug_for(b): b for b in json.loads(text)["blocks"]} b = by_slug[slug] loc = '"title": ' + json.dumps(b["title"], ensure_ascii=False) n = text.count(loc) if n != 1: raise SystemExit(f"ABORT {slug}: title locator occurs {n}x") i = text.index(loc) m = re.search(r'\{\s*"n":', text[i + len(loc):]) end = i + len(loc) + (m.start() if m else len(text) - i - len(loc)) return i, end, text[i:end], loc def _scan_string(text, s): """End offset of the JSON string that starts at s (text[s] == '"').""" j = s + 1 while j < len(text): if text[j] == "\\": j += 2 continue if text[j] == '"': return j + 1 j += 1 raise SystemExit("ABORT: unterminated string") def _value_span(text, key, base=0): """(start, end, raw) of one key's JSON value; objects/arrays bracket-matched.""" m = re.search(r'"' + re.escape(key) + r'": ', text[base:]) if not m: raise SystemExit(f"ABORT: key {key!r} not found") s = base + m.end() if text.startswith('"', s): e = _scan_string(text, s) return s, e, text[s:e] m2 = re.match(r"(null|true|false|-?\d+(?:\.\d+)?)", text[s:]) if m2: return s, s + m2.end(), text[s:s + m2.end()] depth, j = 0, s while j < len(text): ch = text[j] if ch == '"': j = _scan_string(text, j) continue if ch in "{[": depth += 1 elif ch in "}]": depth -= 1 if depth == 0: return s, j + 1, text[s:j + 1] j += 1 raise SystemExit(f"ABORT: unbalanced value at {key!r}") def field_span(region, path): """(start, end, raw) of ONE field's JSON value inside the entry region.""" if path in ("body", "body_provenance", "uncertainty"): return _value_span(region, path) if path == "plain_summary.text": cs, _, _ = _value_span(region, "plain_summary") s, e, _ = _value_span(region, "text", base=cs) return s, e, region[s:e] if path.startswith("excerpts["): idx = int(path[path.index("[") + 1:path.index("]")]) key = path.split(".")[-1] es, _, _ = _value_span(region, "excerpts") arr_end = es + 1 depth = 1 while depth: if region[arr_end] == '"': arr_end = _scan_string(region, arr_end) continue if region[arr_end] in "[{": depth += 1 elif region[arr_end] in "]}": depth -= 1 arr_end += 1 arr = region[es:arr_end] pos = [m.start() for m in re.finditer(r'"' + key + r'": ', arr)] if len(pos) <= idx: raise SystemExit(f"ABORT: {path} not found ({len(pos)} occurrences)") s = es + pos[idx] + len('"' + key + '": ') e = _scan_string(region, s) return s, e, region[s:e] if path.startswith("contributor."): key = path.split(".")[-1] cs, _, _ = _value_span(region, "contributor") return _value_span(region, key, base=cs) raise SystemExit(f"ABORT: unknown field path {path!r}") def plan(cid): c = next((x for x in CLAUSES if x["id"] == cid), None) if c is None: raise SystemExit(f"ABORT: no clause {cid!r}; --list shows the ids") text = BLOCKS.read_text(encoding="utf-8") rstart, rend, reg, loc = region_of(text, c["slug"]) edits = [] for o in c["ops"]: s, e, cur = field_span(reg, o["field"]) if cur.count(o["old"]) != 1: raise SystemExit( f"ABORT {cid} {o['field']}: old occurs {cur.count(o['old'])}x in the field span") if o["old"] == o["new"]: raise SystemExit(f"ABORT {cid} {o['field']}: old == new") # absolute offsets into the full file edits.append((s + rstart, e + rstart, cur, o)) return c, text, reg, loc, edits def main() -> int: argv = sys.argv[1:] if "--list" in argv: for c in CLAUSES: print(f"{c['id']:4s} {c['slug'][:52]:52s} {c['kind']:20s} {c['decided'][:40]}") print(f"\n{len(CLAUSES)} clauses, {sum(len(c['ops']) for c in CLAUSES)} deltas") return 0 apply = "--apply" in argv force = "--force" in argv decision = None if "--decision" in argv: k = argv.index("--decision") if k + 1 >= len(argv): print("ABORT: --decision needs a value") return 2 decision = argv[k + 1] argv = argv[:k] + argv[k + 2:] ids = [a for a in argv if not a.startswith("--")] if len(ids) != 1: print("usage: apply_fix_pack.py [--list] [--check|--apply] " "[--force --decision ]") return 2 cid = ids[0] c, text, reg, loc, edits = plan(cid) print(f"clause {cid} entry {c['slug']} panel {c['panel']}") print(f"authority: {c['authority']}") print(f"decided: {c['decided']}") for _s, _e, _cur, o in edits: print(f" field {o['field']}: old occurs 1x, new differs OK") if c["decided"].startswith("undecided"): if apply and not (force and decision): print("\nREFUSED: this clause is undecided-for-Alexander. Re-run with " "--force --decision to publish it anyway.") return 1 if apply: print(f"\nFORCED with decision artifact: {decision}") if not apply: print("\nCHECK ONLY - blocks.json not written (pass --apply)") return 0 out = text for s, e, cur, o in reversed(edits): out = out[:s] + cur.replace(o["old"], o["new"], 1) + out[e:] json.loads(out) BLOCKS.write_text(out, encoding="utf-8") print(f"\nWROTE {BLOCKS}") print("revert by reversing these deltas (or restore a pre-apply backup):") for _s, _e, _cur, o in edits: print(f" {o['field']}: {o['new'][:70]!r} -> {o['old'][:70]!r}") return 0 if __name__ == "__main__": raise SystemExit(main())