Files
troupe-standup/docs/q13-fix-pack/apply_fix_pack.py
T

644 lines
25 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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 <id> # default: verify, write nothing
python3 apply_fix_pack.py --apply <id> # write blocks.json
A clause marked undecided-for-Alexander refuses to apply unless BOTH --force and
--decision <artifact path> 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] <id> "
"[--force --decision <path>]")
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 <artifact path> 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())