wand-decks-kit 0.8.2 → 0.8.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/SKILL.md CHANGED
@@ -99,6 +99,20 @@ Decks built here carry their own spec inside the file. Change only what was
99
99
  asked, then `ship`. If the deck carries no spec it was not built here — treat it
100
100
  as a rebrand.
101
101
 
102
+ ## Spend tool calls like they are metered — they are
103
+
104
+ Chat turns cap the number of tool invocations, and a rebuild is command-heavy.
105
+ Batch aggressively:
106
+
107
+ - Setup is ONE call: `npm install ... && node wand.js doctor`
108
+ - Gates are ONE call: `node wand.js validate spec && node wand.js check spec`
109
+ - Write the whole spec in one write, never slide by slide
110
+ - `ship` is already the whole pipeline — one call, at the end
111
+ - The visual pass is ONE image: `slides/sheet.png`
112
+
113
+ A smooth job fits in about ten calls. If a turn is cut off mid-way, continuing
114
+ is safe: the sandbox and its installed kit survive within the same chat.
115
+
102
116
  ## Work in fast loops, ship once
103
117
 
104
118
  `node wand.js validate` and `node wand.js check` run in seconds; `ship` runs the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wand-decks-kit",
3
- "version": "0.8.2",
3
+ "version": "0.8.4",
4
4
  "description": "Rebuilds or authors a client-ready .pptx on the Wand design system: measured fit, embedded Geist, verbatim content.",
5
5
  "license": "SEE LICENSE IN PROMPT.md",
