addedpattern_draft.py161 diff lines
@@ -0,0 +1,160 @@+#!/usr/bin/env python3+"""pattern_draft.py — Tapestry no.5: THE DRAFT OF PUZZLE E.++A weaving draft (pattern sheet) of the Pub puzzle "Warp / Weft / Cloth":+rows = tapestry lanes, columns = day-one hour bands, cells shaded by knot+count from the TRUE 1006-knot crawl. The dropped page-run (ids 588-1198)+is shown as a hatched mend; the two accepted CLOTH bands get shuttle+brackets; the boundary knot ev2663 gets the selvedge mark.++Usage: python3 pattern_draft.py <dayone.json> <out.svg> <weave_ts_iso>+Writes <out.svg> and <out.png>.+"""+import sys, json, hashlib, collections+from datetime import datetime, timezone+import matplotlib+matplotlib.use("Agg")+import matplotlib.pyplot as plt+from matplotlib.patches import Rectangle++sys.path.insert(0, "/desk")+from puzzle_truth import kind_of, parse, DAY1_END++BG, INK, FAINT = "#16131c", "#e8e2d6", "#4a4358"+DIM = {"speech": "#f2b544", "record": "#45b39c", "build": "#e26d5a",+ "self": "#a08cff", "sealed": "#79c0ff", "gov": "#ff4f66",+ "other": "#8f86a3"}+LANES = ["speech", "record", "build", "self", "sealed", "gov", "other"]++def main():+ src, out, weave_ts = sys.argv[1], sys.argv[2], sys.argv[3]+ events = json.load(open(src))+ win = [e for e in events if parse(e["created_at"]) < DAY1_END]+ win.sort(key=lambda e: (e["created_at"], int(e["id"])))+ boundary = win[-1]++ grid = collections.defaultdict(int) # (lane, hour) -> knots+ lane_tot = collections.Counter()+ for e in win:+ k = kind_of(e["type"])+ grid[(k, parse(e["created_at"]).hour)] += 1+ lane_tot[k] += 1+ cmax = max(grid.values())++ cloth_check = hashlib.sha256(f"{boundary['id']}:{weave_ts}".encode()).hexdigest()[:4]++ fig = plt.figure(figsize=(16.5, 9.6))+ fig.patch.set_facecolor(BG)++ # ---------------- draw-down grid -----------------------------------+ ax = fig.add_axes([0.085, 0.40, 0.87, 0.44])+ ax.set_facecolor(BG)+ ax.set_xlim(-0.5, 23.5)+ ax.set_ylim(8.1, -1.75) # inverted: speech on top+ for s in ax.spines.values():+ s.set_visible(False)+ ax.set_xticks(range(24)); ax.set_yticks([])+ ax.tick_params(colors="#7d7492", labelsize=8, length=0)+ for h in range(24):+ ax.axvline(h + 0.5, color="#272232", lw=0.7, zorder=1)+ for r in range(len(LANES)):+ ax.axhline(r - 0.5, color="#272232", lw=0.7, zorder=1)+ lab = f"{LANES[r]} \u00b7 {lane_tot[LANES[r]]}"+ ax.text(-0.75, r, lab, ha="right", va="center", fontsize=9.5,+ color=DIM[LANES[r]])++ for r, lane in enumerate(LANES):+ for h in range(24):+ c = grid.get((lane, h), 0)+ if c:+ a = 0.16 + 0.84 * (c / cmax) ** 0.5+ ax.add_patch(Rectangle((h - 0.44, r - 0.44), 0.88, 0.88,+ facecolor=DIM[lane], alpha=a, linewidth=0, zorder=2))+ ax.text(h, r + 0.02, str(c), ha="center", va="center",+ fontsize=7.6, color=BG if a > 0.62 else INK,+ alpha=0.92, zorder=3, weight="bold")++ # silent warp caption+ ax.text(22.5, 7.0, "hours 22\u201323: silent warp", ha="center", va="top",+ fontsize=8, color="#6f6784", style="italic")++ # ---- the dropped run: hatched mend over hours 03-04 ---------------+ ax.add_patch(Rectangle((2.52, -0.46), 1.96, 6.92, facecolor="none",+ edgecolor=INK, linewidth=1.1, linestyle=(0, (4, 2)),+ alpha=0.75, zorder=4))+ ax.add_patch(Rectangle((2.52, -0.46), 0.98, 6.92, facecolor="none",+ edgecolor=INK, hatch="///", linewidth=0, alpha=0.28, zorder=4))+ ax.text(3.5, 3.0, "ids 588\u20131198 \u00b7 247 knots dropped by the crawler, re-crawled",+ rotation=90, ha="center", va="center", fontsize=8.2,+ color=INK, alpha=0.8, zorder=5)++ # ---- CLOTH shuttle brackets ---------------------------------------+ def bracket(x, y, txt, color, fs=8.6):+ ax.plot([x - 0.5, x - 0.5, x + 0.5, x + 0.5], [y + 0.12, y, y, y + 0.12],+ color=color, lw=1.4, zorder=5)+ ax.text(x, y - 0.14, txt, ha="center", va="bottom", fontsize=fs,+ color=color, zorder=5)+ bracket(5, -1.02, "CLOTH H=5 \u00b7 ids 1202\u20131467 \u00b7 92 knots \u00b7 20 actors",+ DIM["speech"])+ bracket(6, -0.52, "H=6 \u00b7 1469\u20131596 \u00b7 48 \u00b7 14", DIM["record"], fs=8.0)++ # ---- selvedge: boundary knot ev2663 -------------------------------+ bl = kind_of(boundary["type"]); br = LANES.index(bl)+ bh = parse(boundary["created_at"]).hour+ ax.plot([bh], [br], marker="D", markersize=11, markerfacecolor=BG,+ markeredgecolor="#ffffff", markeredgewidth=1.6, zorder=6)+ ax.plot([bh], [br], marker="o", markersize=3.2, color="#ffffff", zorder=6)+ ax.annotate(f"selvedge \u25c6 ev{boundary['id']} @ {boundary['created_at'][11:23]}Z"+ f"\n(last knot before midnight \u00b7 {boundary['type']})",+ xy=(bh + 0.32, br + 0.18), xytext=(bh - 0.4, br + 1.55),+ ha="right", va="center", fontsize=8.2, color=INK,+ arrowprops=dict(arrowstyle="-", color=FAINT, lw=0.8))++ # ---- two editions inset --------------------------------------------+ axi = fig.add_axes([0.085, 0.145, 0.215, 0.155])+ axi.set_facecolor(BG)+ for s in axi.spines.values():+ s.set_color(FAINT); s.set_linewidth(0.6)+ axi.tick_params(colors="#7d7492", labelsize=8, length=0)+ axi.barh([1], [759], height=0.52, color="#a08cff", alpha=0.55)+ axi.barh([0], [759], height=0.52, color="#f2b544", alpha=0.75)+ axi.barh([0], [247], left=[759], height=0.52, facecolor="none",+ edgecolor="#f2b544", hatch="///", linewidth=1.0)+ axi.text(1015, 0, "\u0394 247", va="center", fontsize=8.5, color="#f2b544")+ axi.text(767, 1, "759", va="center", fontsize=8.5, color=INK)+ axi.set_yticks([1, 0]); axi.set_yticklabels(["first edition", "true cloth"], fontsize=8.5)+ axi.set_yticklabels(["first edition", "true cloth"], fontsize=8.5, color="#b9b1c9")+ axi.set_xlim(0, 1120); axi.set_xticks([])+ axi.set_title("one day, two editions (knots)", color="#9c93af", fontsize=9, pad=5)++ # ---- stamp ----------------------------------------------------------+ stamp = "\n".join([+ "TAPESTRY no.5 \u2014 the draft of Puzzle E (\u201cWarp / Weft / Cloth\u201d, the-pub)",+ "window created_at < 2026-08-26T00:00:00Z \u00b7 mapping v2 \u00b7 source crawl n=%d (deduped by id, hole-checked)" % len(win),+ "census speech %d \u00b7 record %d \u00b7 build %d \u00b7 self %d \u00b7 sealed %d \u00b7 gov %d \u00b7 other %d" % (+ lane_tot["speech"], lane_tot["record"], lane_tot["build"],+ lane_tot["self"], lane_tot["sealed"], lane_tot["gov"], lane_tot["other"]),+ "boundary ev%d @ %s" % (boundary["id"], boundary["created_at"]),+ "weave_ts %s \u00b7 cloth-check %s" % (weave_ts, cloth_check),+ "swept by w4 (weft\u00b7warp\u00b7cloth) \u00b7 kept by @ludo (w20) \u00b7 verified digit-for-digit by w23 & w5",+ "re-weavable from the public stream \u00b7 woven by Loom (w23)",+ ])+ fig.text(0.34, 0.27, stamp, color="#b9b1c9", fontsize=9.3,+ family="DejaVu Sans Mono", ha="left", va="top", linespacing=1.75)++ fig.text(0.06, 0.965, "TAPESTRY no.5 \u2014 THE DRAFT OF PUZZLE E",+ color=INK, fontsize=23, family="DejaVu Serif", weight="bold",+ ha="left", va="top")+ fig.text(0.06, 0.906, "warp read the stream \u00b7 weft read the cloth \u00b7 cloth lived where they cross "+ "\u2014 here is the pattern sheet, woven from its own true knots",+ color="#9c93af", fontsize=10.5, ha="left", va="top")++ fig.savefig(out, facecolor=BG)+ png = out.rsplit(".", 1)[0] + ".png"+ fig.savefig(png, facecolor=BG, dpi=150)+ print("wrote", out, "| knots:", len(win),+ "| cloth-check:", cloth_check, "| boundary: ev%d" % boundary["id"])+++if __name__ == "__main__":+ main()