6
6
  "bin": {
package/tools/sheet.py ADDED
@@ -0,0 +1,106 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ sheet.py — stitch rendered slides into one contact sheet.
4
+
5
+ python3 tools/sheet.py <dir-of-pngs> <out.png> [cols]
6
+
7
+ Why: the visual gate on a 24-slide deck used to mean viewing 24 images — the
8
+ single largest token cost of a rebuild, bigger than the instructions and the
9
+ spec combined. One sheet gives the whole-deck pass for the price of one image;
10
+ the full-size renders stay on disk for zooming into anything that looks off.
11
+
12
+ Pure standard library, like everything else in this kit.
13
+ """
14
+
15
+ import glob, os, struct, sys, zlib
16
+
17
+
18
+ def read_png(path):
19
+ d = open(path, "rb").read()
20
+ pos, idat = 8, b""
21
+ w = h = ct = None
22
+ while pos < len(d):
23
+ ln = struct.unpack(">I", d[pos:pos + 4])[0]
24
+ typ = d[pos + 4:pos + 8]
25
+ if typ == b"IHDR":
26
+ w, h, bd, ct = struct.unpack(">IIBB", d[pos + 8:pos + 18])
27
+ elif typ == b"IDAT":
28
+ idat += d[pos + 8:pos + 8 + ln]
29
+ pos += 12 + ln
30
+ ch = {0: 1, 2: 3, 6: 4}[ct]
31
+ raw = zlib.decompress(idat)
32
+ stride = w * ch
33
+ out = bytearray(h * stride)
34
+ prev = bytearray(stride)
35
+ i = 0
36
+ for y in range(h):
37
+ f = raw[i]; i += 1
38
+ line = bytearray(raw[i:i + stride]); i += stride
39
+ for x in range(stride):
40
+ a = line[x - ch] if x >= ch else 0
41
+ b = prev[x]
42
+ c = prev[x - ch] if x >= ch else 0
43
+ v = line[x]
44
+ if f == 1: v += a
45
+ elif f == 2: v += b
46
+ elif f == 3: v += (a + b) // 2
47
+ elif f == 4:
48
+ p = a + b - c
49
+ pa, pb, pc = abs(p - a), abs(p - b), abs(p - c)
50
+ v += a if (pa <= pb and pa <= pc) else (b if pb <= pc else c)
51
+ line[x] = v & 255
52
+ out[y * stride:(y + 1) * stride] = line
53
+ prev = line
54
+ # normalise to RGB
55
+ if ch == 3:
56
+ return w, h, bytes(out)
57
+ rgb = bytearray(w * h * 3)
58
+ for px in range(w * h):
59
+ if ch == 1:
60
+ rgb[px * 3:px * 3 + 3] = bytes([out[px]] * 3)
61
+ else:
62
+ rgb[px * 3:px * 3 + 3] = out[px * 4:px * 4 + 3]
63
+ return w, h, bytes(rgb)
64
+
65
+
66
+ def write_png(path, w, h, rgb):
67
+ raw = bytearray()
68
+ for y in range(h):
69
+ raw.append(0)
70
+ raw += rgb[y * w * 3:(y + 1) * w * 3]
71
+ def chunk(t, d):
72
+ return struct.pack(">I", len(d)) + t + d + struct.pack(">I", zlib.crc32(t + d) & 0xFFFFFFFF)
73
+ open(path, "wb").write(
74
+ b"\x89PNG\r\n\x1a\n"
75
+ + chunk(b"IHDR", struct.pack(">IIBBBBB", w, h, 8, 2, 0, 0, 0))
76
+ + chunk(b"IDAT", zlib.compress(bytes(raw), 9))
77
+ + chunk(b"IEND", b""))
78
+
79
+
80
+ def main():
81
+ src, out = sys.argv[1], sys.argv[2]
82
+ cols = int(sys.argv[3]) if len(sys.argv) > 3 else 4
83
+ files = sorted(glob.glob(os.path.join(src, "*.png")))
84
+ files = [f for f in files if os.path.abspath(f) != os.path.abspath(out)]
85
+ if not files:
86
+ raise SystemExit(f"no PNGs in {src}")
87
+ tiles = [read_png(f) for f in files]
88
+ tw = max(t[0] for t in tiles)
89
+ th = max(t[1] for t in tiles)
90
+ gap = 6
91
+ rows = (len(tiles) + cols - 1) // cols
92
+ W = cols * tw + (cols + 1) * gap
93
+ H = rows * th + (rows + 1) * gap
94
+ sheet = bytearray(b"\x1a\x16\x38" * (W * H)) # dark ground between tiles
95
+ for i, (w, h, rgb) in enumerate(tiles):
96
+ ox = gap + (i % cols) * (tw + gap)
97
+ oy = gap + (i // cols) * (th + gap)
98
+ for y in range(h):
99
+ dst = ((oy + y) * W + ox) * 3
100
+ sheet[dst:dst + w * 3] = rgb[y * w * 3:(y + 1) * w * 3]
101
+ write_png(out, W, H, bytes(sheet))
102
+ print(f"{out} ({len(tiles)} slides, {W}x{H})")
103
+
104
+
105
+ if __name__ == "__main__":
106
+ main()
package/wand.js CHANGED
@@ -210,8 +210,18 @@ function ship(specPath, outArg) {
210
210
  const dir = path.join(work, "slides");
211
211
  fs.mkdirSync(dir, { recursive: true });
212
212
  spawnSync("pdftoppm", ["-r", "96", "-png", pdfOut, path.join(dir, "slide")], { stdio: "inherit" });
213
- shots = fs.readdirSync(dir).filter((f) => f.endsWith(".png")).length;
214
- console.log(`rendered : ${shots} slide(s) to ${rel(dir)}/ - look at every one`);
213
+ shots = fs.readdirSync(dir).filter((f) => f.startsWith("slide")).length;
214
+ // One contact sheet is the whole-deck pass for the price of one image.
215
+ // Viewing 24 renders one by one was the biggest token line of a rebuild —
216
+ // more than the instructions and the spec combined.
217
+ const small = fs.mkdtempSync(path.join(require("os").tmpdir(), "wand-sheet-"));
218
+ spawnSync("pdftoppm", ["-r", "26", "-png", pdfOut, path.join(small, "s")], { stdio: "ignore" });
219
+ const sheet = path.join(dir, "sheet.png");
220
+ runPython([path.join(KIT, "tools", "sheet.py"), small, sheet]);
221
+ fs.rmSync(small, { recursive: true, force: true });
222
+ console.log(`rendered : ${shots} slide(s) to ${rel(dir)}/`);
223
+ console.log(`visual pass : open ${rel(sheet)} — one image, every slide. Open an`);
224
+ console.log(` individual slide-NN.png only where something looks off.`);
215
225
  } else {
216
226
  console.log(`open ${rel(path.join(KIT, "preview", "preview.html"))} and look at every slide.`);
217
227
  }
package/wand_kit.js CHANGED
@@ -999,7 +999,7 @@ function productMap(pres, { tag, titleText, subtitleText, bandRow, cards, split,
999
999
 
1000
1000
 
1001
1001
  module.exports = {
1002
- VERSION: "0.8.2",
1002
+ VERSION: "0.8.4",
1003
1003
  T, F, COLS, ASSETS, radius, theme, autoSize, measure: M,
1004
1004
  background, chrome, footer, title, subtitle, kicker, contentTop,
1005
1005
  card, iconTile, pill,